From a2de04bef8d92fc702080d2e64065750954ec2ca Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 28 May 2015 15:27:16 +0200 Subject: [PATCH] rebuilt, updated history --- HISTORY.md | 2 + dist/vis.js | 39684 +++++++++++++++++++++++++------------------------- 2 files changed, 19844 insertions(+), 19842 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d9e9e39e..d210f5b4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -22,6 +22,8 @@ http://visjs.org - Fixed #861, brokenImage only working for one node if nodes have the same image. - Fixed hoverNode and blurNode events and added them to the docs. - Fixed #884, selectNode event. +- Fixed dynamic setting hidden and physics. +- Fixed edit node mode's fallback. ### Graph2d & Timeline diff --git a/dist/vis.js b/dist/vis.js index edfdf1b3..eb64893b 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -84,60 +84,60 @@ return /******/ (function(modules) { // webpackBootstrap // utils 'use strict'; - exports.util = __webpack_require__(14); - exports.DOMutil = __webpack_require__(19); + exports.util = __webpack_require__(2); + exports.DOMutil = __webpack_require__(21); // data - exports.DataSet = __webpack_require__(20); - exports.DataView = __webpack_require__(22); - exports.Queue = __webpack_require__(21); + exports.DataSet = __webpack_require__(22); + exports.DataView = __webpack_require__(24); + exports.Queue = __webpack_require__(23); // Graph3d - exports.Graph3d = __webpack_require__(23); + exports.Graph3d = __webpack_require__(25); exports.graph3d = { - Camera: __webpack_require__(27), - Filter: __webpack_require__(28), - Point2d: __webpack_require__(24), - Point3d: __webpack_require__(26), - Slider: __webpack_require__(29), - StepNumber: __webpack_require__(30) + Camera: __webpack_require__(29), + Filter: __webpack_require__(30), + Point2d: __webpack_require__(26), + Point3d: __webpack_require__(28), + Slider: __webpack_require__(31), + StepNumber: __webpack_require__(32) }; // Timeline - exports.Timeline = __webpack_require__(31); - exports.Graph2d = __webpack_require__(55); + exports.Timeline = __webpack_require__(33); + exports.Graph2d = __webpack_require__(56); exports.timeline = { - DateUtil: __webpack_require__(37), - DataStep: __webpack_require__(58), - Range: __webpack_require__(35), - stack: __webpack_require__(41), - TimeStep: __webpack_require__(43), + DateUtil: __webpack_require__(39), + DataStep: __webpack_require__(59), + Range: __webpack_require__(37), + stack: __webpack_require__(42), + TimeStep: __webpack_require__(44), components: { items: { - Item: __webpack_require__(9), - BackgroundItem: __webpack_require__(46), - BoxItem: __webpack_require__(6), - PointItem: __webpack_require__(45), - RangeItem: __webpack_require__(42) + Item: __webpack_require__(11), + BackgroundItem: __webpack_require__(47), + BoxItem: __webpack_require__(5), + PointItem: __webpack_require__(46), + RangeItem: __webpack_require__(43) }, - Component: __webpack_require__(33), - CurrentTime: __webpack_require__(32), - CustomTime: __webpack_require__(50), - DataAxis: __webpack_require__(57), - GraphGroup: __webpack_require__(59), - Group: __webpack_require__(40), - BackgroundGroup: __webpack_require__(44), - ItemSet: __webpack_require__(39), - Legend: __webpack_require__(63), - LineGraph: __webpack_require__(56), - TimeAxis: __webpack_require__(47) + Component: __webpack_require__(35), + CurrentTime: __webpack_require__(34), + CustomTime: __webpack_require__(51), + DataAxis: __webpack_require__(58), + GraphGroup: __webpack_require__(60), + Group: __webpack_require__(41), + BackgroundGroup: __webpack_require__(45), + ItemSet: __webpack_require__(10), + Legend: __webpack_require__(64), + LineGraph: __webpack_require__(57), + TimeAxis: __webpack_require__(48) } }; // Network - exports.Network = __webpack_require__(65); + exports.Network = __webpack_require__(66); exports.network = { Images: __webpack_require__(112), dotparser: __webpack_require__(110), @@ -156,9 +156,9 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(15); - exports.hammer = __webpack_require__(10); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(10); + exports.moment = __webpack_require__(17); + exports.hammer = __webpack_require__(13); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(13); /***/ }, /* 1 */ @@ -177,1721 +177,1841 @@ return /******/ (function(modules) { // webpackBootstrap /* 2 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var _sharedLabel = __webpack_require__(67); - - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - - var _nodesShapesBox = __webpack_require__(68); - - var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); - - var _nodesShapesCircle = __webpack_require__(70); - - var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); - - var _nodesShapesCircularImage = __webpack_require__(72); - - var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); - - var _nodesShapesDatabase = __webpack_require__(73); - - var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); - - var _nodesShapesDiamond = __webpack_require__(74); + // utility functions - var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. - var _nodesShapesDot = __webpack_require__(76); + 'use strict'; - var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); + var moment = __webpack_require__(17); + var uuid = __webpack_require__(20); - var _nodesShapesEllipse = __webpack_require__(77); + /** + * Test whether given object is a number + * @param {*} object + * @return {Boolean} isNumber + */ + exports.isNumber = function (object) { + return object instanceof Number || typeof object == 'number'; + }; - var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); + /** + * Remove everything in the DOM object + * @param DOMobject + */ + exports.recursiveDOMDelete = function (DOMobject) { + if (DOMobject) { + while (DOMobject.hasChildNodes() === true) { + exports.recursiveDOMDelete(DOMobject.firstChild); + DOMobject.removeChild(DOMobject.firstChild); + } + } + }; - var _nodesShapesIcon = __webpack_require__(78); + /** + * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value. + * + * @param min + * @param max + * @param total + * @param value + * @returns {number} + */ + exports.giveRange = function (min, max, total, value) { + if (max == min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + }; - var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); + /** + * Test whether given object is a string + * @param {*} object + * @return {Boolean} isString + */ + exports.isString = function (object) { + return object instanceof String || typeof object == 'string'; + }; - var _nodesShapesImage = __webpack_require__(79); + /** + * Test whether given object is a Date, or a String containing a Date + * @param {Date | String} object + * @return {Boolean} isDate + */ + exports.isDate = function (object) { + if (object instanceof Date) { + return true; + } else if (exports.isString(object)) { + // test whether this string contains a date + var match = ASPDateRegex.exec(object); + if (match) { + return true; + } else if (!isNaN(Date.parse(object))) { + return true; + } + } - var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); + return false; + }; - var _nodesShapesSquare = __webpack_require__(80); + /** + * Create a semi UUID + * source: http://stackoverflow.com/a/105074/1262753 + * @return {String} uuid + */ + exports.randomUUID = function () { + return uuid.v4(); + }; - var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); + /** + * assign all keys of an object that are not nested objects to a certain value (used for color objects). + * @param obj + * @param value + */ + exports.assignAllKeys = function (obj, value) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] !== 'object') { + obj[prop] = value; + } + } + } + }; - var _nodesShapesStar = __webpack_require__(81); + /** + * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values. + * That means an object is not created on a property if only the b object has it. + * @param obj + * @param value + */ + exports.fillIfDefined = function (a, b) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); + for (var prop in a) { + if (b[prop] !== undefined) { + if (typeof b[prop] !== 'object') { + if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } else { + if (typeof a[prop] === 'object') { + exports.fillIfDefined(a[prop], b[prop], allowDeletion); + } + } + } + } + }; - var _nodesShapesText = __webpack_require__(82); + /** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ + exports.protoExtend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + a[prop] = other[prop]; + } + } + return a; + }; - var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); + /** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ + exports.extend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } + } + } + return a; + }; - var _nodesShapesTriangle = __webpack_require__(83); + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveExtend = function (props, a, b) { + if (!Array.isArray(props)) { + throw new Error('Array with property names expected as first argument'); + } - var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; - var _nodesShapesTriangleDown = __webpack_require__(84); + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } + } + } + return a; + }; - var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - var _sharedValidator = __webpack_require__(53); + // TODO: add support for Arrays to deepExtend + if (Array.isArray(b)) { + throw new TypeError('Arrays are not supported by deepExtend'); + } + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; + } + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop], false, allowDeletion); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } + } else if (Array.isArray(b[prop])) { + throw new TypeError('Arrays are not supported by deepExtend'); + } else { + a[prop] = b[prop]; + } + } + } + } + return a; + }; - var _sharedValidator2 = _interopRequireDefault(_sharedValidator); + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveNotDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - var util = __webpack_require__(14); + // TODO: add support for Arrays to deepExtend + if (Array.isArray(b)) { + throw new TypeError('Arrays are not supported by deepExtend'); + } + for (var prop in b) { + if (b.hasOwnProperty(prop)) { + if (props.indexOf(prop) == -1) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; + } + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop]); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } + } else if (Array.isArray(b[prop])) { + throw new TypeError('Arrays are not supported by deepExtend'); + } else { + a[prop] = b[prop]; + } + } + } + } + return a; + }; /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} options An object containing options for the node. All - * options are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Network.Groups} grouplist A list with groups. Needed for - * retrieving group options - * @param {Object} constants An object with default values for - * example for the color - * + * Deep extend an object a with the properties of object b + * @param {Object} a + * @param {Object} b + * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended. + * (ie. the options objects that inherit from others will also get the inherited options) + * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted + * @returns {Object} */ + exports.deepExtend = function (a, b, protoExtend, allowDeletion) { + for (var prop in b) { + if (b.hasOwnProperty(prop) || protoExtend === true) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; + } + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop], protoExtend); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } + } else if (Array.isArray(b[prop])) { + a[prop] = []; + for (var i = 0; i < b[prop].length; i++) { + a[prop].push(b[prop][i]); + } + } else { + a[prop] = b[prop]; + } + } + } + return a; + }; - var Node = (function () { - function Node(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Node); - - this.options = util.bridgeObject(globalOptions); - this.body = body; + /** + * Test whether all elements in two arrays are equal. + * @param {Array} a + * @param {Array} b + * @return {boolean} Returns true if both arrays have the same length and same + * elements. + */ + exports.equalArray = function (a, b) { + if (a.length != b.length) return false; - this.edges = []; // all edges connected to this node + for (var i = 0, len = a.length; i < len; i++) { + if (a[i] != b[i]) return false; + } - // set defaults for the options - this.id = undefined; - this.imagelist = imagelist; - this.grouplist = grouplist; + return true; + }; - // state options - this.x = undefined; - this.y = undefined; - this.baseSize = this.options.size; - this.baseFontSize = this.options.font.size; - this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate - this.selected = false; - this.hover = false; + /** + * Convert an object to another type + * @param {Boolean | Number | String | Date | Moment | Null | undefined} object + * @param {String | undefined} type Name of the type. Available types: + * 'Boolean', 'Number', 'String', + * 'Date', 'Moment', ISODate', 'ASPDate'. + * @return {*} object + * @throws Error + */ + exports.convert = function (object, type) { + var match; - this.labelModule = new _sharedLabel2['default'](this.body, this.options); - this.setOptions(options); + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; } - _createClass(Node, [{ - key: 'attachEdge', + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } - /** - * Attach a edge to the node - * @param {Edge} edge - */ - value: function attachEdge(edge) { - if (this.edges.indexOf(edge) === -1) { - this.edges.push(edge); - } - } - }, { - key: 'detachEdge', + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); - /** - * Detach a edge from the node - * @param {Edge} edge - */ - value: function detachEdge(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); - } - } - }, { - key: 'togglePhysics', + case 'number': + case 'Number': + return Number(object.valueOf()); - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - this.options.physics = status; - } - }, { - key: 'setOptions', + case 'string': + case 'String': + return String(object); - /** - * Set or overwrite options for the node - * @param {Object} options an object with options - * @param {Object} constants and object with default, global options - */ - value: function setOptions(options) { - if (!options) { - return; + case 'Date': + if (exports.isNumber(object)) { + return new Date(object); } - // basic options - if (options.id !== undefined) { - this.id = options.id; + if (object instanceof Date) { + return new Date(object.valueOf()); + } else if (moment.isMoment(object)) { + return new Date(object.valueOf()); } - - if (this.id === undefined) { - throw 'Node must have an id'; + if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])); // parse number + } else { + return moment(object).toDate(); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } - // set these options locally - if (options.x !== undefined) { - this.x = parseInt(options.x);this.predefinedPosition = true; - } - if (options.y !== undefined) { - this.y = parseInt(options.y);this.predefinedPosition = true; + case 'Moment': + if (exports.isNumber(object)) { + return moment(object); } - if (options.size !== undefined) { - this.baseSize = options.size; + if (object instanceof Date) { + return moment(object.valueOf()); + } else if (moment.isMoment(object)) { + return moment(object); } - if (options.value !== undefined) { - options.value = parseInt(options.value); + if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return moment(Number(match[1])); // parse number + } else { + return moment(object); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } - // copy group options - if (typeof options.group === 'number' || typeof options.group === 'string' && options.group != '') { - var groupObj = this.grouplist.get(options.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); + case 'ISODate': + if (exports.isNumber(object)) { + return new Date(object); + } else if (object instanceof Date) { + return object.toISOString(); + } else if (moment.isMoment(object)) { + return object.toDate().toISOString(); + } else if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])).toISOString(); // parse number + } else { + return new Date(object).toISOString(); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); } - // this transforms all shorthands into fully defined options - Node.parseOptions(this.options, options, true); - - // load the images - if (this.options.image !== undefined && this.options.image != '') { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); + case 'ASPDate': + if (exports.isNumber(object)) { + return '/Date(' + object + ')/'; + } else if (object instanceof Date) { + return '/Date(' + object.valueOf() + ')/'; + } else if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + var value; + if (match) { + // object is an ASP date + value = new Date(Number(match[1])).valueOf(); // parse number } else { - throw 'No imagelist provided'; + value = new Date(object).valueOf(); // parse string } + return '/Date(' + value + ')/'; + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); } - this.updateShape(); - this.updateLabelModule(); + default: + throw new Error('Unknown type "' + type + '"'); + } + }; - // reset the size of the node, this can be changed - this._reset(); + // parse ASP.Net Date pattern, + // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' + // code from http://momentjs.com/ + var ASPDateRegex = /^\/?Date\((\-?\d+)/i; - if (options.hidden !== undefined || options.physics !== undefined) { - return true; - } - return false; + /** + * Get the type of an object, for example exports.getType([]) returns 'Array' + * @param {*} object + * @return {String} type + */ + exports.getType = function (object) { + var type = typeof object; + + if (type == 'object') { + if (object === null) { + return 'null'; } - }, { - key: 'updateLabelModule', - value: function updateLabelModule() { - if (this.options.label === undefined || this.options.label === null) { - this.options.label = ''; - } - this.labelModule.setOptions(this.options, true); - if (this.labelModule.baseSize !== undefined) { - this.baseFontSize = this.labelModule.baseSize; - } + if (object instanceof Boolean) { + return 'Boolean'; } - }, { - key: 'updateShape', - value: function updateShape() { - // choose draw method depending on the shape - switch (this.options.shape) { - case 'box': - this.shape = new _nodesShapesBox2['default'](this.options, this.body, this.labelModule); - break; - case 'circle': - this.shape = new _nodesShapesCircle2['default'](this.options, this.body, this.labelModule); - break; - case 'circularImage': - this.shape = new _nodesShapesCircularImage2['default'](this.options, this.body, this.labelModule, this.imageObj); - break; - case 'database': - this.shape = new _nodesShapesDatabase2['default'](this.options, this.body, this.labelModule); - break; - case 'diamond': - this.shape = new _nodesShapesDiamond2['default'](this.options, this.body, this.labelModule); - break; - case 'dot': - this.shape = new _nodesShapesDot2['default'](this.options, this.body, this.labelModule); - break; - case 'ellipse': - this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); - break; - case 'icon': - this.shape = new _nodesShapesIcon2['default'](this.options, this.body, this.labelModule); - break; - case 'image': - this.shape = new _nodesShapesImage2['default'](this.options, this.body, this.labelModule, this.imageObj); - break; - case 'square': - this.shape = new _nodesShapesSquare2['default'](this.options, this.body, this.labelModule); - break; - case 'star': - this.shape = new _nodesShapesStar2['default'](this.options, this.body, this.labelModule); - break; - case 'text': - this.shape = new _nodesShapesText2['default'](this.options, this.body, this.labelModule); - break; - case 'triangle': - this.shape = new _nodesShapesTriangle2['default'](this.options, this.body, this.labelModule); - break; - case 'triangleDown': - this.shape = new _nodesShapesTriangleDown2['default'](this.options, this.body, this.labelModule); - break; - default: - this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); - break; - } - this._reset(); + if (object instanceof Number) { + return 'Number'; } - }, { - key: 'select', - - /** - * select this node - */ - value: function select() { - this.selected = true; - this._reset(); + if (object instanceof String) { + return 'String'; } - }, { - key: 'unselect', - - /** - * unselect this node - */ - value: function unselect() { - this.selected = false; - this._reset(); + if (Array.isArray(object)) { + return 'Array'; } - }, { - key: '_reset', - - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - value: function _reset() { - this.shape.width = undefined; - this.shape.height = undefined; + if (object instanceof Date) { + return 'Date'; } - }, { - key: 'getTitle', + return 'Object'; + } else if (type == 'number') { + return 'Number'; + } else if (type == 'boolean') { + return 'Boolean'; + } else if (type == 'string') { + return 'String'; + } else if (type === undefined) { + return 'undefined'; + } - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - value: function getTitle() { - return this.options.title; - } - }, { - key: 'distanceToBorder', + return type; + }; - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - value: function distanceToBorder(ctx, angle) { - return this.shape.distanceToBorder(ctx, angle); - } - }, { - key: 'isFixed', + /** + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue + * @returns {Array} + */ + exports.copyAndExtendArray = function (arr, newValue) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + newArr.push(newValue); + return newArr; + }; - /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not - */ - value: function isFixed() { - return this.options.fixed.x && this.options.fixed.y; - } - }, { - key: 'isSelected', + /** + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue + * @returns {Array} + */ + exports.copyArray = function (arr) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + return newArr; + }; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', + /** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ + exports.getAbsoluteLeft = function (elem) { + return elem.getBoundingClientRect().left; + }; - /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', + /** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ + exports.getAbsoluteTop = function (elem) { + return elem.getBoundingClientRect().top; + }; - /** - * Adjust the value range of the node. The node will adjust it's size - * based on its value. - * @param {Number} min - * @param {Number} max - */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var sizeDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.size = this.options.scaling.min + scale * sizeDiff; - } else { - this.options.size = this.baseSize; - this.options.font.size = this.baseFontSize; - } - } - }, { - key: 'draw', + /** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ + exports.addClassName = function (elem, className) { + var classes = elem.className.split(' '); + if (classes.indexOf(className) == -1) { + classes.push(className); // add the class to the array + elem.className = classes.join(' '); + } + }; - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); - } - }, { - key: 'updateBoundingBox', + /** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ + exports.removeClassName = function (elem, className) { + var classes = elem.className.split(' '); + var index = classes.indexOf(className); + if (index != -1) { + classes.splice(index, 1); // remove the class from the array + elem.className = classes.join(' '); + } + }; - /** - * Update the bounding box of the shape - */ - value: function updateBoundingBox() { - this.shape.updateBoundingBox(this.x, this.y); + /** + * For each method for both arrays and objects. + * In case of an array, the built-in Array.forEach() is applied. + * In case of an Object, the method loops over all properties of the object. + * @param {Object | Array} object An Object or Array + * @param {function} callback Callback method, called for each item in + * the object or array with three parameters: + * callback(value, index, object) + */ + exports.forEach = function (object, callback) { + var i, len; + if (Array.isArray(object)) { + // array + for (i = 0, len = object.length; i < len; i++) { + callback(object[i], i, object); } - }, { - key: 'resize', - - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function resize(ctx) { - this.shape.resize(ctx); + } else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } } - }, { - key: 'isOverlappingWith', + } + }; - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isOverlappingWith(obj) { - return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; - } - }, { - key: 'isBoundingBoxOverlappingWith', + /** + * Convert an object into an array: all objects properties are put into the + * array. The resulting array is unordered. + * @param {Object} object + * @param {Array} array + */ + exports.toArray = function (object) { + var array = []; - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isBoundingBoxOverlappingWith(obj) { - return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; - } - }], [{ - key: 'parseOptions', + for (var prop in object) { + if (object.hasOwnProperty(prop)) array.push(object[prop]); + } - /** - * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. - * Static so it can also be used by the handler. - * @param parentOptions - * @param newOptions - */ - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + return array; + }; - var fields = ['color', 'font', 'fixed', 'shadow']; - util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); + /** + * Update a property in an object + * @param {Object} object + * @param {String} key + * @param {*} value + * @return {Boolean} changed + */ + exports.updateProperty = function (object, key, value) { + if (object[key] !== value) { + object[key] = value; + return true; + } else { + return false; + } + }; - // merge the shadow options into the parent. - util.mergeOptions(parentOptions, newOptions, 'shadow'); + /** + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {string} action The action, for example "click", + * without the prefix "on" + * @param {function} listener The callback function to be executed + * @param {boolean} [useCapture] + */ + exports.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) useCapture = false; - // individual shape newOptions - if (newOptions.color !== undefined && newOptions.color !== null) { - var parsedColor = util.parseColor(newOptions.color); - util.fillIfDefined(parentOptions.color, parsedColor); - } else if (allowDeletion === true && newOptions.color === null) { - parentOptions.color = undefined; - delete parentOptions.color; - } + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox + } - // handle the fixed options - if (newOptions.fixed !== undefined && newOptions.fixed !== null) { - if (typeof newOptions.fixed === 'boolean') { - parentOptions.fixed.x = newOptions.fixed; - parentOptions.fixed.y = newOptions.fixed; - } else { - if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { - parentOptions.fixed.x = newOptions.fixed.x; - } - if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { - parentOptions.fixed.y = newOptions.fixed.y; - } - } - } + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent('on' + action, listener); // IE browsers + } + }; - // handle the font options - if (newOptions.font !== undefined) { - _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); - } + /** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} [useCapture] + */ + exports.removeEventListener = function (element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) useCapture = false; - // handle the scaling options, specifically the label part - if (newOptions.scaling !== undefined) { - util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); - } + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - }]); - - return Node; - })(); - exports['default'] = Node; - module.exports = exports['default']; + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent('on' + action, listener); + } + }; -/***/ }, -/* 3 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ + exports.preventDefault = function (event) { + if (!event) event = window.event; - 'use strict'; + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } else { + event.returnValue = false; // IE browsers + } + }; - Object.defineProperty(exports, '__esModule', { - value: true - }); + /** + * Get HTML element which is the target of the event + * @param {Event} event + * @return {Element} target element + */ + exports.getTarget = function (event) { + // code from http://www.quirksmode.org/js/events_properties.html + if (!event) { + event = window.event; + } - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var target; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + if (event.target) { + target = event.target; + } else if (event.srcElement) { + target = event.srcElement; + } + + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } - var util = __webpack_require__(14); - var Hammer = __webpack_require__(10); - var hammerUtil = __webpack_require__(36); + return target; + }; /** - * clears the toolbar div element of children - * - * @private + * Check if given element contains given parent somewhere in the DOM tree + * @param {Element} element + * @param {Element} parent */ + exports.hasParent = function (element, parent) { + var e = element; - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - var _this = this; - - _classCallCheck(this, ManipulationSystem); + while (e) { + if (e === parent) { + return true; + } + e = e.parentNode; + } - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; + return false; + }; - this.editMode = false; - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + exports.option = {}; - this.manipulationHammers = []; - this.temporaryUIFunctions = {}; - this.temporaryEventFunctions = []; + /** + * Convert a value into a boolean + * @param {Boolean | function | undefined} value + * @param {Boolean} [defaultValue] + * @returns {Boolean} bool + */ + exports.option.asBoolean = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - this.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.inMode = false; - this.selectedControlNode = undefined; + if (value != null) { + return value != false; + } - this.options = {}; - this.defaultOptions = { - enabled: false, - initiallyActive: false, - addNode: true, - addEdge: true, - editNode: undefined, - editEdge: true, - deleteNode: true, - deleteEdge: true, - controlNodeStyle: { - shape: 'dot', - size: 6, - color: { background: '#ff0000', border: '#3c3c3c', highlight: { background: '#07f968', border: '#3c3c3c' } }, - borderWidth: 2, - borderWidthSelected: 2 - } - }; - util.extend(this.options, this.defaultOptions); + return defaultValue || null; + }; - this.body.emitter.on('destroy', function () { - _this._clean(); - }); - this.body.emitter.on('_dataChanged', this._restore.bind(this)); - this.body.emitter.on('_resetData', this._restore.bind(this)); + /** + * Convert a value into a number + * @param {Boolean | function | undefined} value + * @param {Number} [defaultValue] + * @returns {Number} number + */ + exports.option.asNumber = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); } - _createClass(ManipulationSystem, [{ - key: '_restore', + if (value != null) { + return Number(value) || defaultValue || null; + } - /** - * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. - * @private - */ - value: function _restore() { - if (this.inMode !== false) { - if (this.options.initiallyActive === true) { - this.enableEditMode(); - } else { - this.disableEditMode(); - } - } - } - }, { - key: 'setOptions', + return defaultValue || null; + }; - /** - * Set the Options - * @param options - */ - value: function setOptions(options, allOptions, globalOptions) { - if (allOptions !== undefined) { - if (allOptions.locale !== undefined) { - this.options.locale = allOptions.locale; - } else { - this.options.locale = globalOptions.locale; - } - if (allOptions.locales !== undefined) { - this.options.locales = allOptions.locales; - } else { - this.options.locales = globalOptions.locales; - } - } + /** + * Convert a value into a string + * @param {String | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} str + */ + exports.option.asString = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - if (options !== undefined) { - if (typeof options === 'boolean') { - this.options.enabled = options; - } else { - this.options.enabled = true; - util.deepExtend(this.options, options); - } - if (this.options.initiallyActive === true) { - this.editMode = true; - } - this._setup(); - } - } - }, { - key: 'toggleEditMode', + if (value != null) { + return String(value); + } - /** - * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. - * - * @private - */ - value: function toggleEditMode() { - if (this.editMode === true) { - this.disableEditMode(); - } else { - this.enableEditMode(); - } - } - }, { - key: 'enableEditMode', - value: function enableEditMode() { - this.editMode = true; + return defaultValue || null; + }; - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'block'; - this.closeDiv.style.display = 'block'; - this.editModeDiv.style.display = 'none'; - this.showManipulatorToolbar(); - } - } - }, { - key: 'disableEditMode', - value: function disableEditMode() { - this.editMode = false; + /** + * Convert a size or location into a string with pixels or a percentage + * @param {String | Number | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} size + */ + exports.option.asSize = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'none'; - this.closeDiv.style.display = 'none'; - this.editModeDiv.style.display = 'block'; - this._createEditButton(); - } - } - }, { - key: 'showManipulatorToolbar', + if (exports.isString(value)) { + return value; + } else if (exports.isNumber(value)) { + return value + 'px'; + } else { + return defaultValue || null; + } + }; - /** - * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - value: function showManipulatorToolbar() { - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + /** + * Convert a value into a DOM element + * @param {HTMLElement | function | undefined} value + * @param {HTMLElement} [defaultValue] + * @returns {HTMLElement | null} dom + */ + exports.option.asElement = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - // reset global letiables - this.manipulationDOM = {}; + return value || defaultValue || null; + }; - // if the gui is enabled, draw all elements. - if (this.guiEnabled === true) { - // a _restore will hide these menus - this.editMode = true; - this.manipulationDiv.style.display = 'block'; - this.closeDiv.style.display = 'block'; + /** + * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + * + * @param {String} hex + * @returns {{r: *, g: *, b: *}} | 255 range + */ + exports.hexToRGB = function (hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + }; - var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - var selectedTotalCount = selectedNodeCount + selectedEdgeCount; - var locale = this.options.locales[this.options.locale]; - var needSeperator = false; + /** + * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. + * @param color + * @param opacity + * @returns {*} + */ + exports.overrideOpacity = function (color, opacity) { + if (color.indexOf('rgba') != -1) { + return color; + } else if (color.indexOf('rgb') != -1) { + var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(','); + return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; + } else { + var rgb = exports.hexToRGB(color); + if (rgb == null) { + return color; + } else { + return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; + } + } + }; - if (this.options.addNode !== false) { - this._createAddNodeButton(locale); - needSeperator = true; - } - if (this.options.addEdge !== false) { - if (needSeperator === true) { - this._createSeperator(1); - } else { - needSeperator = true; - } - this._createAddEdgeButton(locale); - } + /** + * + * @param red 0 -- 255 + * @param green 0 -- 255 + * @param blue 0 -- 255 + * @returns {string} + * @constructor + */ + exports.RGBToHex = function (red, green, blue) { + return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); + }; - if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { - if (needSeperator === true) { - this._createSeperator(2); - } else { - needSeperator = true; - } - this._createEditNodeButton(locale); - } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { - if (needSeperator === true) { - this._createSeperator(3); - } else { - needSeperator = true; - } - this._createEditEdgeButton(locale); + /** + * Parse a color property into an object with border, background, and + * highlight colors + * @param {Object | String} color + * @return {Object} colorObject + */ + exports.parseColor = function (color) { + var c; + if (exports.isString(color) === true) { + if (exports.isValidRGB(color) === true) { + var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { + return parseInt(value); + }); + color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); + } + if (exports.isValidHex(color) === true) { + var hsv = exports.hexToHSV(color); + var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) }; + var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 }; + var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v); + var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); + c = { + background: color, + border: darkerColorHex, + highlight: { + background: lighterColorHex, + border: darkerColorHex + }, + hover: { + background: lighterColorHex, + border: darkerColorHex } - - // remove buttons - if (selectedTotalCount !== 0) { - if (selectedNodeCount === 1 && this.options.deleteNode !== false) { - if (needSeperator === true) { - this._createSeperator(4); - } - this._createDeleteButton(locale); - } else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { - if (needSeperator === true) { - this._createSeperator(4); - } - this._createDeleteButton(locale); - } + }; + } else { + c = { + background: color, + border: color, + highlight: { + background: color, + border: color + }, + hover: { + background: color, + border: color } + }; + } + } else { + c = {}; + c.background = color.background || undefined; + c.border = color.border || undefined; - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - - // refresh this bar based on what has been selected - this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); - } + if (exports.isString(color.highlight)) { + c.highlight = { + border: color.highlight, + background: color.highlight + }; + } else { + c.highlight = {}; + c.highlight.background = color.highlight && color.highlight.background || undefined; + c.highlight.border = color.highlight && color.highlight.border || undefined; + } - // redraw to show any possible changes - this.body.emitter.emit('_redraw'); + if (exports.isString(color.hover)) { + c.hover = { + border: color.hover, + background: color.hover + }; + } else { + c.hover = {}; + c.hover.background = color.hover && color.hover.background || undefined; + c.hover.border = color.hover && color.hover.border || undefined; } - }, { - key: 'addNodeMode', + } - /** - * Create the toolbar for adding Nodes - * - * @private - */ - value: function addNodeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + return c; + }; - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + /** + * http://www.javascripter.net/faq/rgb2hsv.htm + * + * @param red + * @param green + * @param blue + * @returns {*} + * @constructor + */ + exports.RGBToHSV = function (red, green, blue) { + red = red / 255;green = green / 255;blue = blue / 255; + var minRGB = Math.min(red, Math.min(green, blue)); + var maxRGB = Math.max(red, Math.max(green, blue)); - this.inMode = 'addNode'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); + // Black-gray-white + if (minRGB == maxRGB) { + return { h: 0, s: 0, v: minRGB }; + } - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + // Colors other than black-gray-white: + var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; + var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; + var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; + var saturation = (maxRGB - minRGB) / maxRGB; + var value = maxRGB; + return { h: hue, s: saturation, v: value }; + }; - this._temporaryBindEvent('click', this._performAddNode.bind(this)); - } - }, { - key: 'editNode', + var cssUtil = { + // split a string with css styles into an object with key/values + split: function split(cssText) { + var styles = {}; - /** - * call the bound function to handle the editing of the node. The node has to be selected. - * - * @private - */ - value: function editNode() { - var _this2 = this; - - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } - - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - - this.inMode = 'editNode'; - if (typeof this.options.editNode === 'function') { - var node = this.selectionHandler._getSelectedNode(); - if (node.isCluster !== true) { - var data = util.deepExtend({}, node.options, true); - data.x = node.x; - data.y = node.y; - - if (this.options.editNode.length === 2) { - this.options.editNode(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'editNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this2.body.data.nodes.update(finalizedData); - } - _this2.showManipulatorToolbar(); - }); - } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } else { - alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); - } - } else { - throw new Error('No function has been configured to handle the editing of nodes.'); + cssText.split(';').forEach(function (style) { + if (style.trim() != '') { + var parts = style.split(':'); + var key = parts[0].trim(); + var value = parts[1].trim(); + styles[key] = value; } - } - }, { - key: 'addEdgeMode', + }); - /** - * create the toolbar to connect nodes - * - * @private - */ - value: function addEdgeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + return styles; + }, - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + // build a css text string from an object with key/values + join: function join(styles) { + return Object.keys(styles).map(function (key) { + return key + ': ' + styles[key]; + }).join('; '); + } + }; - this.inMode = 'addEdge'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); + /** + * Append a string with css styles to an element + * @param {Element} element + * @param {String} cssText + */ + exports.addCssText = function (element, cssText) { + var currentStyles = cssUtil.split(element.style.cssText); + var newStyles = cssUtil.split(cssText); + var styles = exports.extend(currentStyles, newStyles); - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + element.style.cssText = cssUtil.join(styles); + }; - // temporarily overload functions - this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); - this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); - this._temporaryBindUI('onDrag', this._dragControlNode.bind(this)); - this._temporaryBindUI('onRelease', this._finishConnect.bind(this)); + /** + * Remove a string with css styles from an element + * @param {Element} element + * @param {String} cssText + */ + exports.removeCssText = function (element, cssText) { + var styles = cssUtil.split(element.style.cssText); + var removeStyles = cssUtil.split(cssText); - this._temporaryBindUI('onDragStart', function () {}); - this._temporaryBindUI('onHold', function () {}); + for (var key in removeStyles) { + if (removeStyles.hasOwnProperty(key)) { + delete styles[key]; } - }, { - key: 'editEdgeMode', + } - /** - * create the toolbar to edit edges - * - * @private - */ - value: function editEdgeMode() { - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); - } + element.style.cssText = cssUtil.join(styles); + }; - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + /** + * https://gist.github.com/mjijackson/5311256 + * @param h + * @param s + * @param v + * @returns {{r: number, g: number, b: number}} + * @constructor + */ + exports.HSVToRGB = function (h, s, v) { + var r, g, b; - this.inMode = 'editEdge'; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + switch (i % 6) { + case 0: + r = v, g = t, b = p;break; + case 1: + r = q, g = v, b = p;break; + case 2: + r = p, g = v, b = t;break; + case 3: + r = p, g = q, b = v;break; + case 4: + r = t, g = p, b = v;break; + case 5: + r = v, g = p, b = q;break; + } - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - var edge = this.body.edges[this.edgeBeingEditedId]; + return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; + }; - // create control nodes - var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); - var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + exports.HSVToHex = function (h, s, v) { + var rgb = exports.HSVToRGB(h, s, v); + return exports.RGBToHex(rgb.r, rgb.g, rgb.b); + }; - this.temporaryIds.nodes.push(controlNodeFrom.id); - this.temporaryIds.nodes.push(controlNodeTo.id); + exports.hexToHSV = function (hex) { + var rgb = exports.hexToRGB(hex); + return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); + }; - this.body.nodes[controlNodeFrom.id] = controlNodeFrom; - this.body.nodeIndices.push(controlNodeFrom.id); - this.body.nodes[controlNodeTo.id] = controlNodeTo; - this.body.nodeIndices.push(controlNodeTo.id); + exports.isValidHex = function (hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; + }; - // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI - this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position - this._temporaryBindUI('onTap', function () {}); // disabled - this._temporaryBindUI('onHold', function () {}); // disabled - this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this)); // used to select control node - this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node - this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes - this._temporaryBindUI('onMouseMove', function () {}); // disabled + exports.isValidRGB = function (rgb) { + rgb = rgb.replace(' ', ''); + var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); + return isOk; + }; + exports.isValidRGBA = function (rgba) { + rgba = rgba.replace(' ', ''); + var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba); + return isOk; + }; - // create function to position control nodes correctly on movement - // automatically cleaned up because we use the temporary bind - this._temporaryBindEvent('beforeDrawing', function (ctx) { - var positions = edge.edgeType.findBorderPositions(ctx); - if (controlNodeFrom.selected === false) { - controlNodeFrom.x = positions.from.x; - controlNodeFrom.y = positions.from.y; - } - if (controlNodeTo.selected === false) { - controlNodeTo.x = positions.to.x; - controlNodeTo.y = positions.to.y; + /** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ + exports.selectiveBridgeObject = function (fields, referenceObject) { + if (typeof referenceObject == 'object') { + var objectTo = Object.create(referenceObject); + for (var i = 0; i < fields.length; i++) { + if (referenceObject.hasOwnProperty(fields[i])) { + if (typeof referenceObject[fields[i]] == 'object') { + objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); } - }); - - this.body.emitter.emit('_redraw'); + } } - }, { - key: 'deleteSelected', - - /** - * delete everything in the selection - * - * @private - */ - value: function deleteSelected() { - var _this3 = this; + return objectTo; + } else { + return null; + } + }; - // when using the gui, enable edit mode if it wasnt already. - if (this.editMode !== true) { - this.enableEditMode(); + /** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ + exports.bridgeObject = function (referenceObject) { + if (typeof referenceObject == 'object') { + var objectTo = Object.create(referenceObject); + for (var i in referenceObject) { + if (referenceObject.hasOwnProperty(i)) { + if (typeof referenceObject[i] == 'object') { + objectTo[i] = exports.bridgeObject(referenceObject[i]); + } } + } + return objectTo; + } else { + return null; + } + }; - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - - this.inMode = 'delete'; - var selectedNodes = this.selectionHandler.getSelectedNodes(); - var selectedEdges = this.selectionHandler.getSelectedEdges(); - var deleteFunction = undefined; - if (selectedNodes.length > 0) { - for (var i = 0; i < selectedNodes.length; i++) { - if (this.body.nodes[selectedNodes[i]].isCluster === true) { - alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); - return; - } - } + /** + * this is used to set the options of subobjects in the options object. A requirement of these subobjects + * is that they have an 'enabled' element which is optional for the user but mandatory for the program. + * + * @param [object] mergeTarget | this is either this.options or the options used for the groups. + * @param [object] options | options + * @param [String] option | this is the option key in the options argument + * @private + */ + exports.mergeOptions = function (mergeTarget, options, option) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - if (typeof this.options.deleteNode === 'function') { - deleteFunction = this.options.deleteNode; - } - } else if (selectedEdges.length > 0) { - if (typeof this.options.deleteEdge === 'function') { - deleteFunction = this.options.deleteEdge; + if (options[option] === null) { + mergeTarget[option] = undefined; + delete mergeTarget[option]; + } else { + if (options[option] !== undefined) { + if (typeof options[option] === 'boolean') { + mergeTarget[option].enabled = options[option]; + } else { + if (options[option].enabled === undefined) { + mergeTarget[option].enabled = true; } - } - - if (typeof deleteFunction === 'function') { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length === 2) { - deleteFunction(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this3.inMode === 'delete') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this3.body.data.edges.remove(finalizedData.edges); - _this3.body.data.nodes.remove(finalizedData.nodes); - _this3.body.emitter.emit('startSimulation'); - _this3.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for delete does not support two arguments (data, callback)'); + for (var prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } } - } else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this.body.emitter.emit('startSimulation'); - this.showManipulatorToolbar(); } } - }, { - key: '_setup', + } + }; - //********************************************** PRIVATE ***************************************// + /** + * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses + * this function will then iterate in both directions over this sorted list to find all visible items. + * + * @param {Item[]} orderedItems | Items ordered by start + * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher + * @param {String} field + * @param {String} field2 + * @returns {number} + * @private + */ + exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { + var maxIterations = 10000; + var iteration = 0; + var low = 0; + var high = orderedItems.length - 1; - /** - * draw or remove the DOM - * @private - */ - value: function _setup() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; + while (low <= high && iteration < maxIterations) { + var middle = Math.floor((low + high) / 2); - this._createWrappers(); - if (this.editMode === false) { - this._createEditButton(); - } else { - this.showManipulatorToolbar(); - } - } else { - this._removeManipulationDOM(); + var item = orderedItems[middle]; + var value = field2 === undefined ? item[field] : item[field][field2]; - // disable the gui - this.guiEnabled = false; - } + var searchResult = searchFunction(value); + if (searchResult == 0) { + // jihaa, found a visible item! + return middle; + } else if (searchResult == -1) { + // it is too small --> increase low + low = middle + 1; + } else { + // it is too big --> decrease high + high = middle - 1; } - }, { - key: '_createWrappers', - - /** - * create the div overlays that contain the DOM - * @private - */ - value: function _createWrappers() { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement('div'); - this.manipulationDiv.className = 'vis-manipulation'; - if (this.editMode === true) { - this.manipulationDiv.style.display = 'block'; - } else { - this.manipulationDiv.style.display = 'none'; - } - this.canvas.frame.appendChild(this.manipulationDiv); - } - // container for the edit button. - if (this.editModeDiv === undefined) { - this.editModeDiv = document.createElement('div'); - this.editModeDiv.className = 'vis-edit-mode'; - if (this.editMode === true) { - this.editModeDiv.style.display = 'none'; - } else { - this.editModeDiv.style.display = 'block'; - } - this.canvas.frame.appendChild(this.editModeDiv); - } + iteration++; + } - // container for the close div button - if (this.closeDiv === undefined) { - this.closeDiv = document.createElement('div'); - this.closeDiv.className = 'vis-close'; - this.closeDiv.style.display = this.manipulationDiv.style.display; - this.canvas.frame.appendChild(this.closeDiv); - } - } - }, { - key: '_getNewTargetNode', + return -1; + }; - /** - * generate a new target node. Used for creating new edges and editing edges - * @param x - * @param y - * @returns {*} - * @private - */ - value: function _getNewTargetNode(x, y) { - var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + /** + * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of + * two values, we return either the one before or the one after, depending on user input + * If it is found, we return the index, else -1. + * + * @param {Array} orderedItems + * @param {{start: number, end: number}} target + * @param {String} field + * @param {String} sidePreference 'before' or 'after' + * @returns {number} + * @private + */ + exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { + var maxIterations = 10000; + var iteration = 0; + var low = 0; + var high = orderedItems.length - 1; + var prevValue, value, nextValue, middle; - controlNodeStyle.id = 'targetNode' + util.randomUUID(); - controlNodeStyle.hidden = false; - controlNodeStyle.physics = false; - controlNodeStyle.x = x; - controlNodeStyle.y = y; + while (low <= high && iteration < maxIterations) { + // get a new guess + middle = Math.floor(0.5 * (high + low)); + prevValue = orderedItems[Math.max(0, middle - 1)][field]; + value = orderedItems[middle][field]; + nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; - return this.body.functions.createNode(controlNodeStyle); + if (value == target) { + // we found the target + return middle; + } else if (prevValue < target && value > target) { + // target is in between of the previous and the current + return sidePreference == 'before' ? Math.max(0, middle - 1) : middle; + } else if (value < target && nextValue > target) { + // target is in between of the current and the next + return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1); + } else { + // didnt find the target, we need to change our boundaries. + if (value < target) { + // it is too small --> increase low + low = middle + 1; + } else { + // it is too big --> decrease high + high = middle - 1; + } } - }, { - key: '_createEditButton', + iteration++; + } - /** - * Create the edit button - */ - value: function _createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); + // didnt find anything. Return -1. + return -1; + }; - // reset the manipulationDOM - this.manipulationDOM = {}; - - // empty the editModeDiv - util.recursiveDOMDelete(this.editModeDiv); - - // create the contents for the editMode button - var locale = this.options.locales[this.options.locale]; - var button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']); - this.editModeDiv.appendChild(button); + /* + * Easing Functions - inspired from http://gizma.com/easing/ + * only considering the t value for the range [0, 1] => [0, 1] + * https://gist.github.com/gre/1650294 + */ + exports.easingFunctions = { + // no easing, no acceleration + linear: function linear(t) { + return t; + }, + // accelerating from zero velocity + easeInQuad: function easeInQuad(t) { + return t * t; + }, + // decelerating to zero velocity + easeOutQuad: function easeOutQuad(t) { + return t * (2 - t); + }, + // acceleration until halfway, then deceleration + easeInOutQuad: function easeInOutQuad(t) { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + }, + // accelerating from zero velocity + easeInCubic: function easeInCubic(t) { + return t * t * t; + }, + // decelerating to zero velocity + easeOutCubic: function easeOutCubic(t) { + return --t * t * t + 1; + }, + // acceleration until halfway, then deceleration + easeInOutCubic: function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; + }, + // accelerating from zero velocity + easeInQuart: function easeInQuart(t) { + return t * t * t * t; + }, + // decelerating to zero velocity + easeOutQuart: function easeOutQuart(t) { + return 1 - --t * t * t * t; + }, + // acceleration until halfway, then deceleration + easeInOutQuart: function easeInOutQuart(t) { + return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; + }, + // accelerating from zero velocity + easeInQuint: function easeInQuint(t) { + return t * t * t * t * t; + }, + // decelerating to zero velocity + easeOutQuint: function easeOutQuint(t) { + return 1 + --t * t * t * t * t; + }, + // acceleration until halfway, then deceleration + easeInOutQuint: function easeInOutQuint(t) { + return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; + } + }; - // bind a hammer listener to the button, calling the function toggleEditMode. - this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); - } - }, { - key: '_clean', +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { - /** - * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. - * @private - */ - value: function _clean() { - // not in mode - this.inMode = false; + "use strict"; - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); + Object.defineProperty(exports, "__esModule", { + value: true + }); - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - // restore overloaded UI functions - this._unbindTemporaryUIs(); + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); - // remove the temporaryEventFunctions - this._unbindTemporaryEvents(); + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); + } - // restore the physics if required - this.body.emitter.emit('restorePhysics'); + _createClass(BarnesHutSolver, [{ + key: "setOptions", + value: function setOptions(options) { + this.options = options; + this.thetaInversed = 1 / this.options.theta; + this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius } }, { - key: '_cleanManipulatorHammers', + key: "solve", /** - * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * * @private */ - value: function _cleanManipulatorHammers() { - // _clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].destroy(); + value: function solve() { + if (this.options.gravitationalConstant !== 0 && this.physicsBody.physicsNodeIndices.length > 0) { + var node = undefined; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; + + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + + // for debugging + this.barnesHutTree = barnesHutTree; + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } } - this.manipulationHammers = []; } } }, { - key: '_removeManipulationDOM', + key: "_getForceContribution", /** - * Remove all DOM elements created by this module. + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node * @private */ - value: function _removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx = undefined, + dy = undefined, + distance = undefined; - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); - // remove the manipulation divs - if (this.manipulationDiv) { - this.canvas.frame.removeChild(this.manipulationDiv); - } - if (this.editModeDiv) { - this.canvas.frame.removeChild(this.editModeDiv); - } - if (this.closeDiv) { - this.canvas.frame.removeChild(this.manipulationDiv); + // BarnesHutSolver condition + // original condition : s/d < theta = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.thetaInversed) { + this._calculateForces(distance, dx, dy, node, parentBranch); + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount === 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } else { + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + this._calculateForces(distance, dx, dy, node, parentBranch); + } + } + } } - - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; } }, { - key: '_createSeperator', + key: "_calculateForces", /** - * create a seperator line. the index is to differentiate in the manipulation dom - * @param index + * Calculate the forces based on the distance. + * + * @param distance + * @param dx + * @param dy + * @param node + * @param parentBranch * @private */ - value: function _createSeperator() { - var index = arguments[0] === undefined ? 1 : arguments[0]; - - this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); - this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; - this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); - } - }, { - key: '_createAddNodeButton', + value: function _calculateForces(distance, dx, dy, node, parentBranch) { + if (distance === 0) { + distance = 0.1; + dx = distance; + } - // ---------------------- DOM functions for buttons --------------------------// + if (this.overlapAvoidanceFactor < 1) { + distance = Math.max(0.1 + this.overlapAvoidanceFactor * node.shape.radius, distance - node.shape.radius); + } - value: function _createAddNodeButton(locale) { - var button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addNodeMode.bind(this)); - } - }, { - key: '_createAddEdgeButton', - value: function _createAddEdgeButton(locale) { - var button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); - } - }, { - key: '_createEditNodeButton', - value: function _createEditNodeButton(locale) { - var button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editNode.bind(this)); - } - }, { - key: '_createEditEdgeButton', - value: function _createEditEdgeButton(locale) { - var button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); - } - }, { - key: '_createDeleteButton', - value: function _createDeleteButton(locale) { - var button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.deleteSelected.bind(this)); - } - }, { - key: '_createBackButton', - value: function _createBackButton(locale) { - var button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); - } - }, { - key: '_createButton', - value: function _createButton(id, className, label) { - var labelClassName = arguments[3] === undefined ? 'vis-label' : arguments[3]; + // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines + // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance, 3); + var fx = dx * gravityForce; + var fy = dy * gravityForce; - this.manipulationDOM[id + 'Div'] = document.createElement('div'); - this.manipulationDOM[id + 'Div'].className = className; - this.manipulationDOM[id + 'Label'] = document.createElement('div'); - this.manipulationDOM[id + 'Label'].className = labelClassName; - this.manipulationDOM[id + 'Label'].innerHTML = label; - this.manipulationDOM[id + 'Div'].appendChild(this.manipulationDOM[id + 'Label']); - return this.manipulationDOM[id + 'Div']; - } - }, { - key: '_createDescription', - value: function _createDescription(label) { - this.manipulationDiv.appendChild(this._createButton('description', 'vis-button vis-none', label)); + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; } }, { - key: '_temporaryBindEvent', - - // -------------------------- End of DOM functions for buttons ------------------------------// + key: "_formBarnesHutTree", /** - * this binds an event until cleanup by the clean functions. - * @param event - * @param newFunction + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices * @private */ - value: function _temporaryBindEvent(event, newFunction) { - this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); - this.body.emitter.on(event, newFunction); - } - }, { - key: '_temporaryBindUI', + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node = undefined; + var nodeCount = nodeIndices.length; - /** - * this overrides an UI function until cleanup by the clean function - * @param UIfunctionName - * @param newFunction - * @private - */ - value: function _temporaryBindUI(UIfunctionName, newFunction) { - if (this.body.eventListeners[UIfunctionName] !== undefined) { - this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; - this.body.eventListeners[UIfunctionName] = newFunction; - } else { - throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); + var minX = nodes[nodeIndices[0]].x; + var minY = nodes[nodeIndices[0]].y; + var maxX = nodes[nodeIndices[0]].x; + var maxY = nodes[nodeIndices[0]].y; + + // get the range of the nodes + for (var i = 1; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } } - } - }, { - key: '_unbindTemporaryUIs', + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize - /** - * Restore the overridden UI functions to their original state. - * - * @private - */ - value: function _unbindTemporaryUIs() { - for (var functionName in this.temporaryUIFunctions) { - if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { - this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; - delete this.temporaryUIFunctions[functionName]; + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); } } - this.temporaryUIFunctions = {}; + + // make global + return barnesHutTree; } }, { - key: '_unbindTemporaryEvents', + key: "_updateBranchMass", /** - * Unbind the events created by _temporaryBindEvent + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node * @private */ - value: function _unbindTemporaryEvents() { - for (var i = 0; i < this.temporaryEventFunctions.length; i++) { - var eventName = this.temporaryEventFunctions[i].event; - var boundFunction = this.temporaryEventFunctions[i].boundFunction; - this.body.emitter.off(eventName, boundFunction); - } - this.temporaryEventFunctions = []; - } - }, { - key: '_bindHammerToDiv', + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - /** - * Bind an hammer instance to a DOM element. - * @param domElement - * @param funct - */ - value: function _bindHammerToDiv(domElement, boundFunction) { - var hammer = new Hammer(domElement, {}); - hammerUtil.onTouch(hammer, boundFunction); - this.manipulationHammers.push(hammer); + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; } }, { - key: '_cleanupTemporaryNodesAndEdges', + key: "_placeInTree", /** - * Neatly clean up temporary edges and nodes + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate * @private */ - value: function _cleanupTemporaryNodesAndEdges() { - // _clean temporary edges - for (var i = 0; i < this.temporaryIds.edges.length; i++) { - this.body.edges[this.temporaryIds.edges[i]].disconnect(); - delete this.body.edges[this.temporaryIds.edges[i]]; - var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); - if (indexTempEdge !== -1) { - this.body.edgeIndices.splice(indexTempEdge, 1); - } + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); } - // _clean temporary nodes - for (var i = 0; i < this.temporaryIds.nodes.length; i++) { - delete this.body.nodes[this.temporaryIds.nodes[i]]; - var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); - if (indexTempNode !== -1) { - this.body.nodeIndices.splice(indexTempNode, 1); + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); } } - - this.temporaryIds = { nodes: [], edges: [] }; } }, { - key: '_controlNodeTouch', - - // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// + key: "_placeInRegion", /** - * the touch is used to get the position of the initial click - * @param event + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region * @private */ - value: function _controlNodeTouch(event) { - this.selectionHandler.unselectAll(); - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object - } - }, { - key: '_controlNodeDragStart', - - /** - * the drag start is used to mark one of the control nodes as selected. - * @param event - * @private - */ - value: function _controlNodeDragStart(event) { - var pointer = this.lastTouch; - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - var to = this.body.nodes[this.temporaryIds.nodes[1]]; - var edge = this.body.edges[this.edgeBeingEditedId]; - this.selectedControlNode = undefined; - - var fromSelect = from.isOverlappingWith(pointerObj); - var toSelect = to.isOverlappingWith(pointerObj); - - if (fromSelect === true) { - this.selectedControlNode = from; - edge.edgeType.from = from; - } else if (toSelect === true) { - this.selectedControlNode = to; - edge.edgeType.to = to; - } - - this.body.emitter.emit('_redraw'); - } - }, { - key: '_controlNodeDrag', - - /** - * dragging the control nodes or the canvas - * @param event - * @private - */ - value: function _controlNodeDrag(event) { - this.body.emitter.emit('disablePhysics'); - var pointer = this.body.functions.getPointer(event.center); - var pos = this.canvas.DOMtoCanvas(pointer); - - if (this.selectedControlNode !== undefined) { - this.selectedControlNode.x = pos.x; - this.selectedControlNode.y = pos.y; - } else { - // if the drag was not started properly because the click started outside the network div, start it now. - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; - } - this.body.emitter.emit('_redraw'); - } - }, { - key: '_controlNodeDragEnd', - - /** - * connecting or restoring the control nodes. - * @param event - * @private - */ - value: function _controlNodeDragEnd(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var edge = this.body.edges[this.edgeBeingEditedId]; - - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (overlappingNodeIds[i] !== this.selectedControlNode.id) { - node = this.body.nodes[overlappingNodeIds[i]]; + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); break; - } - } - - // perform the connection - if (node !== undefined && this.selectedControlNode !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - if (this.selectedControlNode.id === from.id) { - this._performEditEdge(node.id, edge.to.id); + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { + //node.x += Math.random(); + //node.y += Math.random(); + node.x += 0.1; + node.y += 0.1; } else { - this._performEditEdge(edge.from.id, node.id); + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); } - } - } else { - edge.updateEdgeType(); - this.body.emitter.emit('restorePhysics'); + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; } - this.body.emitter.emit('_redraw'); } }, { - key: '_handleConnect', - - // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + key: "_splitBranch", - // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. * + * @param parentBranch * @private */ - value: function _handleConnect(event) { - // check to avoid double fireing of this function. - if (new Date().valueOf() - this.touchTime > 100) { - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object - - var pointer = this.lastTouch; - var node = this.selectionHandler.getNodeAt(pointer); - - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - // create a node the temporary line can look at - var targetNode = this._getNewTargetNode(node.x, node.y); - this.body.nodes[targetNode.id] = targetNode; - this.body.nodeIndices.push(targetNode.id); - - // create a temporary edge - var connectionEdge = this.body.functions.createEdge({ - id: 'connectionEdge' + util.randomUUID(), - from: node.id, - to: targetNode.id, - physics: false, - smooth: { - enabled: true, - type: 'continuous', - roundness: 0.5 - } - }); - this.body.edges[connectionEdge.id] = connectionEdge; - this.body.edgeIndices.push(connectionEdge.id); - - this.temporaryIds.nodes.push(targetNode.id); - this.temporaryIds.edges.push(connectionEdge.id); - } - } - this.touchTime = new Date().valueOf(); + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount === 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; } - } - }, { - key: '_dragControlNode', - value: function _dragControlNode(event) { - var pointer = this.body.functions.getPointer(event.center); - if (this.temporaryIds.nodes[0] !== undefined) { - var targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. - targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); - targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); - this.body.emitter.emit('_redraw'); - } else { - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); } } }, { - key: '_finishConnect', + key: "_insertRegion", /** - * Connect the new edge to the target if one exists, otherwise remove temp line - * @param event + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange * @private */ - value: function _finishConnect(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - - // remember the edge id - var connectFromId = undefined; - if (this.temporaryIds.edges[0] !== undefined) { - connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; - } - - // get the overlapping node but NOT the temporary node; - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - // if the node id is NOT a temporary node, accept the node. - if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { - node = this.body.nodes[overlappingNodeIds[i]]; + value: function _insertRegion(parentBranch, region) { + var minX = undefined, + maxX = undefined, + minY = undefined, + maxY = undefined; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; break; - } } - // clean temporary nodes and edges. - this._cleanupTemporaryNodesAndEdges(); - - // perform the connection - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); - } else { - if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - this._performAddEdge(connectFromId, node.id); - } - } - } - this.body.emitter.emit('_redraw'); + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; } }, { - key: '_performAddNode', - - // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + key: "_debug", - // ------------------------------ Performing all the actual data manipulation ------------------------// + //--------------------------- DEBUGGING BELOW ---------------------------// /** - * Adds a node on the specified location + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private */ - value: function _performAddNode(clickData) { - var _this4 = this; + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { - var defaultData = { - id: util.randomUUID(), - x: clickData.pointer.canvas.x, - y: clickData.pointer.canvas.y, - label: 'new' - }; + ctx.lineWidth = 1; - if (typeof this.options.addNode === 'function') { - if (this.options.addNode.length === 2) { - this.options.addNode(defaultData, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'addNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this4.body.data.nodes.add(finalizedData); - _this4.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this.showManipulatorToolbar(); - } - } else { - this.body.data.nodes.add(defaultData); - this.showManipulatorToolbar(); + this._drawBranch(this.barnesHutTree.root, ctx, color); } } }, { - key: '_performAddEdge', + key: "_drawBranch", /** - * connect two nodes with a new edge. + * This function is for debugging purposes. It draws the branches recursively. * + * @param branch + * @param ctx + * @param color * @private */ - value: function _performAddEdge(sourceNodeId, targetNodeId) { - var _this5 = this; + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } - var defaultData = { from: sourceNodeId, to: targetNodeId }; - if (typeof this.options.addEdge === 'function') { - if (this.options.addEdge.length === 2) { - this.options.addEdge(defaultData, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this5.body.data.edges.add(finalizedData); - _this5.selectionHandler.unselectAll(); - _this5.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for connect does not support two arguments (data,callback)'); - } - } else { - this.body.data.edges.add(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); + if (branch.childrenCount === 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); } - } - }, { - key: '_performEditEdge', + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); - /** - * connect two nodes with a new edge. - * - * @private - */ - value: function _performEditEdge(sourceNodeId, targetNodeId) { - var _this6 = this; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); - var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; - if (typeof this.options.editEdge === 'function') { - if (this.options.editEdge.length === 2) { - this.options.editEdge(defaultData, function (finalizedData) { - if (finalizedData === null || finalizedData === undefined || _this6.inMode !== 'editEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this6.body.edges[defaultData.id].updateEdgeType(); - _this6.body.emitter.emit('_redraw'); - } else { - _this6.body.data.edges.update(finalizedData); - _this6.selectionHandler.unselectAll(); - _this6.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } else { - this.body.data.edges.update(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ } }]); - return ManipulationSystem; + return BarnesHutSolver; })(); - exports['default'] = ManipulationSystem; - module.exports = exports['default']; + exports["default"] = BarnesHutSolver; + module.exports = exports["default"]; /***/ }, /* 4 */ @@ -1909,1202 +2029,624 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _sharedLabel = __webpack_require__(67); + var _componentsNode = __webpack_require__(8); - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - - var _edgesBezierEdgeDynamic = __webpack_require__(86); - - var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); - - var _edgesBezierEdgeStatic = __webpack_require__(89); - - var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); + var _componentsNode2 = _interopRequireDefault(_componentsNode); - var _edgesStraightEdge = __webpack_require__(90); + var _componentsSharedLabel = __webpack_require__(68); - var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - var util = __webpack_require__(14); + var util = __webpack_require__(2); + var DataSet = __webpack_require__(22); + var DataView = __webpack_require__(24); - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ + var NodesHandler = (function () { + function NodesHandler(body, images, groups, layoutEngine) { + var _this = this; - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + _classCallCheck(this, NodesHandler); - if (body === undefined) { - throw 'No body provided'; - } - this.options = util.bridgeObject(globalOptions); this.body = body; + this.images = images; + this.groups = groups; + this.layoutEngine = layoutEngine; - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; - - this.baseWidth = this.options.width; - this.baseFontSize = this.options.font.size; - - this.from = undefined; // a node - this.to = undefined; // a node - - this.edgeType = undefined; - - this.connected = false; - - this.labelModule = new _sharedLabel2['default'](this.body, this.options); - - this.setOptions(options); - } - - _createClass(Edge, [{ - key: 'setOptions', + // create the node API in the body container + this.body.functions.createNode = this.create.bind(this); - /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ - value: function setOptions(options) { - if (!options) { - return; + this.nodesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items, params.data); + }, + remove: function remove(event, params) { + _this.remove(params.items); } - this.colorDirty = true; - - Edge.parseOptions(this.options, options, true); + }; - if (options.id !== undefined) { - this.id = options.id; - } - if (options.from !== undefined) { - this.fromId = options.from; - } - if (options.to !== undefined) { - this.toId = options.to; - } - if (options.title !== undefined) { - this.title = options.title; - } - if (options.value !== undefined) { - options.value = parseInt(options.value); - } - - // update label Module - this.updateLabelModule(); - - var dataChanged = this.updateEdgeType(); - - // if anything has been updates, reset the selection width and the hover width - this._setInteractionWidths(); - - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); + this.options = {}; + this.defaultOptions = { + borderWidth: 1, + borderWidthSelected: 2, + brokenImage: undefined, + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' + }, + hover: { + border: '#2B7CE9', + background: '#D2E5FF' + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 0, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + group: undefined, + hidden: false, + icon: { + face: 'FontAwesome', //'FontAwesome', + code: undefined, //'\uf007', + size: 50, //50, + color: '#2B7CE9' //'#aa00ff' + }, + image: undefined, // --> URL + label: undefined, + level: undefined, + mass: 1, + physics: true, + scaling: { + min: 10, + max: 30, + label: { + enabled: false, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 5 + }, + customScalingFunction: function customScalingFunction(min, max, total, value) { + if (max === min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + shape: 'ellipse', + size: 25, + title: undefined, + value: undefined, + x: undefined, + y: undefined + }; + util.extend(this.options, this.defaultOptions); - if (options.hidden !== undefined || options.physics !== undefined) { - dataChanged = true; - } + this.bindEventListeners(); + } - return dataChanged; - } - }, { - key: 'updateLabelModule', + _createClass(NodesHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this2 = this; - /** - * update the options in the label module - */ - value: function updateLabelModule() { - this.labelModule.setOptions(this.options, true); - if (this.labelModule.baseSize !== undefined) { - this.baseFontSize = this.labelModule.baseSize; - } + // refresh the nodes. Used when reverting from hierarchical layout + this.body.emitter.on('refreshNodes', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); + this.body.emitter.on('destroy', function () { + delete _this2.body.functions.createNode; + delete _this2.nodesListeners.add; + delete _this2.nodesListeners.update; + delete _this2.nodesListeners.remove; + delete _this2.nodesListeners; + }); } }, { - key: 'updateEdgeType', + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + _componentsNode2['default'].parseOptions(this.options, options); - /** - * update the edge type, set the options - * @returns {boolean} - */ - value: function updateEdgeType() { - var dataChanged = false; - var changeInType = true; - if (this.edgeType !== undefined) { - if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') { - changeInType = false; - } - if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') { - changeInType = false; - } - if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { - changeInType = false; + // update the shape in all nodes + if (options.shape !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateShape(); + } + } } - if (changeInType === true) { - dataChanged = this.edgeType.cleanup(); + // update the shape size in all nodes + if (options.font !== undefined) { + _componentsSharedLabel2['default'].parseOptions(this.options.font, options); + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateLabelModule(); + this.body.nodes[nodeId]._reset(); + } + } } - } - if (changeInType === true) { - if (this.options.smooth.enabled === true) { - if (this.options.smooth.type === 'dynamic') { - dataChanged = true; - this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); - } else { - this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); + // update the shape size in all nodes + if (options.size !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId]._reset(); + } } - } else { - this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); - } - return dataChanged; + // update the state of the letiables if needed + if (options.hidden !== undefined || options.physics !== undefined) { + this.body.emitter.emit('_dataChanged'); + } + } } }, { - key: 'togglePhysics', + key: 'setData', /** - * Enable or disable the physics. - * @param status + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private */ - value: function togglePhysics(status) { - this.options.physics = status; - this.edgeType.togglePhysics(status); - } - }, { - key: 'connect', + value: function setData(nodes) { + var _this3 = this; - /** - * Connect an edge to its nodes - */ - value: function connect() { - this.disconnect(); + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + var oldNodesData = this.body.data.nodes; - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.body.data.nodes = nodes; + } else if (Array.isArray(nodes)) { + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); + } else if (!nodes) { + this.body.data.nodes = new DataSet(); } else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } + throw new TypeError('Array or DataSet expected'); } - this.edgeType.connect(); - } - }, { - key: 'disconnect', - - /** - * Disconnect an edge from its nodes - */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); } - if (this.to) { - this.to.detachEdge(this); - this.to = undefined; + + // remove drawn nodes + this.body.nodes = {}; + + if (this.body.data.nodes) { + (function () { + // subscribe to new dataset + var me = _this3; + util.forEach(_this3.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); + + // draw all new nodes + var ids = _this3.body.data.nodes.getIds(); + _this3.add(ids, true); + })(); } - this.connected = false; + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); + } } }, { - key: 'getTitle', + key: 'add', /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. + * Add nodes + * @param {Number[] | String[]} ids + * @private */ - value: function getTitle() { - return this.title; - } - }, { - key: 'isSelected', + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', + var id = undefined; + var newNodes = []; + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + var _properties = this.body.data.nodes.get(id); + var node = this.create(_properties); + newNodes.push(node); + this.body.nodes[id] = node; // note: this may replace an existing node + } - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; + this.layoutEngine.positionInitially(newNodes); + + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); + } } }, { - key: 'setValueRange', + key: 'update', /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + value: function update(ids, changedData) { + var nodes = this.body.nodes; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node !== undefined) { + // update node + dataChanged = node.setOptions(data); + } else { + dataChanged = true; + // create node + node = this.create(properties); + nodes[id] = node; } - this.options.width = this.options.scaling.min + scale * widthDiff; - } else { - this.options.width = this.baseWidth; - this.options.font.size = this.baseFontSize; } - this._setInteractionWidths(); - } - }, { - key: '_setInteractionWidths', - value: function _setInteractionWidths() { - if (typeof this.options.hoverWidth === 'function') { - this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); } else { - this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; + this.body.emitter.emit('_dataUpdated'); } + } + }, { + key: 'remove', - if (typeof this.options.selectionWidth === 'function') { - this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); - } else { - this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; + /** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var nodes = this.body.nodes; + + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + delete nodes[id]; } + + this.body.emitter.emit('_dataChanged'); } }, { - key: 'draw', + key: 'create', /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * create a node + * @param properties + * @param constructorClass */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); + value: function create(properties) { + var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; + + return new constructorClass(properties, this.body, this.images, this.groups, this.options); } }, { - key: 'drawArrows', - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); - } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); + key: 'refresh', + value: function refresh() { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + var node = undefined; + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + } + var data = this.body.data.nodes._data[nodeId]; + if (node !== undefined && data !== undefined) { + node.setOptions({ fixed: false }); + node.setOptions(data); + } } } }, { - key: 'drawLabel', - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); + key: 'getPositions', - // if the label has to be rotated: - if (this.options.font.align !== 'horizontal') { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); + /** + * Returns the positions of the nodes. + * @param ids --> optional, can be array of nodeIds, can be string + * @returns {{}} + */ + value: function getPositions(ids) { + var dataArray = {}; + if (ids !== undefined) { + if (Array.isArray(ids) === true) { + for (var i = 0; i < ids.length; i++) { + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; + dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; + } } - - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); } else { - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.shape.width > node1.shape.height) { - x = node1.x + node1.shape.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.shape.height * 0.5; + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; + dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; } - point = this._pointOnCircle(x, y, radius, 0.125); - this.labelModule.draw(ctx, point.x, point.y, selected); } } + return dataArray; } }, { - key: 'isOverlappingWith', + key: 'storePositions', /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge + * Load the XY positions of the nodes into the dataset. */ - value: function isOverlappingWith(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - - return dist < distMax; - } else { - return false; + value: function storePositions() { + // todo: add support for clusters and hierarchical. + var dataArray = []; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { + dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); + } + } } + this.body.data.nodes.update(dataArray); } }, { - key: '_rotateForLabelAlignment', + key: 'getBoundingBox', /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private + * get the bounding box of a node. + * @param nodeId + * @returns {j|*} */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); - - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + value: function getBoundingBox(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].shape.boundingBox; } - - ctx.rotate(angleInDegrees); } }, { - key: '_pointOnCircle', + key: 'getConnectedNodes', /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private + * Get the Ids of nodes connected to this node. + * @param nodeId + * @returns {Array} */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - } - }, { - key: 'select', - value: function select() { - this.selected = true; - } - }, { - key: 'unselect', - value: function unselect() { - this.selected = false; - } - }], [{ - key: 'parseOptions', - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - - var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; - - // only deep extend the items in the field array. These do not have shorthand. - util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); - - util.mergeOptions(parentOptions, newOptions, 'smooth'); - util.mergeOptions(parentOptions, newOptions, 'shadow'); - - if (newOptions.dashes !== undefined && newOptions.dashes !== null) { - parentOptions.dashes = newOptions.dashes; - } else if (allowDeletion === true && newOptions.dashes === null) { - parentOptions.dashes = undefined; - delete parentOptions.dashes; - } - - // set the scaling newOptions - if (newOptions.scaling !== undefined && newOptions.scaling !== null) { - if (newOptions.scaling.min !== undefined) { - parentOptions.scaling.min = newOptions.scaling.min; - } - if (newOptions.scaling.max !== undefined) { - parentOptions.scaling.max = newOptions.scaling.max; - } - util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); - } else if (allowDeletion === true && newOptions.scaling === null) { - parentOptions.scaling = undefined; - delete parentOptions.scaling; - } - - // hanlde multiple input cases for arrows - if (newOptions.arrows !== undefined && newOptions.arrows !== null) { - if (typeof newOptions.arrows === 'string') { - var arrows = newOptions.arrows.toLowerCase(); - if (arrows.indexOf('to') != -1) { - parentOptions.arrows.to.enabled = true; - } - if (arrows.indexOf('middle') != -1) { - parentOptions.arrows.middle.enabled = true; - } - if (arrows.indexOf('from') != -1) { - parentOptions.arrows.from.enabled = true; + value: function getConnectedNodes(nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = {}; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId == nodeId) { + // these are double equals since ids can be numeric or string + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; + } + } else if (edge.fromId == nodeId) { + // these are double equals since ids can be numeric or string + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; + } } - } else if (typeof newOptions.arrows === 'object') { - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); - } else { - throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); } - } else if (allowDeletion === true && newOptions.arrows === null) { - parentOptions.arrows = undefined; - delete parentOptions.arrows; } + return nodeList; + } + }, { + key: 'getConnectedEdges', - // hanlde multiple input cases for color - if (newOptions.color !== undefined && newOptions.color !== null) { - if (util.isString(newOptions.color)) { - parentOptions.color.color = newOptions.color; - parentOptions.color.highlight = newOptions.color; - parentOptions.color.hover = newOptions.color; - parentOptions.color.inherit = false; - } else { - var colorsDefined = false; - if (newOptions.color.color !== undefined) { - parentOptions.color.color = newOptions.color.color;colorsDefined = true; - } - if (newOptions.color.highlight !== undefined) { - parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; - } - if (newOptions.color.hover !== undefined) { - parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; - } - if (newOptions.color.inherit !== undefined) { - parentOptions.color.inherit = newOptions.color.inherit; - } - if (newOptions.color.opacity !== undefined) { - parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); - } - - if (newOptions.color.inherit === undefined && colorsDefined === true) { - parentOptions.color.inherit = false; - } + /** + * Get the ids of the edges connected to this node. + * @param nodeId + * @returns {*} + */ + value: function getConnectedEdges(nodeId) { + var edgeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgeList.push(node.edges[i].id); } - } else if (allowDeletion === true && newOptions.color === null) { - parentOptions.color = undefined; - delete parentOptions.color; - } - - // handle the font settings - if (newOptions.font !== undefined) { - _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); + } else { + console.log('NodeId provided for getConnectedEdges does not exist. Provided: ', nodeId); } + return edgeList; } }]); - return Edge; + return NodesHandler; })(); - exports['default'] = Edge; + exports['default'] = NodesHandler; module.exports = exports['default']; /***/ }, /* 5 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + 'use strict'; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + var Item = __webpack_require__(11); + var util = __webpack_require__(2); - var BarnesHutSolver = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); + /** + * @constructor BoxItem + * @extends Item + * @param {Object} data Object containing parameters start + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe available options + */ + function BoxItem(data, conversion, options) { + this.props = { + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 + } + }; - this.body = body; - this.physicsBody = physicsBody; - this.barnesHutTree; - this.setOptions(options); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } } - _createClass(BarnesHutSolver, [{ - key: "setOptions", - value: function setOptions(options) { - this.options = options; - this.thetaInversed = 1 / this.options.theta; - this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius - } - }, { - key: "solve", + Item.call(this, data, conversion, options); + } - /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ - value: function solve() { - if (this.options.gravitationalConstant !== 0 && this.physicsBody.physicsNodeIndices.length > 0) { - var node = undefined; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var nodeCount = nodeIndices.length; + BoxItem.prototype = new Item(null, null, null); - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + BoxItem.prototype.isVisible = function (range) { + // determine visibility + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return this.data.start > range.start - interval && this.data.start < range.end + interval; + }; - // for debugging - this.barnesHutTree = barnesHutTree; + /** + * Repaint the item + */ + BoxItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); - } - } - } - } - }, { - key: "_getForceContribution", + // create main box + dom.box = document.createElement('DIV'); - /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ - value: function _getForceContribution(parentBranch, node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx = undefined, - dy = undefined, - distance = undefined; + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'vis-item-content'; + dom.box.appendChild(dom.content); - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'vis-line'; - // BarnesHutSolver condition - // original condition : s/d < theta = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.thetaInversed) { - this._calculateForces(distance, dx, dy, node, parentBranch); - } else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount === 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); - } else { - // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { - // if it is not self - this._calculateForces(distance, dx, dy, node, parentBranch); - } - } - } - } - } - }, { - key: "_calculateForces", + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'vis-dot'; - /** - * Calculate the forces based on the distance. - * - * @param distance - * @param dx - * @param dy - * @param node - * @param parentBranch - * @private - */ - value: function _calculateForces(distance, dx, dy, node, parentBranch) { - if (distance === 0) { - distance = 0.1; - dx = distance; - } + // attach this item as attribute + dom.box['timeline-item'] = this; - if (this.overlapAvoidanceFactor < 1) { - distance = Math.max(0.1 + this.overlapAvoidanceFactor * node.shape.radius, distance - node.shape.radius); - } + this.dirty = true; + } - // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines - // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance, 3); - var fx = dx * gravityForce; - var fy = dy * gravityForce; + // append DOM to parent DOM + if (!this.parent) { + throw new Error('Cannot redraw item: no parent attached'); + } + if (!dom.box.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); + foreground.appendChild(dom.box); + } + if (!dom.line.parentNode) { + var background = this.parent.dom.background; + if (!background) throw new Error('Cannot redraw item: parent has no background container element'); + background.appendChild(dom.line); + } + if (!dom.dot.parentNode) { + var axis = this.parent.dom.axis; + if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); + axis.appendChild(dom.dot); + } + this.displayed = true; - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } - }, { - key: "_formBarnesHutTree", + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); - /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private - */ - value: function _formBarnesHutTree(nodes, nodeIndices) { - var node = undefined; - var nodeCount = nodeIndices.length; + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : ''); + dom.box.className = 'vis-item vis-box' + className; + dom.line.className = 'vis-item vis-line' + className; + dom.dot.className = 'vis-item vis-dot' + className; - var minX = nodes[nodeIndices[0]].x; - var minY = nodes[nodeIndices[0]].y; - var maxX = nodes[nodeIndices[0]].x; - var maxY = nodes[nodeIndices[0]].y; + // recalculate size + this.props.dot.height = dom.dot.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.line.width = dom.line.offsetWidth; + this.width = dom.box.offsetWidth; + this.height = dom.box.offsetHeight; - // get the range of the nodes - for (var i = 1; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } - } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) { - minY -= 0.5 * sizeDiff; - maxY += 0.5 * sizeDiff; - } // xSize > ySize - else { - minX += 0.5 * sizeDiff; - maxX -= 0.5 * sizeDiff; - } // xSize < ySize + this.dirty = false; + } - var minimumTreeSize = 0.00001; - var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), - centerY = 0.5 * (minY + maxY); + this._repaintDeleteButton(dom.box); + }; - // construct the barnesHutTree - var barnesHutTree = { - root: { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { - minX: centerX - halfRootSize, maxX: centerX + halfRootSize, - minY: centerY - halfRootSize, maxY: centerY + halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data: null }, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); + /** + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. + */ + BoxItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } + }; - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root, node); - } - } + /** + * Hide the item from the DOM (when visible) + */ + BoxItem.prototype.hide = function () { + if (this.displayed) { + var dom = this.dom; - // make global - return barnesHutTree; - } - }, { - key: "_updateBranchMass", - - /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private - */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; - } - }, { - key: "_placeInTree", - - /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private - */ - value: function _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); - } - } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); - } else { - // in SE - this._placeInRegion(parentBranch, node, "SE"); - } - } - } - }, { - key: "_placeInRegion", - - /** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private - */ - value: function _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { - case 0: - // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); - break; - case 1: - // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { - //node.x += Math.random(); - //node.y += Math.random(); - node.x += 0.1; - node.y += 0.1; - } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); - } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; - } - } - }, { - key: "_splitBranch", - - /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch - * @private - */ - value: function _splitBranch(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount === 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; - parentBranch.centerOfMass.x = 0; - parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch, "NW"); - this._insertRegion(parentBranch, "NE"); - this._insertRegion(parentBranch, "SW"); - this._insertRegion(parentBranch, "SE"); - - if (containedNode != null) { - this._placeInTree(parentBranch, containedNode); - } - } - }, { - key: "_insertRegion", - - /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange - * @private - */ - value: function _insertRegion(parentBranch, region) { - var minX = undefined, - maxX = undefined, - minY = undefined, - maxY = undefined; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } - - parentBranch.children[region] = { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: { data: null }, - maxWidth: 0, - level: parentBranch.level + 1, - childrenCount: 0 - }; - } - }, { - key: "_debug", - - //--------------------------- DEBUGGING BELOW ---------------------------// - - /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color - * @private - */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root, ctx, color); - } - } - }, { - key: "_drawBranch", - - /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ - value: function _drawBranch(branch, ctx, color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount === 4) { - this._drawBranch(branch.children.NW, ctx); - this._drawBranch(branch.children.NE, ctx); - this._drawBranch(branch.children.SE, ctx); - this._drawBranch(branch.children.SW, ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ - } - }]); - - return BarnesHutSolver; - })(); - - exports["default"] = BarnesHutSolver; - module.exports = exports["default"]; - -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Item = __webpack_require__(9); - var util = __webpack_require__(14); - - /** - * @constructor BoxItem - * @extends Item - * @param {Object} data Object containing parameters start - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe available options - */ - function BoxItem(data, conversion, options) { - this.props = { - dot: { - width: 0, - height: 0 - }, - line: { - width: 0, - height: 0 - } - }; - - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); - } - } - - Item.call(this, data, conversion, options); - } - - BoxItem.prototype = new Item(null, null, null); - - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - BoxItem.prototype.isVisible = function (range) { - // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return this.data.start > range.start - interval && this.data.start < range.end + interval; - }; - - /** - * Repaint the item - */ - BoxItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // create main box - dom.box = document.createElement('DIV'); - - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'vis-item-content'; - dom.box.appendChild(dom.content); - - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'vis-line'; - - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'vis-dot'; - - // attach this item as attribute - dom.box['timeline-item'] = this; - - this.dirty = true; - } - - // append DOM to parent DOM - if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); - } - if (!dom.box.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); - foreground.appendChild(dom.box); - } - if (!dom.line.parentNode) { - var background = this.parent.dom.background; - if (!background) throw new Error('Cannot redraw item: parent has no background container element'); - background.appendChild(dom.line); - } - if (!dom.dot.parentNode) { - var axis = this.parent.dom.axis; - if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); - axis.appendChild(dom.dot); - } - this.displayed = true; - - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); - - // update class - var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : ''); - dom.box.className = 'vis-item vis-box' + className; - dom.line.className = 'vis-item vis-line' + className; - dom.dot.className = 'vis-item vis-dot' + className; - - // recalculate size - this.props.dot.height = dom.dot.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.line.width = dom.line.offsetWidth; - this.width = dom.box.offsetWidth; - this.height = dom.box.offsetHeight; - - this.dirty = false; - } - - this._repaintDeleteButton(dom.box); - }; - - /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. - */ - BoxItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); - } - }; - - /** - * Hide the item from the DOM (when visible) - */ - BoxItem.prototype.hide = function () { - if (this.displayed) { - var dom = this.dom; - - if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); - if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); - if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); + if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); + if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); + if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); this.displayed = false; } @@ -3171,7 +2713,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BoxItem; /***/ }, -/* 7 */ +/* 6 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -3182,535 +2724,58 @@ return /******/ (function(modules) { // webpackBootstrap var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNode = __webpack_require__(2); - - var _componentsNode2 = _interopRequireDefault(_componentsNode); + var util = __webpack_require__(2); - var _componentsSharedLabel = __webpack_require__(67); + var LayoutEngine = (function () { + function LayoutEngine(body) { + _classCallCheck(this, LayoutEngine); - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + this.body = body; - var util = __webpack_require__(14); - var DataSet = __webpack_require__(20); - var DataView = __webpack_require__(22); + this.initialRandomSeed = Math.round(Math.random() * 1000000); + this.randomSeed = this.initialRandomSeed; + this.options = {}; + this.optionsBackup = {}; - var NodesHandler = (function () { - function NodesHandler(body, images, groups, layoutEngine) { - var _this = this; + this.defaultOptions = { + randomSeed: undefined, + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: 'UD', // UD, DU, LR, RL + sortMethod: 'hubsize' // hubsize, directed + } + }; + util.extend(this.options, this.defaultOptions); - _classCallCheck(this, NodesHandler); + this.hierarchicalLevels = {}; - this.body = body; - this.images = images; - this.groups = groups; - this.layoutEngine = layoutEngine; + this.bindEventListeners(); + } - // create the node API in the body container - this.body.functions.createNode = this.create.bind(this); + _createClass(LayoutEngine, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this = this; - this.nodesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items, params.data); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; + this.body.emitter.on('_dataChanged', function () { + _this.setupHierarchicalLayout(); + }); + this.body.emitter.on('_resetHierarchicalLayout', function () { + _this.setupHierarchicalLayout(); + }); + } + }, { + key: 'setOptions', + value: function setOptions(options, allOptions) { + if (options !== undefined) { + var prevHierarchicalState = this.options.hierarchical.enabled; - this.options = {}; - this.defaultOptions = { - borderWidth: 1, - borderWidthSelected: 2, - brokenImage: undefined, - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - }, - hover: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 0, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - group: undefined, - hidden: false, - icon: { - face: 'FontAwesome', //'FontAwesome', - code: undefined, //'\uf007', - size: 50, //50, - color: '#2B7CE9' //'#aa00ff' - }, - image: undefined, // --> URL - label: undefined, - level: undefined, - mass: 1, - physics: true, - scaling: { - min: 10, - max: 30, - label: { - enabled: false, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 5 - }, - customScalingFunction: function customScalingFunction(min, max, total, value) { - if (max === min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - shape: 'ellipse', - size: 25, - title: undefined, - value: undefined, - x: undefined, - y: undefined - }; - util.extend(this.options, this.defaultOptions); - - this.bindEventListeners(); - } - - _createClass(NodesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; - - // refresh the nodes. Used when reverting from hierarchical layout - this.body.emitter.on('refreshNodes', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createNode; - delete _this2.nodesListeners.add; - delete _this2.nodesListeners.update; - delete _this2.nodesListeners.remove; - delete _this2.nodesListeners; - }); - } - }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - _componentsNode2['default'].parseOptions(this.options, options); - - // update the shape in all nodes - if (options.shape !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateShape(); - } - } - } - - // update the shape size in all nodes - if (options.font !== undefined) { - _componentsSharedLabel2['default'].parseOptions(this.options.font, options); - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateLabelModule(); - this.body.nodes[nodeId]._reset(); - } - } - } - - // update the shape size in all nodes - if (options.size !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId]._reset(); - } - } - } - - // update the state of the letiables if needed - if (options.hidden !== undefined || options.physics !== undefined) { - this.body.emitter.emit('_dataChanged'); - } - } - } - }, { - key: 'setData', - - /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private - */ - value: function setData(nodes) { - var _this3 = this; - - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var oldNodesData = this.body.data.nodes; - - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.body.data.nodes = nodes; - } else if (Array.isArray(nodes)) { - this.body.data.nodes = new DataSet(); - this.body.data.nodes.add(nodes); - } else if (!nodes) { - this.body.data.nodes = new DataSet(); - } else { - throw new TypeError('Array or DataSet expected'); - } - - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); - } - - // remove drawn nodes - this.body.nodes = {}; - - if (this.body.data.nodes) { - (function () { - // subscribe to new dataset - var me = _this3; - util.forEach(_this3.nodesListeners, function (callback, event) { - me.body.data.nodes.on(event, callback); - }); - - // draw all new nodes - var ids = _this3.body.data.nodes.getIds(); - _this3.add(ids, true); - })(); - } - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'add', - - /** - * Add nodes - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var id = undefined; - var newNodes = []; - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - var _properties = this.body.data.nodes.get(id); - var node = this.create(_properties); - newNodes.push(node); - this.body.nodes[id] = node; // note: this may replace an existing node - } - - this.layoutEngine.positionInitially(newNodes); - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'update', - - /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids, changedData) { - var nodes = this.body.nodes; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node !== undefined) { - // update node - dataChanged = node.setOptions(data); - } else { - dataChanged = true; - // create node - node = this.create(properties); - nodes[id] = node; - } - } - - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } else { - this.body.emitter.emit('_dataUpdated'); - } - } - }, { - key: 'remove', - - /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var nodes = this.body.nodes; - - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - delete nodes[id]; - } - - this.body.emitter.emit('_dataChanged'); - } - }, { - key: 'create', - - /** - * create a node - * @param properties - * @param constructorClass - */ - value: function create(properties) { - var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; - - return new constructorClass(properties, this.body, this.images, this.groups, this.options); - } - }, { - key: 'refresh', - value: function refresh() { - var nodes = this.body.nodes; - for (var nodeId in nodes) { - var node = undefined; - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - } - var data = this.body.data.nodes._data[nodeId]; - if (node !== undefined && data !== undefined) { - node.setOptions({ fixed: false }); - node.setOptions(data); - } - } - } - }, { - key: 'getPositions', - - /** - * Returns the positions of the nodes. - * @param ids --> optional, can be array of nodeIds, can be string - * @returns {{}} - */ - value: function getPositions(ids) { - var dataArray = {}; - if (ids !== undefined) { - if (Array.isArray(ids) === true) { - for (var i = 0; i < ids.length; i++) { - if (this.body.nodes[ids[i]] !== undefined) { - var node = this.body.nodes[ids[i]]; - dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - if (this.body.nodes[ids] !== undefined) { - var node = this.body.nodes[ids]; - dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } - return dataArray; - } - }, { - key: 'storePositions', - - /** - * Load the XY positions of the nodes into the dataset. - */ - value: function storePositions() { - // todo: add support for clusters and hierarchical. - var dataArray = []; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { - dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); - } - } - } - this.body.data.nodes.update(dataArray); - } - }, { - key: 'getBoundingBox', - - /** - * get the bounding box of a node. - * @param nodeId - * @returns {j|*} - */ - value: function getBoundingBox(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].shape.boundingBox; - } - } - }, { - key: 'getConnectedNodes', - - /** - * Get the Ids of nodes connected to this node. - * @param nodeId - * @returns {Array} - */ - value: function getConnectedNodes(nodeId) { - var nodeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - var nodeObj = {}; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId == nodeId) { - // these are double equals since ids can be numeric or string - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } - } else if (edge.fromId == nodeId) { - // these are double equals since ids can be numeric or string - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId); - nodeObj[edge.toId] = true; - } - } - } - } - return nodeList; - } - }, { - key: 'getConnectedEdges', - - /** - * Get the ids of the edges connected to this node. - * @param nodeId - * @returns {*} - */ - value: function getConnectedEdges(nodeId) { - var edgeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - for (var i = 0; i < node.edges.length; i++) { - edgeList.push(node.edges[i].id); - } - } else { - console.log('NodeId provided for getConnectedEdges does not exist. Provided: ', nodeId); - } - return edgeList; - } - }]); - - return NodesHandler; - })(); - - exports['default'] = NodesHandler; - module.exports = exports['default']; - -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(14); - - var LayoutEngine = (function () { - function LayoutEngine(body) { - _classCallCheck(this, LayoutEngine); - - this.body = body; - - this.initialRandomSeed = Math.round(Math.random() * 1000000); - this.randomSeed = this.initialRandomSeed; - this.options = {}; - this.optionsBackup = {}; - - this.defaultOptions = { - randomSeed: undefined, - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: 'UD', // UD, DU, LR, RL - sortMethod: 'hubsize' // hubsize, directed - } - }; - util.extend(this.options, this.defaultOptions); - - this.hierarchicalLevels = {}; - - this.bindEventListeners(); - } - - _createClass(LayoutEngine, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this = this; - - this.body.emitter.on('_dataChanged', function () { - _this.setupHierarchicalLayout(); - }); - this.body.emitter.on('_resetHierarchicalLayout', function () { - _this.setupHierarchicalLayout(); - }); - } - }, { - key: 'setOptions', - value: function setOptions(options, allOptions) { - if (options !== undefined) { - var prevHierarchicalState = this.options.hierarchical.enabled; - - util.mergeOptions(this.options, options, 'hierarchical'); - if (options.randomSeed !== undefined) { - this.initialRandomSeed = options.randomSeed; + util.mergeOptions(this.options, options, 'hierarchical'); + if (options.randomSeed !== undefined) { + this.initialRandomSeed = options.randomSeed; } if (this.options.hierarchical.enabled === true) { @@ -4158,17185 +3223,18877 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 9 */ +/* 7 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(10); - var util = __webpack_require__(14); - - /** - * @constructor Item - * @param {Object} data Object containing (optional) parameters type, - * start, end, content, group, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} options Configuration options - * // TODO: describe available options - */ - function Item(data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; - - this.selected = false; - this.displayed = false; - this.dirty = true; + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.top = null; - this.left = null; - this.width = null; - this.height = null; - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - Item.prototype.stack = true; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + var util = __webpack_require__(2); + var Hammer = __webpack_require__(13); + var hammerUtil = __webpack_require__(38); /** - * Unselect current item + * clears the toolbar div element of children + * + * @private */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; - /** - * Set data for the item. Existing data will be updated. The id should not - * be changed. When the item is displayed, it will be redrawn immediately. - * @param {Object} data - */ - Item.prototype.setData = function (data) { - var groupChanged = data.group != undefined && this.data.group != data.group; - if (groupChanged) { - this.parent.itemSet._moveToGroup(this, data.group); - } - - this.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); - }; - - /** - * Set a parent for the item - * @param {ItemSet | Group} parent - */ - Item.prototype.setParent = function (parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); - } - } else { - this.parent = parent; - } - }; - - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - Item.prototype.isVisible = function (range) { - // Should be implemented by Item implementations - return false; - }; - - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed - */ - Item.prototype.show = function () { - return false; - }; + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { + var _this = this; - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + _classCallCheck(this, ManipulationSystem); - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; - /** - * Reposition the Item horizontally - */ - Item.prototype.repositionX = function () {}; + this.editMode = false; + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; - /** - * Reposition the Item vertically - */ - Item.prototype.repositionY = function () {}; + this.manipulationHammers = []; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; - /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected - */ - Item.prototype._repaintDeleteButton = function (anchor) { - if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { - // create and show button - var me = this; + this.touchTime = 0; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.inMode = false; + this.selectedControlNode = undefined; - var deleteButton = document.createElement('div'); - deleteButton.className = 'vis-delete'; - deleteButton.title = 'Delete this item'; + this.options = {}; + this.defaultOptions = { + enabled: false, + initiallyActive: false, + addNode: true, + addEdge: true, + editNode: undefined, + editEdge: true, + deleteNode: true, + deleteEdge: true, + controlNodeStyle: { + shape: 'dot', + size: 6, + color: { background: '#ff0000', border: '#3c3c3c', highlight: { background: '#07f968', border: '#3c3c3c' } }, + borderWidth: 2, + borderWidthSelected: 2 + } + }; + util.extend(this.options, this.defaultOptions); - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on('tap', function (event) { - event.stopPropagation(); - me.parent.removeFromDataSet(me); + this.body.emitter.on('destroy', function () { + _this._clean(); }); - - anchor.appendChild(deleteButton); - this.dom.deleteButton = deleteButton; - } else if (!this.selected && this.dom.deleteButton) { - // remove button - if (this.dom.deleteButton.parentNode) { - this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); - } - this.dom.deleteButton = null; + this.body.emitter.on('_dataChanged', this._restore.bind(this)); + this.body.emitter.on('_resetData', this._restore.bind(this)); } - }; - /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private - */ - Item.prototype._updateContents = function (element) { - var content; - if (this.options.template) { - var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData); - } else { - content = this.data.content; - } + _createClass(ManipulationSystem, [{ + key: '_restore', - var changed = this._contentToString(this.content) !== this._contentToString(content); - if (changed) { - // only replace the content when changed - if (content instanceof Element) { - element.innerHTML = ''; - element.appendChild(content); - } else if (content != undefined) { - element.innerHTML = content; - } else { - if (!(this.data.type == 'background' && this.data.content === undefined)) { - throw new Error('Property "content" missing in item ' + this.id); + /** + * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. + * @private + */ + value: function _restore() { + if (this.inMode !== false) { + if (this.options.initiallyActive === true) { + this.enableEditMode(); + } else { + this.disableEditMode(); + } } } + }, { + key: 'setOptions', - this.content = content; - } - }; - - /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private - */ - Item.prototype._updateTitle = function (element) { - if (this.data.title != null) { - element.title = this.data.title || ''; - } else { - element.removeAttribute('vis-title'); - } - }; - - /** - * Process dataAttributes timeline option and set as data- attributes on dom.content - * @param {Element} element HTML element to which the attributes will be attached - * @private - */ - Item.prototype._updateDataAttributes = function (element) { - if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { - var attributes = []; + /** + * Set the Options + * @param options + */ + value: function setOptions(options, allOptions, globalOptions) { + if (allOptions !== undefined) { + if (allOptions.locale !== undefined) { + this.options.locale = allOptions.locale; + } else { + this.options.locale = globalOptions.locale; + } + if (allOptions.locales !== undefined) { + this.options.locales = allOptions.locales; + } else { + this.options.locales = globalOptions.locales; + } + } - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == 'all') { - attributes = Object.keys(this.data); - } else { - return; + if (options !== undefined) { + if (typeof options === 'boolean') { + this.options.enabled = options; + } else { + this.options.enabled = true; + util.deepExtend(this.options, options); + } + if (this.options.initiallyActive === true) { + this.editMode = true; + } + this._setup(); + } } + }, { + key: 'toggleEditMode', - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; - - if (value != null) { - element.setAttribute('data-' + name, value); + /** + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * + * @private + */ + value: function toggleEditMode() { + if (this.editMode === true) { + this.disableEditMode(); } else { - element.removeAttribute('data-' + name); + this.enableEditMode(); } } - } - }; + }, { + key: 'enableEditMode', + value: function enableEditMode() { + this.editMode = true; - /** - * Update custom styles of the element - * @param element - * @private - */ - Item.prototype._updateStyle = function (element) { - // remove old styles - if (this.style) { - util.removeCssText(element, this.style); - this.style = null; - } + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; + this.editModeDiv.style.display = 'none'; + this.showManipulatorToolbar(); + } + } + }, { + key: 'disableEditMode', + value: function disableEditMode() { + this.editMode = false; - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; - } - }; + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'none'; + this.closeDiv.style.display = 'none'; + this.editModeDiv.style.display = 'block'; + this._createEditButton(); + } + } + }, { + key: 'showManipulatorToolbar', - /** - * Stringify the items contents - * @param {string | Element | undefined} content - * @returns {string | undefined} - * @private - */ - Item.prototype._contentToString = function (content) { - if (typeof content === 'string') return content; - if (content && 'outerHTML' in content) return content.outerHTML; - return content; - }; + /** + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private + */ + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - module.exports = Item; + // reset global letiables + this.manipulationDOM = {}; - // should be implemented by the item + // if the gui is enabled, draw all elements. + if (this.guiEnabled === true) { + // a _restore will hide these menus + this.editMode = true; + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; - // should be implemented by the item + var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var selectedTotalCount = selectedNodeCount + selectedEdgeCount; + var locale = this.options.locales[this.options.locale]; + var needSeperator = false; - // should be implemented by the item + if (this.options.addNode !== false) { + this._createAddNodeButton(locale); + needSeperator = true; + } + if (this.options.addEdge !== false) { + if (needSeperator === true) { + this._createSeperator(1); + } else { + needSeperator = true; + } + this._createAddEdgeButton(locale); + } -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { + if (selectedNodeCount === 1 && typeof this.options.editNode === 'function') { + if (needSeperator === true) { + this._createSeperator(2); + } else { + needSeperator = true; + } + this._createEditNodeButton(locale); + } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.editEdge !== false) { + if (needSeperator === true) { + this._createSeperator(3); + } else { + needSeperator = true; + } + this._createEditEdgeButton(locale); + } - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - 'use strict'; + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.deleteNode !== false) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } else if (selectedNodeCount === 0 && this.options.deleteEdge !== false) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } + } - if (typeof window !== 'undefined') { - var propagating = __webpack_require__(11); - var Hammer = window['Hammer'] || __webpack_require__(12); - module.exports = propagating(Hammer, { - preventDefault: 'mouse' - }); - } else { - module.exports = function () { - throw Error('hammer.js is only available in a browser, not in node.js.'); - }; - } + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { + // refresh this bar based on what has been selected + this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); + } - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + // redraw to show any possible changes + this.body.emitter.emit('_redraw'); + } + }, { + key: 'addNodeMode', - (function (factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - window.propagating = factory(); - } - }(function () { - var _firstTarget = null; // singleton, will contain the target element where the touch event started - var _processing = false; // singleton, true when a touch event is being handled + /** + * Create the toolbar for adding Nodes + * + * @private + */ + value: function addNodeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - /** - * Extend an Hammer.js instance with event propagation. - * - * Features: - * - Events emitted by hammer will propagate in order from child to parent - * elements. - * - Events are extended with a function `event.stopPropagation()` to stop - * propagation to parent elements. - * - An option `preventDefault` to stop all default browser behavior. - * - * Usage: - * var hammer = propagatingHammer(new Hammer(element)); - * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); - * - * @param {Hammer.Manager} hammer An hammer instance. - * @param {Object} [options] Available options: - * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. - * Enforce preventing the default browser behavior. - * Cannot be set to `false`. - * @return {Hammer.Manager} Returns the same hammer instance with extended - * functionality - */ - return function propagating(hammer, options) { - if (options && options.preventDefault === false) { - throw new Error('Only supports preventDefault == true'); - } - var _options = options || { - preventDefault: false - }; + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; + this.inMode = 'addNode'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']); - var PropagatingHammer = function(element, options) { - return propagating(new Hammer(element, options), _options); - }; - Hammer.extend(PropagatingHammer, Hammer); - PropagatingHammer.Manager = function (element, options) { - return propagating(new Hammer.Manager(element, options), _options); - }; + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - return PropagatingHammer; + this._temporaryBindEvent('click', this._performAddNode.bind(this)); } + }, { + key: 'editNode', - // attach to DOM element - var element = hammer.element; - element.hammer = hammer; + /** + * call the bound function to handle the editing of the node. The node has to be selected. + * + * @private + */ + value: function editNode() { + var _this2 = this; - // move the original functions that we will wrap - hammer._on = hammer.on; - hammer._off = hammer.off; - hammer._emit = hammer.emit; - hammer._destroy = hammer.destroy; + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - /** @type {Object.>} */ - hammer._handlers = {}; + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - // register an event to catch the start of a gesture and store the - // target in a singleton - hammer._on('hammer.input', function (event) { - if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { - event.preventDefault(); + this.inMode = 'editNode'; + if (typeof this.options.editNode === 'function') { + var node = this.selectionHandler._getSelectedNode(); + if (node.isCluster !== true) { + var data = util.deepExtend({}, node.options, true); + data.x = node.x; + data.y = node.y; + + if (this.options.editNode.length === 2) { + this.options.editNode(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this2.inMode === 'editNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this2.body.data.nodes.update(finalizedData); + } + _this2.showManipulatorToolbar(); + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); + } + } else { + alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']); + } + } else { + throw new Error('No function has been configured to handle the editing of nodes.'); } - if (event.isFirst) { - _firstTarget = event.target; - _processing = true; + } + }, { + key: 'addEdgeMode', + + /** + * create the toolbar to connect nodes + * + * @private + */ + value: function addEdgeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - if (event.isFinal) { - _processing = false; + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + this.inMode = 'addEdge'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['edgeDescription'] || this.options.locales['en']['edgeDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } - }); + + // temporarily overload functions + this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); + this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); + this._temporaryBindUI('onDrag', this._dragControlNode.bind(this)); + this._temporaryBindUI('onRelease', this._finishConnect.bind(this)); + + this._temporaryBindUI('onDragStart', function () {}); + this._temporaryBindUI('onHold', function () {}); + } + }, { + key: 'editEdgeMode', /** - * Register a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} handler A callback function, called as handler(event) - * @returns {Hammer.Manager} Returns the hammer instance + * create the toolbar to edit edges + * + * @private */ - hammer.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (!_handlers) { - hammer._handlers[event] = _handlers = []; + value: function editEdgeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } - // register the static, propagated handler - hammer._on(event, propagatedHandler); + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + this.inMode = 'editEdge'; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['editEdgeDescription'] || this.options.locales['en']['editEdgeDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + var edge = this.body.edges[this.edgeBeingEditedId]; + + // create control nodes + var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + + this.temporaryIds.nodes.push(controlNodeFrom.id); + this.temporaryIds.nodes.push(controlNodeTo.id); + + this.body.nodes[controlNodeFrom.id] = controlNodeFrom; + this.body.nodeIndices.push(controlNodeFrom.id); + this.body.nodes[controlNodeTo.id] = controlNodeTo; + this.body.nodeIndices.push(controlNodeTo.id); + + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position + this._temporaryBindUI('onTap', function () {}); // disabled + this._temporaryBindUI('onHold', function () {}); // disabled + this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this)); // used to select control node + this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node + this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes + this._temporaryBindUI('onMouseMove', function () {}); // disabled + + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + this._temporaryBindEvent('beforeDrawing', function (ctx) { + var positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; + } + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; } - _handlers.push(handler); }); - return hammer; - }; + this.body.emitter.emit('_redraw'); + } + }, { + key: 'deleteSelected', /** - * Unregister a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} [handler] Optional. The registered handler. If not - * provided, all handlers for given events - * are removed. - * @returns {Hammer.Manager} Returns the hammer instance + * delete everything in the selection + * + * @private */ - hammer.off = function (events, handler) { - // unregister the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (_handlers) { - _handlers = handler ? _handlers.filter(function (h) { - return h !== handler; - }) : []; + value: function deleteSelected() { + var _this3 = this; - if (_handlers.length > 0) { - hammer._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer._off(event, propagatedHandler); - delete hammer._handlers[event]; + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); + } + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + this.inMode = 'delete'; + var selectedNodes = this.selectionHandler.getSelectedNodes(); + var selectedEdges = this.selectionHandler.getSelectedEdges(); + var deleteFunction = undefined; + if (selectedNodes.length > 0) { + for (var i = 0; i < selectedNodes.length; i++) { + if (this.body.nodes[selectedNodes[i]].isCluster === true) { + alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']); + return; } } - }); - return hammer; - }; + if (typeof this.options.deleteNode === 'function') { + deleteFunction = this.options.deleteNode; + } + } else if (selectedEdges.length > 0) { + if (typeof this.options.deleteEdge === 'function') { + deleteFunction = this.options.deleteEdge; + } + } - /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event - */ - hammer.emit = function(eventType, event) { - if (!_processing) { - _firstTarget = event.target; + if (typeof deleteFunction === 'function') { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length === 2) { + deleteFunction(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this3.inMode === 'delete') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this3.body.data.edges.remove(finalizedData.edges); + _this3.body.data.nodes.remove(finalizedData.nodes); + _this3.body.emitter.emit('startSimulation'); + _this3.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for delete does not support two arguments (data, callback)'); + } + } else { + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this.body.emitter.emit('startSimulation'); + this.showManipulatorToolbar(); } - hammer._emit(eventType, event); - }; + } + }, { + key: '_setup', - hammer.destroy = function () { - // Detach from DOM element - var element = hammer.element; - delete element.hammer; + //********************************************** PRIVATE ***************************************// - // clear all handlers - hammer._handlers = {}; + /** + * draw or remove the DOM + * @private + */ + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; - // call original hammer destroy - hammer._destroy(); - }; + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); + } + } else { + this._removeManipulationDOM(); - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); + // disable the gui + this.guiEnabled = false; + } } + }, { + key: '_createWrappers', /** - * A static event handler, applying event propagation. - * @param {Object} event + * create the div overlays that contain the DOM + * @private */ - function propagatedHandler(event) { - // let only a single hammer instance handle this event - if (event.type !== 'hammer.input') { - // it is possible that the same srcEvent is used with multiple hammer events, - // we keep track on which events are handled in an object _handled - if (!event.srcEvent._handled) { - event.srcEvent._handled = {}; + value: function _createWrappers() { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement('div'); + this.manipulationDiv.className = 'vis-manipulation'; + if (this.editMode === true) { + this.manipulationDiv.style.display = 'block'; + } else { + this.manipulationDiv.style.display = 'none'; } + this.canvas.frame.appendChild(this.manipulationDiv); + } - if (event.srcEvent._handled[event.type]) { - return; - } - else { - event.srcEvent._handled[event.type] = true; + // container for the edit button. + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement('div'); + this.editModeDiv.className = 'vis-edit-mode'; + if (this.editMode === true) { + this.editModeDiv.style.display = 'none'; + } else { + this.editModeDiv.style.display = 'block'; } + this.canvas.frame.appendChild(this.editModeDiv); } - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; + // container for the close div button + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement('div'); + this.closeDiv.className = 'vis-close'; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.canvas.frame.appendChild(this.closeDiv); + } + } + }, { + key: '_getNewTargetNode', - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + /** + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} + * @private + */ + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); - // propagate over all elements (until stopped) - var elem = _firstTarget; - while (elem && !stopped) { - var _handlers = elem.hammer && elem.hammer._handlers[event.type]; - if (_handlers) { - for (var i = 0; i < _handlers.length && !stopped; i++) { - _handlers[i](event); - } - } + controlNodeStyle.id = 'targetNode' + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; - elem = elem.parentNode; - } + return this.body.functions.createNode(controlNodeStyle); } + }, { + key: '_createEditButton', - return hammer; - }; - })); + /** + * Create the edit button + */ + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); + // reset the manipulationDOM + this.manipulationDOM = {}; -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + // create the contents for the editMode button + var locale = this.options.locales[this.options.locale]; + var button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']); + this.editModeDiv.appendChild(button); - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); + } + }, { + key: '_clean', - var TYPE_FUNCTION = 'function'; + /** + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * @private + */ + value: function _clean() { + // not in mode + this.inMode = false; - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - /** - * set a timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} - */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - /** - * if the argument is an array, we want to execute the fn on each entry - * if it aint an array we don't want to do a thing. - * this is used by all the methods that accept a single and array argument. - * @param {*|Array} arg - * @param {String} fn - * @param {Object} [context] - * @returns {Boolean} - */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; - } - return false; - } + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context - */ - function each(obj, iterator, context) { - var i; + // restore overloaded UI functions + this._unbindTemporaryUIs(); - if (!obj) { - return; - } + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - if (obj.forEach) { - obj.forEach(iterator, context); - } else if (obj.length !== undefined) { - i = 0; - while (i < obj.length) { - iterator.call(context, obj[i], i, obj); - i++; - } - } else { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); - } + // restore the physics if required + this.body.emitter.emit('restorePhysics'); } - } + }, { + key: '_cleanManipulatorHammers', - /** - * extend object. - * means that properties in dest will be overwritten by the ones in src. - * @param {Object} dest - * @param {Object} src - * @param {Boolean} [merge] - * @returns {Object} dest - */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; + /** + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * @private + */ + value: function _cleanManipulatorHammers() { + // _clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].destroy(); } - i++; + this.manipulationHammers = []; + } } - return dest; - } + }, { + key: '_removeManipulationDOM', - /** - * merge the values from src in the dest. - * means that properties that exist in dest will not be overwritten by src - * @param {Object} dest - * @param {Object} src - * @returns {Object} dest - */ - function merge(dest, src) { - return extend(dest, src, true); - } + /** + * Remove all DOM elements created by this module. + * @private + */ + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); + + // remove the manipulation divs + if (this.manipulationDiv) { + this.canvas.frame.removeChild(this.manipulationDiv); + } + if (this.editModeDiv) { + this.canvas.frame.removeChild(this.editModeDiv); + } + if (this.closeDiv) { + this.canvas.frame.removeChild(this.manipulationDiv); + } + + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + } + }, { + key: '_createSeperator', + + /** + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index + * @private + */ + value: function _createSeperator() { + var index = arguments[0] === undefined ? 1 : arguments[0]; + + this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); + this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; + this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); + } + }, { + key: '_createAddNodeButton', + + // ---------------------- DOM functions for buttons --------------------------// + + value: function _createAddNodeButton(locale) { + var button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addNodeMode.bind(this)); + } + }, { + key: '_createAddEdgeButton', + value: function _createAddEdgeButton(locale) { + var button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); + } + }, { + key: '_createEditNodeButton', + value: function _createEditNodeButton(locale) { + var button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editNode.bind(this)); + } + }, { + key: '_createEditEdgeButton', + value: function _createEditEdgeButton(locale) { + var button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); + } + }, { + key: '_createDeleteButton', + value: function _createDeleteButton(locale) { + var button = this._createButton('delete', 'vis-button vis-delete', locale['del'] || this.options.locales['en']['del']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.deleteSelected.bind(this)); + } + }, { + key: '_createBackButton', + value: function _createBackButton(locale) { + var button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); + } + }, { + key: '_createButton', + value: function _createButton(id, className, label) { + var labelClassName = arguments[3] === undefined ? 'vis-label' : arguments[3]; + + this.manipulationDOM[id + 'Div'] = document.createElement('div'); + this.manipulationDOM[id + 'Div'].className = className; + this.manipulationDOM[id + 'Label'] = document.createElement('div'); + this.manipulationDOM[id + 'Label'].className = labelClassName; + this.manipulationDOM[id + 'Label'].innerHTML = label; + this.manipulationDOM[id + 'Div'].appendChild(this.manipulationDOM[id + 'Label']); + return this.manipulationDOM[id + 'Div']; + } + }, { + key: '_createDescription', + value: function _createDescription(label) { + this.manipulationDiv.appendChild(this._createButton('description', 'vis-button vis-none', label)); + } + }, { + key: '_temporaryBindEvent', + + // -------------------------- End of DOM functions for buttons ------------------------------// + + /** + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction + * @private + */ + value: function _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); + this.body.emitter.on(event, newFunction); + } + }, { + key: '_temporaryBindUI', + + /** + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction + * @private + */ + value: function _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; + } else { + throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); + } + } + }, { + key: '_unbindTemporaryUIs', + + /** + * Restore the overridden UI functions to their original state. + * + * @private + */ + value: function _unbindTemporaryUIs() { + for (var functionName in this.temporaryUIFunctions) { + if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { + this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; + delete this.temporaryUIFunctions[functionName]; + } + } + this.temporaryUIFunctions = {}; + } + }, { + key: '_unbindTemporaryEvents', + + /** + * Unbind the events created by _temporaryBindEvent + * @private + */ + value: function _unbindTemporaryEvents() { + for (var i = 0; i < this.temporaryEventFunctions.length; i++) { + var eventName = this.temporaryEventFunctions[i].event; + var boundFunction = this.temporaryEventFunctions[i].boundFunction; + this.body.emitter.off(eventName, boundFunction); + } + this.temporaryEventFunctions = []; + } + }, { + key: '_bindHammerToDiv', + + /** + * Bind an hammer instance to a DOM element. + * @param domElement + * @param funct + */ + value: function _bindHammerToDiv(domElement, boundFunction) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, boundFunction); + this.manipulationHammers.push(hammer); + } + }, { + key: '_cleanupTemporaryNodesAndEdges', + + /** + * Neatly clean up temporary edges and nodes + * @private + */ + value: function _cleanupTemporaryNodesAndEdges() { + // _clean temporary edges + for (var i = 0; i < this.temporaryIds.edges.length; i++) { + this.body.edges[this.temporaryIds.edges[i]].disconnect(); + delete this.body.edges[this.temporaryIds.edges[i]]; + var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); + if (indexTempEdge !== -1) { + this.body.edgeIndices.splice(indexTempEdge, 1); + } + } + + // _clean temporary nodes + for (var i = 0; i < this.temporaryIds.nodes.length; i++) { + delete this.body.nodes[this.temporaryIds.nodes[i]]; + var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); + if (indexTempNode !== -1) { + this.body.nodeIndices.splice(indexTempNode, 1); + } + } + + this.temporaryIds = { nodes: [], edges: [] }; + } + }, { + key: '_controlNodeTouch', + + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// + + /** + * the touch is used to get the position of the initial click + * @param event + * @private + */ + value: function _controlNodeTouch(event) { + this.selectionHandler.unselectAll(); + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + } + }, { + key: '_controlNodeDragStart', + + /** + * the drag start is used to mark one of the control nodes as selected. + * @param event + * @private + */ + value: function _controlNodeDragStart(event) { + var pointer = this.lastTouch; + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + var to = this.body.nodes[this.temporaryIds.nodes[1]]; + var edge = this.body.edges[this.edgeBeingEditedId]; + this.selectedControlNode = undefined; + + var fromSelect = from.isOverlappingWith(pointerObj); + var toSelect = to.isOverlappingWith(pointerObj); + + if (fromSelect === true) { + this.selectedControlNode = from; + edge.edgeType.from = from; + } else if (toSelect === true) { + this.selectedControlNode = to; + edge.edgeType.to = to; + } + + this.body.emitter.emit('_redraw'); + } + }, { + key: '_controlNodeDrag', + + /** + * dragging the control nodes or the canvas + * @param event + * @private + */ + value: function _controlNodeDrag(event) { + this.body.emitter.emit('disablePhysics'); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); + + if (this.selectedControlNode !== undefined) { + this.selectedControlNode.x = pos.x; + this.selectedControlNode.y = pos.y; + } else { + // if the drag was not started properly because the click started outside the network div, start it now. + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + } + this.body.emitter.emit('_redraw'); + } + }, { + key: '_controlNodeDragEnd', + + /** + * connecting or restoring the control nodes. + * @param event + * @private + */ + value: function _controlNodeDragEnd(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var edge = this.body.edges[this.edgeBeingEditedId]; + + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (overlappingNodeIds[i] !== this.selectedControlNode.id) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; + } + } + + // perform the connection + if (node !== undefined && this.selectedControlNode !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + if (this.selectedControlNode.id === from.id) { + this._performEditEdge(node.id, edge.to.id); + } else { + this._performEditEdge(edge.from.id, node.id); + } + } + } else { + edge.updateEdgeType(); + this.body.emitter.emit('restorePhysics'); + } + this.body.emitter.emit('_redraw'); + } + }, { + key: '_handleConnect', + + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + value: function _handleConnect(event) { + // check to avoid double fireing of this function. + if (new Date().valueOf() - this.touchTime > 100) { + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + + var pointer = this.lastTouch; + var node = this.selectionHandler.getNodeAt(pointer); + + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + // create a node the temporary line can look at + var targetNode = this._getNewTargetNode(node.x, node.y); + this.body.nodes[targetNode.id] = targetNode; + this.body.nodeIndices.push(targetNode.id); + + // create a temporary edge + var connectionEdge = this.body.functions.createEdge({ + id: 'connectionEdge' + util.randomUUID(), + from: node.id, + to: targetNode.id, + physics: false, + smooth: { + enabled: true, + type: 'continuous', + roundness: 0.5 + } + }); + this.body.edges[connectionEdge.id] = connectionEdge; + this.body.edgeIndices.push(connectionEdge.id); + + this.temporaryIds.nodes.push(targetNode.id); + this.temporaryIds.edges.push(connectionEdge.id); + } + } + this.touchTime = new Date().valueOf(); + } + } + }, { + key: '_dragControlNode', + value: function _dragControlNode(event) { + var pointer = this.body.functions.getPointer(event.center); + if (this.temporaryIds.nodes[0] !== undefined) { + var targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode. + targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x); + targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y); + this.body.emitter.emit('_redraw'); + } else { + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + } + } + }, { + key: '_finishConnect', + + /** + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event + * @private + */ + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + + // remember the edge id + var connectFromId = undefined; + if (this.temporaryIds.edges[0] !== undefined) { + connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; + } + + // get the overlapping node but NOT the temporary node; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + // if the node id is NOT a temporary node, accept the node. + if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; + } + } + + // clean temporary nodes and edges. + this._cleanupTemporaryNodesAndEdges(); + + // perform the connection + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']); + } else { + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performAddEdge(connectFromId, node.id); + } + } + } + this.body.emitter.emit('_redraw'); + } + }, { + key: '_performAddNode', + + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + + // ------------------------------ Performing all the actual data manipulation ------------------------// + + /** + * Adds a node on the specified location + */ + value: function _performAddNode(clickData) { + var _this4 = this; + + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: 'new' + }; + + if (typeof this.options.addNode === 'function') { + if (this.options.addNode.length === 2) { + this.options.addNode(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'addNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this4.body.data.nodes.add(finalizedData); + _this4.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for add does not support two arguments (data,callback)'); + this.showManipulatorToolbar(); + } + } else { + this.body.data.nodes.add(defaultData); + this.showManipulatorToolbar(); + } + } + }, { + key: '_performAddEdge', + + /** + * connect two nodes with a new edge. + * + * @private + */ + value: function _performAddEdge(sourceNodeId, targetNodeId) { + var _this5 = this; + + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (typeof this.options.addEdge === 'function') { + if (this.options.addEdge.length === 2) { + this.options.addEdge(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this5.body.data.edges.add(finalizedData); + _this5.selectionHandler.unselectAll(); + _this5.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for connect does not support two arguments (data,callback)'); + } + } else { + this.body.data.edges.add(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } + } + }, { + key: '_performEditEdge', + + /** + * connect two nodes with a new edge. + * + * @private + */ + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this6 = this; + + var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; + if (typeof this.options.editEdge === 'function') { + if (this.options.editEdge.length === 2) { + this.options.editEdge(defaultData, function (finalizedData) { + if (finalizedData === null || finalizedData === undefined || _this6.inMode !== 'editEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this6.body.edges[defaultData.id].updateEdgeType(); + _this6.body.emitter.emit('_redraw'); + } else { + _this6.body.data.edges.update(finalizedData); + _this6.selectionHandler.unselectAll(); + _this6.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); + } + } else { + this.body.data.edges.update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } + } + }]); + + return ManipulationSystem; + })(); + + exports['default'] = ManipulationSystem; + module.exports = exports['default']; + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _sharedLabel = __webpack_require__(68); + + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + + var _nodesShapesBox = __webpack_require__(69); + + var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); + + var _nodesShapesCircle = __webpack_require__(71); + + var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); + + var _nodesShapesCircularImage = __webpack_require__(73); + + var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); + + var _nodesShapesDatabase = __webpack_require__(74); + + var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); + + var _nodesShapesDiamond = __webpack_require__(75); + + var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); + + var _nodesShapesDot = __webpack_require__(77); + + var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); + + var _nodesShapesEllipse = __webpack_require__(78); + + var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); + + var _nodesShapesIcon = __webpack_require__(79); + + var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); + + var _nodesShapesImage = __webpack_require__(80); + + var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); + + var _nodesShapesSquare = __webpack_require__(81); + + var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); + + var _nodesShapesStar = __webpack_require__(82); + + var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); + + var _nodesShapesText = __webpack_require__(83); + + var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); + + var _nodesShapesTriangle = __webpack_require__(84); + + var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); + + var _nodesShapesTriangleDown = __webpack_require__(85); + + var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); + + var _sharedValidator = __webpack_require__(54); + + var _sharedValidator2 = _interopRequireDefault(_sharedValidator); + + var util = __webpack_require__(2); + + /** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} options An object containing options for the node. All + * options are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "icon" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Network.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Network.Groups} grouplist A list with groups. Needed for + * retrieving group options + * @param {Object} constants An object with default values for + * example for the color + * + */ + + var Node = (function () { + function Node(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Node); + + this.options = util.bridgeObject(globalOptions); + this.body = body; + + this.edges = []; // all edges connected to this node + + // set defaults for the options + this.id = undefined; + this.imagelist = imagelist; + this.grouplist = grouplist; + + // state options + this.x = undefined; + this.y = undefined; + this.baseSize = this.options.size; + this.baseFontSize = this.options.font.size; + this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate + this.selected = false; + this.hover = false; + + this.labelModule = new _sharedLabel2['default'](this.body, this.options); + this.setOptions(options); + } + + _createClass(Node, [{ + key: 'attachEdge', + + /** + * Attach a edge to the node + * @param {Edge} edge + */ + value: function attachEdge(edge) { + if (this.edges.indexOf(edge) === -1) { + this.edges.push(edge); + } + } + }, { + key: 'detachEdge', + + /** + * Detach a edge from the node + * @param {Edge} edge + */ + value: function detachEdge(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + } + }, { + key: 'togglePhysics', + + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; + } + }, { + key: 'setOptions', + + /** + * Set or overwrite options for the node + * @param {Object} options an object with options + * @param {Object} constants and object with default, global options + */ + value: function setOptions(options) { + if (!options) { + return; + } + // basic options + if (options.id !== undefined) { + this.id = options.id; + } + + if (this.id === undefined) { + throw 'Node must have an id'; + } + + // set these options locally + if (options.x !== undefined) { + this.x = parseInt(options.x);this.predefinedPosition = true; + } + if (options.y !== undefined) { + this.y = parseInt(options.y);this.predefinedPosition = true; + } + if (options.size !== undefined) { + this.baseSize = options.size; + } + if (options.value !== undefined) { + options.value = parseInt(options.value); + } + + // copy group options + if (typeof options.group === 'number' || typeof options.group === 'string' && options.group != '') { + var groupObj = this.grouplist.get(options.group); + util.deepExtend(this.options, groupObj); + // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. + this.options.color = util.parseColor(this.options.color); + } + + // this transforms all shorthands into fully defined options + Node.parseOptions(this.options, options, true); + + // load the images + if (this.options.image !== undefined && this.options.image != '') { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); + } else { + throw 'No imagelist provided'; + } + } + + this.updateShape(); + this.updateLabelModule(); + + // reset the size of the node, this can be changed + this._reset(); + + if (options.hidden !== undefined || options.physics !== undefined) { + return true; + } + return false; + } + }, { + key: 'updateLabelModule', + value: function updateLabelModule() { + if (this.options.label === undefined || this.options.label === null) { + this.options.label = ''; + } + this.labelModule.setOptions(this.options, true); + if (this.labelModule.baseSize !== undefined) { + this.baseFontSize = this.labelModule.baseSize; + } + } + }, { + key: 'updateShape', + value: function updateShape() { + // choose draw method depending on the shape + switch (this.options.shape) { + case 'box': + this.shape = new _nodesShapesBox2['default'](this.options, this.body, this.labelModule); + break; + case 'circle': + this.shape = new _nodesShapesCircle2['default'](this.options, this.body, this.labelModule); + break; + case 'circularImage': + this.shape = new _nodesShapesCircularImage2['default'](this.options, this.body, this.labelModule, this.imageObj); + break; + case 'database': + this.shape = new _nodesShapesDatabase2['default'](this.options, this.body, this.labelModule); + break; + case 'diamond': + this.shape = new _nodesShapesDiamond2['default'](this.options, this.body, this.labelModule); + break; + case 'dot': + this.shape = new _nodesShapesDot2['default'](this.options, this.body, this.labelModule); + break; + case 'ellipse': + this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); + break; + case 'icon': + this.shape = new _nodesShapesIcon2['default'](this.options, this.body, this.labelModule); + break; + case 'image': + this.shape = new _nodesShapesImage2['default'](this.options, this.body, this.labelModule, this.imageObj); + break; + case 'square': + this.shape = new _nodesShapesSquare2['default'](this.options, this.body, this.labelModule); + break; + case 'star': + this.shape = new _nodesShapesStar2['default'](this.options, this.body, this.labelModule); + break; + case 'text': + this.shape = new _nodesShapesText2['default'](this.options, this.body, this.labelModule); + break; + case 'triangle': + this.shape = new _nodesShapesTriangle2['default'](this.options, this.body, this.labelModule); + break; + case 'triangleDown': + this.shape = new _nodesShapesTriangleDown2['default'](this.options, this.body, this.labelModule); + break; + default: + this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); + break; + } + this._reset(); + } + }, { + key: 'select', + + /** + * select this node + */ + value: function select() { + this.selected = true; + this._reset(); + } + }, { + key: 'unselect', + + /** + * unselect this node + */ + value: function unselect() { + this.selected = false; + this._reset(); + } + }, { + key: '_reset', + + /** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ + value: function _reset() { + this.shape.width = undefined; + this.shape.height = undefined; + } + }, { + key: 'getTitle', + + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + value: function getTitle() { + return this.options.title; + } + }, { + key: 'distanceToBorder', + + /** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ + value: function distanceToBorder(ctx, angle) { + return this.shape.distanceToBorder(ctx, angle); + } + }, { + key: 'isFixed', + + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + value: function isFixed() { + return this.options.fixed.x && this.options.fixed.y; + } + }, { + key: 'isSelected', + + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + } + }, { + key: 'getValue', + + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; + } + }, { + key: 'setValueRange', + + /** + * Adjust the value range of the node. The node will adjust it's size + * based on its value. + * @param {Number} min + * @param {Number} max + */ + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var sizeDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.size = this.options.scaling.min + scale * sizeDiff; + } else { + this.options.size = this.baseSize; + this.options.font.size = this.baseFontSize; + } + } + }, { + key: 'draw', + + /** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + } + }, { + key: 'updateBoundingBox', + + /** + * Update the bounding box of the shape + */ + value: function updateBoundingBox() { + this.shape.updateBoundingBox(this.x, this.y); + } + }, { + key: 'resize', + + /** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function resize(ctx) { + this.shape.resize(ctx); + } + }, { + key: 'isOverlappingWith', + + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isOverlappingWith(obj) { + return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; + } + }, { + key: 'isBoundingBoxOverlappingWith', + + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isBoundingBoxOverlappingWith(obj) { + return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; + } + }], [{ + key: 'parseOptions', + + /** + * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. + * Static so it can also be used by the handler. + * @param parentOptions + * @param newOptions + */ + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + + var fields = ['color', 'font', 'fixed', 'shadow']; + util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); + + // merge the shadow options into the parent. + util.mergeOptions(parentOptions, newOptions, 'shadow'); + + // individual shape newOptions + if (newOptions.color !== undefined && newOptions.color !== null) { + var parsedColor = util.parseColor(newOptions.color); + util.fillIfDefined(parentOptions.color, parsedColor); + } else if (allowDeletion === true && newOptions.color === null) { + parentOptions.color = undefined; + delete parentOptions.color; + } + + // handle the fixed options + if (newOptions.fixed !== undefined && newOptions.fixed !== null) { + if (typeof newOptions.fixed === 'boolean') { + parentOptions.fixed.x = newOptions.fixed; + parentOptions.fixed.y = newOptions.fixed; + } else { + if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { + parentOptions.fixed.x = newOptions.fixed.x; + } + if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { + parentOptions.fixed.y = newOptions.fixed.y; + } + } + } + + // handle the font options + if (newOptions.font !== undefined) { + _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); + } + + // handle the scaling options, specifically the label part + if (newOptions.scaling !== undefined) { + util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); + } + } + }]); + + return Node; + })(); + + exports['default'] = Node; + module.exports = exports['default']; + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _sharedLabel = __webpack_require__(68); + + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + + var _edgesBezierEdgeDynamic = __webpack_require__(87); + + var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); + + var _edgesBezierEdgeStatic = __webpack_require__(90); + + var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); + + var _edgesStraightEdge = __webpack_require__(91); + + var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); + + var util = __webpack_require__(2); + + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); + + if (body === undefined) { + throw 'No body provided'; + } + this.options = util.bridgeObject(globalOptions); + this.body = body; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; + + this.baseWidth = this.options.width; + this.baseFontSize = this.options.font.size; + + this.from = undefined; // a node + this.to = undefined; // a node + + this.edgeType = undefined; + + this.connected = false; + + this.labelModule = new _sharedLabel2['default'](this.body, this.options); + + this.setOptions(options); + } + + _createClass(Edge, [{ + key: 'setOptions', + + /** + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit + */ + value: function setOptions(options) { + if (!options) { + return; + } + this.colorDirty = true; + + Edge.parseOptions(this.options, options, true); + + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + options.value = parseInt(options.value); + } + + // update label Module + this.updateLabelModule(); + + var dataChanged = this.updateEdgeType(); + + // if anything has been updates, reset the selection width and the hover width + this._setInteractionWidths(); + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + if (options.hidden !== undefined || options.physics !== undefined) { + dataChanged = true; + } + + return dataChanged; + } + }, { + key: 'updateLabelModule', + + /** + * update the options in the label module + */ + value: function updateLabelModule() { + this.labelModule.setOptions(this.options, true); + if (this.labelModule.baseSize !== undefined) { + this.baseFontSize = this.labelModule.baseSize; + } + } + }, { + key: 'updateEdgeType', + + /** + * update the edge type, set the options + * @returns {boolean} + */ + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') { + changeInType = false; + } + if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') { + changeInType = false; + } + if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { + changeInType = false; + } + + if (changeInType === true) { + dataChanged = this.edgeType.cleanup(); + } + } + + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.type === 'dynamic') { + dataChanged = true; + this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); + } else { + this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); + } + } else { + // if nothing changes, we just set the options. + this.edgeType.setOptions(this.options); + } + + return dataChanged; + } + }, { + key: 'togglePhysics', + + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; + this.edgeType.togglePhysics(status); + } + }, { + key: 'connect', + + /** + * Connect an edge to its nodes + */ + value: function connect() { + this.disconnect(); - if (properties) { - extend(childP, properties); + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; + + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); + } + } + + this.edgeType.connect(); } - } + }, { + key: 'disconnect', - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + /** + * Disconnect an edge from its nodes + */ + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; + } + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; + } - /** - * let a boolean value also be a function that must return a boolean - * this first item in args will be used as the context - * @param {Boolean|Function} val - * @param {Array} [args] - * @returns {Boolean} - */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); + this.connected = false; } - return val; - } + }, { + key: 'getTitle', - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + value: function getTitle() { + return this.title; + } + }, { + key: 'isSelected', - /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + } + }, { + key: 'getValue', - /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; + } + }, { + key: 'setValueRange', - /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found - */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total + */ + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } - node = node.parentNode; + this.options.width = this.options.scaling.min + scale * widthDiff; + } else { + this.options.width = this.baseWidth; + this.options.font.size = this.baseFontSize; + } + + this._setInteractionWidths(); } - return false; - } + }, { + key: '_setInteractionWidths', + value: function _setInteractionWidths() { + if (typeof this.options.hoverWidth === 'function') { + this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); + } else { + this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; + } - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } + if (typeof this.options.selectionWidth === 'function') { + this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); + } else { + this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; + } + } + }, { + key: 'draw', - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + /** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); + } + }, { + key: 'drawArrows', + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); + } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); + } + } + }, { + key: 'drawLabel', + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); - /** - * find if a array contains the object using indexOf or a simple polyFill - * @param {Array} src - * @param {String} find - * @param {String} [findByKey] - * @return {Boolean|Number} false when not found, or the index - */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); - } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; + // if the label has to be rotated: + if (this.options.font.align !== 'horizontal') { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); + } + + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.shape.width > node1.shape.height) { + x = node1.x + node1.shape.width * 0.5; + y = node1.y - radius; + } else { + x = node1.x + radius; + y = node1.y - node1.shape.height * 0.5; + } + point = this._pointOnCircle(x, y, radius, 0.125); + this.labelModule.draw(ctx, point.x, point.y, selected); } - return -1; + } } - } + }, { + key: 'isOverlappingWith', - /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} - */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ + value: function isOverlappingWith(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - /** - * unique array with objects based on a key (like 'id') or just by the array's value - * @param {Array} src [{id:1},{id:2},{id:1}] - * @param {String} [key] - * @param {Boolean} [sort=False] - * @returns {Array} [{id:1},{id:2}] - */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); - } - values[i] = val; - i++; + return dist < distMax; + } else { + return false; + } } + }, { + key: '_rotateForLabelAlignment', - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } + /** + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); + + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; + } + + ctx.rotate(angleInDegrees); } + }, { + key: '_pointOnCircle', - return results; - } + /** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; + } + }, { + key: 'select', + value: function select() { + this.selected = true; + } + }, { + key: 'unselect', + value: function unselect() { + this.selected = false; + } + }], [{ + key: 'parseOptions', + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); + var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + // only deep extend the items in the field array. These do not have shorthand. + util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); - if (prop in obj) { - return prop; + util.mergeOptions(parentOptions, newOptions, 'smooth'); + util.mergeOptions(parentOptions, newOptions, 'shadow'); + + if (newOptions.dashes !== undefined && newOptions.dashes !== null) { + parentOptions.dashes = newOptions.dashes; + } else if (allowDeletion === true && newOptions.dashes === null) { + parentOptions.dashes = undefined; + delete parentOptions.dashes; + } + + // set the scaling newOptions + if (newOptions.scaling !== undefined && newOptions.scaling !== null) { + if (newOptions.scaling.min !== undefined) { + parentOptions.scaling.min = newOptions.scaling.min; } - i++; - } - return undefined; - } + if (newOptions.scaling.max !== undefined) { + parentOptions.scaling.max = newOptions.scaling.max; + } + util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); + } else if (allowDeletion === true && newOptions.scaling === null) { + parentOptions.scaling = undefined; + delete parentOptions.scaling; + } - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + // hanlde multiple input cases for arrows + if (newOptions.arrows !== undefined && newOptions.arrows !== null) { + if (typeof newOptions.arrows === 'string') { + var arrows = newOptions.arrows.toLowerCase(); + if (arrows.indexOf('to') != -1) { + parentOptions.arrows.to.enabled = true; + } + if (arrows.indexOf('middle') != -1) { + parentOptions.arrows.middle.enabled = true; + } + if (arrows.indexOf('from') != -1) { + parentOptions.arrows.from.enabled = true; + } + } else if (typeof newOptions.arrows === 'object') { + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); + } else { + throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); + } + } else if (allowDeletion === true && newOptions.arrows === null) { + parentOptions.arrows = undefined; + delete parentOptions.arrows; + } - /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} - */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } + // hanlde multiple input cases for color + if (newOptions.color !== undefined && newOptions.color !== null) { + if (util.isString(newOptions.color)) { + parentOptions.color.color = newOptions.color; + parentOptions.color.highlight = newOptions.color; + parentOptions.color.hover = newOptions.color; + parentOptions.color.inherit = false; + } else { + var colorsDefined = false; + if (newOptions.color.color !== undefined) { + parentOptions.color.color = newOptions.color.color;colorsDefined = true; + } + if (newOptions.color.highlight !== undefined) { + parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; + } + if (newOptions.color.hover !== undefined) { + parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; + } + if (newOptions.color.inherit !== undefined) { + parentOptions.color.inherit = newOptions.color.inherit; + } + if (newOptions.color.opacity !== undefined) { + parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); + } - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + if (newOptions.color.inherit === undefined && colorsDefined === true) { + parentOptions.color.inherit = false; + } + } + } else if (allowDeletion === true && newOptions.color === null) { + parentOptions.color = undefined; + delete parentOptions.color; + } - var SUPPORT_TOUCH = ('ontouchstart' in window); - var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; - var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); + // handle the font settings + if (newOptions.font !== undefined) { + _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); + } + } + }]); - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; + return Edge; + })(); - var COMPUTE_INTERVAL = 25; + exports['default'] = Edge; + module.exports = exports['default']; - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + 'use strict'; - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + var Hammer = __webpack_require__(13); + var util = __webpack_require__(2); + var DataSet = __webpack_require__(22); + var DataView = __webpack_require__(24); + var TimeStep = __webpack_require__(44); + var Component = __webpack_require__(35); + var Group = __webpack_require__(41); + var BackgroundGroup = __webpack_require__(45); + var BoxItem = __webpack_require__(5); + var PointItem = __webpack_require__(46); + var RangeItem = __webpack_require__(43); + var BackgroundItem = __webpack_require__(47); - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var BACKGROUND = '__background__'; // reserved group id for background items without group /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor + * An ItemSet holds a set of items and ranges which can be displayed in a + * range. The width is determined by the parent of the ItemSet, and the height + * is determined by the size of the items. + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body + * @param {Object} [options] See ItemSet.setOptions for the available options. + * @constructor ItemSet + * @extends Component */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; + function ItemSet(body, options) { + this.body = body; - // smaller wrapper around the handler, for the scope and the enabled state of the manager, - // so when disabled the input events are completely bypassed. - this.domHandler = function(ev) { - if (boolOrFn(manager.options.enable, [manager])) { - self.handler(ev); - } - }; + this.defaultOptions = { + type: null, // 'box', 'point', 'range', 'background' + orientation: { + item: 'bottom' // item orientation: 'top' or 'bottom' + }, + align: 'auto', // alignment of box items + stack: true, + groupOrder: null, - this.init(); + selectable: true, + multiselect: false, - } + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, + snap: TimeStep.snap, - /** - * bind the events - */ - init: function() { - this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + onAdd: function onAdd(item, callback) { + callback(item); + }, + onUpdate: function onUpdate(item, callback) { + callback(item); + }, + onMove: function onMove(item, callback) { + callback(item); + }, + onRemove: function onRemove(item, callback) { + callback(item); + }, + onMoving: function onMoving(item, callback) { + callback(item); }, - /** - * unbind the events - */ - destroy: function() { - this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 } - }; + }; - /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} - */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); - if (inputClass) { - Type = inputClass; - } else if (SUPPORT_POINTER_EVENTS) { - Type = PointerEventInput; - } else if (SUPPORT_ONLY_TOUCH) { - Type = TouchInput; - } else if (!SUPPORT_TOUCH) { - Type = MouseInput; - } else { - Type = TouchMouseInput; - } - return new (Type)(manager, inputHandler); - } + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: { start: 'Date', end: 'Date' } + }; - /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input - */ - function inputHandler(manager, eventType, input) { - var pointersLen = input.pointers.length; - var changedPointersLen = input.changedPointers.length; - var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); - var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); + this.conversion = { + toScreen: body.util.toScreen, + toTime: body.util.toTime + }; + this.dom = {}; + this.props = {}; + this.hammer = null; - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - if (isFirst) { - manager.session = {}; + // listeners for the DataSet of the items + this.itemListeners = { + 'add': function add(event, params, senderId) { + me._onAdd(params.items); + }, + 'update': function update(event, params, senderId) { + me._onUpdate(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemove(params.items); } + }; - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; + // listeners for the DataSet of the groups + this.groupListeners = { + 'add': function add(event, params, senderId) { + me._onAddGroups(params.items); + }, + 'update': function update(event, params, senderId) { + me._onUpdateGroups(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemoveGroups(params.items); + } + }; - // compute scale, rotation etc - computeInputData(manager, input); + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; - // emit secret event - manager.emit('hammer.input', input); + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw - manager.recognize(input); - manager.session.prevInput = input; + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM + + this._create(); + + this.setOptions(options); } + ItemSet.prototype = new Component(); + + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem + }; + /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * Create the HTML DOM for the ItemSet */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + ItemSet.prototype._create = function () { + var frame = document.createElement('div'); + frame.className = 'vis-itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } + // create background panel + var background = document.createElement('div'); + background.className = 'vis-background'; + frame.appendChild(background); + this.dom.background = background; - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'vis-foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; + + // create axis panel + var axis = document.createElement('div'); + axis.className = 'vis-axis'; + this.dom.axis = axis; + + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'vis-labelset'; + this.dom.labelSet = labelSet; + + // create ungrouped Group + this._updateUngrouped(); + + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; + + // attach event listeners + // Note: we bind to the centerContainer for the case where the height + // of the center container is larger than of the ItemSet, so we + // can click in the empty area to create a new item or deselect an item. + this.hammer = new Hammer(this.body.dom.centerContainer); + + // drag items when selected + this.hammer.on('hammer.input', (function (event) { + if (event.isFirst) { + this._onTouch(event); } + }).bind(this)); + this.hammer.on('panstart', this._onDragStart.bind(this)); + this.hammer.on('panmove', this._onDrag.bind(this)); + this.hammer.on('panend', this._onDragEnd.bind(this)); - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on('press', this._onMultiSelectItem.bind(this)); - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); + // add item on doubletap + this.hammer.on('doubletap', this._onAddItem.bind(this)); - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); + // attach to the DOM + this.show(); + }; - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + /** + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String} type + * Default type for the items. Choose from 'box' + * (default), 'point', 'range', or 'background'. + * The default style can be overwritten by + * individual items. + * {String} align + * Alignment for the items, only applicable for + * BoxItem. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation.item + * Orientation of the item set. Choose 'top' or + * 'bottom' (default). + * {Function} groupOrder + * A sorting function for ordering groups + * {Boolean} stack + * If true (default), items will be stacked on + * top of each other. + * {Number} margin.axis + * Margin between the axis and the items in pixels. + * Default is 20. + * {Number} margin.item.horizontal + * Horizontal margin between items in pixels. + * Default is 10. + * {Number} margin.item.vertical + * Vertical Margin between items in pixels. + * Default is 10. + * {Number} margin.item + * Margin between items in pixels in both horizontal + * and vertical direction. Default is 10. + * {Number} margin + * Set margin for both axis and items in pixels. + * {Boolean} selectable + * If true (default), items can be selected. + * {Boolean} multiselect + * If true, multiple items can be selected. + * False by default. + * {Boolean} editable + * Set all editable options to true or false + * {Boolean} editable.updateTime + * Allow dragging an item to an other moment in time + * {Boolean} editable.updateGroup + * Allow dragging an item to an other group + * {Boolean} editable.add + * Allow creating new items on double tap + * {Boolean} editable.remove + * Allow removing items by clicking the delete button + * top right of a selected item. + * {Function(item: Item, callback: Function)} onAdd + * Callback function triggered when an item is about to be added: + * when the user double taps an empty space in the Timeline. + * {Function(item: Item, callback: Function)} onUpdate + * Callback function fired when an item is about to be updated. + * This function typically has to show a dialog where the user + * change the item. If not implemented, nothing happens. + * {Function(item: Item, callback: Function)} onMove + * Fired when an item has been moved. If not implemented, + * the move action will be accepted. + * {Function(item: Item, callback: Function)} onRemove + * Fired when an item is about to be deleted. + * If not implemented, the item will be always removed. + */ + ItemSet.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + var fields = ['type', 'align', 'order', 'stack', 'selectable', 'multiselect', 'groupOrder', 'dataAttributes', 'template', 'hide', 'snap']; + util.selectiveExtend(fields, this.options, options); - computeIntervalInputData(session, input); + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom'; + } else if (typeof options.orientation === 'object' && 'item' in options.orientation) { + this.options.orientation.item = options.orientation.item; + } + } - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; + if ('margin' in options) { + if (typeof options.margin === 'number') { + this.options.margin.axis = options.margin; + this.options.margin.item.horizontal = options.margin; + this.options.margin.item.vertical = options.margin; + } else if (typeof options.margin === 'object') { + util.selectiveExtend(['axis'], this.options.margin, options.margin); + if ('item' in options.margin) { + if (typeof options.margin.item === 'number') { + this.options.margin.item.horizontal = options.margin.item; + this.options.margin.item.vertical = options.margin.item; + } else if (typeof options.margin.item === 'object') { + util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item); + } + } + } } - input.target = target; - } - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; + if ('editable' in options) { + if (typeof options.editable === 'boolean') { + this.options.editable.updateTime = options.editable; + this.options.editable.updateGroup = options.editable; + this.options.editable.add = options.editable; + this.options.editable.remove = options.editable; + } else if (typeof options.editable === 'object') { + util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); + } + } - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + // callback functions + var addCallback = (function (name) { + var fn = options[name]; + if (fn) { + if (!(fn instanceof Function)) { + throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); + } + this.options[name] = fn; + } + }).bind(this); + ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback); - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); + } + }; - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + /** + * Mark the ItemSet dirty so it will refresh everything with next redraw. + * Optionally, all items can be marked as dirty and be refreshed. + * @param {{refreshItems: boolean}} [options] + */ + ItemSet.prototype.markDirty = function (options) { + this.groupIds = []; + this.stackDirty = true; + + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); + } + }; /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input + * Destroy the ItemSet */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + this.hammer = null; - var v = getVelocity(deltaTime, deltaX, deltaY); - velocityX = v.x; - velocityY = v.y; - velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; - direction = getDirection(deltaX, deltaY); + this.body = null; + this.conversion = null; + }; - session.lastInterval = input; - } else { - // use latest velocity info if it doesn't overtake a minimum period - velocity = last.velocity; - velocityX = last.velocityX; - velocityY = last.velocityY; - direction = last.direction; - } + /** + * Hide the component from the DOM + */ + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } + + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } + }; /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - function simpleCloneInputData(input) { - // make a simple copy of the pointers because we will get a reference if we don't - // we only need clientXY for the calculations - var pointers = []; - var i = 0; - while (i < input.pointers.length) { - pointers[i] = { - clientX: round(input.pointers[i].clientX), - clientY: round(input.pointers[i].clientY) - }; - i++; - } + ItemSet.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); + } + + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } + }; /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {string[] | string} [ids] An array with zero or more id's of the items to be + * selected, or a single item id. If ids is undefined + * or an empty array, all items will be unselected. */ - function getCenter(pointers) { - var pointersLength = pointers.length; + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; - // no need to loop when only one touch - if (pointersLength === 1) { - return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) - }; - } + if (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); + } - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); + } + } + }; /** - * calculate the velocity between two points. unit is in px per ms. - * @param {Number} deltaTime - * @param {Number} x - * @param {Number} y - * @return {Object} velocity `x` and `y` + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); + }; /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } + ItemSet.prototype.getVisibleItems = function () { + var range = this.body.range.getRange(); + var left = this.body.util.toScreen(range.start); + var right = this.body.util.toScreen(range.end); - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - /** - * calculate the absolute distance between two points - * @param {Object} p1 {x, y} - * @param {Object} p2 {x, y} - * @param {Array} [props] containing x and y keys - * @return {Number} distance - */ - function getDistance(p1, p2, props) { - if (!props) { - props = PROPS_XY; + // filter the "raw" set with visibleItems into a set which is really + // visible by pixels + for (var i = 0; i < rawVisibleItems.length; i++) { + var item = rawVisibleItems[i]; + // TODO: also check whether visible vertically + if (item.left < right && item.left + item.width > left) { + ids.push(item.id); + } + } } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; + } - return Math.sqrt((x * x) + (y * y)); - } + return ids; + }; /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle + * Deselect a selected item + * @param {String | Number} id + * @private */ - function getAngle(p1, p2, props) { - if (!props) { - props = PROPS_XY; + ItemSet.prototype._deselect = function (id) { + var selection = this.selection; + for (var i = 0, ii = selection.length; i < ii; i++) { + if (selection[i] == id) { + // non-strict comparison! + selection.splice(i, 1); + break; } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - return Math.atan2(y, x) * 180 / Math.PI; - } + } + }; /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } + ItemSet.prototype.redraw = function () { + var margin = this.options.margin, + range = this.body.range, + asSize = util.option.asSize, + options = this.options, + orientation = options.orientation.item, + resized = false, + frame = this.dom.frame, + editable = options.editable.updateTime || options.editable.updateGroup; - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } + // recalculate absolute position (before redrawing groups) + this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; + this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; + // update class name + frame.className = 'vis-itemset' + (editable ? ' vis-editable' : ''); - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + // reorder the groups (if needed) + resized = this._orderGroups() || resized; - /** - * Mouse events input - * @constructor - * @extends Input - */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; + // check whether zoomed (in that case we need to re-stack everything) + // TODO: would be nicer to get this as a trigger from Range + var visibleInterval = range.end - range.start; + var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; + if (zoomed) this.stackDirty = true; + this.lastVisibleInterval = visibleInterval; + this.props.lastWidth = this.props.width; - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state + var restack = this.stackDirty; + var firstGroup = this._firstGroup(); + var firstMargin = { + item: margin.item, + axis: margin.axis + }; + var nonFirstMargin = { + item: margin.item, + axis: margin.item.vertical / 2 + }; + var height = 0; + var minHeight = margin.axis + margin.item.vertical; - Input.apply(this, arguments); - } + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; + // redraw all regular groups + util.forEach(this.groups, function (group) { + var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; + var groupResized = group.redraw(range, groupMargin, restack); + resized = groupResized || resized; + height += group.height; + }); + height = Math.max(height, minHeight); + this.stackDirty = false; - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } + // update frame height + frame.style.height = asSize(height); - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; + // reposition axis + this.dom.axis.style.top = asSize(orientation == 'top' ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); + this.dom.axis.style.left = '0'; + + // check if this component is resized + resized = this._isResized() || resized; + + return resized; + }; + + /** + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup + * @private + */ + ItemSet.prototype._firstGroup = function () { + var firstGroupIndex = this.options.orientation.item == 'top' ? 0 : this.groupIds.length - 1; + var firstGroupId = this.groupIds[firstGroupIndex]; + var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + + return firstGroup || null; + }; + + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected + */ + ItemSet.prototype._updateUngrouped = function () { + var ungrouped = this.groups[UNGROUPED]; + var background = this.groups[BACKGROUND]; + var item, itemId; + + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; + + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + item.parent && item.parent.remove(item); + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + group && group.add(item) || item.hide(); } + } + } + } else { + // create a group holding all (unfiltered) items + if (!ungrouped) { + var id = null; + var data = null; + ungrouped = new Group(id, data, this); + this.groups[UNGROUPED] = ungrouped; - if (eventType & INPUT_END) { - this.pressed = false; + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + ungrouped.add(item); } + } - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); + ungrouped.show(); } - }); - - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL + } }; - // in IE10 the pointer types is defined as an enum - var IE10_POINTER_TYPE_ENUM = { - 2: INPUT_TYPE_TOUCH, - 3: INPUT_TYPE_PEN, - 4: INPUT_TYPE_MOUSE, - 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + /** + * Get the element for the labelset + * @return {HTMLElement} labelSet + */ + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; }; - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } - /** - * Pointer events input - * @constructor - * @extends Input + * Set items + * @param {vis.DataSet | null} items */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; + ItemSet.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - Input.apply(this, arguments); + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - this.store = (this.manager.session.pointerEvents = []); - } + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - var isTouch = (pointerType == INPUT_TYPE_TOUCH); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + // update the group holding all ungrouped items + this._updateUngrouped(); + } + }; - // start and mouse must be down - if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { - if (storeIndex < 0) { - store.push(ev); - storeIndex = store.length - 1; - } - } else if (eventType & (INPUT_END | INPUT_CANCEL)) { - removePointer = true; - } + /** + * Get the current items + * @returns {vis.DataSet | null} + */ + ItemSet.prototype.getItems = function () { + return this.itemsData; + }; - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; + + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.off(event, callback); + }); + + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } + + // replace the dataset + if (!groups) { + this.groupsData = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + this.groupsData = groups; + } else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } + + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); + + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } - // update the event in the store - store[storeIndex] = ev; + // update the group holding all ungrouped items + this._updateUngrouped(); - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + // update the order of all items in each group + this._order(); - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); - } - } - }); + this.body.emitter.emit('change', { queue: true }); + }; - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function () { + return this.groupsData; }; - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; + /** + * Remove an item by its id + * @param {String | Number} id + */ + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); + + if (item) { + // confirm deletion + this.options.onRemove(item, function (item) { + if (item) { + // remove by id here, it is possible that an item has no id defined + // itself, so better not delete by the item itself + dataset.remove(id); + } + }); + } + }; /** - * Touch events input - * @constructor - * @extends Input + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type + * @private */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + }; - Input.apply(this, arguments); - } + /** + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId + * @private + */ + ItemSet.prototype._getGroupId = function (itemData) { + var type = this._getType(itemData); + if (type == 'background' && itemData.group == undefined) { + return BACKGROUND; + } else { + return this.groupsData ? itemData.group : UNGROUPED; + } + }; - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + /** + * Handle updated items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } + ids.forEach((function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - if (!this.started) { - return; - } + var constructor = ItemSet.types[type]; + var selected; - var touches = normalizeSingleTouches.call(this, ev, type); + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, delete the item and recreate it + selected = item.selected; // preserve selection of this item + me._removeItem(item); + item = null; + } else { + me._updateItem(item, itemData); + } + } - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; + if (!item) { + // create item + if (constructor) { + item = new constructor(itemData, me.conversion, me.options); + item.id = id; // TODO: not so nice setting id afterwards + me._addItem(item); + if (selected) { + this.selection.push(id); + item.select(); } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + } else if (type == 'rangeoverflow') { + // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day + throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' + '.vis-item.vis-range .vis-item-content {overflow: visible;}'); + } else { + throw new TypeError('Unknown item type "' + type + '"'); + } } - }); + }).bind(this)); + + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * Handle added items + * @param {Number[]} ids + * @protected */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); + /** + * Handle removed items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onRemove = function (ids) { + var count = 0; + var me = this; + ids.forEach(function (id) { + var item = me.items[id]; + if (item) { + count++; + me._removeItem(item); } + }); - return [all, changed]; - } - - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); + } }; - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - /** - * Multi-user touch events input - * @constructor - * @extends Input + * Update the order of item in all groups + * @private */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; - - Input.apply(this, arguments); - } - - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + ItemSet.prototype._order = function () { + // reorder the items in all groups + // TODO: optimization: only reorder groups affected by the changed items + util.forEach(this.groups, function (group) { + group.order(); + }); + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * Handle updated groups + * @param {Number[]} ids + * @private */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - // when there is only one touch, the process can be simplified - if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { - targetIds[allTouches[0].identifier] = true; - return [allTouches, allTouches]; - } + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); - // filter changed touches to only contain touches that exist in the collected target ids - i = 0; - while (i < changedTouches.length) { - if (targetIds[changedTouches[i].identifier]) { - changedTargetTouches.push(changedTouches[i]); - } + group = new Group(id, groupData, me); + me.groups[id] = group; - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; + // add items with this groupId to the new group + for (var itemId in me.items) { + if (me.items.hasOwnProperty(itemId)) { + var item = me.items[itemId]; + if (item.data.group == id) { + group.add(item); + } } - i++; - } + } - if (!changedTargetTouches.length) { - return; + group.order(); + group.show(); + } else { + // update group + group.setData(groupData); } + }); - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } + this.body.emitter.emit('change', { queue: true }); + }; /** - * Combined touch and mouse input - * - * Touch has a higher priority then mouse, and while touching no mouse events are allowed. - * This because touch devices also emit mouse events while doing a touch. - * - * @constructor - * @extends Input + * Handle removed groups + * @param {Number[]} ids + * @private */ - function TouchMouseInput() { - Input.apply(this, arguments); - - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } - - inherit(TouchMouseInput, Input, { - /** - * handle mouse and touch events - * @param {Hammer} manager - * @param {String} inputEvent - * @param {Object} inputData - */ - handler: function TMEhandler(manager, inputEvent, inputData) { - var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), - isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - - // when we're in a touch event, so block all upcoming mouse events - // most mobile browser also emit mouseevents, right after touchstart - if (isTouch) { - this.mouse.allow = false; - } else if (isMouse && !this.mouse.allow) { - return; - } - - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } - - this.callback(manager, inputEvent, inputData); - }, + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); + if (group) { + group.hide(); + delete groups[id]; } - }); + }); - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + this.markDirty(); - // magical touchAction value - var TOUCH_ACTION_COMPUTE = 'compute'; - var TOUCH_ACTION_AUTO = 'auto'; - var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented - var TOUCH_ACTION_NONE = 'none'; - var TOUCH_ACTION_PAN_X = 'pan-x'; - var TOUCH_ACTION_PAN_Y = 'pan-y'; + this.body.emitter.emit('change', { queue: true }); + }; /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor + * Reorder the groups if needed + * @return {boolean} changed + * @private */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); - TouchAction.prototype = { - /** - * set the touchAction value on the element or enable the polyfill - * @param {String} value - */ - set: function(value) { - // find out the touch-action by the event handlers - if (value == TOUCH_ACTION_COMPUTE) { - value = this.compute(); - } + var changed = !util.equalArray(groupIds, this.groupIds); + if (changed) { + // hide all groups, removes them from the DOM + var groups = this.groups; + groupIds.forEach(function (groupId) { + groups[groupId].hide(); + }); - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; - } - this.actions = value.toLowerCase().trim(); - }, + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, + this.groupIds = groupIds; + } - /** - * compute the value for the touchAction property based on the recognizer's settings - * @returns {String} value - */ - compute: function() { - var actions = []; - each(this.manager.recognizers, function(recognizer) { - if (boolOrFn(recognizer.options.enable, [recognizer])) { - actions = actions.concat(recognizer.getTouchAction()); - } - }); - return cleanTouchActions(actions.join(' ')); - }, + return changed; + } else { + return false; + } + }; - /** - * this method is called on each input cycle and provides the preventing of the browser behavior - * @param {Object} input - */ - preventDefaults: function(input) { - // not needed with native support for the touchAction property - if (NATIVE_TOUCH_ACTION) { - return; - } + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + }; - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } + /** + * Update an existing item + * @param {Item} item + * @param {Object} itemData + * @private + */ + ItemSet.prototype._updateItem = function (item, itemData) { + var oldGroupId = item.data.group; + var oldSubGroupId = item.data.subgroup; - var actions = this.actions; - var hasNone = inStr(actions, TOUCH_ACTION_NONE); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + // update the items data (will redraw the item when displayed) + item.setData(itemData); - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); - } - }, + // update group + if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - /** - * call preventDefault to prevent the browser's default behavior (scrolling in most cases) - * @param {Object} srcEvent - */ - preventSrc: function(srcEvent) { - this.manager.session.prevented = true; - srcEvent.preventDefault(); - } + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + } }; /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} + * Delete an item from the ItemSet: remove it from the DOM, from the map + * with items, and from the map with visible items, and from the selection + * @param {Item} item + * @private */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } + ItemSet.prototype._removeItem = function (item) { + // remove from DOM + item.hide(); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + // remove from items + delete this.items[item.id]; - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; - } + // remove from group + item.parent && item.parent.remove(item); + }; - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } + /** + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private + */ + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - return TOUCH_ACTION_AUTO; - } + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); + } + } + return endArray; + }; /** - * Recognizer flow explained; * - * All recognizers have the initial state of POSSIBLE when a input session starts. - * The definition of a input session is from the first input until the last input, with all it's movement in it. * - * Example session for mouse-input: mousedown -> mousemove -> mouseup - * - * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed - * which determines with state it should be. + * Register the clicked item on touch, before dragStart is initiated. * - * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to - * POSSIBLE to give it another change on the next cycle. + * dragStart is initiated from a mousemove event, AFTER the mouse/touch is + * already moving. Therefore, the mouse/touch can sometimes be above an other + * DOM element than the item itself. * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized + * @param {Event} event + * @private */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; + ItemSet.prototype._onTouch = function (event) { + // store the touched item, used in _onDragStart + this.touchParams.item = this.itemFromTarget(event); + this.touchParams.dragLeftItem = event.target.dragLeftItem || false; + this.touchParams.dragRightItem = event.target.dragRightItem || false; + this.touchParams.itemProps = null; + }; /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options + * Start dragging the selected events + * @param {Event} event + * @private */ - function Recognizer(options) { - this.id = uniqueId(); - - this.manager = null; - this.options = merge(options || {}, this.defaults); - - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); - - this.state = STATE_POSSIBLE; + ItemSet.prototype._onDragStart = function (event) { + if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + return; + } - this.simultaneous = {}; - this.requireFail = []; - } + var item = this.touchParams.item || null; + var me = this; + var props; - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, + if (item && item.selected) { + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x, + dragLeft: true, + data: util.extend({}, item.data) // clone the items data + }; - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, + this.touchParams.itemProps = [props]; + } else if (dragRightItem) { + props = { + item: dragRightItem, + initialX: event.center.x, + dragRight: true, + data: util.extend({}, item.data) // clone the items data + }; - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; - } + this.touchParams.itemProps = [props]; + } else { + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item, + initialX: event.center.x, + data: util.extend({}, item.data) // clone the items data + }; - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); - } - return this; - }, + return props; + }); + } - /** - * drop the simultaneous link. it doesnt remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRecognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { - return this; - } + event.stopPropagation(); + } else if (this.options.editable.add && (event.srcEvent.ctrlKey || event.srcEvent.metaKey)) { + // create a new range item when dragging with ctrl key down + this._onDragStartAddItem(event); + } + }; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, + /** + * Start creating a new range item by dragging. + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStartAddItem = function (event) { + var snap = this.options.snap || null; + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px + var time = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var start = snap ? snap(time, scale, step) : start; + var end = start; - /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } + var itemData = { + type: 'range', + start: start, + end: end, + content: 'new item' + }; - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, + var id = util.randomUUID(); + itemData[this.itemsData._fieldId] = id; - /** - * drop the requireFailure link. it does not remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRequireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { - return this; - } + var group = this.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, + var newItem = new RangeItem(itemData, this.conversion, this.options); + newItem.id = id; // TODO: not so nice setting id afterwards + newItem.data = itemData; + this._addItem(newItem); - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, + var props = { + item: newItem, + dragRight: true, + initialX: event.center.x, + data: util.extend({}, itemData) + }; + this.touchParams.itemProps = [props]; - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, + event.stopPropagation(); + }; - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; + /** + * Drag selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } + var me = this; + var snap = this.options.snap || null; + var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } + // move + this.touchParams.itemProps.forEach(function (props) { + var newProps = {}; + var current = me.body.util.toTime(event.center.x - xOffset); + var initial = me.body.util.toTime(props.initialX - xOffset); + var offset = current - initial; - emit(); // simple 'eventName' events + var itemData = util.extend({}, props.item.data); // clone the data - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, + if (me.options.editable.updateTime) { + if (props.dragLeft) { + // drag left side of a range item + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date'); + var start = new Date(initialStart.valueOf() + offset); + itemData.start = snap ? snap(start, scale, step) : start; + } + } else if (props.dragRight) { + // drag right side of a range item + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var end = new Date(initialEnd.valueOf() + offset); + itemData.end = snap ? snap(end, scale, step) : end; + } + } else { + // drag both start and end + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date').valueOf(); + var start = new Date(initialStart + offset); - /** - * Check that all the require failure recognizers has failed, - * if true, it emits a gesture event, - * otherwise, setup the state to FAILED. - * @param {Object} input - */ - tryEmit: function(input) { - if (this.canEmit()) { - return this.emit(input); - } - // it's failing anyway - this.state = STATE_FAILED; - }, + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var duration = initialEnd.valueOf() - initialStart.valueOf(); - /** - * can we emit? - * @returns {boolean} - */ - canEmit: function() { - var i = 0; - while (i < this.requireFail.length) { - if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { - return false; + itemData.start = snap ? snap(start, scale, step) : start; + itemData.end = new Date(itemData.start.valueOf() + duration); + } else { + itemData.start = snap ? snap(start, scale, step) : start; } - i++; - } - return true; - }, - - /** - * update the recognizer - * @param {Object} inputData - */ - recognize: function(inputData) { - // make a new copy of the inputData - // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); - - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; + } } + } - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; + if (me.options.editable.updateGroup && (!props.dragLeft && !props.dragRight)) { + if (itemData.group != undefined) { + // drag from one group to another + var group = me.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } } + } - this.state = this.process(inputDataClone); - - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); + // confirm moving the item + me.options.onMoving(itemData, function (itemData) { + if (itemData) { + props.item.setData(itemData); } - }, - - /** - * return the state of the recognizer - * the actual recognizing happens in this method - * @virtual - * @param {Object} inputData - * @returns {Const} STATE - */ - process: function(inputData) { }, // jshint ignore:line - - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, + }); + }); - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + } }; /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId + * @private */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; - } - return ''; - } + ItemSet.prototype._moveToGroup = function (item, groupId) { + var group = this.groups[groupId]; + if (group && group.groupId != item.data.group) { + var oldGroup = item.parent; + oldGroup.remove(item); + oldGroup.order(); + group.add(item); + group.order(); - /** - * direction cons to string - * @param {Const} direction - * @returns {String} - */ - function directionStr(direction) { - if (direction == DIRECTION_DOWN) { - return 'down'; - } else if (direction == DIRECTION_UP) { - return 'up'; - } else if (direction == DIRECTION_LEFT) { - return 'left'; - } else if (direction == DIRECTION_RIGHT) { - return 'right'; - } - return ''; - } + item.data.group = group.groupId; + } + }; /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} + * End of dragging selected items + * @param {Event} event + * @private */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; - } + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer - */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } + // prepare a change set for the changed items + var changes = []; + var me = this; + var dataset = this.itemsData.getDataSet(); - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + var itemProps = this.touchParams.itemProps; + this.touchParams.itemProps = null; + itemProps.forEach(function (props) { + var id = props.item.id; + var exists = me.itemsData.get(id, me.itemOptions) != null; - /** - * Used to check if it the recognizer receives valid input, like input.distance > 10. - * @memberof AttrRecognizer - * @param {Object} input - * @returns {Boolean} recognized - */ - attrTest: function(input) { - var optionPointers = this.options.pointers; - return optionPointers === 0 || input.pointers.length === optionPointers; - }, + if (!exists) { + // add a new item + me.options.onAdd(props.item.data, function (itemData) { + me._removeItem(props.item); // remove temporary item + if (itemData) { + me.itemsData.getDataSet().add(itemData); + } - /** - * Process the input and return the state for the recognizer - * @memberof AttrRecognizer - * @param {Object} input - * @returns {*} State - */ - process: function(input) { - var state = this.state; - var eventType = input.eventType; + // force re-stacking of all items next redraw + me.stackDirty = true; + me.body.emitter.emit('change'); + }); + } else { + // update existing item + var itemData = util.extend({}, props.item.data); // clone the data + me.options.onMove(itemData, function (itemData) { + if (itemData) { + // apply changes + itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) + changes.push(itemData); + } else { + // restore original values + props.item.setData(props.data); - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit('change'); + } + }); + } + }); - // on cancel input and we've recognized before, return STATE_CANCELLED - if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { - return state | STATE_CANCELLED; - } else if (isRecognized || isValid) { - if (eventType & INPUT_END) { - return state | STATE_ENDED; - } else if (!(state & STATE_BEGAN)) { - return STATE_BEGAN; - } - return state | STATE_CHANGED; - } - return STATE_FAILED; + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); } - }); + } + }; /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - this.pX = null; - this.pY = null; - } + var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); + var shiftKey = event.srcEvent && event.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; + } - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + var oldSelection = this.getSelection(); - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); - } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); - } - return actions; - }, + var item = this.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); - directionTest: function(input) { - var options = this.options; - var hasMoved = true; - var distance = input.distance; - var direction = input.direction; - var x = input.deltaX; - var y = input.deltaY; + var newSelection = this.getSelection(); - // lock to axis? - if (!(direction & options.direction)) { - if (options.direction & DIRECTION_HORIZONTAL) { - direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; - hasMoved = x != this.pX; - distance = Math.abs(input.deltaX); - } else { - direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; - hasMoved = y != this.pY; - distance = Math.abs(input.deltaY); - } - } - input.direction = direction; - return hasMoved && distance > options.threshold && direction & options.direction; - }, + // emit a select event, + // except when old selection is empty and new selection is still empty + if (newSelection.length > 0 || oldSelection.length > 0) { + this.body.emitter.emit('select', { + items: newSelection + }); + } + }; - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + /** + * Handle creation and updates of an item on double tap + * @param event + * @private + */ + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + var me = this; + var snap = this.options.snap || null; + var item = this.itemFromTarget(event); - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + event.stopPropagation(); - this._super.emit.call(this, input); - } - }); + if (item) { + // update item - /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer - */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } + // execute async handler to update the item (or cancel it) + var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset + this.options.onUpdate(itemData, function (itemData) { + if (itemData) { + me.itemsData.getDataSet().update(itemData); + } + }); + } else { + // add item + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs; + var start = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: 'new item' + }; - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + // when default type is a range, add a default end date to the new item + if (this.options.type === 'range') { + var end = this.body.util.toTime(x + this.props.width / 5); + newItem.end = snap ? snap(end, scale, step) : end; + } - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + newItem[this.itemsData._fieldId] = util.randomUUID(); - emit: function(input) { - this._super.emit.call(this, input); - if (input.scale !== 1) { - var inOut = input.scale < 1 ? 'in' : 'out'; - this.manager.emit(this.options.event + inOut, input); - } + var group = this.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; } - }); + + // execute async handler to customize (or cancel) adding an item + this.options.onAdd(newItem, function (item) { + if (item) { + me.itemsData.getDataSet().add(item); + // TODO: need to trigger a redraw? + } + }); + } + }; /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private */ - function PressRecognizer() { - Recognizer.apply(this, arguments); - - this._timer = null; - this._input = null; - } + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; - inherit(PressRecognizer, Recognizer, { - /** - * @namespace - * @memberof PressRecognizer - */ - defaults: { - event: 'press', - pointers: 1, - time: 500, // minimal time of the pointer to be pressed - threshold: 5 // a minimal movement is ok, but keep it low - }, + var item = this.itemFromTarget(event); - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, + if (item) { + // multi select items (if allowed) - process: function(input) { - var options = this.options; - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTime = input.deltaTime > options.time; + var selection = this.options.multiselect ? this.getSelection() // take current selection + : []; // deselect current selection - this._input = input; + var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { - this.reset(); - } else if (input.eventType & INPUT_START) { - this.reset(); - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.time, this); - } else if (input.eventType & INPUT_END) { - return STATE_RECOGNIZED; - } - return STATE_FAILED; - }, + if (shiftKey && this.options.multiselect) { + // select all items between the old selection and the tapped item - reset: function() { - clearTimeout(this._timer); - }, + // determine the selection range + selection.push(item.id); + var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + // select all items within the selection range + selection = []; + for (var id in this.items) { + if (this.items.hasOwnProperty(id)) { + var _item = this.items[id]; + var start = _item.data.start; + var end = _item.data.end !== undefined ? _item.data.end : start; - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); - } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); + if (start >= range.min && end <= range.max && !(_item instanceof BackgroundItem)) { + selection.push(_item.id); // do not use id but item.id, id itself is stringified + } } + } + } else { + // add/remove this item from the current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } else { + // item is already selected -> deselect it + selection.splice(index, 1); + } } - }); - /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer - */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, - - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + this.setSelection(selection); - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } - }); + this.body.emitter.emit('select', { + items: this.getSelection() + }); + } + }; /** - * Swipe - * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. - * @constructor - * @extends AttrRecognizer + * Calculate the time range of a list of items + * @param {Array.} itemsData + * @return {{min: Date, max: Date}} Returns the range of the provided items + * @private */ - function SwipeRecognizer() { - AttrRecognizer.apply(this, arguments); - } + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; + } + } + }); - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + return { + min: min, + max: max + }; + }; - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; - } + /** + * Find an item from an event target: + * searches for the attribute 'timeline-item' in the event target's element tree + * @param {Event} event + * @return {Item | null} item + */ + ItemSet.prototype.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, + return null; + }; - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + /** + * Find the Group from an event target: + * searches for the attribute 'timeline-group' in the event target's element tree + * @param {Event} event + * @return {Group | null} group + */ + ItemSet.prototype.groupFromTarget = function (event) { + var clientY = event.center ? event.center.y : event.clientY; + for (var i = 0; i < this.groupIds.length; i++) { + var groupId = this.groupIds[i]; + var group = this.groups[groupId]; + var foreground = group.dom.foreground; + var top = util.getAbsoluteTop(foreground); + if (clientY > top && clientY < top + foreground.offsetHeight) { + return group; + } - this.manager.emit(this.options.event, input); + if (this.options.orientation.item === 'top') { + if (i === this.groupIds.length - 1 && clientY > top) { + return group; + } + } else { + if (i === 0 && clientY < top + foreground.offset) { + return group; + } } - }); + } + + return null; + }; /** - * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur - * between the given interval and position. The delay option can be used to recognize multi-taps without firing - * a single tap. - * - * The eventData from the emitted event contains the property `tapCount`, which contains the amount of - * multi-taps being recognized. - * @constructor - * @extends Recognizer + * Find the ItemSet from an event target: + * searches for the attribute 'timeline-itemset' in the event target's element tree + * @param {Event} event + * @return {ItemSet | null} item */ - function TapRecognizer() { - Recognizer.apply(this, arguments); + ItemSet.itemSetFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; + } + target = target.parentNode; + } - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + return null; + }; - this._timer = null; - this._input = null; - this.count = 0; - } + module.exports = ItemSet; - inherit(TapRecognizer, Recognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'tap', - pointers: 1, - taps: 1, - interval: 300, // max time between the multi-tap taps - time: 250, // max time of the pointer to be down (like finger on the screen) - threshold: 2, // a minimal movement is ok, but keep it low - posThreshold: 10 // a multi-tap can be a bit off the initial position - }, +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + 'use strict'; - process: function(input) { - var options = this.options; + var Hammer = __webpack_require__(13); + var util = __webpack_require__(2); - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + /** + * @constructor Item + * @param {Object} data Object containing (optional) parameters type, + * start, end, content, group, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} options Configuration options + * // TODO: describe available options + */ + function Item(data, conversion, options) { + this.id = null; + this.parent = null; + this.data = data; + this.dom = null; + this.conversion = conversion || {}; + this.options = options || {}; - this.reset(); + this.selected = false; + this.displayed = false; + this.dirty = true; - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } + this.top = null; + this.left = null; + this.width = null; + this.height = null; + } - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (validMovement && validTouchTime && validPointers) { - if (input.eventType != INPUT_END) { - return this.failTimeout(); - } + Item.prototype.stack = true; - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + /** + * Select current item + */ + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - this.pTime = input.timeStamp; - this.pCenter = input.center; + /** + * Unselect current item + */ + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + /** + * Set data for the item. Existing data will be updated. The id should not + * be changed. When the item is displayed, it will be redrawn immediately. + * @param {Object} data + */ + Item.prototype.setData = function (data) { + var groupChanged = data.group != undefined && this.data.group != data.group; + if (groupChanged) { + this.parent.itemSet._moveToGroup(this, data.group); + } - this._input = input; + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - // if tap count matches we have recognized it, - // else it has began recognizing... - var tapCount = this.count % options.taps; - if (tapCount === 0) { - // no failing requirements, immediately trigger the tap event - // or wait as long as the multitap interval to trigger - if (!this.hasRequireFailures()) { - return STATE_RECOGNIZED; - } else { - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.interval, this); - return STATE_BEGAN; - } - } - } - return STATE_FAILED; - }, + /** + * Set a parent for the item + * @param {ItemSet | Group} parent + */ + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); + } + } else { + this.parent = parent; + } + }; - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; + }; - reset: function() { - clearTimeout(this._timer); - }, + /** + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed + */ + Item.prototype.show = function () { + return false; + }; - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); - } - } - }); + /** + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed + */ + Item.prototype.hide = function () { + return false; + }; /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor + * Repaint the item */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } + Item.prototype.redraw = function () {}; /** - * @const {string} + * Reposition the Item horizontally */ - Hammer.VERSION = '2.0.4'; + Item.prototype.repositionX = function () {}; /** - * default settings - * @namespace + * Reposition the Item vertically */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, + Item.prototype.repositionY = function () {}; - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, + /** + * Repaint a delete button on the top right of the item when the item is selected + * @param {HTMLElement} anchor + * @protected + */ + Item.prototype._repaintDeleteButton = function (anchor) { + if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { + // create and show button + var me = this; - /** - * @type {Boolean} - * @default true - */ - enable: true, + var deleteButton = document.createElement('div'); + deleteButton.className = 'vis-delete'; + deleteButton.title = 'Delete this item'; - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on('tap', function (event) { + event.stopPropagation(); + me.parent.removeFromDataSet(me); + }); - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + anchor.appendChild(deleteButton); + this.dom.deleteButton = deleteButton; + } else if (!this.selected && this.dom.deleteButton) { + // remove button + if (this.dom.deleteButton.parentNode) { + this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); + } + this.dom.deleteButton = null; + } + }; - /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} - */ - preset: [ - // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] - [RotateRecognizer, { enable: false }], - [PinchRecognizer, { enable: false }, ['rotate']], - [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], - [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], - [TapRecognizer], - [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], - [PressRecognizer] - ], + /** + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private + */ + Item.prototype._updateContents = function (element) { + var content; + if (this.options.template) { + var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset + content = this.options.template(itemData); + } else { + content = this.data.content; + } - /** - * Some CSS properties can be used to improve the working of Hammer. - * Add them to this method and they will be set when creating a new Manager. - * @namespace - */ - cssProps: { - /** - * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userSelect: 'none', + var changed = this._contentToString(this.content) !== this._contentToString(content); + if (changed) { + // only replace the content when changed + if (content instanceof Element) { + element.innerHTML = ''; + element.appendChild(content); + } else if (content != undefined) { + element.innerHTML = content; + } else { + if (!(this.data.type == 'background' && this.data.content === undefined)) { + throw new Error('Property "content" missing in item ' + this.id); + } + } - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + this.content = content; + } + }; - /** - * Disables the default callout shown when you touch and hold a touch target. - * On iOS, when you touch and hold a touch target such as a link, Safari displays - * a callout containing information about the link. This property allows you to disable that callout. - * @type {String} - * @default 'none' - */ - touchCallout: 'none', + /** + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private + */ + Item.prototype._updateTitle = function (element) { + if (this.data.title != null) { + element.title = this.data.title || ''; + } else { + element.removeAttribute('vis-title'); + } + }; - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + /** + * Process dataAttributes timeline option and set as data- attributes on dom.content + * @param {Element} element HTML element to which the attributes will be attached + * @private + */ + Item.prototype._updateDataAttributes = function (element) { + if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { + var attributes = []; - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == 'all') { + attributes = Object.keys(this.data); + } else { + return; + } - /** - * Overrides the highlight color shown when the user taps a link or a JavaScript - * clickable element in iOS. This property obeys the alpha value, if specified. - * @type {String} - * @default 'rgba(0,0,0,0)' - */ - tapHighlightColor: 'rgba(0,0,0,0)' + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; + + if (value != null) { + element.setAttribute('data-' + name, value); + } else { + element.removeAttribute('data-' + name); + } } + } }; - var STOP = 1; - var FORCED_STOP = 2; - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor + * Update custom styles of the element + * @param element + * @private */ - function Manager(element, options) { - options = options || {}; + Item.prototype._updateStyle = function (element) { + // remove old styles + if (this.style) { + util.removeCssText(element, this.style); + this.style = null; + } - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } + }; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + /** + * Stringify the items contents + * @param {string | Element | undefined} content + * @returns {string | undefined} + * @private + */ + Item.prototype._contentToString = function (content) { + if (typeof content === 'string') return content; + if (content && 'outerHTML' in content) return content.outerHTML; + return content; + }; - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + module.exports = Item; - toggleCssProps(this, true); + // should be implemented by the item - each(options.recognizers, function(item) { - var recognizer = this.add(new (item[0])(item[1])); - item[2] && recognizer.recognizeWith(item[2]); - item[3] && recognizer.requireFailure(item[3]); - }, this); - } + // should be implemented by the item - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + // should be implemented by the item - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); - } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); - } - return this; - }, +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; - /** - * stop recognizing for this session. - * This session will be discarded, when a new [input]start event is fired. - * When forced, the recognizer cycle is stopped immediately. - * @param {Boolean} [force] - */ - stop: function(force) { - this.session.stopped = force ? FORCED_STOP : STOP; - }, + Object.defineProperty(exports, '__esModule', { + value: true + }); - /** - * run the recognizers! - * called by the inputHandler function on every movement of the pointers (touches) - * it walks through all the recognizers and tries to detect the gesture that is being made - * @param {Object} inputData - */ - recognize: function(inputData) { - var session = this.session; - if (session.stopped) { - return; - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - var recognizer; - var recognizers = this.recognizers; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - // this holds the recognizer that is being recognized. - // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED - // if no recognizer is detecting a thing, it is set to `null` - var curRecognizer = session.curRecognizer; + var _componentsNavigationHandler = __webpack_require__(105); - // reset when the last recognizer is recognized - // or when we're in a new session - if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { - curRecognizer = session.curRecognizer = null; - } + var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + var _componentsPopup = __webpack_require__(106); - // find out if we are allowed try to recognize the input for this one. - // 1. allow if the session is NOT forced stopped (see the .stop() method) - // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one - // that is being recognized. - // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. - // this can be setup with the `recognizeWith()` method on the recognizer. - if (session.stopped !== FORCED_STOP && ( // 1 - !curRecognizer || recognizer == curRecognizer || // 2 - recognizer.canRecognizeWith(curRecognizer))) { // 3 - recognizer.recognize(inputData); - } else { - recognizer.reset(); - } + var _componentsPopup2 = _interopRequireDefault(_componentsPopup); - // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the - // current active recognizer. but only if we don't already have an active recognizer - if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { - curRecognizer = session.curRecognizer = recognizer; - } - i++; - } - }, + var util = __webpack_require__(2); - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; - }, + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + this.navigationHandler = new _componentsNavigationHandler2['default'](body, canvas); - /** - * add a recognizer to the manager - * existing recognizers with the same event name will be removed - * @param {Recognizer} recognizer - * @returns {Recognizer|Manager} - */ - add: function(recognizer) { - if (invokeArrayArg(recognizer, 'add', this)) { - return this; - } + // bind the events from hammer to functions in this object + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); + this.body.eventListeners.onContext = this.onContext.bind(this); - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.popup = undefined; + this.popupObj = undefined; + this.popupTimer = undefined; - this.recognizers.push(recognizer); - recognizer.manager = this; + this.body.functions.getPointer = this.getPointer.bind(this); - this.touchAction.update(); - return recognizer; - }, + this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + hover: false, + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true + }, + navigationButtons: false, + tooltipDelay: 300, + zoomView: true + }; + util.extend(this.options, this.defaultOptions); - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } + this.bindEventListeners(); + } - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); + _createClass(InteractionHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this = this; - this.touchAction.update(); - return this; - }, + this.body.emitter.on('destroy', function () { + clearTimeout(_this.popupTimer); + delete _this.body.functions.getPointer; + }); + } + }, { + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + // extend all but the values in fields + var fields = ['hideEdgesOnDrag', 'hideNodesOnDrag', 'keyboard', 'multiselect', 'selectable', 'selectConnectedEdges']; + util.selectiveNotDeepExtend(fields, this.options, options); + + // merge the keyboard options in. + util.mergeOptions(this.options, options, 'keyboard'); + + if (options.tooltip) { + util.extend(this.options.tooltip, options.tooltip); + if (options.tooltip.color) { + this.options.tooltip.color = util.parseColor(options.tooltip.color); + } + } + } + + this.navigationHandler.setOptions(this.options); + } + }, { + key: 'getPointer', /** - * bind event - * @param {String} events - * @param {Function} handler - * @returns {EventEmitter} this + * Get the pointer location from a touch location + * @param {{x: Number, y: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private */ - on: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - handlers[event] = handlers[event] || []; - handlers[event].push(handler); - }); - return this; - }, + value: function getPointer(touch) { + return { + x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + } + }, { + key: 'onTouch', /** - * unbind event, leave emit blank to remove all handlers - * @param {String} events - * @param {Function} [handler] - * @returns {EventEmitter} this + * On start of a touch gesture, store the pointer + * @param event + * @private */ - off: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - if (!handler) { - delete handlers[event]; - } else { - handlers[event].splice(inArray(handlers[event], handler), 1); - } - }); - return this; - }, + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 50) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + } + }, { + key: 'onTap', /** - * emit event to the listeners - * @param {String} event - * @param {Object} data + * handle tap/click event: select/unselect a node + * @private */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); - } - - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; - } - - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + value: function onTap(event) { + var pointer = this.getPointer(event.center); + var multiselect = this.selectionHandler.options.multiselect && (event.changedPointers[0].ctrlKey || event.changedPointers[0].metaKey); - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; - } - }, + this.checkSelectionChanges(pointer, event, multiselect); + this.selectionHandler._generateClickEvent('click', event, pointer); + } + }, { + key: 'onDoubleTap', /** - * destroy the manager and unbinds all events - * it doesn't unbind dom events, that is the user own responsibility + * handle doubletap event + * @private */ - destroy: function() { - this.element && toggleCssProps(this, false); - - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent('doubleClick', event, pointer); } - }; + }, { + key: 'onHold', - /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add - */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); + var multiselect = this.selectionHandler.options.multiselect; - /** - * trigger dom event - * @param {String} event - * @param {Object} data - */ - function triggerDomEvent(event, data) { - var gestureEvent = document.createEvent('Event'); - gestureEvent.initEvent(event, true, true); - gestureEvent.gesture = data; - data.target.dispatchEvent(gestureEvent); - } + this.checkSelectionChanges(pointer, event, multiselect); - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, + this.selectionHandler._generateClickEvent('click', event, pointer); + this.selectionHandler._generateClickEvent('hold', event, pointer); + } + }, { + key: 'onRelease', - STATE_POSSIBLE: STATE_POSSIBLE, - STATE_BEGAN: STATE_BEGAN, - STATE_CHANGED: STATE_CHANGED, - STATE_ENDED: STATE_ENDED, - STATE_RECOGNIZED: STATE_RECOGNIZED, - STATE_CANCELLED: STATE_CANCELLED, - STATE_FAILED: STATE_FAILED, + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease(event) { + if (new Date().valueOf() - this.touchTime > 10) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent('release', event, pointer); + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + } + }, { + key: 'onContext', + value: function onContext(event) { + var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); + this.selectionHandler._generateClickEvent('oncontext', event, pointer); + } + }, { + key: 'checkSelectionChanges', - DIRECTION_NONE: DIRECTION_NONE, - DIRECTION_LEFT: DIRECTION_LEFT, - DIRECTION_RIGHT: DIRECTION_RIGHT, - DIRECTION_UP: DIRECTION_UP, - DIRECTION_DOWN: DIRECTION_DOWN, - DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, - DIRECTION_VERTICAL: DIRECTION_VERTICAL, - DIRECTION_ALL: DIRECTION_ALL, + /** + * + * @param pointer + * @param add + */ + value: function checkSelectionChanges(pointer, event) { + var add = arguments[2] === undefined ? false : arguments[2]; - Manager: Manager, - Input: Input, - TouchAction: TouchAction, + var previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var previousSelection = this.selectionHandler.getSelection(); + var selected = undefined; + if (add === true) { + selected = this.selectionHandler.selectAdditionalOnPoint(pointer); + } else { + selected = this.selectionHandler.selectOnPoint(pointer); + } + var selectedEdgesCount = this.selectionHandler._getSelectedEdgeCount(); + var selectedNodesCount = this.selectionHandler._getSelectedNodeCount(); + var currentSelection = this.selectionHandler.getSelection(); - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, + var _determineIfDifferent = this._determineIfDifferent(previousSelection, currentSelection); - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, + var nodesChanges = _determineIfDifferent.nodesChanges; + var edgesChanges = _determineIfDifferent.edgesChanges; - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); + if (selectedNodesCount - previouslySelectedNodeCount > 0) { + // node was selected + this.selectionHandler._generateClickEvent('selectNode', event, pointer); + selected = true; + } else if (selectedNodesCount - previouslySelectedNodeCount < 0) { + // node was deselected + this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); + selected = true; + } else if (selectedNodesCount === previouslySelectedNodeCount && nodesChanges === true) { + this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection); + this.selectionHandler._generateClickEvent('selectNode', event, pointer); + selected = true; + } - if ("function" == TYPE_FUNCTION && __webpack_require__(13)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } + if (selectedEdgesCount - previouslySelectedEdgeCount > 0) { + // edge was selected + this.selectionHandler._generateClickEvent('selectEdge', event, pointer); + selected = true; + } else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { + // edge was deselected + this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); + selected = true; + } else if (selectedEdgesCount === previouslySelectedEdgeCount && edgesChanges === true) { + this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection); + this.selectionHandler._generateClickEvent('selectEdge', event, pointer); + selected = true; + } - })(window, document, 'Hammer'); + if (selected === true) { + // select or unselect + this.selectionHandler._generateClickEvent('select', event, pointer); + } + } + }, { + key: '_determineIfDifferent', + /** + * This function checks if the nodes and edges previously selected have changed. + * @param previousSelection + * @param currentSelection + * @returns {{nodesChanges: boolean, edgesChanges: boolean}} + * @private + */ + value: function _determineIfDifferent(previousSelection, currentSelection) { + var nodesChanges = false; + var edgesChanges = false; -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { + for (var i = 0; i < previousSelection.nodes.length; i++) { + if (currentSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) { + nodesChanges = true; + } + } + for (var i = 0; i < currentSelection.nodes.length; i++) { + if (previousSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) { + nodesChanges = true; + } + } + for (var i = 0; i < previousSelection.edges.length; i++) { + if (currentSelection.edges.indexOf(previousSelection.edges[i]) === -1) { + edgesChanges = true; + } + } + for (var i = 0; i < currentSelection.edges.length; i++) { + if (previousSelection.edges.indexOf(previousSelection.edges[i]) === -1) { + edgesChanges = true; + } + } - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + return { nodesChanges: nodesChanges, edgesChanges: edgesChanges }; + } + }, { + key: 'onDragStart', + + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + value: function onDragStart(event) { + //in case the touch event was triggered on an external div, do the initial touch now. + if (this.drag.pointer === undefined) { + this.onTouch(event); + } - /* WEBPACK VAR INJECTION */}.call(exports, {})) + // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = undefined; - // utility functions + this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer); - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. + if (node !== undefined && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); + } - 'use strict'; + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (var nodeId in selection) { + if (selection.hasOwnProperty(nodeId)) { + var object = selection[nodeId]; + var s = { + id: object.id, + node: object, - var moment = __webpack_require__(15); - var uuid = __webpack_require__(18); + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y + }; - /** - * Test whether given object is a number - * @param {*} object - * @return {Boolean} isNumber - */ - exports.isNumber = function (object) { - return object instanceof Number || typeof object == 'number'; - }; + object.options.fixed.x = true; + object.options.fixed.y = true; - /** - * Remove everything in the DOM object - * @param DOMobject - */ - exports.recursiveDOMDelete = function (DOMobject) { - if (DOMobject) { - while (DOMobject.hasChildNodes() === true) { - exports.recursiveDOMDelete(DOMobject.firstChild); - DOMobject.removeChild(DOMobject.firstChild); + this.drag.selection.push(s); + } + } + } } - } - }; + }, { + key: 'onDrag', - /** - * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value. - * - * @param min - * @param max - * @param total - * @param value - * @returns {number} - */ - exports.giveRange = function (min, max, total, value) { - if (max == min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - }; + /** + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this2 = this; - /** - * Test whether given object is a string - * @param {*} object - * @return {Boolean} isString - */ - exports.isString = function (object) { - return object instanceof String || typeof object == 'string'; - }; + if (this.drag.pinched === true) { + return; + } - /** - * Test whether given object is a Date, or a String containing a Date - * @param {Date | String} object - * @return {Boolean} isDate - */ - exports.isDate = function (object) { - if (object instanceof Date) { - return true; - } else if (exports.isString(object)) { - // test whether this string contains a date - var match = ASPDateRegex.exec(object); - if (match) { - return true; - } else if (!isNaN(Date.parse(object))) { - return true; - } - } + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit('unlockNode'); - return false; - }; + var pointer = this.getPointer(event.center); - /** - * Create a semi UUID - * source: http://stackoverflow.com/a/105074/1262753 - * @return {String} uuid - */ - exports.randomUUID = function () { - return uuid.v4(); - }; + this.selectionHandler._generateClickEvent('dragging', event, pointer); - /** - * assign all keys of an object that are not nested objects to a certain value (used for color objects). - * @param obj - * @param value - */ - exports.assignAllKeys = function (obj, value) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (typeof obj[prop] !== 'object') { - obj[prop] = value; - } - } - } - }; + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + (function () { + // calculate delta's and new location + var deltaX = pointer.x - _this2.drag.pointer.x; + var deltaY = pointer.y - _this2.drag.pointer.y; - /** - * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values. - * That means an object is not created on a property if only the b object has it. - * @param obj - * @param value - */ - exports.fillIfDefined = function (a, b) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + // only move the node if it was not fixed initially + if (selection.xFixed === false) { + node.x = _this2.canvas._XconvertDOMtoCanvas(_this2.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + // only move the node if it was not fixed initially + if (selection.yFixed === false) { + node.y = _this2.canvas._YconvertDOMtoCanvas(_this2.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); - for (var prop in a) { - if (b[prop] !== undefined) { - if (typeof b[prop] !== 'object') { - if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } + // start the simulation of the physics + _this2.body.emitter.emit('startSimulation'); + })(); } else { - if (typeof a[prop] === 'object') { - exports.fillIfDefined(a[prop], b[prop], allowDeletion); + // move the network + if (this.options.dragView === true) { + // if the drag was not started properly because the click started outside the network div, start it now. + if (this.drag.pointer === undefined) { + this._handleDragStart(event); + return; + } + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit('_redraw'); } } } - } - }; - - /** - * Extend object a with the properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Object} a - * @param {... Object} b - * @return {Object} a - */ - exports.protoExtend = function (a, b) { - for (var i = 1; i < arguments.length; i++) { - var other = arguments[i]; - for (var prop in other) { - a[prop] = other[prop]; - } - } - return a; - }; + }, { + key: 'onDragEnd', - /** - * Extend object a with the properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Object} a - * @param {... Object} b - * @return {Object} a - */ - exports.extend = function (a, b) { - for (var i = 1; i < arguments.length; i++) { - var other = arguments[i]; - for (var prop in other) { - if (other.hasOwnProperty(prop)) { - a[prop] = other[prop]; + /** + * handle drag start event + * @private + */ + value: function onDragEnd(event) { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection && selection.length) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; + }); + this.body.emitter.emit('startSimulation'); + } else { + this.body.emitter.emit('_requestRedraw'); } + this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center)); } - } - return a; - }; - - /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - exports.selectiveExtend = function (props, a, b) { - if (!Array.isArray(props)) { - throw new Error('Array with property names expected as first argument'); - } + }, { + key: 'onPinch', - for (var i = 2; i < arguments.length; i++) { - var other = arguments[i]; + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + var pointer = this.getPointer(event.center); - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - a[prop] = other[prop]; + this.drag.pinched = true; + if (this.pinch['scale'] === undefined) { + this.pinch.scale = 1; } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); } - } - return a; - }; + }, { + key: 'zoom', - /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - exports.selectiveDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + /** + * Zoom the network in or out + * @param {Number} scale a number around 1, and between 0.01 and 10 + * @param {{x: Number, y: Number}} pointer Position on screen + * @return {Number} appliedScale scale is limited within the boundaries + * @private + */ + value: function zoom(scale, pointer) { + if (this.options.zoomView === true) { + var scaleOld = this.body.view.scale; + if (scale < 0.00001) { + scale = 0.00001; + } + if (scale > 10) { + scale = 10; + } - // TODO: add support for Arrays to deepExtend - if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); - } - for (var i = 2; i < arguments.length; i++) { - var other = arguments[i]; - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; - } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop], false, allowDeletion); - } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } + var preScaleDragPointer = undefined; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); } - } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); - } else { - a[prop] = b[prop]; } - } - } - } - return a; - }; + // + this.canvas.frame.canvas.clientHeight / 2 + var translation = this.body.view.translation; - /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - exports.selectiveNotDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - // TODO: add support for Arrays to deepExtend - if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); - } - for (var prop in b) { - if (b.hasOwnProperty(prop)) { - if (props.indexOf(prop) == -1) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; - } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop]); - } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } - } - } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); - } else { - a[prop] = b[prop]; - } - } - } - } - return a; - }; + this.body.view.scale = scale; + this.body.view.translation = { x: tx, y: ty }; - /** - * Deep extend an object a with the properties of object b - * @param {Object} a - * @param {Object} b - * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended. - * (ie. the options objects that inherit from others will also get the inherited options) - * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted - * @returns {Object} - */ - exports.deepExtend = function (a, b, protoExtend, allowDeletion) { - for (var prop in b) { - if (b.hasOwnProperty(prop) || protoExtend === true) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; + if (preScaleDragPointer != undefined) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop], protoExtend); + + this.body.emitter.emit('_requestRedraw'); + + if (scaleOld < scale) { + this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } - } - } else if (Array.isArray(b[prop])) { - a[prop] = []; - for (var i = 0; i < b[prop].length; i++) { - a[prop].push(b[prop][i]); + this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); } - } else { - a[prop] = b[prop]; } } - } - return a; - }; - - /** - * Test whether all elements in two arrays are equal. - * @param {Array} a - * @param {Array} b - * @return {boolean} Returns true if both arrays have the same length and same - * elements. - */ - exports.equalArray = function (a, b) { - if (a.length != b.length) return false; - - for (var i = 0, len = a.length; i < len; i++) { - if (a[i] != b[i]) return false; - } + }, { + key: 'onMouseWheel', - return true; - }; + /** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ + value: function onMouseWheel(event) { + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } - /** - * Convert an object to another type - * @param {Boolean | Number | String | Date | Moment | Null | undefined} object - * @param {String | undefined} type Name of the type. Available types: - * 'Boolean', 'Number', 'String', - * 'Date', 'Moment', ISODate', 'ASPDate'. - * @return {*} object - * @throws Error - */ - exports.convert = function (object, type) { - var match; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta !== 0) { - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= 1 + zoom; - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); - } + // calculate the pointer location + var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); + // apply the new scale + this.zoom(scale, pointer); + } - case 'number': - case 'Number': - return Number(object.valueOf()); + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + } + }, { + key: 'onMouseMove', - case 'string': - case 'String': - return String(object); + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) { + var _this3 = this; - case 'Date': - if (exports.isNumber(object)) { - return new Date(object); - } - if (object instanceof Date) { - return new Date(object.valueOf()); - } else if (moment.isMoment(object)) { - return new Date(object.valueOf()); - } - if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])); // parse number - } else { - return moment(object).toDate(); // parse string + var pointer = this.getPointer({ x: event.clientX, y: event.clientY }); + var popupVisible = false; + + // check if the previously selected node is still selected + if (this.popup !== undefined) { + if (this.popup.hidden === false) { + this._checkHidePopup(pointer); } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); - } - case 'Moment': - if (exports.isNumber(object)) { - return moment(object); + // if the popup was not hidden above + if (this.popup.hidden === false) { + popupVisible = true; + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.show(); + } } - if (object instanceof Date) { - return moment(object.valueOf()); - } else if (moment.isMoment(object)) { - return moment(object); + + // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. + if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) { + this.canvas.frame.focus(); } - if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return moment(Number(match[1])); // parse number - } else { - return moment(object); // parse string + + // start a timeout that will check if the mouse is positioned above an element + if (popupVisible === false) { + if (this.popupTimer !== undefined) { + clearInterval(this.popupTimer); // stop any running calculationTimer + this.popupTimer = undefined; + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(function () { + return _this3._checkShowPopup(pointer); + }, this.options.tooltipDelay); } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } - case 'ISODate': - if (exports.isNumber(object)) { - return new Date(object); - } else if (object instanceof Date) { - return object.toISOString(); - } else if (moment.isMoment(object)) { - return object.toDate().toISOString(); - } else if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])).toISOString(); // parse number - } else { - return new Date(object).toISOString(); // parse string + /** + * Adding hover highlights + */ + if (this.options.hover === true) { + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj === undefined) { + obj = this.selectionHandler.getEdgeAt(pointer); } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); + this.selectionHandler.hoverObject(obj); } + } + }, { + key: '_checkShowPopup', - case 'ASPDate': - if (exports.isNumber(object)) { - return '/Date(' + object + ')/'; - } else if (object instanceof Date) { - return '/Date(' + object.valueOf() + ')/'; - } else if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - var value; - if (match) { - // object is an ASP date - value = new Date(Number(match[1])).valueOf(); // parse number - } else { - value = new Date(object).valueOf(); // parse string + /** + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkShowPopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; + + var previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = 'node'; + + // check if a node is under the cursor. + if (this.popupObj === undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodeIndices = this.body.nodeIndices; + var nodes = this.body.nodes; + var node = undefined; + var overlappingNodes = []; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + if (node.isOverlappingWith(pointerObj) === true) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(nodeIndices[i]); + } + } + } + + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others + this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; } - return '/Date(' + value + ')/'; - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); } - default: - throw new Error('Unknown type "' + type + '"'); - } - }; + if (this.popupObj === undefined && nodeUnderCursor === false) { + // search the edges for overlap + var edgeIndices = this.body.edgeIndices; + var edges = this.body.edges; + var edge = undefined; + var overlappingEdges = []; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.isOverlappingWith(pointerObj) === true) { + if (edge.connected === true && edge.getTitle() !== undefined) { + overlappingEdges.push(edgeIndices[i]); + } + } + } - // parse ASP.Net Date pattern, - // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' - // code from http://momentjs.com/ - var ASPDateRegex = /^\/?Date\((\-?\d+)/i; + if (overlappingEdges.length > 0) { + this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = 'edge'; + } + } - /** - * Get the type of an object, for example exports.getType([]) returns 'Array' - * @param {*} object - * @return {String} type - */ - exports.getType = function (object) { - var type = typeof object; + if (this.popupObj !== undefined) { + // show popup message window + if (this.popupObj.id !== previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new _componentsPopup2['default'](this.canvas.frame); + } - if (type == 'object') { - if (object === null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'Boolean'; - } - if (object instanceof Number) { - return 'Number'; - } - if (object instanceof String) { - return 'String'; - } - if (Array.isArray(object)) { - return 'Array'; - } - if (object instanceof Date) { - return 'Date'; + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + this.body.emitter.emit('showPopup', this.popupObj.id); + } + } else { + if (this.popup !== undefined) { + this.popup.hide(); + this.body.emitter.emit('hidePopup'); + } + } } - return 'Object'; - } else if (type == 'number') { - return 'Number'; - } else if (type == 'boolean') { - return 'Boolean'; - } else if (type == 'string') { - return 'String'; - } else if (type === undefined) { - return 'undefined'; - } + }, { + key: '_checkHidePopup', - return type; - }; + /** + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkHidePopup(pointer) { + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - /** - * Used to extend an array and copy it. This is used to propagate paths recursively. - * - * @param arr - * @param newValue - * @returns {Array} - */ - exports.copyAndExtendArray = function (arr, newValue) { - var newArr = []; - for (var i = 0; i < arr.length; i++) { - newArr.push(arr[i]); - } - newArr.push(newValue); - return newArr; - }; + var stillOnObj = false; + if (this.popup.popupTargetType === 'node') { + if (this.body.nodes[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - /** - * Used to extend an array and copy it. This is used to propagate paths recursively. - * - * @param arr - * @param newValue - * @returns {Array} - */ - exports.copyArray = function (arr) { - var newArr = []; - for (var i = 0; i < arr.length; i++) { - newArr.push(arr[i]); - } - return newArr; - }; + // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. + // we initially only check stillOnObj because this is much faster. + if (stillOnObj === true) { + var overNode = this.selectionHandler.getNodeAt(pointer); + stillOnObj = overNode.id === this.popup.popupTargetId; + } + } + } else { + if (this.selectionHandler.getNodeAt(pointer) === undefined) { + if (this.body.edges[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + } + } - /** - * Retrieve the absolute left value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {number} left The absolute left position of this element - * in the browser page. - */ - exports.getAbsoluteLeft = function (elem) { - return elem.getBoundingClientRect().left; - }; + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + this.body.emitter.emit('hidePopup'); + } + } + }]); - /** - * Retrieve the absolute top value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {number} top The absolute top position of this element - * in the browser page. - */ - exports.getAbsoluteTop = function (elem) { - return elem.getBoundingClientRect().top; - }; + return InteractionHandler; + })(); - /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className - */ - exports.addClassName = function (elem, className) { - var classes = elem.className.split(' '); - if (classes.indexOf(className) == -1) { - classes.push(className); // add the class to the array - elem.className = classes.join(' '); - } - }; + exports['default'] = InteractionHandler; + module.exports = exports['default']; - /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className - */ - exports.removeClassName = function (elem, className) { - var classes = elem.className.split(' '); - var index = classes.indexOf(className); - if (index != -1) { - classes.splice(index, 1); // remove the class from the array - elem.className = classes.join(' '); - } - }; +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - /** - * For each method for both arrays and objects. - * In case of an array, the built-in Array.forEach() is applied. - * In case of an Object, the method loops over all properties of the object. - * @param {Object | Array} object An Object or Array - * @param {function} callback Callback method, called for each item in - * the object or array with three parameters: - * callback(value, index, object) - */ - exports.forEach = function (object, callback) { - var i, len; - if (Array.isArray(object)) { - // array - for (i = 0, len = object.length; i < len; i++) { - callback(object[i], i, object); - } - } else { - // object - for (i in object) { - if (object.hasOwnProperty(i)) { - callback(object[i], i, object); - } - } - } - }; + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + 'use strict'; - /** - * Convert an object into an array: all objects properties are put into the - * array. The resulting array is unordered. - * @param {Object} object - * @param {Array} array - */ - exports.toArray = function (object) { - var array = []; + if (typeof window !== 'undefined') { + var propagating = __webpack_require__(14); + var Hammer = window['Hammer'] || __webpack_require__(15); + module.exports = propagating(Hammer, { + preventDefault: 'mouse' + }); + } else { + module.exports = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + }; + } - for (var prop in object) { - if (object.hasOwnProperty(prop)) array.push(object[prop]); - } +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { - return array; - }; + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - /** - * Update a property in an object - * @param {Object} object - * @param {String} key - * @param {*} value - * @return {Boolean} changed - */ - exports.updateProperty = function (object, key, value) { - if (object[key] !== value) { - object[key] = value; - return true; + (function (factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); } else { - return false; + // Browser globals (root is window) + window.propagating = factory(); } - }; + }(function () { + var _firstTarget = null; // singleton, will contain the target element where the touch event started + var _processing = false; // singleton, true when a touch event is being handled - /** - * Add and event listener. Works for all browsers - * @param {Element} element An html element - * @param {string} action The action, for example "click", - * without the prefix "on" - * @param {function} listener The callback function to be executed - * @param {boolean} [useCapture] - */ - exports.addEventListener = function (element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) useCapture = false; + /** + * Extend an Hammer.js instance with event propagation. + * + * Features: + * - Events emitted by hammer will propagate in order from child to parent + * elements. + * - Events are extended with a function `event.stopPropagation()` to stop + * propagation to parent elements. + * - An option `preventDefault` to stop all default browser behavior. + * + * Usage: + * var hammer = propagatingHammer(new Hammer(element)); + * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); + * + * @param {Hammer.Manager} hammer An hammer instance. + * @param {Object} [options] Available options: + * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. + * Enforce preventing the default browser behavior. + * Cannot be set to `false`. + * @return {Hammer.Manager} Returns the same hammer instance with extended + * functionality + */ + return function propagating(hammer, options) { + if (options && options.preventDefault === false) { + throw new Error('Only supports preventDefault == true'); + } + var _options = options || { + preventDefault: false + }; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; + + var PropagatingHammer = function(element, options) { + return propagating(new Hammer(element, options), _options); + }; + Hammer.extend(PropagatingHammer, Hammer); + PropagatingHammer.Manager = function (element, options) { + return propagating(new Hammer.Manager(element, options), _options); + }; + + return PropagatingHammer; } - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent('on' + action, listener); // IE browsers - } - }; + // attach to DOM element + var element = hammer.element; + element.hammer = hammer; - /** - * Remove an event listener from an element - * @param {Element} element An html dom element - * @param {string} action The name of the event, for example "mousedown" - * @param {function} listener The listener function - * @param {boolean} [useCapture] - */ - exports.removeEventListener = function (element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) useCapture = false; + // move the original functions that we will wrap + hammer._on = hammer.on; + hammer._off = hammer.off; + hammer._emit = hammer.emit; + hammer._destroy = hammer.destroy; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox - } + /** @type {Object.>} */ + hammer._handlers = {}; - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent('on' + action, listener); - } - }; + // register an event to catch the start of a gesture and store the + // target in a singleton + hammer._on('hammer.input', function (event) { + if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { + event.preventDefault(); + } + if (event.isFirst) { + _firstTarget = event.target; + _processing = true; + } + if (event.isFinal) { + _processing = false; + } + }); - /** - * Cancels the event if it is cancelable, without stopping further propagation of the event. - */ - exports.preventDefault = function (event) { - if (!event) event = window.event; + /** + * Register a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} handler A callback function, called as handler(event) + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.on = function (events, handler) { + // register the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (!_handlers) { + hammer._handlers[event] = _handlers = []; - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } else { - event.returnValue = false; // IE browsers - } - }; + // register the static, propagated handler + hammer._on(event, propagatedHandler); + } + _handlers.push(handler); + }); - /** - * Get HTML element which is the target of the event - * @param {Event} event - * @return {Element} target element - */ - exports.getTarget = function (event) { - // code from http://www.quirksmode.org/js/events_properties.html - if (!event) { - event = window.event; - } + return hammer; + }; - var target; + /** + * Unregister a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} [handler] Optional. The registered handler. If not + * provided, all handlers for given events + * are removed. + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.off = function (events, handler) { + // unregister the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (_handlers) { + _handlers = handler ? _handlers.filter(function (h) { + return h !== handler; + }) : []; - if (event.target) { - target = event.target; - } else if (event.srcElement) { - target = event.srcElement; - } + if (_handlers.length > 0) { + hammer._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer._off(event, propagatedHandler); + delete hammer._handlers[event]; + } + } + }); - if (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } + return hammer; + }; - return target; - }; + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + hammer.emit = function(eventType, event) { + if (!_processing) { + _firstTarget = event.target; + } + hammer._emit(eventType, event); + }; - /** - * Check if given element contains given parent somewhere in the DOM tree - * @param {Element} element - * @param {Element} parent - */ - exports.hasParent = function (element, parent) { - var e = element; + hammer.destroy = function () { + // Detach from DOM element + var element = hammer.element; + delete element.hammer; - while (e) { - if (e === parent) { - return true; - } - e = e.parentNode; - } + // clear all handlers + hammer._handlers = {}; - return false; - }; + // call original hammer destroy + hammer._destroy(); + }; - exports.option = {}; + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); + } - /** - * Convert a value into a boolean - * @param {Boolean | function | undefined} value - * @param {Boolean} [defaultValue] - * @returns {Boolean} bool - */ - exports.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + // it is possible that the same srcEvent is used with multiple hammer events, + // we keep track on which events are handled in an object _handled + if (!event.srcEvent._handled) { + event.srcEvent._handled = {}; + } - if (value != null) { - return value != false; - } + if (event.srcEvent._handled[event.type]) { + return; + } + else { + event.srcEvent._handled[event.type] = true; + } + } - return defaultValue || null; - }; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - /** - * Convert a value into a number - * @param {Boolean | function | undefined} value - * @param {Number} [defaultValue] - * @returns {Number} number - */ - exports.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - if (value != null) { - return Number(value) || defaultValue || null; - } + // propagate over all elements (until stopped) + var elem = _firstTarget; + while (elem && !stopped) { + var _handlers = elem.hammer && elem.hammer._handlers[event.type]; + if (_handlers) { + for (var i = 0; i < _handlers.length && !stopped; i++) { + _handlers[i](event); + } + } - return defaultValue || null; - }; + elem = elem.parentNode; + } + } - /** - * Convert a value into a string - * @param {String | function | undefined} value - * @param {String} [defaultValue] - * @returns {String} str - */ - exports.option.asString = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + return hammer; + }; + })); - if (value != null) { - return String(value); - } - return defaultValue || null; - }; +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Convert a size or location into a string with pixels or a percentage - * @param {String | Number | function | undefined} value - * @param {String} [defaultValue] - * @returns {String} size - */ - exports.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ + * + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; - if (exports.isString(value)) { - return value; - } else if (exports.isNumber(value)) { - return value + 'px'; - } else { - return defaultValue || null; - } - }; + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - /** - * Convert a value into a DOM element - * @param {HTMLElement | function | undefined} value - * @param {HTMLElement} [defaultValue] - * @returns {HTMLElement | null} dom - */ - exports.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + var TYPE_FUNCTION = 'function'; - return value || defaultValue || null; - }; + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb - * - * @param {String} hex - * @returns {{r: *, g: *, b: *}} | 255 range + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - exports.hexToRGB = function (hex) { - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function (m, r, g, b) { - return r + r + g + g + b + b; - }); - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; - }; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } /** - * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. - * @param color - * @param opacity - * @returns {*} + * if the argument is an array, we want to execute the fn on each entry + * if it aint an array we don't want to do a thing. + * this is used by all the methods that accept a single and array argument. + * @param {*|Array} arg + * @param {String} fn + * @param {Object} [context] + * @returns {Boolean} */ - exports.overrideOpacity = function (color, opacity) { - if (color.indexOf('rgba') != -1) { - return color; - } else if (color.indexOf('rgb') != -1) { - var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(','); - return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; - } else { - var rgb = exports.hexToRGB(color); - if (rgb == null) { - return color; - } else { - return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; } - } - }; + return false; + } /** - * - * @param red 0 -- 255 - * @param green 0 -- 255 - * @param blue 0 -- 255 - * @returns {string} - * @constructor + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - exports.RGBToHex = function (red, green, blue) { - return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); - }; + function each(obj, iterator, context) { + var i; - /** - * Parse a color property into an object with border, background, and - * highlight colors - * @param {Object | String} color - * @return {Object} colorObject - */ - exports.parseColor = function (color) { - var c; - if (exports.isString(color) === true) { - if (exports.isValidRGB(color) === true) { - var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { - return parseInt(value); - }); - color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); + if (!obj) { + return; } - if (exports.isValidHex(color) === true) { - var hsv = exports.hexToHSV(color); - var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) }; - var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 }; - var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v); - var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); - c = { - background: color, - border: darkerColorHex, - highlight: { - background: lighterColorHex, - border: darkerColorHex - }, - hover: { - background: lighterColorHex, - border: darkerColorHex + + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj.length !== undefined) { + i = 0; + while (i < obj.length) { + iterator.call(context, obj[i], i, obj); + i++; } - }; } else { - c = { - background: color, - border: color, - highlight: { - background: color, - border: color - }, - hover: { - background: color, - border: color + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); } - }; - } - } else { - c = {}; - c.background = color.background || undefined; - c.border = color.border || undefined; - - if (exports.isString(color.highlight)) { - c.highlight = { - border: color.highlight, - background: color.highlight - }; - } else { - c.highlight = {}; - c.highlight.background = color.highlight && color.highlight.background || undefined; - c.highlight.border = color.highlight && color.highlight.border || undefined; - } - - if (exports.isString(color.hover)) { - c.hover = { - border: color.hover, - background: color.hover - }; - } else { - c.hover = {}; - c.hover.background = color.hover && color.hover.background || undefined; - c.hover.border = color.hover && color.hover.border || undefined; } - } - - return c; - }; + } /** - * http://www.javascripter.net/faq/rgb2hsv.htm - * - * @param red - * @param green - * @param blue - * @returns {*} - * @constructor - */ - exports.RGBToHSV = function (red, green, blue) { - red = red / 255;green = green / 255;blue = blue / 255; - var minRGB = Math.min(red, Math.min(green, blue)); - var maxRGB = Math.max(red, Math.max(green, blue)); - - // Black-gray-white - if (minRGB == maxRGB) { - return { h: 0, s: 0, v: minRGB }; - } - - // Colors other than black-gray-white: - var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; - var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; - var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; - var saturation = (maxRGB - minRGB) / maxRGB; - var value = maxRGB; - return { h: hue, s: saturation, v: value }; - }; - - var cssUtil = { - // split a string with css styles into an object with key/values - split: function split(cssText) { - var styles = {}; - - cssText.split(';').forEach(function (style) { - if (style.trim() != '') { - var parts = style.split(':'); - var key = parts[0].trim(); - var value = parts[1].trim(); - styles[key] = value; - } - }); - - return styles; - }, - - // build a css text string from an object with key/values - join: function join(styles) { - return Object.keys(styles).map(function (key) { - return key + ': ' + styles[key]; - }).join('; '); - } - }; + * extend object. + * means that properties in dest will be overwritten by the ones in src. + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge] + * @returns {Object} dest + */ + function extend(dest, src, merge) { + var keys = Object.keys(src); + var i = 0; + while (i < keys.length) { + if (!merge || (merge && dest[keys[i]] === undefined)) { + dest[keys[i]] = src[keys[i]]; + } + i++; + } + return dest; + } /** - * Append a string with css styles to an element - * @param {Element} element - * @param {String} cssText + * merge the values from src in the dest. + * means that properties that exist in dest will not be overwritten by src + * @param {Object} dest + * @param {Object} src + * @returns {Object} dest */ - exports.addCssText = function (element, cssText) { - var currentStyles = cssUtil.split(element.style.cssText); - var newStyles = cssUtil.split(cssText); - var styles = exports.extend(currentStyles, newStyles); - - element.style.cssText = cssUtil.join(styles); - }; + function merge(dest, src) { + return extend(dest, src, true); + } /** - * Remove a string with css styles from an element - * @param {Element} element - * @param {String} cssText + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] */ - exports.removeCssText = function (element, cssText) { - var styles = cssUtil.split(element.style.cssText); - var removeStyles = cssUtil.split(cssText); + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - for (var key in removeStyles) { - if (removeStyles.hasOwnProperty(key)) { - delete styles[key]; - } - } + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - element.style.cssText = cssUtil.join(styles); - }; + if (properties) { + extend(childP, properties); + } + } /** - * https://gist.github.com/mjijackson/5311256 - * @param h - * @param s - * @param v - * @returns {{r: number, g: number, b: number}} - * @constructor + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} */ - exports.HSVToRGB = function (h, s, v) { - var r, g, b; + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - var i = Math.floor(h * 6); - var f = h * 6 - i; - var p = v * (1 - s); - var q = v * (1 - f * s); - var t = v * (1 - (1 - f) * s); + /** + * let a boolean value also be a function that must return a boolean + * this first item in args will be used as the context + * @param {Boolean|Function} val + * @param {Array} [args] + * @returns {Boolean} + */ + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); + } + return val; + } - switch (i % 6) { - case 0: - r = v, g = t, b = p;break; - case 1: - r = q, g = v, b = p;break; - case 2: - r = p, g = v, b = t;break; - case 3: - r = p, g = q, b = v;break; - case 4: - r = t, g = p, b = v;break; - case 5: - r = v, g = p, b = q;break; - } + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } - return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; - }; + /** + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } - exports.HSVToHex = function (h, s, v) { - var rgb = exports.HSVToRGB(h, s, v); - return exports.RGBToHex(rgb.r, rgb.g, rgb.b); - }; + /** + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } - exports.hexToHSV = function (hex) { - var rgb = exports.hexToRGB(hex); - return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); - }; + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } - exports.isValidHex = function (hex) { - var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); - return isOk; - }; + /** + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found + */ + function inStr(str, find) { + return str.indexOf(find) > -1; + } - exports.isValidRGB = function (rgb) { - rgb = rgb.replace(' ', ''); - var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); - return isOk; - }; - exports.isValidRGBA = function (rgba) { - rgba = rgba.replace(' ', ''); - var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba); - return isOk; - }; + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } /** - * This recursively redirects the prototype of JSON objects to the referenceObject - * This is used for default options. - * - * @param referenceObject - * @returns {*} + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index */ - exports.selectiveBridgeObject = function (fields, referenceObject) { - if (typeof referenceObject == 'object') { - var objectTo = Object.create(referenceObject); - for (var i = 0; i < fields.length; i++) { - if (referenceObject.hasOwnProperty(fields[i])) { - if (typeof referenceObject[fields[i]] == 'object') { - objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); + } else { + var i = 0; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; + } + i++; } - } + return -1; } - return objectTo; - } else { - return null; - } - }; + } /** - * This recursively redirects the prototype of JSON objects to the referenceObject - * This is used for default options. - * - * @param referenceObject - * @returns {*} + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} */ - exports.bridgeObject = function (referenceObject) { - if (typeof referenceObject == 'object') { - var objectTo = Object.create(referenceObject); - for (var i in referenceObject) { - if (referenceObject.hasOwnProperty(i)) { - if (typeof referenceObject[i] == 'object') { - objectTo[i] = exports.bridgeObject(referenceObject[i]); - } - } - } - return objectTo; - } else { - return null; - } - }; + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } /** - * this is used to set the options of subobjects in the options object. A requirement of these subobjects - * is that they have an 'enabled' element which is optional for the user but mandatory for the program. - * - * @param [object] mergeTarget | this is either this.options or the options used for the groups. - * @param [object] options | options - * @param [String] option | this is the option key in the options argument - * @private + * unique array with objects based on a key (like 'id') or just by the array's value + * @param {Array} src [{id:1},{id:2},{id:1}] + * @param {String} [key] + * @param {Boolean} [sort=False] + * @returns {Array} [{id:1},{id:2}] */ - exports.mergeOptions = function (mergeTarget, options, option) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - if (options[option] === null) { - mergeTarget[option] = undefined; - delete mergeTarget[option]; - } else { - if (options[option] !== undefined) { - if (typeof options[option] === 'boolean') { - mergeTarget[option].enabled = options[option]; - } else { - if (options[option].enabled === undefined) { - mergeTarget[option].enabled = true; + while (i < src.length) { + var val = key ? src[i][key] : src[i]; + if (inArray(values, val) < 0) { + results.push(src[i]); } - for (var prop in options[option]) { - if (options[option].hasOwnProperty(prop)) { - mergeTarget[option][prop] = options[option][prop]; - } + values[i] = val; + i++; + } + + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); } - } } - } - }; + + return results; + } /** - * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses - * this function will then iterate in both directions over this sorted list to find all visible items. - * - * @param {Item[]} orderedItems | Items ordered by start - * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher - * @param {String} field - * @param {String} field2 - * @returns {number} - * @private + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { - var maxIterations = 10000; - var iteration = 0; - var low = 0; - var high = orderedItems.length - 1; - - while (low <= high && iteration < maxIterations) { - var middle = Math.floor((low + high) / 2); + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); - var item = orderedItems[middle]; - var value = field2 === undefined ? item[field] : item[field][field2]; + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; - var searchResult = searchFunction(value); - if (searchResult == 0) { - // jihaa, found a visible item! - return middle; - } else if (searchResult == -1) { - // it is too small --> increase low - low = middle + 1; - } else { - // it is too big --> decrease high - high = middle - 1; + if (prop in obj) { + return prop; + } + i++; } + return undefined; + } - iteration++; - } - - return -1; - }; + /** + * get a unique id + * @returns {number} uniqueId + */ + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } /** - * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of - * two values, we return either the one before or the one after, depending on user input - * If it is found, we return the index, else -1. - * - * @param {Array} orderedItems - * @param {{start: number, end: number}} target - * @param {String} field - * @param {String} sidePreference 'before' or 'after' - * @returns {number} - * @private + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} */ - exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { - var maxIterations = 10000; - var iteration = 0; - var low = 0; - var high = orderedItems.length - 1; - var prevValue, value, nextValue, middle; + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - while (low <= high && iteration < maxIterations) { - // get a new guess - middle = Math.floor(0.5 * (high + low)); - prevValue = orderedItems[Math.max(0, middle - 1)][field]; - value = orderedItems[middle][field]; - nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - if (value == target) { - // we found the target - return middle; - } else if (prevValue < target && value > target) { - // target is in between of the previous and the current - return sidePreference == 'before' ? Math.max(0, middle - 1) : middle; - } else if (value < target && nextValue > target) { - // target is in between of the current and the next - return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1); - } else { - // didnt find the target, we need to change our boundaries. - if (value < target) { - // it is too small --> increase low - low = middle + 1; - } else { - // it is too big --> decrease high - high = middle - 1; - } - } - iteration++; - } + var SUPPORT_TOUCH = ('ontouchstart' in window); + var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; + var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); - // didnt find anything. Return -1. - return -1; - }; + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - /* - * Easing Functions - inspired from http://gizma.com/easing/ - * only considering the t value for the range [0, 1] => [0, 1] - * https://gist.github.com/gre/1650294 - */ - exports.easingFunctions = { - // no easing, no acceleration - linear: function linear(t) { - return t; - }, - // accelerating from zero velocity - easeInQuad: function easeInQuad(t) { - return t * t; - }, - // decelerating to zero velocity - easeOutQuad: function easeOutQuad(t) { - return t * (2 - t); - }, - // acceleration until halfway, then deceleration - easeInOutQuad: function easeInOutQuad(t) { - return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; - }, - // accelerating from zero velocity - easeInCubic: function easeInCubic(t) { - return t * t * t; - }, - // decelerating to zero velocity - easeOutCubic: function easeOutCubic(t) { - return --t * t * t + 1; - }, - // acceleration until halfway, then deceleration - easeInOutCubic: function easeInOutCubic(t) { - return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; - }, - // accelerating from zero velocity - easeInQuart: function easeInQuart(t) { - return t * t * t * t; - }, - // decelerating to zero velocity - easeOutQuart: function easeOutQuart(t) { - return 1 - --t * t * t * t; - }, - // acceleration until halfway, then deceleration - easeInOutQuart: function easeInOutQuart(t) { - return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; - }, - // accelerating from zero velocity - easeInQuint: function easeInQuint(t) { - return t * t * t * t * t; - }, - // decelerating to zero velocity - easeOutQuint: function easeOutQuint(t) { - return 1 + --t * t * t * t * t; - }, - // acceleration until halfway, then deceleration - easeInOutQuint: function easeInOutQuint(t) { - return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; - } - }; + var COMPUTE_INTERVAL = 25; -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; + + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; + + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. - 'use strict'; + /** + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor + */ + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(16); + // smaller wrapper around the handler, for the scope and the enabled state of the manager, + // so when disabled the input events are completely bypassed. + this.domHandler = function(ev) { + if (boolOrFn(manager.options.enable, [manager])) { + self.handler(ev); + } + }; -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { + this.init(); - /* WEBPACK VAR INJECTION */(function(module) {//! moment.js - //! version : 2.10.3 - //! authors : Tim Wood, Iskren Chernev, Moment.js contributors - //! license : MIT - //! momentjs.com + } - (function (global, factory) { - true ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() - }(this, function () { 'use strict'; + Input.prototype = { + /** + * should handle the inputEvent data and trigger the callback + * @virtual + */ + handler: function() { }, - var hookCallback; + /** + * bind the events + */ + init: function() { + this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + }, - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); + /** + * unbind the events + */ + destroy: function() { + this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); } + }; - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; - } + /** + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} + */ + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + if (inputClass) { + Type = inputClass; + } else if (SUPPORT_POINTER_EVENTS) { + Type = PointerEventInput; + } else if (SUPPORT_ONLY_TOUCH) { + Type = TouchInput; + } else if (!SUPPORT_TOUCH) { + Type = MouseInput; + } else { + Type = TouchMouseInput; } + return new (Type)(manager, inputHandler); + } - function isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; - } + /** + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input + */ + function inputHandler(manager, eventType, input) { + var pointersLen = input.pointers.length; + var changedPointersLen = input.changedPointers.length; + var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); + var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); + if (isFirst) { + manager.session = {}; } - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + // compute scale, rotation etc + computeInputData(manager, input); - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } + // emit secret event + manager.emit('hammer.input', input); - return a; - } + manager.recognize(input); + manager.session.prevInput = input; + } - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } + /** + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input + */ + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - function defaultParsingFlags() { - // We need to deep clone this object. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso : false - }; + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); } - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); - } - return m._pf; + // to compute scale and rotation we need to store the multiple touches + if (pointersLength > 1 && !session.firstMultiple) { + session.firstMultiple = simpleCloneInputData(input); + } else if (pointersLength === 1) { + session.firstMultiple = false; } - function valid__isValid(m) { - if (m._isValid == null) { - var flags = getParsingFlags(m); - m._isValid = !isNaN(m._d.getTime()) && - flags.overflow < 0 && - !flags.empty && - !flags.invalidMonth && - !flags.nullInput && - !flags.invalidFormat && - !flags.userInvalidated; + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; - } - } - return m._isValid; + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; + + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); + + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); + + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + + computeIntervalInputData(session, input); + + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; } + input.target = target; + } - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; - } + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - return m; + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; + + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; } - var momentProperties = utils_hooks__hooks.momentProperties = []; + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } - function copyConfig(to, from) { - var i, prop, val; + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; - } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); - } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; - } + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } + var v = getVelocity(deltaTime, deltaX, deltaY); + velocityX = v.x; + velocityY = v.y; + velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; + direction = getDirection(deltaX, deltaY); - return to; + session.lastInterval = input; + } else { + // use latest velocity info if it doesn't overtake a minimum period + velocity = last.velocity; + velocityX = last.velocityX; + velocityY = last.velocityY; + direction = last.direction; } - var updateInProgress = false; + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } - // Moment prototype object - function Moment(config) { - copyConfig(this, config); - this._d = new Date(+config._d); - // Prevent infinite loop in case updateOffset creates new moment - // objects. - if (updateInProgress === false) { - updateInProgress = true; - utils_hooks__hooks.updateOffset(this); - updateInProgress = false; - } + /** + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData + */ + function simpleCloneInputData(input) { + // make a simple copy of the pointers because we will get a reference if we don't + // we only need clientXY for the calculations + var pointers = []; + var i = 0; + while (i < input.pointers.length) { + pointers[i] = { + clientX: round(input.pointers[i].clientX), + clientY: round(input.pointers[i].clientY) + }; + i++; } - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } + + /** + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties + */ + function getCenter(pointers) { + var pointersLength = pointers.length; + + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; + } - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } - return value; + /** + * calculate the velocity between two points. unit is in px per ms. + * @param {Number} deltaTime + * @param {Number} x + * @param {Number} y + * @return {Object} velocity `x` and `y` + */ + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } + + /** + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction + */ + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; } - function compareArrays(array1, array2, dontConvert) { - var len = Math.min(array1.length, array2.length), - lengthDiff = Math.abs(array1.length - array2.length), - diffs = 0, - i; - for (i = 0; i < len; i++) { - if ((dontConvert && array1[i] !== array2[i]) || - (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { - diffs++; - } - } - return diffs + lengthDiff; + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } - function Locale() { + /** + * calculate the absolute distance between two points + * @param {Object} p1 {x, y} + * @param {Object} p2 {x, y} + * @param {Array} [props] containing x and y keys + * @return {Number} distance + */ + function getDistance(p1, p2, props) { + if (!props) { + props = PROPS_XY; } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; - var locales = {}; - var globalLocale; + return Math.sqrt((x * x) + (y * y)); + } - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; + /** + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle + */ + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } - // pick the locale from the array - // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - function chooseLocale(names) { - var i = 0, j, next, locale, split; + /** + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation + */ + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } + /** + * calculate the scale factor between two pointersets + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} scale + */ + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); + } - function loadLocale(name) { - var oldLocale = null; - // TODO: Find a better way to register and load all the locales in Node - if (!locales[name] && typeof module !== 'undefined' && - module && module.exports) { - try { - oldLocale = globalLocale._abbr; - !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); - // because defineLocale currently also sets the global locale, we - // want to undo that for lazy loaded locales - locale_locales__getSetGlobalLocale(oldLocale); - } catch (e) { } - } - return locales[name]; - } + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - function locale_locales__getSetGlobalLocale (key, values) { - var data; - if (key) { - if (typeof values === 'undefined') { - data = locale_locales__getLocale(key); - } - else { - data = defineLocale(key, values); - } + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } - } + /** + * Mouse events input + * @constructor + * @extends Input + */ + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - return globalLocale._abbr; - } + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + Input.apply(this, arguments); + } - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; } - } - - // returns locale data - function locale_locales__getLocale (key) { - var locale; - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; } - if (!key) { - return globalLocale; + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; } - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; + if (eventType & INPUT_END) { + this.pressed = false; } - return chooseLocale(key); + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } + }); - var aliases = {}; + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL + }; - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; - } + // in IE10 the pointer types is defined as an enum + var IE10_POINTER_TYPE_ENUM = { + 2: INPUT_TYPE_TOUCH, + 3: INPUT_TYPE_PEN, + 4: INPUT_TYPE_MOUSE, + 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + }; - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; - } + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } + /** + * Pointer events input + * @constructor + * @extends Input + */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - return normalizedInput; - } + Input.apply(this, arguments); - function makeGetSet (unit, keepTime) { - return function (value) { - if (value != null) { - get_set__set(this, unit, value); - utils_hooks__hooks.updateOffset(this, keepTime); - return this; - } else { - return get_set__get(this, unit); - } - }; - } + this.store = (this.manager.session.pointerEvents = []); + } - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - // MOMENTS + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + + // start and mouse must be down + if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { + if (storeIndex < 0) { + store.push(ev); + storeIndex = store.length - 1; } + } else if (eventType & (INPUT_END | INPUT_CANCEL)) { + removePointer = true; } - return this; - } - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - while (output.length < targetLength) { - output = '0' + output; + // update the event in the store + store[storeIndex] = ev; + + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); + + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); } - return (sign ? (forceSign ? '+' : '') : '-') + output; } + }); - var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - var formatFunctions = {}; + /** + * Touch events input + * @constructor + * @extends Input + */ + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - var formatTokenFunctions = {}; + Input.apply(this, arguments); + } - // token: 'M' - // padded: ['MM', 2] - // ordinal: 'Mo' - // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { - var func = callback; - if (typeof callback === 'string') { - func = function () { - return this[callback](); - }; - } - if (token) { - formatTokenFunctions[token] = func; - } - if (padded) { - formatTokenFunctions[padded[0]] = function () { - return zeroFill(func.apply(this, arguments), padded[1], padded[2]); - }; - } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; } - } - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); + if (!this.started) { + return; } - return input.replace(/\\/g, ''); - } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + var touches = normalizeSingleTouches.call(this, ev, type); - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; } - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } + }); - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); - format = expandFormat(format, m.localeData()); + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); + } - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + return [all, changed]; + } - return formatFunctions[format](m); - } + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - function expandFormat(format, locale) { - var i = 5; + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + /** + * Multi-user touch events input + * @constructor + * @extends Input + */ + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; + Input.apply(this, arguments); + } + + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; } - return format; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } + }); - var match1 = /\d/; // 0 - 9 - var match2 = /\d\d/; // 00 - 99 - var match3 = /\d{3}/; // 000 - 999 - var match4 = /\d{4}/; // 0000 - 9999 - var match6 = /[+-]?\d{6}/; // -999999 - 999999 - var match1to2 = /\d\d?/; // 0 - 99 - var match1to3 = /\d{1,3}/; // 0 - 999 - var match1to4 = /\d{1,4}/; // 0 - 9999 - var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf - - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function getTouches(ev, type) { + var allTouches = toArray(ev.touches); + var targetIds = this.targetIds; - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + // when there is only one touch, the process can be simplified + if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { + targetIds[allTouches[0].identifier] = true; + return [allTouches, allTouches]; + } - // any word (or two) characters or numbers including two/three word month in arabic. - var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - var regexes = {}; + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); + }); - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; + } } - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); + // filter changed touches to only contain touches that exist in the collected target ids + i = 0; + while (i < changedTouches.length) { + if (targetIds[changedTouches[i].identifier]) { + changedTargetTouches.push(changedTouches[i]); } - return regexes[token](config._strict, config._locale); + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; } - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function unescapeFormat(s) { - return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + if (!changedTargetTouches.length) { + return; } - var tokens = {}; + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - function addParseToken (token, callback) { - var i, func = callback; - if (typeof token === 'string') { - token = [token]; - } - if (typeof callback === 'number') { - func = function (input, array) { - array[callback] = toInt(input); - }; - } - for (i = 0; i < token.length; i++) { - tokens[token[i]] = func; - } - } + /** + * Combined touch and mouse input + * + * Touch has a higher priority then mouse, and while touching no mouse events are allowed. + * This because touch devices also emit mouse events while doing a touch. + * + * @constructor + * @extends Input + */ + function TouchMouseInput() { + Input.apply(this, arguments); + + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } + + inherit(TouchMouseInput, Input, { + /** + * handle mouse and touch events + * @param {Hammer} manager + * @param {String} inputEvent + * @param {Object} inputData + */ + handler: function TMEhandler(manager, inputEvent, inputData) { + var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), + isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); - }); - } + // when we're in a touch event, so block all upcoming mouse events + // most mobile browser also emit mouseevents, right after touchstart + if (isTouch) { + this.mouse.allow = false; + } else if (isMouse && !this.mouse.allow) { + return; + } - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; } - } - var YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; + this.callback(manager, inputEvent, inputData); + }, - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); } + }); - // FORMATTING + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; - }); + // magical touchAction value + var TOUCH_ACTION_COMPUTE = 'compute'; + var TOUCH_ACTION_AUTO = 'auto'; + var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented + var TOUCH_ACTION_NONE = 'none'; + var TOUCH_ACTION_PAN_X = 'pan-x'; + var TOUCH_ACTION_PAN_Y = 'pan-y'; - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); - }); + /** + * Touch Action + * sets the touchAction property or uses the js alternative + * @param {Manager} manager + * @param {String} value + * @constructor + */ + function TouchAction(manager, value) { + this.manager = manager; + this.set(value); + } - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); + TouchAction.prototype = { + /** + * set the touchAction value on the element or enable the polyfill + * @param {String} value + */ + set: function(value) { + // find out the touch-action by the event handlers + if (value == TOUCH_ACTION_COMPUTE) { + value = this.compute(); + } - // ALIASES + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + } + this.actions = value.toLowerCase().trim(); + }, - addUnitAlias('month', 'M'); + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); + }, - // PARSING + /** + * compute the value for the touchAction property based on the recognizer's settings + * @returns {String} value + */ + compute: function() { + var actions = []; + each(this.manager.recognizers, function(recognizer) { + if (boolOrFn(recognizer.options.enable, [recognizer])) { + actions = actions.concat(recognizer.getTouchAction()); + } + }); + return cleanTouchActions(actions.join(' ')); + }, - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); + /** + * this method is called on each input cycle and provides the preventing of the browser behavior + * @param {Object} input + */ + preventDefaults: function(input) { + // not needed with native support for the touchAction property + if (NATIVE_TOUCH_ACTION) { + return; + } - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; - }); + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { - var month = config._locale.monthsParse(input, token, config._strict); - // if we didn't find a month name, mark the date as invalid. - if (month != null) { - array[MONTH] = month; - } else { - getParsingFlags(config).invalidMonth = input; + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; } - }); - // LOCALES + var actions = this.actions; + var hasNone = inStr(actions, TOUCH_ACTION_NONE); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); + } + }, + + /** + * call preventDefault to prevent the browser's default behavior (scrolling in most cases) + * @param {Object} srcEvent + */ + preventSrc: function(srcEvent) { + this.manager.session.prevented = true; + srcEvent.preventDefault(); } + }; - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; + /** + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} + */ + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; } - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - mom = create_utc__createUTC([2000, i]); - if (strict && !this._longMonthsParse[i]) { - this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); - this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); - } - if (!strict && !this._monthsParse[i]) { - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { - return i; - } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; - } - } + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } - // MOMENTS + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } - function setMonth (mom, value) { - var dayOfMonth; + return TOUCH_ACTION_AUTO; + } - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } + /** + * Recognizer flow explained; * + * All recognizers have the initial state of POSSIBLE when a input session starts. + * The definition of a input session is from the first input until the last input, with all it's movement in it. * + * Example session for mouse-input: mousedown -> mousemove -> mouseup + * + * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed + * which determines with state it should be. + * + * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to + * POSSIBLE to give it another change on the next cycle. + * + * Possible + * | + * +-----+---------------+ + * | | + * +-----+-----+ | + * | | | + * Failed Cancelled | + * +-------+------+ + * | | + * Recognized Began + * | + * Changed + * | + * Ended/Recognized + */ + var STATE_POSSIBLE = 1; + var STATE_BEGAN = 2; + var STATE_CHANGED = 4; + var STATE_ENDED = 8; + var STATE_RECOGNIZED = STATE_ENDED; + var STATE_CANCELLED = 16; + var STATE_FAILED = 32; - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } + /** + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options + */ + function Recognizer(options) { + this.id = uniqueId(); - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); - } - } + this.manager = null; + this.options = merge(options || {}, this.defaults); - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); - } + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - function checkOverflow (m) { - var overflow; - var a = m._a; + this.state = STATE_POSSIBLE; - if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : - a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : - a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : - a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : - a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : - a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : - -1; + this.simultaneous = {}; + this.requireFail = []; + } - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - getParsingFlags(m).overflow = overflow; - } + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - return m; - } + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; } - } - function deprecate(msg, fn) { - var firstTime = true, - msgWithStack = msg + '\n' + (new Error()).stack; + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); + } + return this; + }, - return extend(function () { - if (firstTime) { - warn(msgWithStack); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } + /** + * drop the simultaneous link. it doesnt remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRecognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { + return this; + } - var deprecations = {}; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; + /** + * recognizer can only run when an other is failing + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + requireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { + return this; } - } - utils_hooks__hooks.suppressDeprecationWarnings = false; + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; + /** + * drop the requireFailure link. it does not remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRequireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { + return this; + } - var isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ]; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - // iso time formats and regexes - var isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ]; + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); + /** + * You should use `tryEmit` instead of `emit` directly to check + * that all the needed recognizers has failed before emitting. + * @param {Object} input + */ + emit: function(input) { + var self = this; + var state = this.state; - if (match) { - getParsingFlags(config).iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be 'T' or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(matchOffset)) { - config._f += 'Z'; - } - configFromStringAndFormat(config); - } else { - config._isValid = false; + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); } - } - - // date from iso format or fallback - function configFromString(config) { - var matched = aspNetJsonRegex.exec(config._i); - if (matched !== null) { - config._d = new Date(+matched[1]); - return; + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); } - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); - } - } + emit(); // simple 'eventName' events - utils_hooks__hooks.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); } - ); - - function createDate (y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); + }, - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); + /** + * Check that all the require failure recognizers has failed, + * if true, it emits a gesture event, + * otherwise, setup the state to FAILED. + * @param {Object} input + */ + tryEmit: function(input) { + if (this.canEmit()) { + return this.emit(input); } - return date; - } + // it's failing anyway + this.state = STATE_FAILED; + }, - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); + /** + * can we emit? + * @returns {boolean} + */ + canEmit: function() { + var i = 0; + while (i < this.requireFail.length) { + if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { + return false; + } + i++; } - return date; - } + return true; + }, - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; - }); + /** + * update the recognizer + * @param {Object} inputData + */ + recognize: function(inputData) { + // make a new copy of the inputData + // so we can change the inputData without messing up the other recognizers + var inputDataClone = extend({}, inputData); - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; + } - // ALIASES + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; + } - addUnitAlias('year', 'y'); + this.state = this.process(inputDataClone); - // PARSING + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); + /** + * return the state of the recognizer + * the actual recognizing happens in this method + * @virtual + * @param {Object} inputData + * @returns {Const} STATE + */ + process: function(inputData) { }, // jshint ignore:line - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - // HELPERS + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; + /** + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; } + return ''; + } - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + /** + * direction cons to string + * @param {Const} direction + * @returns {String} + */ + function directionStr(direction) { + if (direction == DIRECTION_DOWN) { + return 'down'; + } else if (direction == DIRECTION_UP) { + return 'up'; + } else if (direction == DIRECTION_LEFT) { + return 'left'; + } else if (direction == DIRECTION_RIGHT) { + return 'right'; } + return ''; + } - // HOOKS - - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; - - // MOMENTS - - var getSetYear = makeGetSet('FullYear', false); - - function getIsLeapYear () { - return isLeapYear(this.year()); + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} + */ + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); } + return otherRecognizer; + } - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - // ALIASES + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); + /** + * Used to check if it the recognizer receives valid input, like input.distance > 10. + * @memberof AttrRecognizer + * @param {Object} input + * @returns {Boolean} recognized + */ + attrTest: function(input) { + var optionPointers = this.options.pointers; + return optionPointers === 0 || input.pointers.length === optionPointers; + }, - // PARSING + /** + * Process the input and return the state for the recognizer + * @memberof AttrRecognizer + * @param {Object} input + * @returns {*} State + */ + process: function(input) { + var state = this.state; + var eventType = input.eventType; - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); + // on cancel input and we've recognized before, return STATE_CANCELLED + if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { + return state | STATE_CANCELLED; + } else if (isRecognized || isValid) { + if (eventType & INPUT_END) { + return state | STATE_ENDED; + } else if (!(state & STATE_BEGAN)) { + return STATE_BEGAN; + } + return state | STATE_CHANGED; + } + return STATE_FAILED; + } + }); - // HELPERS + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; + this.pX = null; + this.pY = null; + } + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); } - - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); } + return actions; + }, - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } + directionTest: function(input) { + var options = this.options; + var hasMoved = true; + var distance = input.distance; + var direction = input.direction; + var x = input.deltaX; + var y = input.deltaY; - // LOCALES + // lock to axis? + if (!(direction & options.direction)) { + if (options.direction & DIRECTION_HORIZONTAL) { + direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + hasMoved = x != this.pX; + distance = Math.abs(input.deltaX); + } else { + direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; + hasMoved = y != this.pY; + distance = Math.abs(input.deltaY); + } + } + input.direction = direction; + return hasMoved && distance > options.threshold && direction & options.direction; + }, - function localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - } + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - var defaultLocaleWeek = { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }; + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - function localeFirstDayOfWeek () { - return this._week.dow; - } + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - function localeFirstDayOfYear () { - return this._week.doy; + this._super.emit.call(this, input); } + }); - // MOMENTS - - function getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - } + /** + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer + */ + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - } + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - // ALIASES + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - addUnitAlias('dayOfYear', 'DDD'); + emit: function(input) { + this._super.emit.call(this, input); + if (input.scale !== 1) { + var inOut = input.scale < 1 ? 'in' : 'out'; + this.manager.emit(this.options.event + inOut, input); + } + } + }); - // PARSING + /** + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer + */ + function PressRecognizer() { + Recognizer.apply(this, arguments); - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); + this._timer = null; + this._input = null; + } - // HELPERS + inherit(PressRecognizer, Recognizer, { + /** + * @namespace + * @memberof PressRecognizer + */ + defaults: { + event: 'press', + pointers: 1, + time: 500, // minimal time of the pointer to be pressed + threshold: 5 // a minimal movement is ok, but keep it low + }, - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = createUTCDate(year, 0, 1).getUTCDay(); - var daysToAdd; - var dayOfYear; + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + process: function(input) { + var options = this.options; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTime = input.deltaTime > options.time; - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + this._input = input; - // MOMENTS + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { + this.reset(); + } else if (input.eventType & INPUT_START) { + this.reset(); + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.time, this); + } else if (input.eventType & INPUT_END) { + return STATE_RECOGNIZED; + } + return STATE_FAILED; + }, - function getSetDayOfYear (input) { - var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - } + reset: function() { + clearTimeout(this._timer); + }, - // Pick the first defined of two or three arguments. - function defaults(a, b, c) { - if (a != null) { - return a; - } - if (b != null) { - return b; + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - return c; - } - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; + if (input && (input.eventType & INPUT_END)) { + this.manager.emit(this.options.event + 'up', input); + } else { + this._input.timeStamp = now(); + this.manager.emit(this.options.event, this._input); } - return [now.getFullYear(), now.getMonth(), now.getDate()]; } + }); - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function configFromArray (config) { - var i, date, input = [], currentDate, yearToUse; - - if (config._d) { - return; - } + /** + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer + */ + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - currentDate = currentDateArray(config); + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; - } + /** + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - // Default to current date. - // * if no year, month, day of month are given, default to today - // * if day of month is given, default month and year - // * if month is given, default only year - // * if year is given, don't default anything - for (i = 0; i < 3 && config._a[i] == null; ++i) { - config._a[i] = input[i] = currentDate[i]; - } + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - // Zero out whatever was not defaulted, including time - for (; i < 7; i++) { - config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; - } + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { - config._nextDay = true; - config._a[HOUR] = 0; + if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { + velocity = input.velocity; + } else if (direction & DIRECTION_HORIZONTAL) { + velocity = input.velocityX; + } else if (direction & DIRECTION_VERTICAL) { + velocity = input.velocityY; } - config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); - } + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - if (config._nextDay) { - config._a[HOUR] = 24; + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); } + + this.manager.emit(this.options.event, input); } + }); - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; + /** + * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur + * between the given interval and position. The delay option can be used to recognize multi-taps without firing + * a single tap. + * + * The eventData from the emitted event contains the property `tapCount`, which contains the amount of + * multi-taps being recognized. + * @constructor + * @extends Recognizer + */ + function TapRecognizer() { + Recognizer.apply(this, arguments); - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); - week = defaults(w.W, 1); - weekday = defaults(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; + this._timer = null; + this._input = null; + this.count = 0; + } - weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); - week = defaults(w.w, 1); + inherit(TapRecognizer, Recognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'tap', + pointers: 1, + taps: 1, + interval: 300, // max time between the multi-tap taps + time: 250, // max time of the pointer to be down (like finger on the screen) + threshold: 2, // a minimal movement is ok, but keep it low + posThreshold: 10 // a multi-tap can be a bit off the initial position + }, - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } - } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } + process: function(input) { + var options = this.options; - utils_hooks__hooks.ISO_8601 = function () {}; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - // date from string and format string - function configFromStringAndFormat(config) { - // TODO: Move this to another part of the creation flow to prevent circular deps - if (config._f === utils_hooks__hooks.ISO_8601) { - configFromISO(config); - return; + this.reset(); + + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - config._a = []; - getParsingFlags(config).empty = true; + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (validMovement && validTouchTime && validPointers) { + if (input.eventType != INPUT_END) { + return this.failTimeout(); + } - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + this.pTime = input.timeStamp; + this.pCenter = input.center; - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; - if (parsedInput) { - skipped = string.substr(0, string.indexOf(parsedInput)); - if (skipped.length > 0) { - getParsingFlags(config).unusedInput.push(skipped); - } - string = string.slice(string.indexOf(parsedInput) + parsedInput.length); - totalParsedInputLength += parsedInput.length; + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; } - // don't parse if it's not a known token - if (formatTokenFunctions[token]) { - if (parsedInput) { - getParsingFlags(config).empty = false; - } - else { - getParsingFlags(config).unusedTokens.push(token); + + this._input = input; + + // if tap count matches we have recognized it, + // else it has began recognizing... + var tapCount = this.count % options.taps; + if (tapCount === 0) { + // no failing requirements, immediately trigger the tap event + // or wait as long as the multitap interval to trigger + if (!this.hasRequireFailures()) { + return STATE_RECOGNIZED; + } else { + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.interval, this); + return STATE_BEGAN; } - addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { - getParsingFlags(config).unusedTokens.push(token); } } + return STATE_FAILED; + }, - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, - // clear _12h flag if hour is <= 12 - if (getParsingFlags(config).bigHour === true && - config._a[HOUR] <= 12 && - config._a[HOUR] > 0) { - getParsingFlags(config).bigHour = undefined; - } - // handle meridiem - config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); + reset: function() { + clearTimeout(this._timer); + }, - configFromArray(config); - checkOverflow(config); + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } } + }); + /** + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); + } - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; + /** + * @const {string} + */ + Hammer.VERSION = '2.0.4'; - if (meridiem == null) { - // nothing to do - return hour; - } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { - // Fallback - isPm = locale.isPM(meridiem); - if (isPm && hour < 12) { - hour += 12; - } - if (!isPm && hour === 12) { - hour = 0; - } - return hour; - } else { - // this is not supposed to happen - return hour; - } - } + /** + * default settings + * @namespace + */ + Hammer.defaults = { + /** + * set if DOM events are being triggered. + * But this is slower and unused by simple implementations, so disabled by default. + * @type {Boolean} + * @default false + */ + domEvents: false, - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, + /** + * The value for the touchAction property/fallback. + * When set to `compute` it will magically set the correct value based on the added recognizers. + * @type {String} + * @default compute + */ + touchAction: TOUCH_ACTION_COMPUTE, - scoreToBeat, - i, - currentScore; + /** + * @type {Boolean} + * @default true + */ + enable: true, - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } + /** + * EXPERIMENTAL FEATURE -- can be removed/changed + * Change the parent input target element. + * If Null, then it is being set the to main element. + * @type {Null|EventTarget} + * @default null + */ + inputTarget: null, - for (i = 0; i < config._f.length; i++) { - currentScore = 0; - tempConfig = copyConfig({}, config); - if (config._useUTC != null) { - tempConfig._useUTC = config._useUTC; - } - tempConfig._f = config._f[i]; - configFromStringAndFormat(tempConfig); + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - if (!valid__isValid(tempConfig)) { - continue; - } + /** + * Default recognizer setup when calling `Hammer()` + * When creating a new Manager these will be skipped. + * @type {Array} + */ + preset: [ + // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] + [RotateRecognizer, { enable: false }], + [PinchRecognizer, { enable: false }, ['rotate']], + [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], + [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], + [TapRecognizer], + [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], + [PressRecognizer] + ], - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; + /** + * Some CSS properties can be used to improve the working of Hammer. + * Add them to this method and they will be set when creating a new Manager. + * @namespace + */ + cssProps: { + /** + * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userSelect: 'none', - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - getParsingFlags(tempConfig).score = currentScore; + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @type {String} + * @default 'none' + */ + touchCallout: 'none', - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - extend(config, bestMoment || tempConfig); + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', + + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' } + }; - function configFromObject(config) { - if (config._d) { - return; - } + var STOP = 1; + var FORCED_STOP = 2; - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; + /** + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Manager(element, options) { + options = options || {}; - configFromArray(config); - } + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; + this.handlers = {}; + this.session = {}; + this.recognizers = []; - config._locale = config._locale || locale_locales__getLocale(config._l); + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); - } + toggleCssProps(this, true); - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } + each(options.recognizers, function(item) { + var recognizer = this.add(new (item[0])(item[1])); + item[2] && recognizer.recognizeWith(item[2]); + item[3] && recognizer.requireFailure(item[3]); + }, this); + } - if (isMoment(input)) { - return new Moment(checkOverflow(input)); - } else if (isArray(format)) { - configFromStringAndArray(config); - } else if (format) { - configFromStringAndFormat(config); - } else if (isDate(input)) { - config._d = input; - } else { - configFromInput(config); + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); + + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); + } + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); } + return this; + }, - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; + /** + * stop recognizing for this session. + * This session will be discarded, when a new [input]start event is fired. + * When forced, the recognizer cycle is stopped immediately. + * @param {Boolean} [force] + */ + stop: function(force) { + this.session.stopped = force ? FORCED_STOP : STOP; + }, + + /** + * run the recognizers! + * called by the inputHandler function on every movement of the pointers (touches) + * it walks through all the recognizers and tries to detect the gesture that is being made + * @param {Object} inputData + */ + recognize: function(inputData) { + var session = this.session; + if (session.stopped) { + return; } - return res; - } + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - function configFromInput(config) { - var input = config._i; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if (typeof input === 'string') { - configFromString(config); - } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { - return parseInt(obj, 10); - }); - configFromArray(config); - } else if (typeof(input) === 'object') { - configFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - utils_hooks__hooks.createFromInputFallback(config); - } - } + var recognizer; + var recognizers = this.recognizers; - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; + // this holds the recognizer that is being recognized. + // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED + // if no recognizer is detecting a thing, it is set to `null` + var curRecognizer = session.curRecognizer; - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; + // reset when the last recognizer is recognized + // or when we're in a new session + if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { + curRecognizer = session.curRecognizer = null; } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c._isAMomentObject = true; - c._useUTC = c._isUTC = isUTC; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - return createFromConfig(c); - } + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } + // find out if we are allowed try to recognize the input for this one. + // 1. allow if the session is NOT forced stopped (see the .stop() method) + // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one + // that is being recognized. + // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. + // this can be setup with the `recognizeWith()` method on the recognizer. + if (session.stopped !== FORCED_STOP && ( // 1 + !curRecognizer || recognizer == curRecognizer || // 2 + recognizer.canRecognizeWith(curRecognizer))) { // 3 + recognizer.recognize(inputData); + } else { + recognizer.reset(); + } - var prototypeMin = deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other < this ? this : other; - } - ); + // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the + // current active recognizer. but only if we don't already have an active recognizer + if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { + curRecognizer = session.curRecognizer = recognizer; + } + i++; + } + }, - var prototypeMax = deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other > this ? this : other; + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; } - ); - // Pick a moment m from moments so that m[fn](other) is true for all - // other. This relies on the function fn to be transitive. - // - // moments should either be an array of moment objects or an array, whose - // first element is an array of moment objects. - function pickBy(fn, moments) { - var res, i; - if (moments.length === 1 && isArray(moments[0])) { - moments = moments[0]; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } } - if (!moments.length) { - return local__createLocal(); + return null; + }, + + /** + * add a recognizer to the manager + * existing recognizers with the same event name will be removed + * @param {Recognizer} recognizer + * @returns {Recognizer|Manager} + */ + add: function(recognizer) { + if (invokeArrayArg(recognizer, 'add', this)) { + return this; } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } + + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); } - return res; - } - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); + this.recognizers.push(recognizer); + recognizer.manager = this; - return pickBy('isBefore', args); - } + this.touchAction.update(); + return recognizer; + }, - function max () { - var args = [].slice.call(arguments, 0); + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - return pickBy('isAfter', args); - } + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - function Duration (duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; + this.touchAction.update(); + return this; + }, - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; + /** + * bind event + * @param {String} events + * @param {Function} handler + * @returns {EventEmitter} this + */ + on: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + handlers[event] = handlers[event] || []; + handlers[event].push(handler); + }); + return this; + }, - this._data = {}; + /** + * unbind event, leave emit blank to remove all handlers + * @param {String} events + * @param {Function} [handler] + * @returns {EventEmitter} this + */ + off: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + if (!handler) { + delete handlers[event]; + } else { + handlers[event].splice(inArray(handlers[event], handler), 1); + } + }); + return this; + }, - this._locale = locale_locales__getLocale(); + /** + * emit event to the listeners + * @param {String} event + * @param {Object} data + */ + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); + } - this._bubble(); - } + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; + } - function isDuration (obj) { - return obj instanceof Duration; - } + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - function offset (token, separator) { - addFormatToken(token, 0, 0, function () { - var offset = this.utcOffset(); - var sign = '+'; - if (offset < 0) { - offset = -offset; - sign = '-'; - } - return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); - }); - } + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; + } + }, - offset('Z', ':'); - offset('ZZ', ''); + /** + * destroy the manager and unbinds all events + * it doesn't unbind dom events, that is the user own responsibility + */ + destroy: function() { + this.element && toggleCssProps(this, false); - // PARSING + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; + } + }; - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); + /** + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add + */ + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; }); + } - // HELPERS - - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; + /** + * trigger dom event + * @param {String} event + * @param {Object} data + */ + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } - function offsetFromString(string) { - var matches = ((string || '').match(matchOffset) || []); - var chunk = matches[matches.length - 1] || []; - var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; - var minutes = +(parts[1] * 60) + toInt(parts[2]); + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - return parts[0] === '+' ? minutes : -minutes; - } + STATE_POSSIBLE: STATE_POSSIBLE, + STATE_BEGAN: STATE_BEGAN, + STATE_CHANGED: STATE_CHANGED, + STATE_ENDED: STATE_ENDED, + STATE_RECOGNIZED: STATE_RECOGNIZED, + STATE_CANCELLED: STATE_CANCELLED, + STATE_FAILED: STATE_FAILED, - // Return a moment from input, that is local/utc/zone equivalent to model. - function cloneWithOffset(input, model) { - var res, diff; - if (model._isUTC) { - res = model.clone(); - diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); - // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); - utils_hooks__hooks.updateOffset(res, false); - return res; - } else { - return local__createLocal(input).local(); - } - return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); - } + DIRECTION_NONE: DIRECTION_NONE, + DIRECTION_LEFT: DIRECTION_LEFT, + DIRECTION_RIGHT: DIRECTION_RIGHT, + DIRECTION_UP: DIRECTION_UP, + DIRECTION_DOWN: DIRECTION_DOWN, + DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, + DIRECTION_VERTICAL: DIRECTION_VERTICAL, + DIRECTION_ALL: DIRECTION_ALL, - function getDateOffset (m) { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(m._d.getTimezoneOffset() / 15) * 15; - } + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - // HOOKS + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - utils_hooks__hooks.updateOffset = function () {}; + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, - // MOMENTS + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - function getSetOffset (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = offsetFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = getDateOffset(this); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - utils_hooks__hooks.updateOffset(this, true); - this._changeInProgress = null; - } - } - return this; - } else { - return this._isUTC ? offset : getDateOffset(this); - } - } + if ("function" == TYPE_FUNCTION && __webpack_require__(16)) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { + return Hammer; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof module != 'undefined' && module.exports) { + module.exports = Hammer; + } else { + window[exportName] = Hammer; + } - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } + })(window, document, 'Hammer'); - this.utcOffset(input, keepLocalTime); - return this; - } else { - return -this.utcOffset(); - } - } +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + /* WEBPACK VAR INJECTION */}.call(exports, {})) - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; - } +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); - } - return this; - } + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + 'use strict'; - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); - } + module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(18); - return (this.utcOffset() - input) % 60 === 0; - } +/***/ }, +/* 18 */ +/***/ function(module, exports, __webpack_require__) { - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); - } + /* WEBPACK VAR INJECTION */(function(module) {//! moment.js + //! version : 2.10.3 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com - function isDaylightSavingTimeShifted () { - if (this._a) { - var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); - return this.isValid() && compareArrays(this._a, other.toArray()) > 0; - } + (function (global, factory) { + true ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() + }(this, function () { 'use strict'; - return false; - } + var hookCallback; - function isLocal () { - return !this._isUTC; + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); } - function isUtcOffset () { - return this._isUTC; + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; } - function isUtc () { - return this._isUTC && this._offset === 0; + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; } - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + } - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + } - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = create__isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); - - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; } - ret = new Duration(duration); + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; } - return ret; + return a; } - create__createDuration.fn = Duration.prototype; - - function parseIso (inp, sign) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); } - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; + function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso : false + }; + } - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); } + return m._pf; + } - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + function valid__isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m); + m._isValid = !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidMonth && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated; - return res; + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + return m._isValid; } - function momentsDifference(base, other) { - var res; - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; } - return res; + return m; } - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = create__createDuration(val, period); - add_subtract__addSubtract(this, dur, direction); - return this; - }; - } + var momentProperties = utils_hooks__hooks.momentProperties = []; - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + function copyConfig(to, from) { + var i, prop, val; - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; } - if (days) { - get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); + if (typeof from._i !== 'undefined') { + to._i = from._i; } - if (months) { - setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); + if (typeof from._f !== 'undefined') { + to._f = from._f; } - if (updateOffset) { - utils_hooks__hooks.updateOffset(mom, days || months); + if (typeof from._l !== 'undefined') { + to._l = from._l; + } + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; + } + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; + } + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; + } + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; + } + if (typeof from._pf !== 'undefined') { + to._pf = getParsingFlags(from); + } + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; } - } - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - function moment_calendar__calendar (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || local__createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this, local__createLocal(now))); + return to; } - function clone () { - return new Moment(this); - } + var updateInProgress = false; - function isAfter (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this > +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return inputMs < +this.clone().startOf(units); + // Moment prototype object + function Moment(config) { + copyConfig(this, config); + this._d = new Date(+config._d); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + utils_hooks__hooks.updateOffset(this); + updateInProgress = false; } } - function isBefore (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this < +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return +this.clone().endOf(units) < inputMs; - } + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); } - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - function isSame (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this === +input; - } else { - inputMs = +local__createLocal(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } } + + return value; } - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } } + return diffs + lengthDiff; } - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; + function Locale() { + } - units = normalizeUnits(units); + var locales = {}; + var globalLocale; - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } - } else { - delta = this - that; - output = units === 'second' ? delta / 1e3 : // 1000 - units === 'minute' ? delta / 6e4 : // 1000 * 60 - units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - delta; - } - return asFloat ? output : absFloor(output); + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; } - function monthDiff (a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, j, next, locale, split; - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; } + return null; + } - return -(wholeMonthDiff + adjust); + function loadLocale(name) { + var oldLocale = null; + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && typeof module !== 'undefined' && + module && module.exports) { + try { + oldLocale = globalLocale._abbr; + !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); + // because defineLocale currently also sets the global locale, we + // want to undo that for lazy loaded locales + locale_locales__getSetGlobalLocale(oldLocale); + } catch (e) { } + } + return locales[name]; } - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function locale_locales__getSetGlobalLocale (key, values) { + var data; + if (key) { + if (typeof values === 'undefined') { + data = locale_locales__getLocale(key); + } + else { + data = defineLocale(key, values); + } - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } + } + + return globalLocale._abbr; } - function moment_format__toISOString () { - var m = this.clone().utc(); - if (0 < m.year() && m.year() <= 9999) { - if ('function' === typeof Date.prototype.toISOString) { - // native implementation is ~50x faster, use it when we can - return this.toDate().toISOString(); - } else { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + function defineLocale (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); } + locales[name].set(values); + + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); + + return locales[name]; } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + // useful for testing + delete locales[name]; + return null; } } - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); - } + // returns locale data + function locale_locales__getLocale (key) { + var locale; - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - } - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); + return chooseLocale(key); } - function to (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + var aliases = {}; + + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; } - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; } - function locale (key) { - var newLocaleData; + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } } - return this; } + + return normalizedInput; } - var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); + function makeGetSet (unit, keepTime) { + return function (value) { + if (value != null) { + get_set__set(this, unit, value); + utils_hooks__hooks.updateOffset(this, keepTime); + return this; } else { - return this.locale(key); + return get_set__get(this, unit); } - } - ); + }; + } - function localeData () { - return this._locale; + function get_set__get (mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); } - function startOf (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - } + function get_set__set (mom, unit, value) { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } + // MOMENTS - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } else { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + return this[units](value); + } } - return this; } - function endOf (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; + function zeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; + + while (output.length < targetLength) { + output = '0' + output; } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + return (sign ? (forceSign ? '+' : '') : '-') + output; } - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); - } + var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; - function unix () { - return Math.floor(+this / 1000); - } + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - function toDate () { - return this._offset ? new Date(+this) : this._d; - } + var formatFunctions = {}; - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; - } + var formatTokenFunctions = {}; - function moment_valid__isValid () { - return valid__isValid(this); + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } } - function parsingFlags () { - return extend({}, getParsingFlags(this)); + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); } - function invalidAt () { - return getParsingFlags(this).overflow; + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ''; + for (i = 0; i < length; i++) { + output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; + } + return output; + }; } - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); + format = expandFormat(format, m.localeData()); - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); } - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + function expandFormat(format, locale) { + var i = 5; - // ALIASES + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - // PARSING + return format; + } - addRegexToken('G', matchSigned); - addRegexToken('g', matchSigned); - addRegexToken('GG', match1to2, match2); - addRegexToken('gg', match1to2, match2); - addRegexToken('GGGG', match1to4, match4); - addRegexToken('gggg', match1to4, match4); - addRegexToken('GGGGG', match1to6, match6); - addRegexToken('ggggg', match1to6, match6); + var match1 = /\d/; // 0 - 9 + var match2 = /\d\d/; // 00 - 99 + var match3 = /\d{3}/; // 000 - 999 + var match4 = /\d{4}/; // 0000 - 9999 + var match6 = /[+-]?\d{6}/; // -999999 - 999999 + var match1to2 = /\d\d?/; // 0 - 99 + var match1to3 = /\d{1,3}/; // 0 - 999 + var match1to4 = /\d{1,4}/; // 0 - 9999 + var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - // HELPERS + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; - } + // any word (or two) characters or numbers including two/three word month in arabic. + var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; - // MOMENTS + var regexes = {}; - function getSetWeekYear (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); + function addRegexToken (token, regex, strictRegex) { + regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; } - function getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - } + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); + return regexes[token](config._strict, config._locale); } - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function unescapeFormat(s) { + return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } - addFormatToken('Q', 0, 0, 'quarter'); - - // ALIASES - - addUnitAlias('quarter', 'Q'); - - // PARSING + var tokens = {}; - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); + function addParseToken (token, callback) { + var i, func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (typeof callback === 'number') { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } + } - // MOMENTS + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } } - addFormatToken('D', ['DD', 2], 'Do', 'date'); + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; - // ALIASES + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - addUnitAlias('date', 'D'); + // FORMATTING - // PARSING + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); }); - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); }); - // MOMENTS + // ALIASES - var getSetDayOfMonth = makeGetSet('Date', true); + addUnitAlias('month', 'M'); - addFormatToken('d', 0, 'do', 'day'); + // PARSING - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', matchWord); + addRegexToken('MMMM', matchWord); - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; }); - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); + addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } }); - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); - - // ALIASES + // LOCALES - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m) { + return this._months[m.month()]; + } - // PARSING + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m) { + return this._monthsShort[m.month()]; + } - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { - var weekday = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (weekday != null) { - week.d = weekday; - } else { - getParsingFlags(config).invalidWeekday = input; + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; } - }); - - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); - - // HELPERS - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; } } - return input; } - // LOCALES + // MOMENTS - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; + function setMonth (mom, value) { + var dayOfMonth; + + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; } - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; + function getSetMonth (value) { + if (value != null) { + setMonth(this, value); + utils_hooks__hooks.updateOffset(this, true); + return this; + } else { + return get_set__get(this, 'Month'); + } } - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); } - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; + function checkOverflow (m) { + var overflow; + var a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } - if (!this._weekdaysParse) { - this._weekdaysParse = []; + getParsingFlags(m).overflow = overflow; } - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = local__createLocal([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } + return m; } - // MOMENTS - - function getSetDayOfWeek (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); } } - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - } + function deprecate(msg, fn) { + var firstTime = true, + msgWithStack = msg + '\n' + (new Error()).stack; - function getSetISODayOfWeek (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + return extend(function () { + if (firstTime) { + warn(msgWithStack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); } - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); + var deprecations = {}; - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } } - meridiem('a', true); - meridiem('A', false); - - // ALIASES - - addUnitAlias('hour', 'h'); - - // PARSING + utils_hooks__hooks.suppressDeprecationWarnings = false; - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } + var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); + var isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ]; - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - getParsingFlags(config).bigHour = true; - }); + // iso time formats and regexes + var isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ]; - // LOCALES + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; - function localeIsPM (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - } + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = from_string__isoRegex.exec(string); - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; + if (match) { + getParsingFlags(config).iso = true; + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(string)) { + // match[5] should be 'T' or undefined + config._f = isoDates[i][0] + (match[6] || ' '); + break; + } + } + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (string.match(matchOffset)) { + config._f += 'Z'; + } + configFromStringAndFormat(config); } else { - return isLower ? 'am' : 'AM'; + config._isValid = false; } } + // date from iso format or fallback + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); - // MOMENTS - - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); - - addFormatToken('m', ['mm', 2], 0, 'minute'); + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } - // ALIASES + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); + } + } - addUnitAlias('minute', 'm'); + utils_hooks__hooks.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); - // PARSING + function createDate (y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } - // MOMENTS + function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } - var getSetMinute = makeGetSet('Minutes', false); + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); - addFormatToken('s', ['ss', 2], 0, 'second'); + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); // ALIASES - addUnitAlias('second', 's'); + addUnitAlias('year', 'y'); // PARSING - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); - - // MOMENTS - - var getSetSecond = makeGetSet('Seconds', false); + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); + addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); + addParseToken('YY', function (input, array) { + array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); }); - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); + // HELPERS - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; } - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); - - // ALIASES - - addUnitAlias('millisecond', 'ms'); + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } - // PARSING + // HOOKS - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); - addRegexToken('SSSS', matchUnsigned); - addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); - }); + utils_hooks__hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; // MOMENTS - var getSetMillisecond = makeGetSet('Milliseconds', false); - - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); - - // MOMENTS + var getSetYear = makeGetSet('FullYear', false); - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; + function getIsLeapYear () { + return isLeapYear(this.year()); } - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - var momentPrototype__proto = Moment.prototype; + // ALIASES - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.to = to; - momentPrototype__proto.toNow = toNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = moment_format__toISOString; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; + // PARSING - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + }); - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; + // HELPERS - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), + adjustedMoment; - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + // LOCALES - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; + var defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }; - // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); - momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + function localeFirstDayOfWeek () { + return this._week.dow; + } - var momentPrototype = momentPrototype__proto; + function localeFirstDayOfYear () { + return this._week.doy; + } - function moment__createUnix (input) { - return local__createLocal(input * 1000); + // MOMENTS + + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); } - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); } - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; - } + // ALIASES - var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }; + addUnitAlias('dayOfYear', 'DDD'); - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - } + // PARSING - var defaultInvalidDate = 'Invalid date'; + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); - function invalidDate () { - return this._invalidDate; - } + // HELPERS - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = createUTCDate(year, 0, 1).getUTCDay(); + var daysToAdd; + var dayOfYear; - function ordinal (number) { - return this._ordinal.replace('%d', number); - } + d = d === 0 ? 7 : d; + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - function preParsePostFormat (string) { - return string; + return { + year : dayOfYear > 0 ? year : year - 1, + dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; } - var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }; + // MOMENTS - function relative__relativeTime (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); + function getSetDayOfYear (input) { + var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); } - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { + return a; + } + if (b != null) { + return b; + } + return c; } - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); + return [now.getFullYear(), now.getMonth(), now.getDate()]; } - var prototype__proto = Locale.prototype; + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function configFromArray (config) { + var i, date, input = [], currentDate, yearToUse; - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; - prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; + if (config._d) { + return; + } - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + currentDate = currentDateArray(config); - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; - prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; + if (config._dayOfYear > daysInYear(yearToUse)) { + getParsingFlags(config)._overflowDayOfYear = true; + } - function lists__get (format, index, field, setter) { - var locale = locale_locales__getLocale(); - var utc = create_utc__createUTC().set(setter, index); - return locale[field](utc, format); - } + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; } - format = format || ''; + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } - if (index != null) { - return lists__get(format, index, field, setter); + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; } - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); } - return out; - } - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); + if (config._nextDay) { + config._a[HOUR] = 24; + } } - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); - } + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); - } + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); - } + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); - locale_locales__getSetGlobalLocale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < dow) { + ++week; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + } else { + // default to begining of week + weekday = dow; + } } - }); + temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - // Side effect imports - utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); - utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } - var mathAbs = Math.abs; + utils_hooks__hooks.ISO_8601 = function () {}; - function duration_abs__abs () { - var data = this._data; + // date from string and format string + function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === utils_hooks__hooks.ISO_8601) { + configFromISO(config); + return; + } - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + config._a = []; + getParsingFlags(config).empty = true; - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; - return this; - } + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + getParsingFlags(config).unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + getParsingFlags(config).empty = false; + } + else { + getParsingFlags(config).unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + getParsingFlags(config).unusedTokens.push(token); + } + } - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } - return duration._bubble(); - } + // clear _12h flag if hour is <= 12 + if (getParsingFlags(config).bigHour === true && + config._a[HOUR] <= 12 && + config._a[HOUR] > 0) { + getParsingFlags(config).bigHour = undefined; + } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { - return duration_add_subtract__addSubtract(this, input, value, 1); + configFromArray(config); + checkOverflow(config); } - // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); - } - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } + } - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; + function configFromStringAndArray(config) { + var tempConfig, + bestMoment, - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; + scoreToBeat, + i, + currentScore; - hours = absFloor(minutes / 60); - data.hours = hours % 24; + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } - days += absFloor(hours / 24); + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._f = config._f[i]; + configFromStringAndFormat(tempConfig); - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); + if (!valid__isValid(tempConfig)) { + continue; + } - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - data.days = days; - data.months = months; - data.years = years; + getParsingFlags(tempConfig).score = currentScore; - return this; - } + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; + extend(config, bestMoment || tempConfig); } - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; + function configFromObject(config) { + if (config._d) { + return; + } + + var i = normalizeObjectUnits(config._i); + config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; + + configFromArray(config); } - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; + function createFromConfig (config) { + var input = config._i, + format = config._f, + res; - units = normalizeUnits(units); + config._locale = config._locale || locale_locales__getLocale(config._l); - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); - } + if (input === null || (format === undefined && input === '')) { + return valid__createInvalid({nullInput: true}); } - } - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); - } + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isArray(format)) { + configFromStringAndArray(config); + } else if (format) { + configFromStringAndFormat(config); + } else if (isDate(input)) { + config._d = input; + } else { + configFromInput(config); + } - var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); + res = new Moment(checkOverflow(config)); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); + return res; } - function makeGetter(name) { - return function () { - return this._data[name]; - }; + function configFromInput(config) { + var input = config._i; + if (input === undefined) { + config._d = new Date(); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (typeof(input) === 'object') { + configFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + utils_hooks__hooks.createFromInputFallback(config); + } } - var duration_get__milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); - - function weeks () { - return absFloor(this.days() / 7); - } + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; - var round = Math.round; - var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }; + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + return createFromConfig(c); } - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { - var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); + } - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; + var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + return other < this ? this : other; + } + ); - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } + var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + return other > this ? this : other; + } + ); - // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; + // Pick a moment m from moments so that m[fn](other) is true for all + // other. This relies on the function fn to be transitive. + // + // moments should either be an array of moment objects or an array, whose + // first element is an array of moment objects. + function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; } - if (limit === undefined) { - return thresholds[threshold]; + if (!moments.length) { + return local__createLocal(); } - thresholds[threshold] = limit; - return true; + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (moments[i][fn](res)) { + res = moments[i]; + } + } + return res; } - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); - - if (withSuffix) { - output = locale.pastFuture(+this, output); - } + // TODO: Use [].sort instead? + function min () { + var args = [].slice.call(arguments, 0); - return locale.postformat(output); + return pickBy('isBefore', args); } - var iso_string__abs = Math.abs; + function max () { + var args = [].slice.call(arguments, 0); - function iso_string__toISOString() { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = iso_string__abs(this.years()); - var M = iso_string__abs(this.months()); - var D = iso_string__abs(this.days()); - var h = iso_string__abs(this.hours()); - var m = iso_string__abs(this.minutes()); - var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); - var total = this.asSeconds(); + return pickBy('isAfter', args); + } - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + function Duration (duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); - } + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; - var duration_prototype__proto = Duration.prototype; + this._data = {}; - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; - duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = duration_get__milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; + this._locale = locale_locales__getLocale(); - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; + this._bubble(); + } - // Side effect imports + function isDuration (obj) { + return obj instanceof Duration; + } - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); + function offset (token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(); + var sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + }); + } + + offset('Z', ':'); + offset('ZZ', ''); // PARSING - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); + addRegexToken('Z', matchOffset); + addRegexToken('ZZ', matchOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(input); }); - // Side effect imports + // HELPERS + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; - utils_hooks__hooks.version = '2.10.3'; + function offsetFromString(string) { + var matches = ((string || '').match(matchOffset) || []); + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var minutes = +(parts[1] * 60) + toInt(parts[2]); - setHookCallback(local__createLocal); + return parts[0] === '+' ? minutes : -minutes; + } - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; - utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; + // Return a moment from input, that is local/utc/zone equivalent to model. + function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + utils_hooks__hooks.updateOffset(res, false); + return res; + } else { + return local__createLocal(input).local(); + } + return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); + } - var _moment = utils_hooks__hooks; + function getDateOffset (m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset() / 15) * 15; + } - return _moment; + // HOOKS - })); - /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(17)(module))) + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + utils_hooks__hooks.updateOffset = function () {}; -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { + // MOMENTS - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + utils_hooks__hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + this.utcOffset(input, keepLocalTime); - /* WEBPACK VAR INJECTION */(function(global) {'use strict'; + return this; + } else { + return -this.utcOffset(); + } + } - var _rng; + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } - var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - if (globalVar && globalVar.crypto && crypto.getRandomValues) { - // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto - // Moderately fast, high quality - var _rnds8 = new Uint8Array(16); - _rng = function whatwgRNG() { - crypto.getRandomValues(_rnds8); - return _rnds8; - }; - } + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } - if (!_rng) { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var _rnds = new Array(16); - _rng = function () { - for (var i = 0, r; i < 16; i++) { - if ((i & 3) === 0) r = Math.random() * 4294967296; - _rnds[i] = r >>> ((i & 3) << 3) & 255; + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(this._i)); + } + return this; } - return _rnds; - }; - } + function hasAlignedHourOffset (input) { + if (!input) { + input = 0; + } + else { + input = local__createLocal(input).utcOffset(); + } - // uuid.js - // - // Copyright (c) 2010-2012 Robert Kieffer - // MIT License - http://opensource.org/licenses/mit-license.php + return (this.utcOffset() - input) % 60 === 0; + } - // Unique ID creation requires a high quality random # generator. We feature - // detect to determine the best RNG source, normalizing to a function that - // returns 128-bits of randomness, since that's what's usually required + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } - //var _rng = require('./rng'); + function isDaylightSavingTimeShifted () { + if (this._a) { + var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); + return this.isValid() && compareArrays(this._a, other.toArray()) > 0; + } - // Maps for number <-> hex string conversion - var _byteToHex = []; - var _hexToByte = {}; - for (var i = 0; i < 256; i++) { - _byteToHex[i] = (i + 256).toString(16).substr(1); - _hexToByte[_byteToHex[i]] = i; - } + return false; + } - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = buf && offset || 0, - ii = 0; + function isLocal () { + return !this._isUTC; + } - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { - if (ii < 16) { - // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; + function isUtcOffset () { + return this._isUTC; } - }); - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } + function isUtc () { + return this._isUTC && this._offset === 0; + } - return buf; - } + var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - // **`unparse()` - Convert UUID byte array (ala parse()) into a string** - function unparse(buf, offset) { - var i = offset || 0, - bth = _byteToHex; - return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]]; - } + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html + function create__createDuration (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; + + if (isDuration(input)) { + duration = { + ms : input._milliseconds, + d : input._days, + M : input._months + }; + } else if (typeof input === 'number') { + duration = {}; + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y : 0, + d : toInt(match[DATE]) * sign, + h : toInt(match[HOUR]) * sign, + m : toInt(match[MINUTE]) * sign, + s : toInt(match[SECOND]) * sign, + ms : toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = create__isoRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y : parseIso(match[2], sign), + M : parseIso(match[3], sign), + d : parseIso(match[4], sign), + h : parseIso(match[5], sign), + m : parseIso(match[6], sign), + s : parseIso(match[7], sign), + w : parseIso(match[8], sign) + }; + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - var _nodeId = [_seedBytes[0] | 1, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]]; + return ret; + } - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; + create__createDuration.fn = Duration.prototype; - // Previous uuid creation time - var _lastMSecs = 0, - _lastNSecs = 0; + function parseIso (inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + } - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - options = options || {}; + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' - // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. - var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + return res; + } - // Per 4.2.1.2, use count of uuid's generated during the current clock - // cycle to simulate higher resolution clock - var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + function momentsDifference(base, other) { + var res; + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } - // Time since last uuid creation (in msecs) - var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; + return res; + } - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq === undefined) { - clockseq = clockseq + 1 & 16383; - } + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; + } - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { - nsecs = 0; - } + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); + return this; + }; + } - // Per 4.2.1.2 Throw error if too many uuids are requested - if (nsecs >= 10000) { - throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); - } + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); + } + if (months) { + setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + utils_hooks__hooks.updateOffset(mom, days || months); + } + } - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); - // `time_low` - var tl = ((msecs & 268435455) * 10000 + nsecs) % 4294967296; - b[i++] = tl >>> 24 & 255; - b[i++] = tl >>> 16 & 255; - b[i++] = tl >>> 8 & 255; - b[i++] = tl & 255; + function moment_calendar__calendar (time) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || local__createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.localeData().calendar(format, this, local__createLocal(now))); + } - // `time_mid` - var tmh = msecs / 4294967296 * 10000 & 268435455; - b[i++] = tmh >>> 8 & 255; - b[i++] = tmh & 255; + function clone () { + return new Moment(this); + } - // `time_high_and_version` - b[i++] = tmh >>> 24 & 15 | 16; // include version - b[i++] = tmh >>> 16 & 255; + function isAfter (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this > +input; + } else { + inputMs = isMoment(input) ? +input : +local__createLocal(input); + return inputMs < +this.clone().startOf(units); + } + } - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 128; + function isBefore (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this < +input; + } else { + inputMs = isMoment(input) ? +input : +local__createLocal(input); + return +this.clone().endOf(units) < inputMs; + } + } - // `clock_seq_low` - b[i++] = clockseq & 255; + function isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + } - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } + function isSame (input, units) { + var inputMs; + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this === +input; + } else { + inputMs = +local__createLocal(input); + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + } - return buf ? buf : unparse(b); - } + function absFloor (number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } - // **`v4()` - Generate random UUID** + function diff (input, units, asFloat) { + var that = cloneWithOffset(input, this), + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, + delta, output; - // See https://github.com/broofa/node-uuid for API details - function v4(options, buf, offset) { - // Deprecated - 'format' argument, as supported in v1.2 - var i = buf && offset || 0; + units = normalizeUnits(units); - if (typeof options == 'string') { - buf = options == 'binary' ? new Array(16) : null; - options = null; - } - options = options || {}; + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + delta = this - that; + output = units === 'second' ? delta / 1e3 : // 1000 + units === 'minute' ? delta / 6e4 : // 1000 * 60 + units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + delta; + } + return asFloat ? output : absFloor(output); + } - var rnds = options.random || (options.rng || _rng)(); + function monthDiff (a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = rnds[6] & 15 | 64; - rnds[8] = rnds[8] & 63 | 128; + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; + return -(wholeMonthDiff + adjust); } - } - return buf || unparse(rnds); - } - - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - module.exports = uuid; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + function moment_format__toISOString () { + var m = this.clone().utc(); + if (0 < m.year() && m.year() <= 9999) { + if ('function' === typeof Date.prototype.toISOString) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } - // DOM utility methods + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); + } - /** - * this prepares the JSON container for allocating SVG elements - * @param JSONcontainer - * @private - */ - 'use strict'; + function from (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + } - exports.prepareElements = function (JSONcontainer) { - // cleanup the redundant svgElements; - for (var elementType in JSONcontainer) { - if (JSONcontainer.hasOwnProperty(elementType)) { - JSONcontainer[elementType].redundant = JSONcontainer[elementType].used; - JSONcontainer[elementType].used = []; + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); } - } - }; - /** - * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from - * which to remove the redundant elements. - * - * @param JSONcontainer - * @private - */ - exports.cleanupElements = function (JSONcontainer) { - // cleanup the redundant svgElements; - for (var elementType in JSONcontainer) { - if (JSONcontainer.hasOwnProperty(elementType)) { - if (JSONcontainer[elementType].redundant) { - for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { - JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); + function to (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); } - JSONcontainer[elementType].redundant = []; - } + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); } - } - }; - /** - * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer - * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. - * - * @param elementType - * @param JSONcontainer - * @param svgContainer - * @returns {*} - * @private - */ - exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { - var element; - // allocate SVG element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { - // this element has been created before - // check if there is an redundant element - if (JSONcontainer[elementType].redundant.length > 0) { - element = JSONcontainer[elementType].redundant[0]; - JSONcontainer[elementType].redundant.shift(); - } else { - // create a new element and add it to the SVG - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - svgContainer.appendChild(element); + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); } - } else { - // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - JSONcontainer[elementType] = { used: [], redundant: [] }; - svgContainer.appendChild(element); - } - JSONcontainer[elementType].used.push(element); - return element; - }; - /** - * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer - * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. - * - * @param elementType - * @param JSONcontainer - * @param DOMContainer - * @returns {*} - * @private - */ - exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { - var element; - // allocate DOM element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { - // this element has been created before - // check if there is an redundant element - if (JSONcontainer[elementType].redundant.length > 0) { - element = JSONcontainer[elementType].redundant[0]; - JSONcontainer[elementType].redundant.shift(); - } else { - // create a new element and add it to the SVG - element = document.createElement(elementType); - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); - } + function locale (key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } } - } else { - // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElement(elementType); - JSONcontainer[elementType] = { used: [], redundant: [] }; - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); + + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); + + function localeData () { + return this._locale; } - } - JSONcontainer[elementType].used.push(element); - return element; - }; - /** - * draw a point object. this is a seperate function because it can also be called by the legend. - * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions - * as well. - * - * @param x - * @param y - * @param group - * @param JSONcontainer - * @param svgContainer - * @param labelObj - * @returns {*} - */ - exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { - var point; - if (group.options.drawPoints.style == 'circle') { - point = exports.getSVGElement('circle', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'cx', x); - point.setAttributeNS(null, 'cy', y); - point.setAttributeNS(null, 'r', 0.5 * group.options.drawPoints.size); - } else { - point = exports.getSVGElement('rect', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'x', x - 0.5 * group.options.drawPoints.size); - point.setAttributeNS(null, 'y', y - 0.5 * group.options.drawPoints.size); - point.setAttributeNS(null, 'width', group.options.drawPoints.size); - point.setAttributeNS(null, 'height', group.options.drawPoints.size); - } + function startOf (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } + + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } + + return this; + } + + function endOf (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + } - if (group.options.drawPoints.styles !== undefined) { - point.setAttributeNS(null, 'style', group.group.options.drawPoints.styles); - } - point.setAttributeNS(null, 'class', group.className + ' vis-point'); - //handle label + function to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); + } - if (labelObj) { - var label = exports.getSVGElement('text', JSONcontainer, svgContainer); - if (labelObj.xOffset) { - x = x + labelObj.xOffset; + function unix () { + return Math.floor(+this / 1000); } - if (labelObj.yOffset) { - y = y + labelObj.yOffset; + function toDate () { + return this._offset ? new Date(+this) : this._d; } - if (labelObj.content) { - label.textContent = labelObj.content; + + function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; } - if (labelObj.className) { - label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); + function moment_valid__isValid () { + return valid__isValid(this); } - label.setAttributeNS(null, 'x', x); - label.setAttributeNS(null, 'y', y); - } - return point; - }; + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } - /** - * draw a bar SVG element centered on the X coordinate - * - * @param x - * @param y - * @param className - */ - exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) { - if (height != 0) { - if (height < 0) { - height *= -1; - y -= height; + function invalidAt () { + return getParsingFlags(this).overflow; } - var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer); - rect.setAttributeNS(null, 'x', x - 0.5 * width); - rect.setAttributeNS(null, 'y', y); - rect.setAttributeNS(null, 'width', width); - rect.setAttributeNS(null, 'height', height); - rect.setAttributeNS(null, 'class', className); - if (style) { - rect.setAttributeNS(null, 'style', style); + + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); + + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); + + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); } - } - }; -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - 'use strict'; + // ALIASES - var util = __webpack_require__(14); - var Queue = __webpack_require__(21); + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); - /** - * DataSet - * - * Usage: - * var dataSet = new DataSet({ - * fieldId: '_id', - * type: { - * // ... - * } - * }); - * - * dataSet.add(item); - * dataSet.add(data); - * dataSet.update(item); - * dataSet.update(data); - * dataSet.remove(id); - * dataSet.remove(ids); - * var data = dataSet.get(); - * var data = dataSet.get(id); - * var data = dataSet.get(ids); - * var data = dataSet.get(ids, options, data); - * dataSet.clear(); - * - * A data set can: - * - add/remove/update data - * - gives triggers upon changes in the data - * - can import/export data in various data formats - * - * @param {Array} [data] Optional array with initial data - * @param {Object} [options] Available options: - * {String} fieldId Field name of the id in the - * items, 'id' by default. - * {Object.} [type] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by a field name or custom sort function. - * @throws Error - */ - DataSet.prototype.get = function (args) { - var me = this; + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); - // parse the arguments - var id, ids, options; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { - // get(id [, options]) - id = arguments[0]; - options = arguments[1]; - } else if (firstType == 'Array') { - // get(ids [, options]) - ids = arguments[0]; - options = arguments[1]; - } else { - // get([, options]) - options = arguments[0]; - } + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); - // determine the return type - var returnType; - if (options && options.returnType) { - var allowedValues = ['Array', 'Object']; - returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; - } else { - returnType = 'Array'; - } + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); - // convert items - if (id != undefined) { - // return a single item - item = me._getItem(id, type); - if (filter && !filter(item)) { - item = null; - } - } else if (ids != undefined) { - // return a subset of items - for (i = 0, len = ids.length; i < len; i++) { - item = me._getItem(ids[i], type); - if (!filter || filter(item)) { - items.push(item); - } - } - } else { - // return all items - for (itemId in this._data) { - if (this._data.hasOwnProperty(itemId)) { - item = me._getItem(itemId, type); - if (!filter || filter(item)) { - items.push(item); + // ALIASES + + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); + + // PARSING + + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { + var weekday = config._locale.weekdaysParse(input); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = locale.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } } - } + return input; } - } - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } + // LOCALES - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m) { + return this._weekdays[m.day()]; } - } - // return the results - if (returnType == 'Object') { - var result = {}; - for (i = 0; i < items.length; i++) { - result[items[i].id] = items[i]; + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; } - return result; - } else { - if (id != undefined) { - // a single item - return item; - } else { - // just return our array - return items; + + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; } - } - }; - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataSet.prototype.getIds = function (options) { - var data = this._data, - filter = options && options.filter, - order = options && options.order, - type = options && options.type || this._options.type, - i, - len, - id, - item, - items, - ids = []; + function localeWeekdaysParse (weekdayName) { + var i, mom, regex; - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - items.push(item); - } + if (!this._weekdaysParse) { + this._weekdaysParse = []; } - } - - this._sort(items, order); - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - ids.push(item[this._fieldId]); - } + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = local__createLocal([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } } - } } - } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); - } - } - this._sort(items, order); + // MOMENTS - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = data[id]; - ids.push(item[this._fieldId]); + function getSetDayOfWeek (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; } - } } - } - return ids; - }; + function getSetLocaleDayOfWeek (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } - /** - * Returns the DataSet itself. Is overwritten for example by the DataView, - * which returns the DataSet it is connected to instead. - */ - DataSet.prototype.getDataSet = function () { - return this; - }; + function getSetISODayOfWeek (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + } - /** - * Execute a callback function for every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - */ - DataSet.prototype.forEach = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - data = this._data, - item, - id; + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, function () { + return this.hours() % 12 || 12; + }); - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); + } - for (var i = 0, len = items.length; i < len; i++) { - item = items[i]; - id = item[this._fieldId]; - callback(item, id); + meridiem('a', true); + meridiem('A', false); + + // ALIASES + + addUnitAlias('hour', 'h'); + + // PARSING + + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; } - } else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - callback(item, id); + + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); + + addParseToken(['H', 'HH'], HOUR); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; + }); + + // LOCALES + + function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + } + + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; } - } } - } - }; - /** - * Map every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Object[]} mappedItems - */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } - } + // MOMENTS - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + var getSetHour = makeGetSet('Hours', true); - return mappedItems; - }; + addFormatToken('m', ['mm', 2], 0, 'minute'); - /** - * Filter the fields of an item - * @param {Object | null} item - * @param {String[]} fields Field names - * @return {Object | null} filteredItem or null if no item is provided - * @private - */ - DataSet.prototype._filterFields = function (item, fields) { - if (!item) { - // item is null - return item; - } + // ALIASES - var filteredItem = {}; + addUnitAlias('minute', 'm'); - if (Array.isArray(fields)) { - for (var field in item) { - if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { - filteredItem[field] = item[field]; - } - } - } else { - for (var field in item) { - if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { - filteredItem[fields[field]] = item[field]; - } - } - } + // PARSING - return filteredItem; - }; + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - /** - * Sort the provided array with items - * @param {Object[]} items - * @param {String | function} order A field name or custom sort function. - * @private - */ - DataSet.prototype._sort = function (items, order) { - if (util.isString(order)) { - // order by provided field name - var name = order; // field name - items.sort(function (a, b) { - var av = a[name]; - var bv = b[name]; - return av > bv ? 1 : av < bv ? -1 : 0; + // MOMENTS + + var getSetMinute = makeGetSet('Minutes', false); + + addFormatToken('s', ['ss', 2], 0, 'second'); + + // ALIASES + + addUnitAlias('second', 's'); + + // PARSING + + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); + + // MOMENTS + + var getSetSecond = makeGetSet('Seconds', false); + + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); }); - } else if (typeof order === 'function') { - // order by sort function - items.sort(order); - } - // TODO: extend order by an Object {field:String, direction:String} - // where direction can be 'asc' or 'desc' - else { - throw new TypeError('Order must be a function or a string'); - } - }; - /** - * Remove an object by pointer or by id - * @param {String | Number | Object | Array} id Object or id, or an array with - * objects or ids to be removed - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds - */ - DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, - len, - removedId; + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); - if (Array.isArray(id)) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); - } - } - } else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); + function millisecond__milliseconds (token) { + addFormatToken(0, [token, 3], 0, 'millisecond'); } - } - if (removedIds.length) { - this._trigger('remove', { items: removedIds }, senderId); - } + millisecond__milliseconds('SSS'); + millisecond__milliseconds('SSSS'); - return removedIds; - }; + // ALIASES - /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private - */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; + addUnitAlias('millisecond', 'ms'); + + // PARSING + + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + addRegexToken('SSSS', matchUnsigned); + addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + }); + + // MOMENTS + + var getSetMillisecond = makeGetSet('Milliseconds', false); + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; + + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; } - } - return null; - }; - /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items - */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); + var momentPrototype__proto = Moment.prototype; + + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.to = to; + momentPrototype__proto.toNow = toNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = moment_format__toISOString; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; + + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; + + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; + + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; + + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - this._data = {}; - this.length = 0; + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - this._trigger('remove', { items: ids }, senderId); + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - return ids; - }; + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - /** - * Find the item with maximum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.max = function (field) { - var data = this._data, - max = null, - maxField = null; + // Offset + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!max || itemField > maxField)) { - max = item; - maxField = itemField; - } - } - } + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; - return max; - }; + // Deprecations + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); - /** - * Find the item with minimum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.min = function (field) { - var data = this._data, - min = null, - minField = null; + var momentPrototype = momentPrototype__proto; - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!min || itemField < minField)) { - min = item; - minField = itemField; - } + function moment__createUnix (input) { + return local__createLocal(input * 1000); } - } - return min; - }; + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); + } - /** - * Find all distinct values of a specified field - * @param {String} field - * @return {Array} values Array containing all distinct values. If data items - * do not contain the specified field are ignored. - * The returned array is unordered. - */ - DataSet.prototype.distinct = function (field) { - var data = this._data; - var values = []; - var fieldType = this._options.type && this._options.type[field] || null; - var count = 0; - var i; + var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }; - for (var prop in data) { - if (data.hasOwnProperty(prop)) { - var item = data[prop]; - var value = item[field]; - var exists = false; - for (i = 0; i < count; i++) { - if (values[i] == value) { - exists = true; - break; - } - } - if (!exists && value !== undefined) { - values[count] = value; - count++; - } + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.call(mom, now) : output; } - } - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } - } + var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY LT', + LLLL : 'dddd, MMMM D, YYYY LT' + }; - return values; - }; + function longDateFormat (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + } - /** - * Add a single item. Will fail when an item with the same id already exists. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._addItem = function (item) { - var id = item[this._fieldId]; + var defaultInvalidDate = 'Invalid date'; - if (id != undefined) { - // check whether this id is already taken - if (this._data[id]) { - // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); + function invalidDate () { + return this._invalidDate; } - } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; - } - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); } - } - this._data[id] = d; - this.length++; - return id; - }; + function preParsePostFormat (string) { + return string; + } - /** - * Get an item. Fields can be converted to a specific type - * @param {String} id - * @param {Object.} [types] field types to convert - * @return {Object | null} item - * @private - */ - DataSet.prototype._getItem = function (id, types) { - var field, value; + var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }; - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; - } + function relative__relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + } - // convert the items field types - var converted = {}; - if (types) { - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = util.convert(value, types[field]); - } + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); } - } else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = value; - } + + function locale_set__set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); } - } - return converted; - }; - /** - * Update a single item: merge with existing item. - * Will fail when the item has no id, or when there does not exist an item - * with the same id. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._updateItem = function (item) { - var id = item[this._fieldId]; - if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); - } - var d = this._data[id]; - if (!d) { - // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); - } + var prototype__proto = Locale.prototype; - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._longDateFormat = defaultLongDateFormat; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; - return id; - }; + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; - module.exports = DataSet; + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { + // Day of Week + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; + prototype__proto.weekdaysParse = localeWeekdaysParse; - /** - * A queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @constructor - */ - 'use strict'; + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; + function lists__get (format, index, field, setter) { + var locale = locale_locales__getLocale(); + var utc = create_utc__createUTC().set(setter, index); + return locale[field](utc, format); + } - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; + } - this.setOptions(options); - } + format = format || ''; - /** - * Update the configuration of the queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @param options - */ - Queue.prototype.setOptions = function (options) { - if (options && typeof options.delay !== 'undefined') { - this.delay = options.delay; - } - if (options && typeof options.max !== 'undefined') { - this.max = options.max; - } + if (index != null) { + return lists__get(format, index, field, setter); + } - this._flushIfNeeded(); - }; + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; + } - /** - * Extend an object with queuing functionality. - * The object will be extended with a function flush, and the methods provided - * in options.replace will be replaced with queued ones. - * @param {Object} object - * @param {Object} options - * Available options: - * - replace: Array. - * A list with method names of the methods - * on the object to be replaced with queued ones. - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @return {Queue} Returns the created queue - */ - Queue.extend = function (object, options) { - var queue = new Queue(options); + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } - if (object.flush !== undefined) { - throw new Error('Target object already has a property flush'); - } - object.flush = function () { - queue.flush(); - }; + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } - var methods = [{ - name: 'flush', - original: undefined - }]; + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } - if (options && options.replace) { - for (var i = 0; i < options.replace.length; i++) { - var name = options.replace[i]; - methods.push({ - name: name, - original: object[name] - }); - queue.replace(object, name); + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); } - } - queue._extended = { - object: object, - methods: methods - }; + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); + } - return queue; - }; + locale_locales__getSetGlobalLocale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); - /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. - */ - Queue.prototype.destroy = function () { - this.flush(); + // Side effect imports + utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); + utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - if (this._extended) { - var object = this._extended.object; - var methods = this._extended.methods; - for (var i = 0; i < methods.length; i++) { - var method = methods[i]; - if (method.original) { - object[method.name] = method.original; - } else { - delete object[method.name]; - } - } - this._extended = null; - } - }; + var mathAbs = Math.abs; - /** - * Replace a method on an object with a queued version - * @param {Object} object Object having the method - * @param {string} method The method name - */ - Queue.prototype.replace = function (object, method) { - var me = this; - var original = object[method]; - if (!original) { - throw new Error('Method ' + method + ' undefined'); - } + function duration_abs__abs () { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; } - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this - }); - }; - }; + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); - /** - * Queue a call - * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry - */ - Queue.prototype.queue = function (entry) { - if (typeof entry === 'function') { - this._queue.push({ fn: entry }); - } else { - this._queue.push(entry); - } + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; - this._flushIfNeeded(); - }; + return duration._bubble(); + } - /** - * Check whether the queue needs to be flushed - * @private - */ - Queue.prototype._flushIfNeeded = function () { - // flush when the maximum is exceeded. - if (this._queue.length > this.max) { - this.flush(); - } + // supports only 2.0-style add(1, 's') or add(duration) + function duration_add_subtract__add (input, value) { + return duration_add_subtract__addSubtract(this, input, value, 1); + } - // flush after a period of inactivity when a delay is configured - clearTimeout(this._timeout); - if (this.queue.length > 0 && typeof this.delay === 'number') { - var me = this; - this._timeout = setTimeout(function () { - me.flush(); - }, this.delay); - } - }; + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function duration_add_subtract__subtract (input, value) { + return duration_add_subtract__addSubtract(this, input, value, -1); + } - /** - * Flush all queued calls - */ - Queue.prototype.flush = function () { - while (this._queue.length > 0) { - var entry = this._queue.shift(); - entry.fn.apply(entry.context || entry.fn, entry.args || []); - } - }; + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years = 0; - module.exports = Queue; + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - 'use strict'; + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - var util = __webpack_require__(14); - var DataSet = __webpack_require__(20); + hours = absFloor(minutes / 60); + data.hours = hours % 24; - /** - * DataView - * - * a dataview offers a filtered view on a dataset or an other dataview. - * - * @param {DataSet | DataView} data - * @param {Object} [options] Available options: see method get - * - * @constructor DataView - */ - function DataView(data, options) { - this._data = null; - this._ids = {}; // ids of the items currently in memory (just contains a boolean true) - this.length = 0; // number of items in the DataView - this._options = options || {}; - this._fieldId = 'id'; // name of the field containing id - this._subscribers = {}; // event subscribers + days += absFloor(hours / 24); - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + // Accurately convert days to years, assume start from year 0. + years = absFloor(daysToYears(days)); + days -= absFloor(yearsToDays(years)); - this.setData(data); - } + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absFloor(days / 30); + days %= 30; - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + // 12 months -> 1 year + years += absFloor(months / 12); + months %= 12; - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + data.days = days; + data.months = months; + data.years = years; - if (this._data) { - // unsubscribe from current dataset - if (this._data.off) { - this._data.off('*', this.listener); + return this; } - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; } - this._ids = {}; - this.length = 0; - this._trigger('remove', { items: ids }); - } - this._data = data; + function yearsToDays (years) { + // years * 365 + absFloor(years / 4) - + // absFloor(years / 100) + absFloor(years / 400); + return years * 146097 / 400; + } - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; - // trigger an add of all added items - ids = this._data.getIds({ filter: this._options && this._options.filter }); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this._ids[id] = true; + units = normalizeUnits(units); + + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToYears(days) * 12; + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(yearsToDays(this._months / 12)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 1440 + milliseconds / 6e4; + case 'second' : return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 864e5) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } } - this.length = ids.length; - this._trigger('add', { items: ids }); - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); } - } - }; - /** - * Refresh the DataView. Useful when the DataView has a filter function - * containing a variable parameter. - */ - DataView.prototype.refresh = function () { - var id; - var ids = this._data.getIds({ filter: this._options && this._options.filter }); - var newIds = {}; - var added = []; - var removed = []; + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } - // check for additions - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - newIds[id] = true; - if (!this._ids[id]) { - added.push(id); - this._ids[id] = true; - this.length++; + var asMilliseconds = makeAs('ms'); + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); + + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); } - } - // check for removals - for (id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - if (!newIds[id]) { - removed.push(id); - delete this._ids[id]; - this.length--; - } + function makeGetter(name) { + return function () { + return this._data[name]; + }; } - } - // trigger events - if (added.length) { - this._trigger('add', { items: added }); - } - if (removed.length) { - this._trigger('remove', { items: removed }); - } - }; + var duration_get__milliseconds = makeGetter('milliseconds'); + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); - /** - * Get data from the data view - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number) - * get(id: Number, options: Object) - * get(id: Number, options: Object, data: Array | DataTable) - * - * get(ids: Number[]) - * get(ids: Number[], options: Object) - * get(ids: Number[], options: Object, data: Array | DataTable) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [type] Type of data to be returned. Can - * be 'DataTable' or 'Array' (default) - * {Object.} [convert] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * {Array | DataTable} [data] If provided, items will be appended to this - * array or table. Required in case of Google - * DataTable. - * @param args - */ - DataView.prototype.get = function (args) { - var me = this; + function weeks () { + return absFloor(this.days() / 7); + } - // parse the arguments - var ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { - // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids - options = arguments[1]; - data = arguments[2]; - } else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } + var round = Math.round; + var thresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }; - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } - // create a combined filter method when needed - if (this._options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me._options.filter(item) && options.filter(item); - }; - } + function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + var duration = create__createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + var a = seconds < thresholds.s && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours === 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days === 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months === 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years === 1 && ['y'] || ['yy', years]; - return this._data && this._data.get.apply(this._data, getArguments); - }; + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataView.prototype.getIds = function (options) { - var ids; + // This function allows you to set a threshold for relative time strings + function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + return true; + } - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); } - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; - } + var iso_string__abs = Math.abs; - return ids; - }; + function iso_string__toISOString() { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = iso_string__abs(this.years()); + var M = iso_string__abs(this.months()); + var D = iso_string__abs(this.days()); + var h = iso_string__abs(this.hours()); + var m = iso_string__abs(this.minutes()); + var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); + var total = this.asSeconds(); - /** - * Get the DataSet to which this DataView is connected. In case there is a chain - * of multiple DataViews, the root DataSet of this chain is returned. - * @return {DataSet} dataSet - */ - DataView.prototype.getDataSet = function () { - var dataSet = this; - while (dataSet instanceof DataView) { - dataSet = dataSet._data; - } - return dataSet || null; - }; + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - /** - * Event listener. Will propagate all events from the connected data set to - * the subscribers of the DataView, but will filter the items and only trigger - * when there are changes in the filtered data set. - * @param {String} event - * @param {Object | null} params - * @param {String} senderId - * @private - */ - DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item; - var ids = params && params.items; - var data = this._data; - var updatedData = []; - var added = []; - var updated = []; - var removed = []; + return (total < 0 ? '-' : '') + + 'P' + + (Y ? Y + 'Y' : '') + + (M ? M + 'M' : '') + + (D ? D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? h + 'H' : '') + + (m ? m + 'M' : '') + + (s ? s + 'S' : ''); + } + + var duration_prototype__proto = Duration.prototype; + + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = duration_get__milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; - if (ids && data) { - switch (event) { - case 'add': - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this._ids[id] = true; - added.push(id); - } - } + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; - break; + // Side effect imports - case 'update': - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); - if (item) { - if (this._ids[id]) { - updated.push(id); - updatedData.push(params.data[i]); - } else { - this._ids[id] = true; - added.push(id); - } - } else { - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } else {} - } - } + // PARSING - break; + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); - case 'remove': - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } - } + // Side effect imports - break; - } - this.length += added.length - removed.length; + utils_hooks__hooks.version = '2.10.3'; - if (added.length) { - this._trigger('add', { items: added }, senderId); - } - if (updated.length) { - this._trigger('update', { items: updated, data: updatedData }, senderId); - } - if (removed.length) { - this._trigger('remove', { items: removed }, senderId); - } - } - }; + setHookCallback(local__createLocal); - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) - DataView.prototype.subscribe = DataView.prototype.on; - DataView.prototype.unsubscribe = DataView.prototype.off; + var _moment = utils_hooks__hooks; - module.exports = DataView; + return _moment; - // nothing interesting for me :-( + })); + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(19)(module))) /***/ }, -/* 23 */ +/* 19 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; - - var Emitter = __webpack_require__(25); - var DataSet = __webpack_require__(20); - var DataView = __webpack_require__(22); - var util = __webpack_require__(14); - var Point3d = __webpack_require__(26); - var Point2d = __webpack_require__(24); - var Camera = __webpack_require__(27); - var Filter = __webpack_require__(28); - var Slider = __webpack_require__(29); - var StepNumber = __webpack_require__(30); + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } - /** - * @constructor Graph3d - * Graph3d displays data in 3d. - * - * Graph3d is developed in javascript as a Google Visualization Chart. - * - * @param {Element} container The DOM element in which the Graph3d will - * be created. Normally a div element. - * @param {DataSet | DataView | Array} [data] - * @param {Object} [options] - */ - function Graph3d(container, data, options) { - if (!(this instanceof Graph3d)) { - throw new SyntaxError('Constructor must be called with the new operator'); - } - // create variables and set default values - this.containerElement = container; - this.width = '400px'; - this.height = '400px'; - this.margin = 10; // px - this.defaultXCenter = '55%'; - this.defaultYCenter = '50%'; +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; + /* WEBPACK VAR INJECTION */(function(global) {'use strict'; - var passValueFn = function passValueFn(v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + var _rng; - this.filterLabel = 'time'; - this.legendLabel = 'value'; + var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; - this.style = Graph3d.STYLE.DOT; - this.showPerspective = true; - this.showGrid = true; - this.keepAspectRatio = true; - this.showShadow = false; - this.showGrayBottom = false; // TODO: this does not work correctly - this.showTooltip = false; - this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' + if (globalVar && globalVar.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + _rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; + } - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + if (!_rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + _rng = function () { + for (var i = 0, r; i < 16; i++) { + if ((i & 3) === 0) r = Math.random() * 4294967296; + _rnds[i] = r >>> ((i & 3) << 3) & 255; + } - this.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + return _rnds; + }; + } - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + // uuid.js + // + // Copyright (c) 2010-2012 Robert Kieffer + // MIT License - http://opensource.org/licenses/mit-license.php - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + // Unique ID creation requires a high quality random # generator. We feature + // detect to determine the best RNG source, normalizing to a function that + // returns 128-bits of randomness, since that's what's usually required - this.xMin = 0; - this.xStep = undefined; // auto by default - this.xMax = 1; - this.yMin = 0; - this.yStep = undefined; // auto by default - this.yMax = 1; - this.zMin = 0; - this.zStep = undefined; // auto by default - this.zMax = 1; - this.valueMin = 0; - this.valueMax = 1; - this.xBarWidth = 1; - this.yBarWidth = 1; - // TODO: customize axis range + //var _rng = require('./rng'); - // constants - this.colorAxis = '#4D4D4D'; - this.colorGrid = '#D3D3D3'; - this.colorDot = '#7DC1FF'; - this.colorDotBorder = '#3267D2'; + // Maps for number <-> hex string conversion + var _byteToHex = []; + var _hexToByte = {}; + for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 256).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; + } - // create a frame and canvas - this.create(); + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = buf && offset || 0, + ii = 0; - // apply options (also when undefined) - this.setOptions(options); + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { + if (ii < 16) { + // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); - // apply data - if (data) { - this.setData(data); + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; } + + return buf; } - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + // **`unparse()` - Convert UUID byte array (ala parse()) into a string** + function unparse(buf, offset) { + var i = offset || 0, + bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]]; + } - /** - * Calculate the scaling values, dependent on the range in x, y, and z direction - */ - Graph3d.prototype._setScale = function () { - this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html - // keep aspect ration between x and y scale if desired - if (this.keepAspectRatio) { - if (this.scale.x < this.scale.y) { - //noinspection JSSuspiciousNameCombination - this.scale.y = this.scale.x; - } else { - //noinspection JSSuspiciousNameCombination - this.scale.x = this.scale.y; - } - } + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + var _nodeId = [_seedBytes[0] | 1, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]]; - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; - // position the camera arm - var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; - var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; - var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; - this.camera.setArmLocation(xCenter, yCenter, zCenter); - }; + // Previous uuid creation time + var _lastMSecs = 0, + _lastNSecs = 0; - /** - * Convert a 3D location to a 2D location on screen - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point2d} point2d A 2D point with parameters x, y - */ - Graph3d.prototype._convert3Dto2D = function (point3d) { - var translation = this._convertPointToTranslation(point3d); - return this._convertTranslationToScreen(translation); - }; + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; - /** - * Convert a 3D location its translation seen from the camera - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - */ - Graph3d.prototype._convertPointToTranslation = function (point3d) { - var ax = point3d.x * this.scale.x, - ay = point3d.y * this.scale.y, - az = point3d.z * this.scale.z, - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, + options = options || {}; - // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; - // calculate translation - dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), - dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), - dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); - return new Point3d(dx, dy, dz); - }; + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; - /** - * Convert a translation point to a point on the screen - * @param {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - * @return {Point2d} point2d A 2D point with parameters x, y - */ - Graph3d.prototype._convertTranslationToScreen = function (translation) { - var ex = this.eye.x, - ey = this.eye.y, - ez = this.eye.z, - dx = translation.x, - dy = translation.y, - dz = translation.z; + // Time since last uuid creation (in msecs) + var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; - // calculate position on screen from translation - var bx; - var by; - if (this.showPerspective) { - bx = (dx - ex) * (ez / dz); - by = (dy - ey) * (ez / dz); - } else { - bx = dx * -(ez / this.camera.getArmLength()); - by = dy * -(ez / this.camera.getArmLength()); + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 16383; } - // shift and scale the point to the center of the screen - // use the width of the graph to scale both horizontally and vertically. - return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); - }; - - /** - * Set the background styling for the graph - * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor - */ - Graph3d.prototype._setBackgroundColor = function (backgroundColor) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; - - if (typeof backgroundColor === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } else if (typeof backgroundColor === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } else if (backgroundColor === undefined) {} else { - throw 'Unsupported type of backgroundColor'; + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; - }; - - /// enumerate the available styles - Graph3d.STYLE = { - BAR: 0, - BARCOLOR: 1, - BARSIZE: 2, - DOT: 3, - DOTLINE: 4, - DOTCOLOR: 5, - DOTSIZE: 6, - GRID: 7, - LINE: 8, - SURFACE: 9 - }; - - /** - * Retrieve the style index from given styleName - * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found - */ - Graph3d.prototype._getStyleNumber = function (styleName) { - switch (styleName) { - case 'dot': - return Graph3d.STYLE.DOT; - case 'dot-line': - return Graph3d.STYLE.DOTLINE; - case 'dot-color': - return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': - return Graph3d.STYLE.DOTSIZE; - case 'line': - return Graph3d.STYLE.LINE; - case 'grid': - return Graph3d.STYLE.GRID; - case 'surface': - return Graph3d.STYLE.SURFACE; - case 'bar': - return Graph3d.STYLE.BAR; - case 'bar-color': - return Graph3d.STYLE.BARCOLOR; - case 'bar-size': - return Graph3d.STYLE.BARSIZE; + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); } - return -1; - }; + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; - /** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style - */ - Graph3d.prototype._determineColumnIndexes = function (data, style) { - if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; + // `time_low` + var tl = ((msecs & 268435455) * 10000 + nsecs) % 4294967296; + b[i++] = tl >>> 24 & 255; + b[i++] = tl >>> 16 & 255; + b[i++] = tl >>> 8 & 255; + b[i++] = tl & 255; + + // `time_mid` + var tmh = msecs / 4294967296 * 10000 & 268435455; + b[i++] = tmh >>> 8 & 255; + b[i++] = tmh & 255; - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } else { - throw 'Unknown style "' + this.style + '"'; - } - }; + // `time_high_and_version` + b[i++] = tmh >>> 24 & 15 | 16; // include version + b[i++] = tmh >>> 16 & 255; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; - }; + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 128; - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; - } - } - return counter; - }; + // `clock_seq_low` + b[i++] = clockseq & 255; - Graph3d.prototype.getDistinctValues = function (data, column) { - var distinctValues = []; - for (var i = 0; i < data.length; i++) { - if (distinctValues.indexOf(data[i][column]) == -1) { - distinctValues.push(data[i][column]); - } + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; } - return distinctValues; - }; - Graph3d.prototype.getColumnRange = function (data, column) { - var minMax = { min: data[0][column], max: data[0][column] }; - for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { - minMax.min = data[i][column]; - } - if (minMax.max < data[i][column]) { - minMax.max = data[i][column]; - } - } - return minMax; - }; + return buf ? buf : unparse(b); + } - /** - * Initialize the data from the data table. Calculate minimum and maximum values - * and column index values - * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. - * @param {Number} style Style Number - */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; + // **`v4()` - Generate random UUID** - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); + // See https://github.com/broofa/node-uuid for API details + function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof options == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; } + options = options || {}; - if (rawData === undefined) return; + var rnds = options.random || (options.rng || _rng)(); - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = rnds[6] & 15 | 64; + rnds[8] = rnds[8] & 63 | 128; - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error('Array, DataSet, or DataView expected'); + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } } - if (data.length == 0) return; + return buf || unparse(rnds); + } - this.dataSet = rawData; - this.dataTable = data; + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); + module.exports = uuid; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange +/***/ }, +/* 21 */ +/***/ function(module, exports, __webpack_require__) { - // determine the location of x,y,z,value,filter columns - this.colX = 'x'; - this.colY = 'y'; - this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; + // DOM utility methods - // check if a filter column is provided - if (data[0].hasOwnProperty('filter')) { - if (this.dataFilter === undefined) { - this.dataFilter = new Filter(rawData, this.colFilter, this); - this.dataFilter.setOnLoadCallback(function () { - me.redraw(); - }); + /** + * this prepares the JSON container for allocating SVG elements + * @param JSONcontainer + * @private + */ + 'use strict'; + + exports.prepareElements = function (JSONcontainer) { + // cleanup the redundant svgElements; + for (var elementType in JSONcontainer) { + if (JSONcontainer.hasOwnProperty(elementType)) { + JSONcontainer[elementType].redundant = JSONcontainer[elementType].used; + JSONcontainer[elementType].used = []; } } + }; - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + /** + * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from + * which to remove the redundant elements. + * + * @param JSONcontainer + * @private + */ + exports.cleanupElements = function (JSONcontainer) { + // cleanup the redundant svgElements; + for (var elementType in JSONcontainer) { + if (JSONcontainer.hasOwnProperty(elementType)) { + if (JSONcontainer[elementType].redundant) { + for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { + JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); + } + JSONcontainer[elementType].redundant = []; + } + } + } + }; - // determine barWidth from data - if (withBars) { - if (this.defaultXBarWidth !== undefined) { - this.xBarWidth = this.defaultXBarWidth; + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param svgContainer + * @returns {*} + * @private + */ + exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { + var element; + // allocate SVG element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); } else { - var dataX = this.getDistinctValues(data, this.colX); - this.xBarWidth = dataX[1] - dataX[0] || 1; + // create a new element and add it to the SVG + element = document.createElementNS('http://www.w3.org/2000/svg', elementType); + svgContainer.appendChild(element); } + } else { + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElementNS('http://www.w3.org/2000/svg', elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + svgContainer.appendChild(element); + } + JSONcontainer[elementType].used.push(element); + return element; + }; - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param DOMContainer + * @returns {*} + * @private + */ + exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { + var element; + // allocate DOM element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; + // create a new element and add it to the SVG + element = document.createElement(elementType); + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } + } + } else { + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElement(elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); } } + JSONcontainer[elementType].used.push(element); + return element; + }; - // calculate minimums and maximums - var xRange = this.getColumnRange(data, this.colX); - if (withBars) { - xRange.min -= this.xBarWidth / 2; - xRange.max += this.xBarWidth / 2; + /** + * draw a point object. this is a seperate function because it can also be called by the legend. + * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions + * as well. + * + * @param x + * @param y + * @param group + * @param JSONcontainer + * @param svgContainer + * @param labelObj + * @returns {*} + */ + exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { + var point; + if (group.options.drawPoints.style == 'circle') { + point = exports.getSVGElement('circle', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'cx', x); + point.setAttributeNS(null, 'cy', y); + point.setAttributeNS(null, 'r', 0.5 * group.options.drawPoints.size); + } else { + point = exports.getSVGElement('rect', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'x', x - 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, 'y', y - 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, 'width', group.options.drawPoints.size); + point.setAttributeNS(null, 'height', group.options.drawPoints.size); } - this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; - this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; - if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; - this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; - var yRange = this.getColumnRange(data, this.colY); - if (withBars) { - yRange.min -= this.yBarWidth / 2; - yRange.max += this.yBarWidth / 2; + if (group.options.drawPoints.styles !== undefined) { + point.setAttributeNS(null, 'style', group.group.options.drawPoints.styles); } - this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; - this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; - if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; - this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; + point.setAttributeNS(null, 'class', group.className + ' vis-point'); + //handle label - var zRange = this.getColumnRange(data, this.colZ); - this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; - this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; - if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; + if (labelObj) { + var label = exports.getSVGElement('text', JSONcontainer, svgContainer); + if (labelObj.xOffset) { + x = x + labelObj.xOffset; + } - if (this.colValue !== undefined) { - var valueRange = this.getColumnRange(data, this.colValue); - this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; - this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; - if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } + + if (labelObj.className) { + label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); + } + label.setAttributeNS(null, 'x', x); + label.setAttributeNS(null, 'y', y); } - // set the scale dependent on the ranges. - this._setScale(); + return point; }; /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen + * draw a bar SVG element centered on the X coordinate + * + * @param x + * @param y + * @param className */ - Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time - var x, y, i, z, obj, point; - - var dataPoints = []; - - if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { - // copy all values from the google data table to a matrix - // the provided values are supposed to form a grid of (x,y) positions - - // create two lists with all present x and y values - var dataX = []; - var dataY = []; - for (i = 0; i < this.getNumberOfRows(data); i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; - - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } + exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) { + if (height != 0) { + if (height < 0) { + height *= -1; + y -= height; } + var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer); + rect.setAttributeNS(null, 'x', x - 0.5 * width); + rect.setAttributeNS(null, 'y', y); + rect.setAttributeNS(null, 'width', width); + rect.setAttributeNS(null, 'height', height); + rect.setAttributeNS(null, 'class', className); + if (style) { + rect.setAttributeNS(null, 'style', style); + } + } + }; - var sortNumber = function sortNumber(a, b) { - return a - b; - }; - dataX.sort(sortNumber); - dataY.sort(sortNumber); - - // create a grid, a 2d matrix, with all values. - var dataMatrix = []; // temporary data matrix - for (i = 0; i < data.length; i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; - z = data[i][this.colZ] || 0; - - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); - - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + 'use strict'; - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + var util = __webpack_require__(2); + var Queue = __webpack_require__(23); - dataMatrix[xIndex][yIndex] = obj; + /** + * DataSet + * + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * type: { + * // ... + * } + * }); + * + * dataSet.add(item); + * dataSet.add(data); + * dataSet.update(item); + * dataSet.update(data); + * dataSet.remove(id); + * dataSet.remove(ids); + * var data = dataSet.get(); + * var data = dataSet.get(id); + * var data = dataSet.get(ids); + * var data = dataSet.get(ids, options, data); + * dataSet.clear(); + * + * A data set can: + * - add/remove/update data + * - gives triggers upon changes in the data + * - can import/export data in various data formats + * + * @param {Array} [data] Optional array with initial data + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object.} [type] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by a field name or custom sort function. + * @throws Error */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; - - this.animationStop(); - - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + DataSet.prototype.get = function (args) { + var me = this; - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + // parse the arguments + var id, ids, options; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options]) + id = arguments[0]; + options = arguments[1]; + } else if (firstType == 'Array') { + // get(ids [, options]) + ids = arguments[0]; + options = arguments[1]; + } else { + // get([, options]) + options = arguments[0]; + } - if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; - if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; - if (options.xLabel !== undefined) this.xLabel = options.xLabel; - if (options.yLabel !== undefined) this.yLabel = options.yLabel; - if (options.zLabel !== undefined) this.zLabel = options.zLabel; + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ['Array', 'Object']; + returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; + } else { + returnType = 'Array'; + } - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; + } + } else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], type); + if (!filter || filter(item)) { + items.push(item); } } - if (options.showGrid !== undefined) this.showGrid = options.showGrid; - if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; - if (options.showShadow !== undefined) this.showShadow = options.showShadow; - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; - if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; - if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; - if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - - if (options.xMin !== undefined) this.defaultXMin = options.xMin; - if (options.xStep !== undefined) this.defaultXStep = options.xStep; - if (options.xMax !== undefined) this.defaultXMax = options.xMax; - if (options.yMin !== undefined) this.defaultYMin = options.yMin; - if (options.yStep !== undefined) this.defaultYStep = options.yStep; - if (options.yMax !== undefined) this.defaultYMax = options.yMax; - if (options.zMin !== undefined) this.defaultZMin = options.zMin; - if (options.zStep !== undefined) this.defaultZStep = options.zStep; - if (options.zMax !== undefined) this.defaultZMax = options.zMax; - if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; - if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); - } else { - this.camera.setArmRotation(1, 0.5); - this.camera.setArmLength(1.7); + } else { + // return all items + for (itemId in this._data) { + if (this._data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, type); + if (!filter || filter(item)) { + items.push(item); + } + } } } - this._setBackgroundColor(options && options.backgroundColor); - - this.setSize(this.width, this.height); + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } + } } - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); + // return the results + if (returnType == 'Object') { + var result = {}; + for (i = 0; i < items.length; i++) { + result[items[i].id] = items[i]; + } + return result; + } else { + if (id != undefined) { + // a single item + return item; + } else { + // just return our array + return items; + } } }; /** - * Redraw the Graph. + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } + DataSet.prototype.getIds = function (options) { + var data = this._data, + filter = options && options.filter, + order = options && options.order, + type = options && options.type || this._options.type, + i, + len, + id, + item, + items, + ids = []; + + if (filter) { + // get filtered items + if (order) { + // create ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (filter(item)) { + items.push(item); + } + } + } - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + this._sort(items, order); - if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); - } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } + } else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (filter(item)) { + ids.push(item[this._fieldId]); + } + } + } + } } else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } + + this._sort(items, order); + + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } + } else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this._fieldId]); + } + } + } } - this._redrawInfo(); - this._redrawLegend(); + return ids; }; /** - * Clear the canvas before redrawing + * Returns the DataSet itself. Is overwritten for example by the DataView, + * which returns the DataSet it is connected to instead. */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, canvas.width, canvas.height); + DataSet.prototype.getDataSet = function () { + return this; }; /** - * Redraw the legend showing the colors + * Execute a callback function for every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. */ - Graph3d.prototype._redrawLegend = function () { - var y; - - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - var dotSize = this.frame.clientWidth * 0.02; + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - var widthMin, widthMax; - if (this.style === Graph3d.STYLE.DOTSIZE) { - widthMin = dotSize / 2; // px - widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function - } else { - widthMin = 20; // px - widthMax = 20; // px + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this._fieldId]; + callback(item, id); + } + } else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (!filter || filter(item)) { + callback(item, id); + } + } } - - var height = Math.max(this.frame.clientHeight * 0.25, 100); - var top = this.margin; - var right = this.frame.clientWidth - this.margin; - var left = right - widthMax; - var bottom = top + height; } + }; - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options - - if (this.style === Graph3d.STYLE.DOTCOLOR) { - // draw the color bar - var ymin = 0; - var ymax = height; // Todo: make height customizable - for (y = ymin; y < ymax; y++) { - var f = (y - ymin) / (ymax - ymin); - - //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function - var hue = f * 240; - var color = this._hsv2rgb(hue, 1, 1); + /** + * Map every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Object[]} mappedItems + */ + DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); + // convert and filter items + for (var id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (!filter || filter(item)) { + mappedItems.push(callback(item, id)); + } } - - ctx.strokeStyle = this.colorAxis; - ctx.strokeRect(left, top, widthMax, height); } - if (this.style === Graph3d.STYLE.DOTSIZE) { - // draw border around color bar - ctx.strokeStyle = this.colorAxis; - ctx.fillStyle = this.colorDot; - ctx.beginPath(); - ctx.moveTo(left, top); - ctx.lineTo(right, top); - ctx.lineTo(right - widthMax + widthMin, bottom); - ctx.lineTo(left, bottom); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); } - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - // print values along the color bar - var gridLineLen = 5; // px - var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); - step.start(); - if (step.getCurrent() < this.valueMin) { - step.next(); - } - while (!step.end()) { - y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); - - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - - step.next(); - } - - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } + return mappedItems; }; /** - * Redraw the filter + * Filter the fields of an item + * @param {Object | null} item + * @param {String[]} fields Field names + * @return {Object | null} filteredItem or null if no item is provided + * @private */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ''; - - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; - - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; - - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); - - // create an event handler - var me = this; - var onchange = function onchange() { - var index = slider.getIndex(); + DataSet.prototype._filterFields = function (item, fields) { + if (!item) { + // item is null + return item; + } - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + var filteredItem = {}; - me.redraw(); - }; - slider.setOnChangeCallback(onchange); + if (Array.isArray(fields)) { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; + } + } } else { - this.frame.filter.slider = undefined; + for (var field in item) { + if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { + filteredItem[fields[field]] = item[field]; + } + } } + + return filteredItem; }; /** - * Redraw the slider + * Sort the provided array with items + * @param {Object[]} items + * @param {String | function} order A field name or custom sort function. + * @private */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); + DataSet.prototype._sort = function (items, order) { + if (util.isString(order)) { + // order by provided field name + var name = order; // field name + items.sort(function (a, b) { + var av = a[name]; + var bv = b[name]; + return av > bv ? 1 : av < bv ? -1 : 0; + }); + } else if (typeof order === 'function') { + // order by sort function + items.sort(order); + } + // TODO: extend order by an Object {field:String, direction:String} + // where direction can be 'asc' or 'desc' + else { + throw new TypeError('Order must be a function or a string'); } }; /** - * Redraw common information + * Remove an object by pointer or by id + * @param {String | Number | Object | Array} id Object or id, or an array with + * objects or ids to be removed + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, + len, + removedId; - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } + } + } else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); + } + } - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); + if (removedIds.length) { + this._trigger('remove', { items: removedIds }, senderId); } + + return removedIds; }; /** - * Redraw the axis + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private */ - Graph3d.prototype._redrawAxis = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - from, - to, - step, - prettyStep, - text, - xText, - yText, - zText, - offset, - xOffset, - yOffset, - xMin2d, - xMax2d; + DataSet.prototype._remove = function (id) { + if (util.isNumber(id) || util.isString(id)) { + if (this._data[id]) { + delete this._data[id]; + this.length--; + return id; + } + } else if (id instanceof Object) { + var itemId = id[this._fieldId]; + if (itemId && this._data[itemId]) { + delete this._data[itemId]; + this.length--; + return itemId; + } + } + return null; + }; - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + /** + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items + */ + DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this._data); - // calculate the length for the short grid lines - var gridLenX = 0.025 / this.scale.x; - var gridLenY = 0.025 / this.scale.y; - var textMargin = 5 / this.camera.getArmLength(); // px - var armAngle = this.camera.getArmRotation().horizontal; + this._data = {}; + this.length = 0; - // draw x-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultXStep === undefined; - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } - while (!step.end()) { - var x = step.getCurrent(); + this._trigger('remove', { items: ids }, senderId); - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } else { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + return ids; + }; - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } + /** + * Find the item with maximum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items + */ + DataSet.prototype.max = function (field) { + var data = this._data, + max = null, + maxField = null; - yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax; - text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } else if (Math.sin(armAngle * 2) < 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!max || itemField > maxField)) { + max = item; + maxField = itemField; + } } - ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); - - step.next(); } - // draw y-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultYStep === undefined; - step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); - step.start(); - if (step.getCurrent() < this.yMin) { - step.next(); - } - while (!step.end()) { - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } else { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + return max; + }; - from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } + /** + * Find the item with minimum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items + */ + DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; - text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!min || itemField < minField)) { + min = item; + minField = itemField; + } } - ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); - - step.next(); } - // draw z-grid lines and axis - ctx.lineWidth = 1; - prettyStep = this.defaultZStep === undefined; - step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); - step.start(); - if (step.getCurrent() < this.zMin) { - step.next(); - } - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - while (!step.end()) { - // TODO: make z-grid lines really 3d? - from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); + return min; + }; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + /** + * Find all distinct values of a specified field + * @param {String} field + * @return {Array} values Array containing all distinct values. If data items + * do not contain the specified field are ignored. + * The returned array is unordered. + */ + DataSet.prototype.distinct = function (field) { + var data = this._data; + var values = []; + var fieldType = this._options.type && this._options.type[field] || null; + var count = 0; + var i; - step.next(); + for (var prop in data) { + if (data.hasOwnProperty(prop)) { + var item = data[prop]; + var value = item[field]; + var exists = false; + for (i = 0; i < count; i++) { + if (values[i] == value) { + exists = true; + break; + } + } + if (!exists && value !== undefined) { + values[count] = value; + count++; + } + } } - ctx.lineWidth = 1; - from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // draw x-axis - ctx.lineWidth = 1; - // line at yMin - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); - // line at ymax - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); + } + } - // draw y-axis - ctx.lineWidth = 1; - // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + return values; + }; - // draw x-label - var xLabel = this.xLabel; - if (xLabel.length > 0) { - yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } else if (Math.sin(armAngle * 2) < 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + /** + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id + * @private + */ + DataSet.prototype._addItem = function (item) { + var id = item[this._fieldId]; + + if (id != undefined) { + // check whether this id is already taken + if (this._data[id]) { + // item already exists + throw new Error('Cannot add item: item with id ' + id + ' already exists'); } - ctx.fillStyle = this.colorAxis; - ctx.fillText(xLabel, text.x, text.y); + } else { + // generate an id + id = util.randomUUID(); + item[this._fieldId] = id; } - // draw y-label - var yLabel = this.yLabel; - if (yLabel.length > 0) { - xOffset = 0.1 / this.scale.x; - xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); } - ctx.fillStyle = this.colorAxis; - ctx.fillText(yLabel, text.x, text.y); } + this._data[id] = d; + this.length++; - // draw z-label - var zLabel = this.zLabel; - if (zLabel.length > 0) { - offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(zLabel, text.x - offset, text.y); - } + return id; }; /** - * Calculate the color based on the given value. - * @param {Number} H Hue, a value be between 0 and 360 - * @param {Number} S Saturation, a value between 0 and 1 - * @param {Number} V Value, a value between 0 and 1 + * Get an item. Fields can be converted to a specific type + * @param {String} id + * @param {Object.} [types] field types to convert + * @return {Object | null} item + * @private */ - Graph3d.prototype._hsv2rgb = function (H, S, V) { - var R, G, B, C, Hi, X; + DataSet.prototype._getItem = function (id, types) { + var field, value; - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } - switch (Hi) { - case 0: - R = C;G = X;B = 0;break; - case 1: - R = X;G = C;B = 0;break; - case 2: - R = 0;G = C;B = X;break; - case 3: - R = 0;G = X;B = C;break; - case 4: - R = X;G = 0;B = C;break; - case 5: - R = C;G = 0;B = X;break; + // convert the items field types + var converted = {}; + if (types) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = util.convert(value, types[field]); + } + } + } else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = value; + } + } + } + return converted; + }; - default: - R = 0;G = 0;B = 0;break; + /** + * Update a single item: merge with existing item. + * Will fail when the item has no id, or when there does not exist an item + * with the same id. + * @param {Object} item + * @return {String} id + * @private + */ + DataSet.prototype._updateItem = function (item) { + var id = item[this._fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this._data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); } - return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; }; + module.exports = DataSet; + +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { + /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' + * A queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @constructor */ - Graph3d.prototype._redrawDataGrid = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - right, - top, - cross, - i, - topSideVisible, - fillStyle, - strokeStyle, - lineWidth, - h, - s, - v, - zAvg; + 'use strict'; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; - // calculate the translations and screen position of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + // properties + this._queue = []; + this._timeout = null; + this._extended = null; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + this.setOptions(options); + } - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + /** + * Update the configuration of the queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @param options + */ + Queue.prototype.setOptions = function (options) { + if (options && typeof options.delay !== 'undefined') { + this.delay = options.delay; + } + if (options && typeof options.max !== 'undefined') { + this.max = options.max; } - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + this._flushIfNeeded(); + }; - if (this.style === Graph3d.STYLE.SURFACE) { - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - cross = this.dataPoints[i].pointCross; + /** + * Extend an object with queuing functionality. + * The object will be extended with a function flush, and the methods provided + * in options.replace will be replaced with queued ones. + * @param {Object} object + * @param {Object} options + * Available options: + * - replace: Array. + * A list with method names of the methods + * on the object to be replaced with queued ones. + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @return {Queue} Returns the created queue + */ + Queue.extend = function (object, options) { + var queue = new Queue(options); - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + if (object.flush !== undefined) { + throw new Error('Target object already has a property flush'); + } + object.flush = function () { + queue.flush(); + }; - if (this.showGrayBottom || this.showShadow) { - // calculate the cross product of the two vectors from center - // to left and right, in order to know whether we are looking at the - // bottom or at the top side. We can also use the cross product - // for calculating light intensity - var aDiff = Point3d.subtract(cross.trans, point.trans); - var bDiff = Point3d.subtract(top.trans, right.trans); - var crossproduct = Point3d.crossProduct(aDiff, bDiff); - var len = crossproduct.length(); - // FIXME: there is a bug with determining the surface side (shadow or colored) + var methods = [{ + name: 'flush', + original: undefined + }]; - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; - } + if (options && options.replace) { + for (var i = 0; i < options.replace.length; i++) { + var name = options.replace[i]; + methods.push({ + name: name, + original: object[name] + }); + queue.replace(object, name); + } + } - if (topSideVisible) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - s = 1; // saturation + queue._extended = { + object: object, + methods: methods + }; - if (this.showShadow) { - v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = fillStyle; - } else { - v = 1; - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = this.colorAxis; - } - } else { - fillStyle = 'gray'; - strokeStyle = this.colorAxis; - } - lineWidth = 0.5; + return queue; + }; - ctx.lineWidth = lineWidth; - ctx.fillStyle = fillStyle; - ctx.strokeStyle = strokeStyle; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.lineTo(cross.screen.x, cross.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + /** + * Destroy the queue. The queue will first flush all queued actions, and in + * case it has extended an object, will restore the original object. + */ + Queue.prototype.destroy = function () { + this.flush(); + + if (this._extended) { + var object = this._extended.object; + var methods = this._extended.methods; + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + if (method.original) { + object[method.name] = method.original; + } else { + delete object[method.name]; } } - } else { - // grid style - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; + this._extended = null; + } + }; - if (point !== undefined) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); - } - } + /** + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name + */ + Queue.prototype.replace = function (object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error('Method ' + method + ' undefined'); + } - if (point !== undefined && right !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; + }; - ctx.lineWidth = lineWidth; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.stroke(); - } + /** + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry + */ + Queue.prototype.queue = function (entry) { + if (typeof entry === 'function') { + this._queue.push({ fn: entry }); + } else { + this._queue.push(entry); + } - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + this._flushIfNeeded(); + }; - ctx.lineWidth = lineWidth; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.stroke(); - } - } + /** + * Check whether the queue needs to be flushed + * @private + */ + Queue.prototype._flushIfNeeded = function () { + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } + + // flush after a period of inactivity when a delay is configured + clearTimeout(this._timeout); + if (this.queue.length > 0 && typeof this.delay === 'number') { + var me = this; + this._timeout = setTimeout(function () { + me.flush(); + }, this.delay); } }; /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' + * Flush all queued calls */ - Graph3d.prototype._redrawDataDot = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i; + Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); + } + }; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + module.exports = Queue; - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + 'use strict'; - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; + var util = __webpack_require__(2); + var DataSet = __webpack_require__(22); + + /** + * DataView + * + * a dataview offers a filtered view on a dataset or an other dataview. + * + * @param {DataSet | DataView} data + * @param {Object} [options] Available options: see method get + * + * @constructor DataView + */ + function DataView(data, options) { + this._data = null; + this._ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.length = 0; // number of items in the DataView + this._options = options || {}; + this._fieldId = 'id'; // name of the field containing id + this._subscribers = {}; // event subscribers + + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); }; - this.dataPoints.sort(sortDepth); - // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * 0.02; // px - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + this.setData(data); + } - if (this.style === Graph3d.STYLE.DOTLINE) { - // draw a vertical line from the bottom to the graph value - //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); - var from = this._convert3Dto2D(point.bottom); - ctx.lineWidth = 1; - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); - } + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly - // calculate radius for the circle - var size; - if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } else { - size = dotSize; - } + /** + * Set a data source for the view + * @param {DataSet | DataView} data + */ + DataView.prototype.setData = function (data) { + var ids, i, len; - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); + if (this._data) { + // unsubscribe from current dataset + if (this._data.off) { + this._data.off('*', this.listener); } - if (radius < 0) { - radius = 0; + + // trigger a remove of all items in memory + ids = []; + for (var id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + ids.push(id); + } } + this._ids = {}; + this.length = 0; + this._trigger('remove', { items: ids }); + } - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.DOTCOLOR) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } else if (this.style === Graph3d.STYLE.DOTSIZE) { - color = this.colorDot; - borderColor = this.colorDotBorder; - } else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); + this._data = data; + + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; + + // trigger an add of all added items + ids = this._data.getIds({ filter: this._options && this._options.filter }); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; } + this.length = ids.length; + this._trigger('add', { items: ids }); - // draw the circle - ctx.lineWidth = 1; - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true); - ctx.fill(); - ctx.stroke(); + // subscribe to new dataset + if (this._data.on) { + this._data.on('*', this.listener); + } } }; /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' + * Refresh the DataView. Useful when the DataView has a filter function + * containing a variable parameter. */ - Graph3d.prototype._redrawDataBar = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i, j, surface, corners; - - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + DataView.prototype.refresh = function () { + var id; + var ids = this._data.getIds({ filter: this._options && this._options.filter }); + var newIds = {}; + var added = []; + var removed = []; - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + // check for additions + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + newIds[id] = true; + if (!this._ids[id]) { + added.push(id); + this._ids[id] = true; + this.length++; + } + } - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + // check for removals + for (id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + if (!newIds[id]) { + removed.push(id); + delete this._ids[id]; + this.length--; + } + } } - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // trigger events + if (added.length) { + this._trigger('add', { items: added }); + } + if (removed.length) { + this._trigger('remove', { items: removed }); + } + }; - // draw the datapoints as bars - var xWidth = this.xBarWidth / 2; - var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + /** + * Get data from the data view + * + * Usage: + * + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) + * + * get(id: Number) + * get(id: Number, options: Object) + * get(id: Number, options: Object, data: Array | DataTable) + * + * get(ids: Number[]) + * get(ids: Number[], options: Object) + * get(ids: Number[], options: Object, data: Array | DataTable) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [type] Type of data to be returned. Can + * be 'DataTable' or 'Array' (default) + * {Object.} [convert] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * @param args + */ + DataView.prototype.get = function (args) { + var me = this; - // determine color - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } else if (this.style === Graph3d.STYLE.BARSIZE) { - color = this.colorDot; - borderColor = this.colorDotBorder; - } else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - } + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - // calculate all corner points - var me = this; - var point3d = point.point; - var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }]; - var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }]; + // create a combined filter method when needed + if (this._options.filter && options && options.filter) { + viewOptions.filter = function (item) { + return me._options.filter(item) && options.filter(item); + }; + } - // calculate screen location of the points - top.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - bottom.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); - // create five sides, calculate both corner points and center points - var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }]; - point.surfaces = surfaces; + return this._data && this._data.get.apply(this._data, getArguments); + }; - // calculate the distance of each of the surface centers to the camera - for (j = 0; j < surfaces.length; j++) { - surface = surfaces[j]; - var transCenter = this._convertPointToTranslation(surface.center); - surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; - // TODO: this dept calculation doesn't work 100% of the cases due to perspective, - // but the current solution is fast/simple and works in 99.9% of all cases - // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) - } + /** + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids + */ + DataView.prototype.getIds = function (options) { + var ids; - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; + } - // both are equal - return 0; + ids = this._data.getIds({ + filter: filter, + order: options && options.order }); + } else { + ids = []; + } - // draw the ordered surfaces - ctx.lineWidth = 1; - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside - for (j = 2; j < surfaces.length; j++) { - surface = surfaces[j]; - corners = surface.corners; - ctx.beginPath(); - ctx.moveTo(corners[3].screen.x, corners[3].screen.y); - ctx.lineTo(corners[0].screen.x, corners[0].screen.y); - ctx.lineTo(corners[1].screen.x, corners[1].screen.y); - ctx.lineTo(corners[2].screen.x, corners[2].screen.y); - ctx.lineTo(corners[3].screen.x, corners[3].screen.y); - ctx.fill(); - ctx.stroke(); - } + return ids; + }; + + /** + * Get the DataSet to which this DataView is connected. In case there is a chain + * of multiple DataViews, the root DataSet of this chain is returned. + * @return {DataSet} dataSet + */ + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; } + return dataSet || null; }; /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' + * Event listener. Will propagate all events from the connected data set to + * the subscribers of the DataView, but will filter the items and only trigger + * when there are changes in the filtered data set. + * @param {String} event + * @param {Object | null} params + * @param {String} senderId + * @private */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - i; + DataView.prototype._onEvent = function (event, params, senderId) { + var i, len, id, item; + var ids = params && params.items; + var data = this._data; + var updatedData = []; + var added = []; + var updated = []; + var removed = []; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + if (ids && data) { + switch (event) { + case 'add': + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this._ids[id] = true; + added.push(id); + } + } - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + break; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + if (item) { + if (this._ids[id]) { + updated.push(id); + updatedData.push(params.data[i]); + } else { + this._ids[id] = true; + added.push(id); + } + } else { + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } else {} + } + } + + break; + + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } + } - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = 'blue'; // TODO: make customizable - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - } + break; + } - // draw the datapoints as colored circles - for (i = 1; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - ctx.lineTo(point.screen.x, point.screen.y); - } + this.length += added.length - removed.length; - // finish the line - if (this.dataPoints.length > 0) { - ctx.stroke(); + if (added.length) { + this._trigger('add', { items: added }, senderId); + } + if (updated.length) { + this._trigger('update', { items: updated, data: updatedData }, senderId); + } + if (removed.length) { + this._trigger('remove', { items: removed }, senderId); + } } }; - /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) - */ - Graph3d.prototype._onMouseDown = function (event) { - event = event || window.event; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); - } + // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) + DataView.prototype.subscribe = DataView.prototype.on; + DataView.prototype.unsubscribe = DataView.prototype.off; - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; + module.exports = DataView; - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); + // nothing interesting for me :-( - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - this.frame.style.cursor = 'move'; + 'use strict'; - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) { - me._onMouseMove(event); - }; - this.onmouseup = function (event) { - me._onMouseUp(event); - }; - util.addEventListener(document, 'mousemove', me.onmousemove); - util.addEventListener(document, 'mouseup', me.onmouseup); - util.preventDefault(event); - }; + var Emitter = __webpack_require__(27); + var DataSet = __webpack_require__(22); + var DataView = __webpack_require__(24); + var util = __webpack_require__(2); + var Point3d = __webpack_require__(28); + var Point2d = __webpack_require__(26); + var Camera = __webpack_require__(29); + var Filter = __webpack_require__(30); + var Slider = __webpack_require__(31); + var StepNumber = __webpack_require__(32); /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * @constructor Graph3d + * Graph3d displays data in 3d. + * + * Graph3d is developed in javascript as a Google Visualization Chart. + * + * @param {Element} container The DOM element in which the Graph3d will + * be created. Normally a div element. + * @param {DataSet | DataView | Array} [data] + * @param {Object} [options] */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + // create variables and set default values + this.containerElement = container; + this.width = '400px'; + this.height = '400px'; + this.margin = 10; // px + this.defaultXCenter = '55%'; + this.defaultYCenter = '50%'; - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + var passValueFn = function passValueFn(v) { + return v; + }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... - // the -0.001 is to take care that the vertical axis is always drawn at the left front corner - if (Math.abs(Math.sin(horizontalNew)) < snapValue) { - horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; - } - if (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; - } + this.filterLabel = 'time'; + this.legendLabel = 'value'; - // snap vertically to nice angles - if (Math.abs(Math.sin(verticalNew)) < snapValue) { - verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; - } - if (Math.abs(Math.cos(verticalNew)) < snapValue) { - verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; - } + this.style = Graph3d.STYLE.DOT; + this.showPerspective = true; + this.showGrid = true; + this.keepAspectRatio = true; + this.showShadow = false; + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + this.camera = new Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - util.preventDefault(event); - }; + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event - */ - Graph3d.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - this.leftButtonDown = false; + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + this.xMin = 0; + this.xStep = undefined; // auto by default + this.xMax = 1; + this.yMin = 0; + this.yStep = undefined; // auto by default + this.yMax = 1; + this.zMin = 0; + this.zStep = undefined; // auto by default + this.zMax = 1; + this.valueMin = 0; + this.valueMax = 1; + this.xBarWidth = 1; + this.yBarWidth = 1; + // TODO: customize axis range - /** - * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point - * @param {Event} event A mouse move event - */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var boundingRect = this.frame.getBoundingClientRect(); - var mouseX = getMouseX(event) - boundingRect.left; - var mouseY = getMouseY(event) - boundingRect.top; + // constants + this.colorAxis = '#4D4D4D'; + this.colorGrid = '#D3D3D3'; + this.colorDot = '#7DC1FF'; + this.colorDotBorder = '#3267D2'; - if (!this.showTooltip) { - return; - } + // create a frame and canvas + this.create(); - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } + // apply options (also when undefined) + this.setOptions(options); - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; + // apply data + if (data) { + this.setData(data); } + } - if (this.tooltip && this.tooltip.dataPoint) { - // tooltip is currently visible - var dataPoint = this._dataPointFromXY(mouseX, mouseY); - if (dataPoint !== this.tooltip.dataPoint) { - // datapoint changed - if (dataPoint) { - this._showTooltip(dataPoint); - } else { - this._hideTooltip(); - } - } - } else { - // tooltip is currently not visible - var me = this; - this.tooltipTimeout = setTimeout(function () { - me.tooltipTimeout = null; - - // show a tooltip if we have a data point - var dataPoint = me._dataPointFromXY(mouseX, mouseY); - if (dataPoint) { - me._showTooltip(dataPoint); - } - }, delay); - } - }; + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); /** - * Event handler for touchstart event on mobile devices + * Calculate the scaling values, dependent on the range in x, y, and z direction */ - Graph3d.prototype._onTouchStart = function (event) { - this.touchDown = true; + Graph3d.prototype._setScale = function () { + this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); - var me = this; - this.ontouchmove = function (event) { - me._onTouchMove(event); - }; - this.ontouchend = function (event) { - me._onTouchEnd(event); - }; - util.addEventListener(document, 'touchmove', me.ontouchmove); - util.addEventListener(document, 'touchend', me.ontouchend); + // keep aspect ration between x and y scale if desired + if (this.keepAspectRatio) { + if (this.scale.x < this.scale.y) { + //noinspection JSSuspiciousNameCombination + this.scale.y = this.scale.x; + } else { + //noinspection JSSuspiciousNameCombination + this.scale.x = this.scale.y; + } + } - this._onMouseDown(event); + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); }; /** - * Event handler for touchmove event on mobile devices + * Convert a 3D location to a 2D location on screen + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point2d} point2d A 2D point with parameters x, y */ - Graph3d.prototype._onTouchMove = function (event) { - this._onMouseMove(event); + Graph3d.prototype._convert3Dto2D = function (point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Event handler for touchend event on mobile devices + * Convert a 3D location its translation seen from the camera + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera */ - Graph3d.prototype._onTouchEnd = function (event) { - this.touchDown = false; + Graph3d.prototype._convertPointToTranslation = function (point3d) { + var ax = point3d.x * this.scale.x, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); + // calculate angles + sinTx = Math.sin(this.camera.getCameraRotation().x), + cosTx = Math.cos(this.camera.getCameraRotation().x), + sinTy = Math.sin(this.camera.getCameraRotation().y), + cosTy = Math.cos(this.camera.getCameraRotation().y), + sinTz = Math.sin(this.camera.getCameraRotation().z), + cosTz = Math.cos(this.camera.getCameraRotation().z), - this._onMouseUp(event); + // calculate translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + + return new Point3d(dx, dy, dz); }; /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event + * Convert a translation point to a point on the screen + * @param {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + * @return {Point2d} point2d A 2D point with parameters x, y */ - Graph3d.prototype._onWheel = function (event) { - if (!event) /* For IE. */ - event = window.event; - - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } - - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - var oldLength = this.camera.getArmLength(); - var newLength = oldLength * (1 - delta / 10); - - this.camera.setArmLength(newLength); - this.redraw(); + Graph3d.prototype._convertTranslationToScreen = function (translation) { + var ex = this.eye.x, + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; - this._hideTooltip(); + // calculate position on screen from translation + var bx; + var by; + if (this.showPerspective) { + bx = (dx - ex) * (ez / dz); + by = (dy - ey) * (ez / dz); + } else { + bx = dx * -(ez / this.camera.getArmLength()); + by = dy * -(ez / this.camera.getArmLength()); } - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); - - // Prevent default actions caused by mouse wheel. - // That might be ugly, but we handle scrolls somehow - // anyway, so don't bother here.. - util.preventDefault(event); + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); }; /** - * Test whether a point lies inside given 2D triangle - * @param {Point2d} point - * @param {Point2d[]} triangle - * @return {boolean} Returns true if given point lies inside or on the edge of the triangle - * @private + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; + Graph3d.prototype._setBackgroundColor = function (backgroundColor) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; - function sign(x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; + if (typeof backgroundColor === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } else if (typeof backgroundColor === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } else if (backgroundColor === undefined) {} else { + throw 'Unsupported type of backgroundColor'; } - var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); - var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); - var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + 'px'; + this.frame.style.borderStyle = 'solid'; + }; - // each of the three signs must be either equal to each other or zero - return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); + /// enumerate the available styles + Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT: 3, + DOTLINE: 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID: 7, + LINE: 8, + SURFACE: 9 }; /** - * Find a data point close to given screen position (x, y) - * @param {Number} x - * @param {Number} y - * @return {Object | null} The closest data point or null if not close to any data point - * @private + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, - // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); - - if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - // the data points are ordered from far away to closest - for (i = this.dataPoints.length - 1; i >= 0; i--) { - dataPoint = this.dataPoints[i]; - var surfaces = dataPoint.surfaces; - if (surfaces) { - for (var s = surfaces.length - 1; s >= 0; s--) { - // split each surface in two triangles, and see if the center point is inside one of these - var surface = surfaces[s]; - var corners = surface.corners; - var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; - var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; - if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { - // return immediately at the first hit - return dataPoint; - } - } - } - } - } else { - // find the closest data point, using distance to the center of the point on 2d screen - for (i = 0; i < this.dataPoints.length; i++) { - dataPoint = this.dataPoints[i]; - var point = dataPoint.screen; - if (point) { - var distX = Math.abs(x - point.x); - var distY = Math.abs(y - point.y); - var dist = Math.sqrt(distX * distX + distY * distY); - - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } + Graph3d.prototype._getStyleNumber = function (styleName) { + switch (styleName) { + case 'dot': + return Graph3d.STYLE.DOT; + case 'dot-line': + return Graph3d.STYLE.DOTLINE; + case 'dot-color': + return Graph3d.STYLE.DOTCOLOR; + case 'dot-size': + return Graph3d.STYLE.DOTSIZE; + case 'line': + return Graph3d.STYLE.LINE; + case 'grid': + return Graph3d.STYLE.GRID; + case 'surface': + return Graph3d.STYLE.SURFACE; + case 'bar': + return Graph3d.STYLE.BAR; + case 'bar-color': + return Graph3d.STYLE.BARCOLOR; + case 'bar-size': + return Graph3d.STYLE.BARSIZE; } - return closestDataPoint; + return -1; }; /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; - - if (!this.tooltip) { - content = document.createElement('div'); - content.style.position = 'absolute'; - content.style.padding = '10px'; - content.style.border = '1px solid #4d4d4d'; - content.style.color = '#1a1a1a'; - content.style.background = 'rgba(255,255,255,0.7)'; - content.style.borderRadius = '2px'; - content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; - - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; + Graph3d.prototype._determineColumnIndexes = function (data, style) { + if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; - dot = document.createElement('div'); - dot.style.position = 'absolute'; - dot.style.height = '0'; - dot.style.width = '0'; - dot.style.border = '5px solid #4d4d4d'; - dot.style.borderRadius = '5px'; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; - this.tooltip = { - dataPoint: null, - dom: { - content: content, - line: line, - dot: dot - } - }; + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } } else { - content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; + throw 'Unknown style "' + this.style + '"'; } + }; - this._hideTooltip(); + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - this.tooltip.dataPoint = dataPoint; - if (typeof this.showTooltip === 'function') { - content.innerHTML = this.showTooltip(dataPoint.point); - } else { - content.innerHTML = '' + '' + '' + '' + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } } - - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); - - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; - - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - - line.style.left = dataPoint.screen.x + 'px'; - line.style.top = dataPoint.screen.y - lineHeight + 'px'; - content.style.left = left + 'px'; - content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px'; - dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px'; - dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px'; + return counter; }; - /** - * Hide the tooltip when displayed - * @private - */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; - - for (var prop in this.tooltip.dom) { - if (this.tooltip.dom.hasOwnProperty(prop)) { - var elem = this.tooltip.dom[prop]; - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } + Graph3d.prototype.getDistinctValues = function (data, column) { + var distinctValues = []; + for (var i = 0; i < data.length; i++) { + if (distinctValues.indexOf(data[i][column]) == -1) { + distinctValues.push(data[i][column]); } } + return distinctValues; }; - /**--------------------------------------------------------------------------**/ + Graph3d.prototype.getColumnRange = function (data, column) { + var minMax = { min: data[0][column], max: data[0][column] }; + for (var i = 0; i < data.length; i++) { + if (minMax.min > data[i][column]) { + minMax.min = data[i][column]; + } + if (minMax.max < data[i][column]) { + minMax.max = data[i][column]; + } + } + return minMax; + }; /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x + * Initialize the data from the data table. Calculate minimum and maximum values + * and column index values + * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. + * @param {Number} style Style Number */ - function getMouseX(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - } + Graph3d.prototype._dataInitialize = function (rawData, style) { + var me = this; - /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y - */ - function getMouseY(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - } + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); + } - module.exports = Graph3d; + if (rawData === undefined) return; - // use use defaults + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error('Array, DataSet, or DataView expected'); + } - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - "use strict"; + if (data.length == 0) return; - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - } + this.dataSet = rawData; + this.dataTable = data; - module.exports = Point2d; + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on('*', this._onChange); -/***/ }, -/* 25 */ -/***/ function(module, exports, __webpack_require__) { + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - - /** - * Expose `Emitter`. - */ + // determine the location of x,y,z,value,filter columns + this.colX = 'x'; + this.colY = 'y'; + this.colZ = 'z'; + this.colValue = 'style'; + this.colFilter = 'filter'; - module.exports = Emitter; + // check if a filter column is provided + if (data[0].hasOwnProperty('filter')) { + if (this.dataFilter === undefined) { + this.dataFilter = new Filter(rawData, this.colFilter, this); + this.dataFilter.setOnLoadCallback(function () { + me.redraw(); + }); + } + } - /** - * Initialize a new `Emitter`. - * - * @api public - */ + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - function Emitter(obj) { - if (obj) return mixin(obj); - }; + // determine barWidth from data + if (withBars) { + if (this.defaultXBarWidth !== undefined) { + this.xBarWidth = this.defaultXBarWidth; + } else { + var dataX = this.getDistinctValues(data, this.colX); + this.xBarWidth = dataX[1] - dataX[0] || 1; + } - /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; + } + } - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; + // calculate minimums and maximums + var xRange = this.getColumnRange(data, this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; } - return obj; - } + this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; + this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; + if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; + this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; - /** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ + var yRange = this.getColumnRange(data, this.colY); + if (withBars) { + yRange.min -= this.yBarWidth / 2; + yRange.max += this.yBarWidth / 2; + } + this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; + this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; + if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; + this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; + var zRange = this.getColumnRange(data, this.colZ); + this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; + this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; + + if (this.colValue !== undefined) { + var valueRange = this.getColumnRange(data, this.colValue); + this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; + this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + + // set the scale dependent on the ranges. + this._setScale(); }; /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ + Graph3d.prototype._getDataPoints = function (data) { + // TODO: store the created matrix dataPoints in the filters instead of reloading each time + var x, y, i, z, obj, point; - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; + var dataPoints = []; - function on() { - self.off(event, on); - fn.apply(this, arguments); - } + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + // copy all values from the google data table to a matrix + // the provided values are supposed to form a grid of (x,y) positions - on.fn = fn; - this.on(event, on); - return this; - }; + // create two lists with all present x and y values + var dataX = []; + var dataY = []; + for (i = 0; i < this.getNumberOfRows(data); i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; - /** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } + } - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; + var sortNumber = function sortNumber(a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.length; i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; + z = data[i][this.colZ] || 0; - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; - }; + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; + dataMatrix[xIndex][yIndex] = obj; - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + dataPoints.push(obj); } - } - return this; - }; + // fill in the pointers to the neighbors. + for (x = 0; x < dataMatrix.length; x++) { + for (y = 0; y < dataMatrix[x].length; y++) { + if (dataMatrix[x][y]) { + dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined; + dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined; + dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined; + } + } + } + } else { + // 'dot', 'dot-line', etc. + // copy all values from the google data table to a list with Point3d objects + for (i = 0; i < data.length; i++) { + point = new Point3d(); + point.x = data[i][this.colX] || 0; + point.y = data[i][this.colY] || 0; + point.z = data[i][this.colZ] || 0; - /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; + + dataPoints.push(obj); + } + } + + return dataPoints; }; /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public + * Create the main frame for the Graph3d. + * This function is executed once when a Graph3d object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. */ + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; - }; + this.frame = document.createElement('div'); + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement('canvas'); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + //if (!this.frame.canvas.getContext) { + { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { + this.frame.filter = document.createElement('div'); + this.frame.filter.style.position = 'absolute'; + this.frame.filter.style.bottom = '0px'; + this.frame.filter.style.left = '0px'; + this.frame.filter.style.width = '100%'; + this.frame.appendChild(this.frame.filter); - /** - * @prototype Point3d - * @param {Number} [x] - * @param {Number} [y] - * @param {Number} [z] - */ - "use strict"; + // add event listeners to handle moving and zooming the contents + var me = this; + var onmousedown = function onmousedown(event) { + me._onMouseDown(event); + }; + var ontouchstart = function ontouchstart(event) { + me._onTouchStart(event); + }; + var onmousewheel = function onmousewheel(event) { + me._onWheel(event); + }; + var ontooltip = function ontooltip(event) { + me._onTooltip(event); + }; + // TODO: these events are never cleaned up... can give a 'memory leakage' - function Point3d(x, y, z) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - this.z = z !== undefined ? z : 0; - }; + util.addEventListener(this.frame.canvas, 'keydown', onkeydown); + util.addEventListener(this.frame.canvas, 'mousedown', onmousedown); + util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart); + util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel); + util.addEventListener(this.frame.canvas, 'mousemove', ontooltip); - /** - * Subtract the two provided points, returns a-b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a-b - */ - Point3d.subtract = function (a, b) { - var sub = new Point3d(); - sub.x = a.x - b.x; - sub.y = a.y - b.y; - sub.z = a.z - b.z; - return sub; + // add the new graph to the container element + this.containerElement.appendChild(this.frame); }; /** - * Add the two provided points, returns a+b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a+b + * Set a new size for the graph + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ - Point3d.add = function (a, b) { - var sum = new Point3d(); - sum.x = a.x + b.x; - sum.y = a.y + b.y; - sum.z = a.z + b.z; - return sum; - }; + Graph3d.prototype.setSize = function (width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - /** - * Calculate the average of two 3d points - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} The average, (a+b)/2 - */ - Point3d.avg = function (a, b) { - return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); + this._resizeCanvas(); }; /** - * Calculate the cross product of the two provided points, returns axb - * Documentation: http://en.wikipedia.org/wiki/Cross_product - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} cross product axb + * Resize the canvas to the current size of the frame */ - Point3d.crossProduct = function (a, b) { - var crossproduct = new Point3d(); + Graph3d.prototype._resizeCanvas = function () { + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - crossproduct.x = a.y * b.z - a.z * b.y; - crossproduct.y = a.z * b.x - a.x * b.z; - crossproduct.z = a.x * b.y - a.y * b.x; + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - return crossproduct; + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; }; /** - * Rtrieve the length of the vector (or the distance from this point to the origin - * @return {Number} length + * Start animation */ - Point3d.prototype.length = function () { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - }; - - module.exports = Point3d; - -/***/ }, -/* 27 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; - var Point3d = __webpack_require__(26); + this.frame.filter.slider.play(); + }; /** - * @class Camera - * The camera is mounted on a (virtual) camera arm. The camera arm can rotate - * The camera is always looking in the direction of the origin of the arm. - * This way, the camera always rotates around one fixed point, the location - * of the camera arm. - * - * Documentation: - * http://en.wikipedia.org/wiki/3D_projection + * Stop animation */ - function Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; - - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - this.calculateCameraOrientation(); - } + this.frame.filter.slider.stop(); + }; /** - * Set the location (origin) of the arm - * @param {Number} x Normalized value of x - * @param {Number} y Normalized value of y - * @param {Number} z Normalized value of z + * Resize the center position based on the current values in this.defaultXCenter + * and this.defaultYCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.xCenter + * and this.yCenter */ - Camera.prototype.setArmLocation = function (x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; + Graph3d.prototype._resizeCenter = function () { + // calculate the horizontal center position + if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') { + this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; + } else { + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px + } - this.calculateCameraOrientation(); + // calculate the vertical center position + if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') { + this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } else { + this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + } }; /** - * Set the rotation of the camera arm - * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. */ - Camera.prototype.setArmRotation = function (horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; + Graph3d.prototype.setCameraPosition = function (pos) { + if (pos === undefined) { + return; } - if (vertical !== undefined) { - this.armRotation.vertical = vertical; - if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; - if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); } - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); } + + this.redraw(); }; /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; - - return rot; + Graph3d.prototype.getCameraPosition = function () { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 + * Load data into the 3D Graph */ - Camera.prototype.setArmLength = function (length) { - if (length === undefined) return; - - this.armLength = length; + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - // Radius must be larger than the corner of the graph, - // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the - // graph - if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5) this.armLength = 5; + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } - this.calculateCameraOrientation(); + // draw the filter + this._redrawFilter(); }; /** - * Retrieve the arm length - * @return {Number} length + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - Camera.prototype.getArmLength = function () { - return this.armLength; - }; + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - /** - * Retrieve the camera location - * @return {Point3d} cameraLocation - */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } }; /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation + * Update the options. Options will be merged with current options + * @param {Object} options */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; - }; + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - /** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm - */ - Camera.prototype.calculateCameraOrientation = function () { - // calculate location of the camera - this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + this.animationStop(); - // calculate rotation of the camera - this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; - }; + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - module.exports = Camera; + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; -/***/ }, -/* 28 */ -/***/ function(module, exports, __webpack_require__) { + if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; + if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; + if (options.xLabel !== undefined) this.xLabel = options.xLabel; + if (options.yLabel !== undefined) this.yLabel = options.yLabel; + if (options.zLabel !== undefined) this.zLabel = options.zLabel; - 'use strict'; + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - var DataView = __webpack_require__(22); + if (options.style !== undefined) { + var styleNumber = this._getStyleNumber(options.style); + if (styleNumber !== -1) { + this.style = styleNumber; + } + } + if (options.showGrid !== undefined) this.showGrid = options.showGrid; + if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; + if (options.showShadow !== undefined) this.showShadow = options.showShadow; + if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; + if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; + if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - /** - * @class Filter - * - * @param {DataSet} data The google data table - * @param {Number} column The index of the column to be filtered - * @param {Graph} graph The graph - */ - function Filter(data, column, graph) { - this.data = data; - this.column = column; - this.graph = graph; // the parent graph + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - this.index = undefined; - this.value = undefined; + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - // read all distinct values and select the first one - this.values = graph.getDistinctValues(data.get(), this.column); + if (options.xMin !== undefined) this.defaultXMin = options.xMin; + if (options.xStep !== undefined) this.defaultXStep = options.xStep; + if (options.xMax !== undefined) this.defaultXMax = options.xMax; + if (options.yMin !== undefined) this.defaultYMin = options.yMin; + if (options.yStep !== undefined) this.defaultYStep = options.yStep; + if (options.yMax !== undefined) this.defaultYMax = options.yMax; + if (options.zMin !== undefined) this.defaultZMin = options.zMin; + if (options.zStep !== undefined) this.defaultZStep = options.zStep; + if (options.zMax !== undefined) this.defaultZMax = options.zMax; + if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; + if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - // sort both numeric and string values correctly - this.values.sort(function (a, b) { - return a > b ? 1 : a < b ? -1 : 0; - }); + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - if (this.values.length > 0) { - this.selectValue(0); + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } else { + this.camera.setArmRotation(1, 0.5); + this.camera.setArmLength(1.7); + } } - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; + this._setBackgroundColor(options && options.backgroundColor); - this.loaded = false; - this.onLoadCallback = undefined; + this.setSize(this.width, this.height); - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); - } else { - this.loaded = true; + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); } - }; - /** - * Return the label - * @return {string} label - */ - Filter.prototype.isLoaded = function () { - return this.loaded; + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } }; /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 + * Redraw the Graph. */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } - var i = 0; - while (this.dataPoints[i]) { - i++; + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); + + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); } - return Math.round(i / len * 100); + this._redrawInfo(); + this._redrawLegend(); }; /** - * Return the label - * @return {string} label + * Clear the canvas before redrawing */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; - }; + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function () { - return this.column; + ctx.clearRect(0, 0, canvas.width, canvas.height); }; /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * Redraw the legend showing the colors */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; + Graph3d.prototype._redrawLegend = function () { + var y; + + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + + var dotSize = this.frame.clientWidth * 0.02; + + var widthMin, widthMax; + if (this.style === Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } else { + widthMin = 20; // px + widthMax = 20; // px + } + + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; + } + + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options + + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); + + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); + + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } + + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); + } + + if (this.style === Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.colorAxis; + ctx.fillStyle = this.colorDot; + ctx.beginPath(); + ctx.moveTo(left, top); + ctx.lineTo(right, top); + ctx.lineTo(right - widthMax + widthMin, bottom); + ctx.lineTo(left, bottom); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + // print values along the color bar + var gridLineLen = 5; // px + var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); + step.start(); + if (step.getCurrent() < this.valueMin) { + step.next(); + } + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - return this.values[this.index]; - }; + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - /** - * Retrieve all values of the filter - * @return {Array} values - */ - Filter.prototype.getValues = function () { - return this.values; - }; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value - */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; + step.next(); + } - return this.values[index]; + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); + } }; /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataP