From c28051b1a887a6f8d906df4b9e26016494eb6d3b Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 30 Jun 2015 17:12:49 +0200 Subject: [PATCH] - Added blurEdge and hoverEdge events. --- HISTORY.md | 1 + dist/vis.js | 39366 +++++++++++----------- docs/network/index.html | 10 + lib/network/modules/SelectionHandler.js | 14 +- 4 files changed, 19706 insertions(+), 19685 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 42b15e18..e59a824a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -25,6 +25,7 @@ http://visjs.org - Fixed #1036, bug in lockedRedraw. Thanks @vges! - Added getDataset to all manipulation functions. Thanks @ericvandever! - Fixed #1039, icon now returns correct distance to border +- Added blurEdge and hoverEdge events. ### Graph2d diff --git a/dist/vis.js b/dist/vis.js index 5e146d82..4a1f9e44 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -84,60 +84,60 @@ return /******/ (function(modules) { // webpackBootstrap // utils 'use strict'; - exports.util = __webpack_require__(19); - exports.DOMutil = __webpack_require__(24); + exports.util = __webpack_require__(9); + exports.DOMutil = __webpack_require__(14); // data - exports.DataSet = __webpack_require__(25); - exports.DataView = __webpack_require__(27); - exports.Queue = __webpack_require__(26); + exports.DataSet = __webpack_require__(15); + exports.DataView = __webpack_require__(17); + exports.Queue = __webpack_require__(16); // Graph3d - exports.Graph3d = __webpack_require__(28); + exports.Graph3d = __webpack_require__(18); exports.graph3d = { - Camera: __webpack_require__(32), - Filter: __webpack_require__(33), - Point2d: __webpack_require__(29), - Point3d: __webpack_require__(31), - Slider: __webpack_require__(34), - StepNumber: __webpack_require__(35) + Camera: __webpack_require__(22), + Filter: __webpack_require__(23), + Point2d: __webpack_require__(19), + Point3d: __webpack_require__(21), + Slider: __webpack_require__(24), + StepNumber: __webpack_require__(25) }; // Timeline - exports.Timeline = __webpack_require__(36); - exports.Graph2d = __webpack_require__(57); + exports.Timeline = __webpack_require__(26); + exports.Graph2d = __webpack_require__(50); exports.timeline = { - DateUtil: __webpack_require__(42), - DataStep: __webpack_require__(60), - Range: __webpack_require__(40), - stack: __webpack_require__(44), - TimeStep: __webpack_require__(46), + DateUtil: __webpack_require__(32), + DataStep: __webpack_require__(53), + Range: __webpack_require__(30), + stack: __webpack_require__(36), + TimeStep: __webpack_require__(38), components: { items: { - Item: __webpack_require__(14), - BackgroundItem: __webpack_require__(49), - BoxItem: __webpack_require__(48), - PointItem: __webpack_require__(9), - RangeItem: __webpack_require__(45) + Item: __webpack_require__(4), + BackgroundItem: __webpack_require__(41), + BoxItem: __webpack_require__(40), + PointItem: __webpack_require__(2), + RangeItem: __webpack_require__(37) }, - Component: __webpack_require__(38), - CurrentTime: __webpack_require__(37), - CustomTime: __webpack_require__(13), - DataAxis: __webpack_require__(59), - GraphGroup: __webpack_require__(61), - Group: __webpack_require__(43), - BackgroundGroup: __webpack_require__(47), - ItemSet: __webpack_require__(12), - Legend: __webpack_require__(64), - LineGraph: __webpack_require__(58), - TimeAxis: __webpack_require__(50) + Component: __webpack_require__(28), + CurrentTime: __webpack_require__(27), + CustomTime: __webpack_require__(45), + DataAxis: __webpack_require__(52), + GraphGroup: __webpack_require__(54), + Group: __webpack_require__(35), + BackgroundGroup: __webpack_require__(39), + ItemSet: __webpack_require__(34), + Legend: __webpack_require__(58), + LineGraph: __webpack_require__(51), + TimeAxis: __webpack_require__(42) } }; // Network - exports.Network = __webpack_require__(66); + exports.Network = __webpack_require__(60); exports.network = { Images: __webpack_require__(112), dotparser: __webpack_require__(110), @@ -157,10 +157,10 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(20); - exports.hammer = __webpack_require__(15); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(15); - exports.keycharm = __webpack_require__(52); + exports.moment = __webpack_require__(10); + exports.hammer = __webpack_require__(5); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(5); + exports.keycharm = __webpack_require__(44); /***/ }, /* 1 */ @@ -181,20503 +181,18288 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); + var Item = __webpack_require__(4); - 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; }; })(); + /** + * @constructor PointItem + * @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 PointItem(data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } + } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + Item.call(this, data, conversion, options); + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + PointItem.prototype = new Item(null, null, null); - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + /** + * 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 + */ + PointItem.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; + }; - var _EdgeBase2 = __webpack_require__(6); + /** + * Repaint the item + */ + PointItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - var _EdgeBase3 = _interopRequireDefault(_EdgeBase2); + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() - var BezierEdgeBase = (function (_EdgeBase) { - function BezierEdgeBase(options, body, labelModule) { - _classCallCheck(this, BezierEdgeBase); + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'vis-item-content'; + dom.point.appendChild(dom.content); - _get(Object.getPrototypeOf(BezierEdgeBase.prototype), 'constructor', this).call(this, options, body, labelModule); - } + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); - _inherits(BezierEdgeBase, _EdgeBase); + // attach this item as attribute + dom.point['timeline-item'] = this; - _createClass(BezierEdgeBase, [{ - key: '_findBorderPositionBezier', + this.dirty = true; + } - /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - */ - value: function _findBorderPositionBezier(nearNode, ctx) { - var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + // append DOM to parent DOM + if (!this.parent) { + throw new Error('Cannot redraw item: no parent attached'); + } + if (!dom.point.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) { + throw new Error('Cannot redraw item: parent has no foreground container element'); + } + foreground.appendChild(dom.point); + } + this.displayed = true; - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToPoint, difference; - var threshold = 0.2; - var node = this.to; - var from = false; - if (nearNode.id === this.from.id) { - node = this.from; - from = 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.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; + var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; - pos = this.getPoint(middle, viaNode); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference < 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (from === false) { - low = middle; - } else { - high = middle; - } - } else { - if (from === false) { - high = middle; - } else { - low = middle; - } - } + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); + dom.point.className = 'vis-item vis-point' + className; + dom.dot.className = 'vis-item vis-dot' + className; - iteration++; - } - pos.t = middle; + // recalculate size of dot and contents + this.props.dot.width = dom.dot.offsetWidth; + this.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; - return pos; - } - }, { - key: '_getDistanceToBezierEdge', + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - /** - * Calculate the distance between a point (x3,y3) and a line segment from - * (x1,y1) to (x2,y2). - * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment - * @param {number} x1 - * @param {number} y1 - * @param {number} x2 - * @param {number} y2 - * @param {number} x3 - * @param {number} y3 - * @private - */ - value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var xVia = undefined, - yVia = undefined; - xVia = via.x; - yVia = via.y; - var minDistance = 1000000000; - var distance = undefined; - var i = undefined, - t = undefined, - x = undefined, - y = undefined; - var lastX = x1; - var lastY = y1; - for (i = 1; i < 10; i++) { - t = 0.1 * i; - x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; - y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); - minDistance = distance < minDistance ? distance : minDistance; - } - lastX = x; - lastY = y; - } + dom.dot.style.top = (this.height - this.props.dot.height) / 2 + 'px'; + dom.dot.style.left = this.props.dot.width / 2 + 'px'; - return minDistance; - } - }]); + // recalculate size + this.width = dom.point.offsetWidth; + this.height = dom.point.offsetHeight; - return BezierEdgeBase; - })(_EdgeBase3['default']); + this.dirty = false; + } - exports['default'] = BezierEdgeBase; - module.exports = exports['default']; + this._repaintDeleteButton(dom.point); + }; -/***/ }, -/* 3 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + */ + PointItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } + }; - 'use strict'; + /** + * Hide the item from the DOM (when visible) + */ + PointItem.prototype.hide = function () { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); + } - Object.defineProperty(exports, '__esModule', { - value: true - }); + this.displayed = false; + } + }; - 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; }; })(); + /** + * Reposition the item horizontally + * @Override + */ + PointItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + this.left = start - this.props.dot.width; - var NodeBase = (function () { - function NodeBase(options, body, labelModule) { - _classCallCheck(this, NodeBase); + // reposition point + this.dom.point.style.left = this.left + 'px'; + }; - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.top = undefined; - this.left = undefined; - this.height = undefined; - this.width = undefined; - this.radius = undefined; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + /** + * Reposition the item vertically + * @Override + */ + PointItem.prototype.repositionY = function () { + var orientation = this.options.orientation.item; + var point = this.dom.point; + + if (orientation == 'top') { + point.style.top = this.top + 'px'; + } else { + point.style.top = this.parent.height - this.top - this.height + 'px'; } + }; - _createClass(NodeBase, [{ - key: 'setOptions', - value: function setOptions(options) { - this.options = options; - } - }, { - key: '_distanceToBorder', - value: function _distanceToBorder(angle) { - var borderWidth = 1; - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - } - }, { - key: 'enableShadow', - value: function enableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0.5)'; - ctx.shadowBlur = this.options.shadow.size; - ctx.shadowOffsetX = this.options.shadow.x; - ctx.shadowOffsetY = this.options.shadow.y; - } - } - }, { - key: 'disableShadow', - value: function disableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0)'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } - } - }]); + /** + * Return the width of the item left from its start date + * @return {number} + */ + PointItem.prototype.getWidthLeft = function () { + return this.props.dot.width; + }; - return NodeBase; - })(); + /** + * Return the width of the item right from its start date + * @return {number} + */ + PointItem.prototype.getWidthRight = function () { + return this.width - this.props.dot.width; + }; - exports['default'] = NodeBase; - module.exports = exports['default']; + module.exports = PointItem; /***/ }, -/* 4 */ +/* 3 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + 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; }; })(); - - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + 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"); } } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + var Node = __webpack_require__(63); + var Edge = __webpack_require__(83); + var util = __webpack_require__(9); - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + var _this = this; - var _utilBezierEdgeBase = __webpack_require__(2); + _classCallCheck(this, SelectionHandler); - var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); + this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; + this.hoverObj = { nodes: {}, edges: {} }; - var BezierEdgeDynamic = (function (_BezierEdgeBase) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); + this.options = {}; + this.defaultOptions = { + multiselect: false, + selectable: true, + selectConnectedEdges: true, + hoverConnectedEdges: true + }; + util.extend(this.options, this.defaultOptions); - //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); } - _inherits(BezierEdgeDynamic, _BezierEdgeBase); - - _createClass(BezierEdgeDynamic, [{ - key: 'setOptions', + _createClass(SelectionHandler, [{ + key: "setOptions", value: function setOptions(options) { - this.options = options; - this.id = this.options.id; - this.setupSupportNode(); - this.connect(); + if (options !== undefined) { + var fields = ["multiselect", "hoverConnectedEdges", "selectable", "selectConnectedEdges"]; + util.selectiveDeepExtend(fields, this.options, options); + } } }, { - key: 'connect', - value: function connect() { - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - if (this.from === undefined || this.to === undefined || this.options.physics === false) { - this.via.setOptions({ physics: false }); - } else { - // fix weird behaviour where a selfreferencing node has physics enabled - if (this.from.id === this.to.id) { - this.via.setOptions({ physics: false }); - } else { - this.via.setOptions({ physics: true }); + key: "selectOnPoint", + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.selectable === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + + // unselect after getting the objects in order to restore width and height. + this.unselectAll(); + + if (obj !== undefined) { + selected = this.selectObject(obj); } + this.body.emitter.emit("_requestRedraw"); } + return selected; } }, { - key: 'cleanup', - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; + key: "selectAdditionalOnPoint", + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.selectable === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } + + this.body.emitter.emit("_requestRedraw"); + } + } + return selectionChanged; + } + }, { + key: "_generateClickEvent", + value: function _generateClickEvent(eventType, event, pointer, oldSelection) { + var emptySelection = arguments[4] === undefined ? false : arguments[4]; + + var properties = undefined; + if (emptySelection === true) { + properties = { nodes: [], edges: [] }; + } else { + properties = this.getSelection(); + } + properties["pointer"] = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + properties["event"] = event; + + if (oldSelection !== undefined) { + properties["previousSelection"] = oldSelection; + } + this.body.emitter.emit(eventType, properties); + } + }, { + key: "selectObject", + value: function selectObject(obj) { + var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; + + if (obj !== undefined) { + if (obj instanceof Node) { + if (highlightEdges === true) { + this._selectConnectedEdges(obj); + } + } + obj.select(); + this._addToSelection(obj); return true; } return false; } }, { - key: 'togglePhysics', - value: function togglePhysics(status) { - this.via.setOptions({ physics: status }); - this.positionBezierNode(); + key: "deselectObject", + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); + } } }, { - key: 'setupSupportNode', + key: "_getAllNodesOverlappingWith", /** - * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but - * are used for the force calculation. - * - * The changed data is not called, if needed, it is returned by the main edge constructor. + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes * @private */ - value: function setupSupportNode() { - if (this.via === undefined) { - var nodeId = 'edgeId:' + this.id; - var node = this.body.functions.createNode({ - id: nodeId, - shape: 'circle', - physics: true, - hidden: true - }); - this.body.nodes[nodeId] = node; - this.via = node; - this.via.parentEdgeId = this.id; - this.positionBezierNode(); - } - } - }, { - key: 'positionBezierNode', - value: function positionBezierNode() { - if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== undefined) { - this.via.x = 0; - this.via.y = 0; + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + var nodes = this.body.nodes; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } } + return overlappingNodes; } }, { - key: '_line', + key: "_pointerToPositionObject", /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - // draw shadow if enabled - this.enableShadow(ctx); - ctx.stroke(); - this.disableShadow(ctx); - return this.via; + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x - 1, + top: canvasPos.y + 1, + right: canvasPos.x + 1, + bottom: canvasPos.y - 1 + }; } }, { - key: 'getPoint', + key: "getNodeAt", /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node * @private */ - value: function getPoint(percentage) { - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + value: function getNodeAt(pointer) { + var returnNode = arguments[1] === undefined ? true : arguments[1]; - return { x: x, y: y }; + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + if (returnNode === true) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return overlappingNodes[overlappingNodes.length - 1]; + } + } else { + return undefined; + } } }, { - key: '_findBorderPosition', - value: function _findBorderPosition(nearNode, ctx) { - return this._findBorderPositionBezier(nearNode, ctx, this.via); + key: "_getEdgesOverlappingWith", + + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } } }, { - key: '_getDistanceToEdge', - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); - } - }]); - - return BezierEdgeDynamic; - })(_utilBezierEdgeBase2['default']); - - exports['default'] = BezierEdgeDynamic; - 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; }; })(); - - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - - 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'); } } - - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - - var _utilNodeBase = __webpack_require__(3); - - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + key: "_getAllEdgesOverlappingWith", - var Icon = (function (_NodeBase) { - function Icon(options, body, labelModule) { - _classCallCheck(this, Icon); + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + } + }, { + key: "getEdgeAt", - _get(Object.getPrototypeOf(Icon.prototype), 'constructor', this).call(this, options, body, labelModule); - } + /** + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {undefined} + * @private + */ + value: function getEdgeAt(pointer) { + var returnEdge = arguments[1] === undefined ? true : arguments[1]; - _inherits(Icon, _NodeBase); + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - _createClass(Icon, [{ - key: 'resize', - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var iconSize = { - width: Number(this.options.icon.size), - height: Number(this.options.icon.size) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; - this.radius = 0.5 * this.width; + if (overlappingEdges.length > 0) { + if (returnEdge === true) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return overlappingEdges[overlappingEdges.length - 1]; + } + } else { + return undefined; } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.options.icon.size = this.options.icon.size || 50; - - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - this._icon(ctx, x, y, selected); + key: "_addToSelection", - if (this.options.label !== undefined) { - var iconTextSpacing = 5; - this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; } - - this.updateBoundingBox(x, y); } }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.icon.size * 0.5; - this.boundingBox.left = x - this.options.icon.size * 0.5; - this.boundingBox.right = x + this.options.icon.size * 0.5; - this.boundingBox.bottom = y + this.options.icon.size * 0.5; + key: "_addToHover", - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - var iconTextSpacing = 5; - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing); + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; } } }, { - key: '_icon', - value: function _icon(ctx, x, y, selected) { - var iconSize = Number(this.options.icon.size); - - if (this.options.icon.code !== undefined) { - ctx.font = (selected ? 'bold ' : '') + iconSize + 'px ' + this.options.icon.face; - - // draw icon - ctx.fillStyle = this.options.icon.color || 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fillText(this.options.icon.code, x, y); + key: "_removeFromSelection", - // disable shadows for other elements. - this.disableShadow(ctx); + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; } else { - console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.'); + delete this.selectionObj.edges[obj.id]; } } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - } - }]); - - return Icon; - })(_utilNodeBase2['default']); - - exports['default'] = Icon; - module.exports = exports['default']; - -/***/ }, -/* 6 */ -/***/ 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 _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var util = __webpack_require__(19); - - var EdgeBase = (function () { - function EdgeBase(options, body, labelModule) { - _classCallCheck(this, EdgeBase); + key: "unselectAll", - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.colorDirty = true; - this.color = {}; - this.selectionWidth = 2; - this.hoverWidth = 1.5; - } + /** + * Unselect all. The selectionObj is useful for this. + * + * @private + */ + value: function unselectAll() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } - _createClass(EdgeBase, [{ - key: 'connect', - value: function connect() { - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - } - }, { - key: 'cleanup', - value: function cleanup() { - return false; - } - }, { - key: 'setOptions', - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; + this.selectionObj = { nodes: {}, edges: {} }; } }, { - key: 'togglePhysics', + key: "_getSelectedNodeCount", /** - * overloadable if the shape has to toggle the via node to disabled - * @param status + * return the number of selected nodes + * + * @returns {number} + * @private */ - value: function togglePhysics(status) {} + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + } }, { - key: 'drawLine', + key: "_getSelectedNode", /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * return the selected node + * + * @returns {number} * @private */ - value: function drawLine(ctx, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx, selected, hover); - ctx.lineWidth = this.getLineWidth(selected, hover); - var via = undefined; - if (this.options.dashes !== false) { - via = this._drawDashedLine(ctx); - } else { - via = this._drawLine(ctx); + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } } - return via; + return undefined; } }, { - key: '_drawLine', - value: function _drawLine(ctx) { - var via = undefined; - if (this.from != this.to) { - // draw line - via = this._line(ctx); - } else { - var _getCircleData2 = this._getCircleData(ctx); - - var _getCircleData22 = _slicedToArray(_getCircleData2, 3); - - var x = _getCircleData22[0]; - var y = _getCircleData22[1]; - var radius = _getCircleData22[2]; + key: "_getSelectedEdge", - this._circle(ctx, x, y, radius); + /** + * return the selected edge + * + * @returns {number} + * @private + */ + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } } - return via; + return undefined; } }, { - key: '_drawDashedLine', - value: function _drawDashedLine(ctx) { - var via = undefined; - ctx.lineCap = 'round'; - var pattern = [5, 5]; - if (Array.isArray(this.options.dashes) === true) { - pattern = this.options.dashes; - } - - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { - ctx.save(); - - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; - - // draw the line - if (this.from != this.to) { - // draw line - via = this._line(ctx); - } else { - var _getCircleData3 = this._getCircleData(ctx); - - var _getCircleData32 = _slicedToArray(_getCircleData3, 3); - - var x = _getCircleData32[0]; - var y = _getCircleData32[1]; - var radius = _getCircleData32[2]; - - this._circle(ctx, x, y, radius); - } - - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } else { - // unsupporting smooth lines - - if (this.from != this.to) { - // draw line - ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern); - } else { - var _getCircleData4 = this._getCircleData(ctx); - - var _getCircleData42 = _slicedToArray(_getCircleData4, 3); - - var x = _getCircleData42[0]; - var y = _getCircleData42[1]; - var radius = _getCircleData42[2]; + key: "_getSelectedEdgeCount", - this._circle(ctx, x, y, radius); + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } - // draw shadow if enabled - this.enableShadow(ctx); - - ctx.stroke(); - - // disable shadows for other elements. - this.disableShadow(ctx); } - return via; + return count; } }, { - key: 'findBorderPosition', - value: function findBorderPosition(nearNode, ctx, options) { - if (this.from != this.to) { - return this._findBorderPosition(nearNode, ctx, options); - } else { - return this._findBorderPositionCircle(nearNode, ctx, options); + key: "_getSelectedObjectCount", + + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } } + return count; } }, { - key: 'findBorderPositions', - value: function findBorderPositions(ctx) { - var from = {}; - var to = {}; - if (this.from != this.to) { - from = this._findBorderPosition(this.from, ctx); - to = this._findBorderPosition(this.to, ctx); - } else { - var _getCircleData5 = this._getCircleData(ctx); - - var _getCircleData52 = _slicedToArray(_getCircleData5, 3); - - var x = _getCircleData52[0]; - var y = _getCircleData52[1]; - var radius = _getCircleData52[2]; + key: "_selectionIsEmpty", - from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } } - return { from: from, to: to }; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; } }, { - key: '_getCircleData', - value: function _getCircleData(ctx) { - var x = undefined, - y = undefined; - var node = this.from; - var radius = this.options.selfReferenceSize; + key: "_clusterInSelection", - if (ctx !== undefined) { - if (node.shape.width === undefined) { - node.shape.resize(ctx); + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } } } + return false; + } + }, { + key: "_selectConnectedEdges", - // get circle coordinates - if (node.shape.width > node.shape.height) { - x = node.x + node.shape.width * 0.5; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - node.shape.height * 0.5; + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); } - return [x, y, radius]; } }, { - key: '_pointOnCircle', + key: "_hoverConnectedEdges", /** - * 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 + * select the edges connected to the node that is being selected + * + * @param {Node} node * @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) - }; + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } } }, { - key: '_findBorderPositionCircle', + key: "_unselectConnectedEdges", /** - * This function uses binary search to look for the point where the circle crosses the border of the node. - * @param node - * @param ctx - * @param options - * @returns {*} + * unselect the edges connected to the node that is being selected + * + * @param {Node} node * @private */ - value: function _findBorderPositionCircle(node, ctx, options) { - var x = options.x; - var y = options.y; - var low = options.low; - var high = options.high; - var direction = options.direction; - - var maxIterations = 10; - var iteration = 0; - var radius = this.options.selfReferenceSize; - var pos = undefined, - angle = undefined, - distanceToBorder = undefined, - distanceToPoint = undefined, - difference = undefined; - var threshold = 0.05; - var middle = (low + high) * 0.5; - - while (low <= high && iteration < maxIterations) { - middle = (low + high) * 0.5; - - pos = this._pointOnCircle(x, y, radius, middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference > 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (direction > 0) { - low = middle; - } else { - high = middle; - } - } else { - if (direction > 0) { - high = middle; - } else { - low = middle; - } - } - iteration++; + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); } - pos.t = middle; - - return pos; } }, { - key: 'getLineWidth', + key: "blurObject", /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object * @private */ - value: function getLineWidth(selected, hover) { - if (selected === true) { - return Math.max(this.selectionWidth, 0.3 / this.body.view.scale); - } else { - if (hover === true) { - return Math.max(this.hoverWidth, 0.3 / this.body.view.scale); + value: function blurObject(object) { + if (object.hover === true) { + object.hover = false; + if (object instanceof Node) { + this.body.emitter.emit("blurNode", { node: object.id }); } else { - return Math.max(this.options.width, 0.3 / this.body.view.scale); + this.body.emitter.emit("blurEdge", { edge: object.id }); } } } }, { - key: 'getColor', - value: function getColor(ctx, selected, hover) { - var colorOptions = this.options.color; - if (colorOptions.inherit !== false) { - // when this is a loop edge, just use the 'from' method - if (colorOptions.inherit === 'both' && this.from.id !== this.to.id) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor = undefined, - toColor = undefined; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; + key: "hoverObject", - if (this.from.selected === false && this.to.selected === false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else if (this.from.selected === true && this.to.selected === false) { - toColor = this.to.options.color.border; - } else if (this.from.selected === false && this.to.selected === true) { - fromColor = this.from.options.color.border; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function hoverObject(object) { + var hoverChanged = false; + // remove all node hover highlights + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (object === undefined) { + this.blurObject(this.hoverObj.nodes[nodeId]); + hoverChanged = true; + } else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) { + this.blurObject(this.hoverObj.nodes[nodeId]); + hoverChanged = true; + delete this.hoverObj.nodes[nodeId]; } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); + } + } - // -------------------- this returns -------------------- // - return grd; + // removing all edge hover highlights + for (var edgeId in this.hoverObj.edges) { + if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + this.hoverObj.edges[edgeId].hover = false; + delete this.hoverObj.edges[edgeId]; } + } - if (this.colorDirty === true) { - if (colorOptions.inherit === 'to') { - this.color.highlight = this.to.options.color.highlight.border; - this.color.hover = this.to.options.color.hover.border; - this.color.color = util.overrideOpacity(this.to.options.color.border, colorOptions.opacity); + if (object !== undefined) { + if (object.hover === false) { + object.hover = true; + this._addToHover(object); + hoverChanged = true; + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); } else { - // (this.options.color.inherit.source === "from") { - this.color.highlight = this.from.options.color.highlight.border; - this.color.hover = this.from.options.color.hover.border; - this.color.color = util.overrideOpacity(this.from.options.color.border, colorOptions.opacity); + this.body.emitter.emit("hoverEdge", { edge: object.id }); } } - } else if (this.colorDirty === true) { - this.color.highlight = colorOptions.highlight; - this.color.hover = colorOptions.hover; - this.color.color = util.overrideOpacity(colorOptions.color, colorOptions.opacity); + if (object instanceof Node && this.options.hoverConnectedEdges === true) { + this._hoverConnectedEdges(object); + } } - // if color inherit is on and gradients are used, the function has already returned by now. - this.colorDirty = false; - - if (selected === true) { - return this.color.highlight; - } else if (hover === true) { - return this.color.hover; - } else { - return this.color.color; + if (hoverChanged === true) { + this.body.emitter.emit("_requestRedraw"); } } }, { - key: '_circle', + key: "getSelection", /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @private + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection */ - value: function _circle(ctx, x, y, radius) { - // draw shadow if enabled - this.enableShadow(ctx); + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + } + }, { + key: "getSelectedNodes", - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.selectable === true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray; + } + }, { + key: "getSelectedEdges", - // disable shadows for other elements. - this.disableShadow(ctx); + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedEdges() { + var idArray = []; + if (this.options.selectable === true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } + } + return idArray; } }, { - key: 'getDistanceToEdge', + key: "selectNodes", /** - * Calculate the distance between a point (x3,y3) and a line segment from - * (x1,y1) to (x2,y2). - * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment - * @param {number} x1 - * @param {number} y1 - * @param {number} x2 - * @param {number} y2 - * @param {number} x3 - * @param {number} y3 - * @private + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] */ - value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); - } else { - var _getCircleData6 = this._getCircleData(); + value: function selectNodes(selection) { + var highlightEdges = arguments[1] === undefined ? true : arguments[1]; - var _getCircleData62 = _slicedToArray(_getCircleData6, 3); + var i = undefined, + id = undefined; - var x = _getCircleData62[0]; - var y = _getCircleData62[1]; - var radius = _getCircleData62[2]; + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - var dx = x - x3; - var dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); - } + // first unselect any selected node + this.unselectAll(); - if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { - return 0; - } else { - return returnValue; + for (i = 0; i < selection.length; i++) { + id = selection[i]; + + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); + } + this.selectObject(node, highlightEdges); } + this.body.emitter.emit("_requestRedraw"); } }, { - key: '_getDistanceToLine', - value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { - var px = x2 - x1; - var py = y2 - y1; - var something = px * px + py * py; - var u = ((x3 - x1) * px + (y3 - y1) * py) / something; + key: "selectEdges", - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; - } + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + value: function selectEdges(selection) { + var i = undefined, + id = undefined; - var x = x1 + u * px; - var y = y1 + u * py; - var dx = x - x3; - var dy = y - y3; + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + // first unselect any selected objects + this.unselectAll(); - return Math.sqrt(dx * dx + dy * dy); + for (i = 0; i < selection.length; i++) { + id = selection[i]; + + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this.selectObject(edge); + } + this.body.emitter.emit("_requestRedraw"); } }, { - key: 'drawArrowHead', + key: "updateSelection", /** - * - * @param ctx - * @param position - * @param viaNode + * Validate the selection: remove ids of nodes which no longer exist + * @private */ - value: function drawArrowHead(ctx, position, viaNode, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx, selected, hover); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(selected, hover); - - // set lets - var angle = undefined; - var length = undefined; - var arrowPos = undefined; - var node1 = undefined; - var node2 = undefined; - var guideOffset = undefined; - var scaleFactor = undefined; - - if (position === 'from') { - node1 = this.from; - node2 = this.to; - guideOffset = 0.1; - scaleFactor = this.options.arrows.from.scaleFactor; - } else if (position === 'to') { - node1 = this.to; - node2 = this.from; - guideOffset = -0.1; - scaleFactor = this.options.arrows.to.scaleFactor; - } else { - node1 = this.to; - node2 = this.from; - scaleFactor = this.options.arrows.middle.scaleFactor; + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } } - - // if not connected to itself - if (node1 != node2) { - if (position !== 'middle') { - // draw arrow head - if (this.options.smooth.enabled === true) { - arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); - var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); - } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.findBorderPosition(node1, ctx); + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; } - } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. } - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + } + } + }]); - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + return SelectionHandler; + })(); - // disable shadows for other elements. - this.disableShadow(ctx); - ctx.stroke(); - } else { - // draw circle - var _angle = undefined, - point = undefined; + exports["default"] = SelectionHandler; + module.exports = exports["default"]; - var _getCircleData7 = this._getCircleData(ctx); +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { - var _getCircleData72 = _slicedToArray(_getCircleData7, 3); + 'use strict'; - var x = _getCircleData72[0]; - var y = _getCircleData72[1]; - var radius = _getCircleData72[2]; + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); - if (position === 'from') { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } else if (position === 'to') { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; - } else { - point = this._pointOnCircle(x, y, radius, 0.175); - _angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } + /** + * @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 || {}; - // draw the arrowhead - var _length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(point.x, point.y, _angle, _length); + this.selected = false; + this.displayed = false; + this.dirty = true; - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + this.top = null; + this.left = null; + this.width = null; + this.height = null; - // disable shadows for other elements. - this.disableShadow(ctx); - ctx.stroke(); - } - } - }, { - key: 'enableShadow', - value: function enableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0.5)'; - ctx.shadowBlur = this.options.shadow.size; - ctx.shadowOffsetX = this.options.shadow.x; - ctx.shadowOffsetY = this.options.shadow.y; - } - } - }, { - key: 'disableShadow', - value: function disableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0)'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } - } - }]); + this.editable = null; + if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { + this.editable = data.editable; + } + } - return EdgeBase; - })(); + Item.prototype.stack = true; - exports['default'] = EdgeBase; - module.exports = exports['default']; + /** + * Select current item + */ + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); + }; -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Unselect current item + */ + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - 'use strict'; + /** + * 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); + } - Object.defineProperty(exports, '__esModule', { - value: true - }); + if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { + this.editable = data.editable; + } - 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; }; })(); + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * 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; + } + }; - var util = __webpack_require__(19); - var Hammer = __webpack_require__(15); - var hammerUtil = __webpack_require__(41); - var keycharm = __webpack_require__(52); + /** + * 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; + }; - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; + /** + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed + */ + Item.prototype.show = function () { + return false; + }; - _classCallCheck(this, NavigationHandler); + /** + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed + */ + Item.prototype.hide = function () { + return false; + }; - this.body = body; - this.canvas = canvas; + /** + * Repaint the item + */ + Item.prototype.redraw = function () {}; - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; - this.touchTime = 0; - this.activated = false; + /** + * Reposition the Item horizontally + */ + Item.prototype.repositionX = function () {}; - this.body.emitter.on('activate', function () { - _this.activated = true;_this.configureKeyboardBindings(); - }); - this.body.emitter.on('deactivate', function () { - _this.activated = false;_this.configureKeyboardBindings(); - }); - this.body.emitter.on('destroy', function () { - if (_this.keycharm !== undefined) { - _this.keycharm.destroy(); - } + /** + * Reposition the Item vertically + */ + Item.prototype.repositionY = function () {}; + + /** + * 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) { + var editable = (this.options.editable.remove || this.data.editable === true) && this.data.editable !== false; + + if (this.selected && editable && !this.dom.deleteButton) { + // create and show button + var me = this; + + var deleteButton = document.createElement('div'); + deleteButton.className = 'vis-delete'; + deleteButton.title = 'Delete this item'; + + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on('tap', function (event) { + event.stopPropagation(); + me.parent.removeFromDataSet(me); }); - this.options = {}; + 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; } + }; - _createClass(NavigationHandler, [{ - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); + /** + * 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; + } + + 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); } } - }, { - key: 'create', - value: function create() { - if (this.options.navigationButtons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); - } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); - } - this.configureKeyboardBindings(); - } - }, { - key: 'cleanNavigation', - value: function cleanNavigation() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].destroy(); - } - this.navigationHammers = []; - } + this.content = content; + } + }; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { - this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); - } + /** + * 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'); + } + }; - this.iconsCreated = false; - } - }, { - key: 'loadNavigationElements', + /** + * 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 = []; - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private - */ - value: function loadNavigationElements() { - var _this2 = this; + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == 'all') { + attributes = Object.keys(this.data); + } else { + return; + } - this.cleanNavigation(); + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; - this.navigationDOM = {}; - var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; - var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; + if (value != null) { + element.setAttribute('data-' + name, value); + } else { + element.removeAttribute('data-' + name); + } + } + } + }; - this.navigationDOM['wrapper'] = document.createElement('div'); - this.navigationDOM['wrapper'].className = 'vis-navigation'; - this.canvas.frame.appendChild(this.navigationDOM['wrapper']); + /** + * 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; + } - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement('div'); - this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i]; - this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } + }; - var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); - if (navigationDivActions[i] === '_fit') { - hammerUtil.onTouch(hammer, this._fit.bind(this)); - } else { - hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); - } + /** + * 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.navigationHammers.push(hammer); - } + /** + * Return the width of the item left from its start date + * @return {number} + */ + Item.prototype.getWidthLeft = function () { + return 0; + }; - // use a hammer for the release so we do not require the one used in the rest of the network - // the one the rest uses can be overloaded by the manipulation system. - var hammerFrame = new Hammer(this.canvas.frame); - hammerUtil.onRelease(hammerFrame, function () { - _this2._stopMovement(); - }); - this.navigationHammers.push(hammerFrame); + /** + * Return the width of the item right from the max of its start and end date + * @return {number} + */ + Item.prototype.getWidthRight = function () { + return 0; + }; - this.iconsCreated = true; - } - }, { - key: 'bindToRedraw', - value: function bindToRedraw(action) { - if (this.boundFunctions[action] === undefined) { - this.boundFunctions[action] = this[action].bind(this); - this.body.emitter.on('initRedraw', this.boundFunctions[action]); - this.body.emitter.emit('_startRendering'); - } - } - }, { - key: 'unbindFromRedraw', - value: function unbindFromRedraw(action) { - if (this.boundFunctions[action] !== undefined) { - this.body.emitter.off('initRedraw', this.boundFunctions[action]); - this.body.emitter.emit('_stopRendering'); - delete this.boundFunctions[action]; - } - } - }, { - key: '_fit', + module.exports = Item; - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - value: function _fit() { - if (new Date().valueOf() - this.touchTime > 700) { - // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) - this.body.emitter.emit('fit', { duration: 700 }); - this.touchTime = new Date().valueOf(); - } - } - }, { - key: '_stopMovement', + // should be implemented by the item - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - value: function _stopMovement() { - for (var boundAction in this.boundFunctions) { - if (this.boundFunctions.hasOwnProperty(boundAction)) { - this.body.emitter.off('initRedraw', this.boundFunctions[boundAction]); - this.body.emitter.emit('_stopRendering'); - } - } - this.boundFunctions = {}; - } - }, { - key: '_moveUp', - value: function _moveUp() { - this.body.view.translation.y += this.options.keyboard.speed.y; - } - }, { - key: '_moveDown', - value: function _moveDown() { - this.body.view.translation.y -= this.options.keyboard.speed.y; - } - }, { - key: '_moveLeft', - value: function _moveLeft() { - this.body.view.translation.x += this.options.keyboard.speed.x; - } - }, { - key: '_moveRight', - value: function _moveRight() { - this.body.view.translation.x -= this.options.keyboard.speed.x; - } - }, { - key: '_zoomIn', - value: function _zoomIn() { - this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; - this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); - } - }, { - key: '_zoomOut', - value: function _zoomOut() { - this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; - this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); - } - }, { - key: 'configureKeyboardBindings', + // should be implemented by the item - /** - * bind all keys using keycharm. - */ - value: function configureKeyboardBindings() { - var _this3 = this; + // should be implemented by the item - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { - if (this.options.keyboard.enabled === true) { - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: true }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); - } + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + 'use strict'; - this.keycharm.reset(); - - if (this.activated === true) { - this.keycharm.bind('up', function () { - _this3.bindToRedraw('_moveUp'); - }, 'keydown'); - this.keycharm.bind('down', function () { - _this3.bindToRedraw('_moveDown'); - }, 'keydown'); - this.keycharm.bind('left', function () { - _this3.bindToRedraw('_moveLeft'); - }, 'keydown'); - this.keycharm.bind('right', function () { - _this3.bindToRedraw('_moveRight'); - }, 'keydown'); - this.keycharm.bind('=', function () { - _this3.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('num+', function () { - _this3.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('num-', function () { - _this3.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind('-', function () { - _this3.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind('[', function () { - _this3.bindToRedraw('_zoomOut'); - }, 'keydown'); - this.keycharm.bind(']', function () { - _this3.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('pageup', function () { - _this3.bindToRedraw('_zoomIn'); - }, 'keydown'); - this.keycharm.bind('pagedown', function () { - _this3.bindToRedraw('_zoomOut'); - }, 'keydown'); - - this.keycharm.bind('up', function () { - _this3.unbindFromRedraw('_moveUp'); - }, 'keyup'); - this.keycharm.bind('down', function () { - _this3.unbindFromRedraw('_moveDown'); - }, 'keyup'); - this.keycharm.bind('left', function () { - _this3.unbindFromRedraw('_moveLeft'); - }, 'keyup'); - this.keycharm.bind('right', function () { - _this3.unbindFromRedraw('_moveRight'); - }, 'keyup'); - this.keycharm.bind('=', function () { - _this3.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('num+', function () { - _this3.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('num-', function () { - _this3.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind('-', function () { - _this3.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind('[', function () { - _this3.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - this.keycharm.bind(']', function () { - _this3.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('pageup', function () { - _this3.unbindFromRedraw('_zoomIn'); - }, 'keyup'); - this.keycharm.bind('pagedown', function () { - _this3.unbindFromRedraw('_zoomOut'); - }, 'keyup'); - } - } - } - }]); - - return NavigationHandler; - })(); - - exports['default'] = NavigationHandler; - module.exports = exports['default']; + if (typeof window !== 'undefined') { + var propagating = __webpack_require__(6); + var Hammer = window['Hammer'] || __webpack_require__(7); + module.exports = propagating(Hammer, { + preventDefault: 'mouse' + }); + } else { + module.exports = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + }; + } /***/ }, -/* 8 */ +/* 6 */ /***/ 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 __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - var util = __webpack_require__(19); - var Hammer = __webpack_require__(15); - var hammerUtil = __webpack_require__(41); + (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 - /** - * clears the toolbar div element of children - * - * @private - */ + /** + * 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) { + var _options = options || { + preventDefault: false + }; - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - var _this = this; + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; - _classCallCheck(this, ManipulationSystem); + var PropagatingHammer = function(element, options) { + var o = Object.create(_options); + if (options) Hammer.extend(o, options); + return propagating(new Hammer(element, o), o); + }; + Hammer.extend(PropagatingHammer, Hammer); - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; + PropagatingHammer.Manager = function (element, options) { + var o = Object.create(_options); + if (options) Hammer.extend(o, options); + return propagating(new Hammer.Manager(element, o), o); + }; - this.editMode = false; - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + return PropagatingHammer; + } - this.manipulationHammers = []; - this.temporaryUIFunctions = {}; - this.temporaryEventFunctions = []; + // create a wrapper object which will override the functions + // `on`, `off`, `destroy`, and `emit` of the hammer instance + var wrapper = Object.create(hammer); - this.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.inMode = false; - this.selectedControlNode = undefined; + // attach to DOM element + var element = hammer.element; + element.hammer = wrapper; - 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 + // 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; } - }; - util.extend(this.options, this.defaultOptions); - - 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)); - } - _createClass(ManipulationSystem, [{ - key: '_restore', + /** @type {Object.>} */ + wrapper._handlers = {}; /** - * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. - * @private + * 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 */ - value: function _restore() { - if (this.inMode !== false) { - if (this.options.initiallyActive === true) { - this.enableEditMode(); - } else { - this.disableEditMode(); + wrapper.on = function (events, handler) { + // register the handler + split(events).forEach(function (event) { + var _handlers = wrapper._handlers[event]; + if (!_handlers) { + wrapper._handlers[event] = _handlers = []; + + // register the static, propagated handler + hammer.on(event, propagatedHandler); } - } - } - }, { - key: 'setOptions', + _handlers.push(handler); + }); + + return wrapper; + }; /** - * Set the Options - * @param options + * 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 */ - 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; - } - } + wrapper.off = function (events, handler) { + // unregister the handler + split(events).forEach(function (event) { + var _handlers = wrapper._handlers[event]; + if (_handlers) { + _handlers = handler ? _handlers.filter(function (h) { + return h !== handler; + }) : []; - 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; + if (_handlers.length > 0) { + wrapper._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer.off(event, propagatedHandler); + delete wrapper._handlers[event]; + } } - this._setup(); - } - } - }, { - key: 'toggleEditMode', + }); + + return wrapper; + }; /** - * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. - * - * @private + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event */ - value: function toggleEditMode() { - if (this.editMode === true) { - this.disableEditMode(); - } else { - this.enableEditMode(); - } - } - }, { - key: 'enableEditMode', - value: function enableEditMode() { - this.editMode = true; + wrapper.emit = function(eventType, event) { + _firstTarget = event.target; + hammer.emit(eventType, event); + }; - 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; + wrapper.destroy = function () { + // Detach from DOM element + delete hammer.element.hammer; - this._clean(); - if (this.guiEnabled === true) { - this.manipulationDiv.style.display = 'none'; - this.closeDiv.style.display = 'none'; - this.editModeDiv.style.display = 'block'; - this._createEditButton(); - } + // clear all handlers + wrapper._handlers = {}; + + // call original hammer destroy + hammer.destroy(); + }; + + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); } - }, { - key: 'showManipulatorToolbar', /** - * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private + * A static event handler, applying event propagation. + * @param {Object} event */ - value: function showManipulatorToolbar() { - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - - // reset global letiables - this.manipulationDOM = {}; - - // 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'; - - 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; + 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 (this.options.addNode !== false) { - this._createAddNodeButton(locale); - needSeperator = true; + if (event.srcEvent._handled[event.type]) { + return; } - if (this.options.addEdge !== false) { - if (needSeperator === true) { - this._createSeperator(1); - } else { - needSeperator = true; - } - this._createAddEdgeButton(locale); + else { + event.srcEvent._handled[event.type] = true; } + } - 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); - } + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - // 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); + // attach firstTarget property to the event + event.firstTarget = _firstTarget; + + // 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); } } - // 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)); + elem = elem.parentNode; } - - // redraw to show any possible changes - this.body.emitter.emit('_redraw'); } - }, { - 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 wrapper; + }; + })); - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); - 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']); +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + 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'; - this._temporaryBindEvent('click', this._performAddNode.bind(this)); - } - }, { - key: 'editNode', + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - /** - * 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(); - var node = this.selectionHandler._getSelectedNode(); - if (node !== undefined) { - this.inMode = 'editNode'; - if (typeof this.options.editNode === 'function') { - 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.getDataSet().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.'); - } - } else { - this.showManipulatorToolbar(); - } - } - }, { - 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(); - } - - // 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', - - /** - * create the toolbar to edit edges - * - * @private - */ - value: function editEdgeMode() { - var _this3 = 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 = '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]; - if (this.edgeBeingEditedId !== undefined) { - (function () { - var edge = _this3.body.edges[_this3.edgeBeingEditedId]; - - // create control nodes - var controlNodeFrom = _this3._getNewTargetNode(edge.from.x, edge.from.y); - var controlNodeTo = _this3._getNewTargetNode(edge.to.x, edge.to.y); - - _this3.temporaryIds.nodes.push(controlNodeFrom.id); - _this3.temporaryIds.nodes.push(controlNodeTo.id); - - _this3.body.nodes[controlNodeFrom.id] = controlNodeFrom; - _this3.body.nodeIndices.push(controlNodeFrom.id); - _this3.body.nodes[controlNodeTo.id] = controlNodeTo; - _this3.body.nodeIndices.push(controlNodeTo.id); - - // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI - _this3._temporaryBindUI('onTouch', _this3._controlNodeTouch.bind(_this3)); // used to get the position - _this3._temporaryBindUI('onTap', function () {}); // disabled - _this3._temporaryBindUI('onHold', function () {}); // disabled - _this3._temporaryBindUI('onDragStart', _this3._controlNodeDragStart.bind(_this3)); // used to select control node - _this3._temporaryBindUI('onDrag', _this3._controlNodeDrag.bind(_this3)); // used to drag control node - _this3._temporaryBindUI('onDragEnd', _this3._controlNodeDragEnd.bind(_this3)); // used to connect or revert control nodes - _this3._temporaryBindUI('onMouseMove', function () {}); // disabled - - // create function to position control nodes correctly on movement - // automatically cleaned up because we use the temporary bind - _this3._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; - } - }); - - _this3.body.emitter.emit('_redraw'); - })(); - } else { - this.showManipulatorToolbar(); - } - } - }, { - key: 'deleteSelected', - - /** - * delete everything in the selection - * - * @private - */ - value: function deleteSelected() { - var _this4 = 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 = '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; - } - } - - 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 (typeof deleteFunction === 'function') { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length === 2) { - deleteFunction(data, function (finalizedData) { - if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'delete') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this4.body.data.edges.getDataSet().remove(finalizedData.edges); - _this4.body.data.nodes.getDataSet().remove(finalizedData.nodes); - _this4.body.emitter.emit('startSimulation'); - _this4.showManipulatorToolbar(); - } else { - _this4.body.emitter.emit('startSimulation'); - _this4.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for delete does not support two arguments (data, callback)'); - } - } else { - this.body.data.edges.getDataSet().remove(selectedEdges); - this.body.data.nodes.getDataSet().remove(selectedNodes); - this.body.emitter.emit('startSimulation'); - this.showManipulatorToolbar(); - } - } - }, { - key: '_setup', - - //********************************************** PRIVATE ***************************************// - - /** - * draw or remove the DOM - * @private - */ - value: function _setup() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; - - this._createWrappers(); - if (this.editMode === false) { - this._createEditButton(); - } else { - this.showManipulatorToolbar(); - } - } else { - this._removeManipulationDOM(); - - // disable the gui - this.guiEnabled = false; - } - } - }, { - 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); - } - - // 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', - - /** - * 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); - - controlNodeStyle.id = 'targetNode' + util.randomUUID(); - controlNodeStyle.hidden = false; - controlNodeStyle.physics = false; - controlNodeStyle.x = x; - controlNodeStyle.y = y; - - return this.body.functions.createNode(controlNodeStyle); - } - }, { - key: '_createEditButton', - - /** - * Create the edit button - */ - value: function _createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); - - // 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); - - // bind a hammer listener to the button, calling the function toggleEditMode. - this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); - } - }, { - key: '_clean', - - /** - * 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; - - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); - - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } - - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); - - // restore overloaded UI functions - this._unbindTemporaryUIs(); - - // remove the temporaryEventFunctions - this._unbindTemporaryEvents(); - - // restore the physics if required - this.body.emitter.emit('restorePhysics'); - } - }, { - key: '_cleanManipulatorHammers', - - /** - * 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(); - } - this.manipulationHammers = []; - } - } - }, { - key: '_removeManipulationDOM', - - /** - * Remove all DOM elements created by this module. - * @private - */ - value: function _removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); - - // 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 _this5 = 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 && _this5.inMode === 'addNode') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this5.body.data.nodes.getDataSet().add(finalizedData); - _this5.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this.showManipulatorToolbar(); - } - } else { - this.body.data.nodes.getDataSet().add(defaultData); - this.showManipulatorToolbar(); - } - } - }, { - key: '_performAddEdge', - - /** - * connect two nodes with a new edge. - * - * @private - */ - value: function _performAddEdge(sourceNodeId, targetNodeId) { - var _this6 = 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 && _this6.inMode === 'addEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback - _this6.body.data.edges.getDataSet().add(finalizedData); - _this6.selectionHandler.unselectAll(); - _this6.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for connect does not support two arguments (data,callback)'); - } - } else { - this.body.data.edges.getDataSet().add(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } - } - }, { - key: '_performEditEdge', - - /** - * connect two nodes with a new edge. - * - * @private - */ - value: function _performEditEdge(sourceNodeId, targetNodeId) { - var _this7 = 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 || _this7.inMode !== 'editEdge') { - // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { - _this7.body.edges[defaultData.id].updateEdgeType(); - _this7.body.emitter.emit('_redraw'); - } else { - _this7.body.data.edges.getDataSet().update(finalizedData); - _this7.selectionHandler.unselectAll(); - _this7.showManipulatorToolbar(); - } - }); - } else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } else { - this.body.data.edges.getDataSet().update(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } - } - }]); - - return ManipulationSystem; - })(); - - exports['default'] = ManipulationSystem; - module.exports = exports['default']; - -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Item = __webpack_require__(14); - - /** - * @constructor PointItem - * @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 PointItem(data, conversion, options) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 - } - }; - - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); - } - } - - Item.call(this, data, conversion, options); - } - - PointItem.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 - */ - PointItem.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 - */ - PointItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // background box - dom.point = document.createElement('div'); - // className is updated in redraw() - - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); - - // attach this item as attribute - dom.point['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.point.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); - } - foreground.appendChild(dom.point); - } - 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.point); - this._updateDataAttributes(this.dom.point); - this._updateStyle(this.dom.point); - - var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; - - // update class - var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.point.className = 'vis-item vis-point' + className; - dom.dot.className = 'vis-item vis-dot' + className; - - // recalculate size of dot and contents - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; - - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - - dom.dot.style.top = (this.height - this.props.dot.height) / 2 + 'px'; - dom.dot.style.left = this.props.dot.width / 2 + 'px'; - - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - - this.dirty = false; - } - - this._repaintDeleteButton(dom.point); - }; - - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - PointItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); - } - }; - - /** - * Hide the item from the DOM (when visible) - */ - PointItem.prototype.hide = function () { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); - } - - this.displayed = false; - } - }; - - /** - * Reposition the item horizontally - * @Override - */ - PointItem.prototype.repositionX = function () { - var start = this.conversion.toScreen(this.data.start); - - this.left = start - this.props.dot.width; - - // reposition point - this.dom.point.style.left = this.left + 'px'; - }; - - /** - * Reposition the item vertically - * @Override - */ - PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation.item; - var point = this.dom.point; - - if (orientation == 'top') { - point.style.top = this.top + 'px'; - } else { - point.style.top = this.parent.height - this.top - this.height + 'px'; - } - }; - - /** - * Return the width of the item left from its start date - * @return {number} - */ - PointItem.prototype.getWidthLeft = function () { - return this.props.dot.width; - }; - - /** - * Return the width of the item right from its start date - * @return {number} - */ - PointItem.prototype.getWidthRight = function () { - return this.width - this.props.dot.width; - }; - - module.exports = PointItem; - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var DOMutil = __webpack_require__(24); - - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; - } - - Points.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; - - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); - }; - - /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] - */ - Points.draw = function (dataset, group, framework, offset) { - offset = offset || 0; - var callback = getCallback(); - - for (var i = 0; i < dataset.length; i++) { - if (!callback) { - // draw the point the simple way. - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label); - } else { - var callbackResult = callback(dataset[i], group, framework); // result might be true, false or an object - if (callbackResult === true || typeof callbackResult === 'object') { - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label); - } - } - } - - function getGroupTemplate(callbackResult) { - callbackResult = typeof callbackResult === 'undefined' ? {} : callbackResult; - return { - style: callbackResult.style || group.options.drawPoints.style, - size: callbackResult.size || group.options.drawPoints.size, - className: callbackResult.className || group.className - }; - } - - function getCallback() { - var callback = undefined; - // check for the graph2d onRender - if (framework.options.drawPoints.onRender && typeof framework.options.drawPoints.onRender == 'function') { - callback = framework.options.drawPoints.onRender; - } - - // override it with the group onRender if defined - if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') { - callback = group.group.options.drawPoints.onRender; - } - - return callback; - } - }; - - module.exports = Points; - -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Emitter = __webpack_require__(30); - var Hammer = __webpack_require__(15); - var hammerUtil = __webpack_require__(41); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var Range = __webpack_require__(40); - var ItemSet = __webpack_require__(12); - var TimeAxis = __webpack_require__(50); - var Activator = __webpack_require__(51); - var DateUtil = __webpack_require__(42); - var CustomTime = __webpack_require__(13); - - /** - * Create a timeline visualization - * @constructor - */ - function Core() {} - - // turn Core into an event emitter - Emitter(Core.prototype); - - /** - * Create the main DOM for the Core: a root panel containing left, right, - * top, bottom, content, and background panel. - * @param {Element} container The container element where the Core will - * be attached. - * @protected - */ - Core.prototype._create = function (container) { - this.dom = {}; - - this.dom.container = container; - - this.dom.root = document.createElement('div'); - this.dom.background = document.createElement('div'); - this.dom.backgroundVertical = document.createElement('div'); - this.dom.backgroundHorizontal = document.createElement('div'); - this.dom.centerContainer = document.createElement('div'); - this.dom.leftContainer = document.createElement('div'); - this.dom.rightContainer = document.createElement('div'); - this.dom.center = document.createElement('div'); - this.dom.left = document.createElement('div'); - this.dom.right = document.createElement('div'); - this.dom.top = document.createElement('div'); - this.dom.bottom = document.createElement('div'); - this.dom.shadowTop = document.createElement('div'); - this.dom.shadowBottom = document.createElement('div'); - this.dom.shadowTopLeft = document.createElement('div'); - this.dom.shadowBottomLeft = document.createElement('div'); - this.dom.shadowTopRight = document.createElement('div'); - this.dom.shadowBottomRight = document.createElement('div'); - - this.dom.root.className = 'vis-timeline'; - this.dom.background.className = 'vis-panel vis-background'; - this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical'; - this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal'; - this.dom.centerContainer.className = 'vis-panel vis-center'; - this.dom.leftContainer.className = 'vis-panel vis-left'; - this.dom.rightContainer.className = 'vis-panel vis-right'; - this.dom.top.className = 'vis-panel vis-top'; - this.dom.bottom.className = 'vis-panel vis-bottom'; - this.dom.left.className = 'vis-content'; - this.dom.center.className = 'vis-content'; - this.dom.right.className = 'vis-content'; - this.dom.shadowTop.className = 'vis-shadow vis-top'; - this.dom.shadowBottom.className = 'vis-shadow vis-bottom'; - this.dom.shadowTopLeft.className = 'vis-shadow vis-top'; - this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; - this.dom.shadowTopRight.className = 'vis-shadow vis-top'; - this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; - - this.dom.root.appendChild(this.dom.background); - this.dom.root.appendChild(this.dom.backgroundVertical); - this.dom.root.appendChild(this.dom.backgroundHorizontal); - this.dom.root.appendChild(this.dom.centerContainer); - this.dom.root.appendChild(this.dom.leftContainer); - this.dom.root.appendChild(this.dom.rightContainer); - this.dom.root.appendChild(this.dom.top); - this.dom.root.appendChild(this.dom.bottom); - - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); - - this.dom.centerContainer.appendChild(this.dom.shadowTop); - this.dom.centerContainer.appendChild(this.dom.shadowBottom); - this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); - this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); - this.dom.rightContainer.appendChild(this.dom.shadowTopRight); - this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - - this.on('rangechange', this.redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pan', this._onDrag.bind(this)); - - var me = this; - this.on('change', function (properties) { - if (properties && properties.queue == true) { - // redraw once on next tick - if (!me._redrawTimer) { - me._redrawTimer = setTimeout(function () { - me._redrawTimer = null; - me._redraw(); - }, 0); - } - } else { - // redraw immediately - me._redraw(); - } - }); - - // create event listeners for all interesting events, these events will be - // emitted via emitter - this.hammer = new Hammer(this.dom.root); - this.hammer.get('pinch').set({ enable: true }); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - this.listeners = {}; - - var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend' - // TODO: cleanup - //'touch', 'pinch', - //'tap', 'doubletap', 'hold', - //'dragstart', 'drag', 'dragend', - //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (type) { - var listener = function listener(event) { - if (me.isActive()) { - me.emit(type, event); - } - }; - me.hammer.on(type, listener); - me.listeners[type] = listener; - }); - - // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) - hammerUtil.onTouch(this.hammer, (function (event) { - me.emit('touch', event); - }).bind(this)); - - // emulate a release event (emitted after a pan, pinch, tap, or press) - hammerUtil.onRelease(this.hammer, (function (event) { - me.emit('release', event); - }).bind(this)); - - function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); - } - } - this.dom.root.addEventListener('mousewheel', onMouseWheel); - this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); - - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; - - this.customTimes = []; - - // store state information needed for touch events - this.touch = {}; - - this.redrawCount = 0; - - // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); - container.appendChild(this.dom.root); - }; - - /** - * Set options. Options will be passed to all components loaded in the Timeline. - * @param {Object} [options] - * {String} orientation - * Vertical orientation for the Timeline, - * can be 'bottom' (default) or 'top'. - * {String | Number} width - * Width for the timeline, a number in pixels or - * a css string like '1000px' or '75%'. '100%' by default. - * {String | Number} height - * Fixed height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. If undefined, - * The Timeline will automatically size such that - * its contents fit. - * {String | Number} minHeight - * Minimum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {String | Number} maxHeight - * Maximum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {Number | Date | String} start - * Start date for the visible window - * {Number | Date | String} end - * End date for the visible window - */ - Core.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; - util.selectiveExtend(fields, this.options, options); - - if ('orientation' in options) { - if (typeof options.orientation === 'string') { - this.options.orientation = { - item: options.orientation, - axis: options.orientation - }; - } else if (typeof options.orientation === 'object') { - if ('item' in options.orientation) { - this.options.orientation.item = options.orientation.item; - } - if ('axis' in options.orientation) { - this.options.orientation.axis = options.orientation.axis; - } - } - } - - if (this.options.orientation.axis === 'both') { - if (!this.timeAxis2) { - var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); - timeAxis2.setOptions = function (options) { - var _options = options ? util.extend({}, options) : {}; - _options.orientation = 'top'; // override the orientation option, always top - TimeAxis.prototype.setOptions.call(timeAxis2, _options); - }; - this.components.push(timeAxis2); - } - } else { - if (this.timeAxis2) { - var index = this.components.indexOf(this.timeAxis2); - if (index !== -1) { - this.components.splice(index, 1); - } - this.timeAxis2.destroy(); - this.timeAxis2 = null; - } - } - - // if the graph2d's drawPoints is a function delegate the callback to the onRender property - if (typeof options.drawPoints == 'function') { - options.drawPoints = { - onRender: options.drawPoints - }; - } - - if ('hiddenDates' in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); - } - - if ('clickToUse' in options) { - if (options.clickToUse) { - if (!this.activator) { - this.activator = new Activator(this.dom.root); - } - } else { - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } - } - } - - if ('showCustomTime' in options) { - throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); - } - - // enable/disable autoResize - this._initAutoResize(); - } - - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); - - // enable/disable configure - if ('configure' in options) { - if (!this.configurator) { - this.configurator = this._createConfigurator(); - } - - this.configurator.setOptions(options.configure); - - // collect the settings of all components, and pass them to the configuration system - var appliedOptions = util.deepExtend({}, this.options); - this.components.forEach(function (component) { - util.deepExtend(appliedOptions, component.options); - }); - this.configurator.setModuleOptions({ global: appliedOptions }); - } - - // redraw everything - this._redraw(); - }; - - /** - * Returns true when the Timeline is active. - * @returns {boolean} - */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; - }; - - /** - * Destroy the Core, clean up all DOM elements and event listeners. - */ - Core.prototype.destroy = function () { - // unbind datasets - this.setItems(null); - this.setGroups(null); - - // remove all event listeners - this.off(); - - // stop checking for changed size - this._stopAutoResize(); - - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); - } - this.dom = null; - - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } - - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; - } - } - this.listeners = null; - this.hammer = null; - - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); - - this.body = null; - }; - - /** - * Set a custom time bar - * @param {Date} time - * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. - */ - Core.prototype.setCustomTime = function (time, id) { - var customTimes = this.customTimes.filter(function (component) { - return id === component.options.id; - }); - - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); - } - - if (customTimes.length > 0) { - customTimes[0].setCustomTime(time); - } - }; - - /** - * Retrieve the current custom time. - * @param {number} [id=undefined] Id of the custom time bar. - * @return {Date | undefined} customTime - */ - Core.prototype.getCustomTime = function (id) { - var customTimes = this.customTimes.filter(function (component) { - return component.options.id === id; - }); - - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); - } - return customTimes[0].getCustomTime(); - }; - - /** - * Add custom vertical bar - * @param {Date | String | Number} [time] A Date, unix timestamp, or - * ISO date string. Time point where - * the new bar should be placed. - * If not provided, `new Date()` will - * be used. - * @param {Number | String} [id=undefined] Id of the new bar. Optional - * @return {Number | String} Returns the id of the new bar - */ - Core.prototype.addCustomTime = function (time, id) { - var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); - - var exists = this.customTimes.some(function (customTime) { - return customTime.options.id === id; - }); - if (exists) { - throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists'); - } - - var customTime = new CustomTime(this.body, { - time: timestamp, - id: id - }); - - this.customTimes.push(customTime); - this.components.push(customTime); - this.redraw(); - - return id; - }; - - /** - * Remove previously added custom bar - * @param {int} id ID of the custom bar to be removed - * @return {boolean} True if the bar exists and is removed, false otherwise - */ - Core.prototype.removeCustomTime = function (id) { - var customTimes = this.customTimes.filter(function (bar) { - return bar.options.id === id; - }); - - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); - } - - customTimes.forEach((function (customTime) { - this.customTimes.splice(this.customTimes.indexOf(customTime), 1); - this.components.splice(this.components.indexOf(customTime), 1); - customTime.destroy(); - }).bind(this)); - }; - - /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items - */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; - }; - - /** - * Set Core window such that it fits all items - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - */ - Core.prototype.fit = function (options) { - var range = this.getDataRange(); - - // skip range set if there is no min and max date - if (range.min === null && range.max === null) { - return; - } - - // apply a margin of 1% left and right of the data - var interval = range.max - range.min; - var min = new Date(range.min.valueOf() - interval * 0.01); - var max = new Date(range.max.valueOf() + interval * 0.01); - - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(min, max, animation); - }; - - /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} - * @protected - */ - Core.prototype.getDataRange = function () { - // must be implemented by Timeline and Graph2d - throw new Error('Cannot invoke abstract method getDataRange'); - }; - - /** - * Set the visible window. Both parameters are optional, you can change only - * start or only end. Syntax: - * - * TimeLine.setWindow(start, end) - * TimeLine.setWindow(start, end, options) - * TimeLine.setWindow(range) - * - * Where start and end can be a Date, number, or string, and range is an - * object with properties start and end. - * - * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - */ - Core.prototype.setWindow = function (start, end, options) { - var animation; - if (arguments.length == 1) { - var range = arguments[0]; - animation = range.animation !== undefined ? range.animation : true; - this.range.setRange(range.start, range.end, animation); - } else { - animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(start, end, animation); - } - }; - - /** - * Move the window such that given time is centered on screen. - * @param {Date | Number | String} time - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - */ - Core.prototype.moveTo = function (time, options) { - var interval = this.range.end - this.range.start; - var t = util.convert(time, 'Date').valueOf(); - - var start = t - interval / 2; - var end = t + interval / 2; - var animation = options && options.animation !== undefined ? options.animation : true; - - this.range.setRange(start, end, animation); - }; - - /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range - */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; - }; - - /** - * Force a redraw. Can be overridden by implementations of Core - */ - Core.prototype.redraw = function () { - this._redraw(); - }; - - /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. - * @protected - */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; - - if (!dom) return; // when destroyed - - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - - // update class names - if (options.orientation == 'top') { - util.addClassName(dom.root, 'vis-top'); - util.removeClassName(dom.root, 'vis-bottom'); - } else { - util.removeClassName(dom.root, 'vis-top'); - util.addClassName(dom.root, 'vis-bottom'); - } - - // update root width and height options - dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); - dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); - dom.root.style.width = util.option.asSize(options.width, ''); - - // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; - props.border.bottom = props.border.top; - var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; - var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; - - // workaround for a bug in IE: the clientWidth of an element with - // a height:0px and overflow:hidden is not calculated and always has value 0 - if (dom.centerContainer.clientHeight === 0) { - props.border.left = props.border.top; - props.border.right = props.border.left; - } - if (dom.root.clientHeight === 0) { - borderRootWidth = borderRootHeight; - } - - // calculate the heights. If any of the side panels is empty, we set the height to - // minus the border width, such that the border will be invisible - props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; - props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; - - // TODO: compensate borders when any of the panels is empty. - - // apply auto height - // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) - var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); - - // calculate heights of the content panels - props.root.height = dom.root.offsetHeight; - props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; - - // calculate the widths of the panels - props.root.width = dom.root.offsetWidth; - props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; - props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; - props.rightContainer.width = props.right.width; - var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; - props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; - - // resize the panels - dom.background.style.height = props.background.height + 'px'; - dom.backgroundVertical.style.height = props.background.height + 'px'; - dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; - dom.centerContainer.style.height = props.centerContainer.height + 'px'; - dom.leftContainer.style.height = props.leftContainer.height + 'px'; - dom.rightContainer.style.height = props.rightContainer.height + 'px'; - - dom.background.style.width = props.background.width + 'px'; - dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; - dom.backgroundHorizontal.style.width = props.background.width + 'px'; - dom.centerContainer.style.width = props.center.width + 'px'; - dom.top.style.width = props.top.width + 'px'; - dom.bottom.style.width = props.bottom.width + 'px'; - - // reposition the panels - dom.background.style.left = '0'; - dom.background.style.top = '0'; - dom.backgroundVertical.style.left = props.left.width + props.border.left + 'px'; - dom.backgroundVertical.style.top = '0'; - dom.backgroundHorizontal.style.left = '0'; - dom.backgroundHorizontal.style.top = props.top.height + 'px'; - dom.centerContainer.style.left = props.left.width + 'px'; - dom.centerContainer.style.top = props.top.height + 'px'; - dom.leftContainer.style.left = '0'; - dom.leftContainer.style.top = props.top.height + 'px'; - dom.rightContainer.style.left = props.left.width + props.center.width + 'px'; - dom.rightContainer.style.top = props.top.height + 'px'; - dom.top.style.left = props.left.width + 'px'; - dom.top.style.top = '0'; - dom.bottom.style.left = props.left.width + 'px'; - dom.bottom.style.top = props.top.height + props.centerContainer.height + 'px'; - - // update the scrollTop, feasible range for the offset can be changed - // when the height of the Core or of the contents of the center changed - this._updateScrollTop(); - - // reposition the scrollable contents - var offset = this.props.scrollTop; - if (options.orientation.item != 'top') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); - } - dom.center.style.left = '0'; - dom.center.style.top = offset + 'px'; - dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; - dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; - - // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; - - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; - }); - if (resized) { - // keep repainting until all sizes are settled - var MAX_REDRAWS = 3; // maximum number of consecutive redraws - if (this.redrawCount < MAX_REDRAWS) { - this.redrawCount++; - this._redraw(); - } else { - console.log('WARNING: infinite loop in redraw?'); - } - this.redrawCount = 0; - } - }; - - // TODO: deprecated since version 1.1.0, remove some day - Core.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); - }; - - /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * Only applicable when option `showCurrentTime` is true. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. - */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); - } - - this.currentTime.setCurrentTime(time); - }; - - /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. - */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); - } - - return this.currentTime.getCurrentTime(); - }; - - /** - * Convert a position on screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @protected - */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); - }; - - /** - * Convert a position on the global screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @protected - */ - // TODO: move this function to Range - Core.prototype._toGlobalTime = function (x) { - return DateUtil.toTime(this, x, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return new Date(x / conversion.scale + conversion.offset); - }; - - /** - * Convert a datetime (Date object) into a position on the screen - * @param {Date} time A date - * @return {int} x The position on the screen in pixels which corresponds - * with the given date. - * @protected - */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); - }; - - /** - * Convert a datetime (Date object) into a position on the root - * This is used to get the pixel density estimate for the screen, not the center panel - * @param {Date} time A date - * @return {int} x The position on root in pixels which corresponds - * with the given date. - * @protected - */ - // TODO: move this function to Range - Core.prototype._toGlobalScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return (time.valueOf() - conversion.offset) * conversion.scale; - }; - - /** - * Initialize watching when option autoResize is true - * @private - */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); - } else { - this._stopAutoResize(); - } - }; - - /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private - */ - Core.prototype._startAutoResize = function () { - var me = this; - - this._stopAutoResize(); - - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; - } - - if (me.dom.root) { - // check whether the frame is resized - // Note: we compare offsetWidth here, not clientWidth. For some reason, - // IE does not restore the clientWidth from 0 to the actual width after - // changing the timeline's container display style from none to visible - if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { - me.props.lastWidth = me.dom.root.offsetWidth; - me.props.lastHeight = me.dom.root.offsetHeight; - - me.emit('change'); - } - } - }; - - // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); - - this.watchTimer = setInterval(this._onResize, 1000); - }; - - /** - * Stop watching for a resize of the frame. - * @private - */ - Core.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } - - // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); - this._onResize = null; - }; - - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Core.prototype._onTouch = function (event) { - this.touch.allowDragging = true; - this.touch.initialScrollTop = this.props.scrollTop; - }; - - /** - * Start moving the timeline vertically - * @param {Event} event - * @private - */ - Core.prototype._onPinch = function (event) { - this.touch.allowDragging = false; - }; - - /** - * Move the timeline vertically - * @param {Event} event - * @private - */ - Core.prototype._onDrag = function (event) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.touch.allowDragging) return; - - var delta = event.deltaY; - - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + var TYPE_FUNCTION = 'function'; - if (newScrollTop != oldScrollTop) { - this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already - this.emit('verticalDrag'); - } - }; + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; - }; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * 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} */ - Core.prototype._updateScrollTop = function () { - // recalculate the scrollTopMin - var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero - if (scrollTopMin != this.props.scrollTopMin) { - // in case of bottom orientation, change the scrollTop such that the contents - // do not move relative to the time axis at the bottom - if (this.options.orientation.item != 'top') { - this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; } - this.props.scrollTopMin = scrollTopMin; - } - - // limit the scrollTop to the feasible scroll range - if (this.props.scrollTop > 0) this.props.scrollTop = 0; - if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; - - return this.props.scrollTop; - }; - - /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private - */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; - }; - - /** - * Load a configurator - * @return {Object} - * @private - */ - Core.prototype._createConfigurator = function () { - throw new Error('Cannot invoke abstract method _createConfigurator'); - }; - - module.exports = Core; - -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var TimeStep = __webpack_require__(46); - var Component = __webpack_require__(38); - var Group = __webpack_require__(43); - var BackgroundGroup = __webpack_require__(47); - var BoxItem = __webpack_require__(48); - var PointItem = __webpack_require__(9); - var RangeItem = __webpack_require__(45); - var BackgroundItem = __webpack_require__(49); - - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items - var BACKGROUND = '__background__'; // reserved group id for background items without group + return false; + } /** - * 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 + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - function ItemSet(body, options) { - this.body = body; - - 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, - - selectable: true, - multiselect: false, - - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, - - snap: TimeStep.snap, - - 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); - }, + function each(obj, iterator, context) { + var i; - margin: { - item: { - horizontal: 10, - vertical: 10 - }, - axis: 20 + if (!obj) { + return; } - }; - - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); - - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: { start: 'Date', end: 'Date' } - }; - - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; - - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - // 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); + 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); + } } - }; + } - // 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); + /** + * 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++; } - }; - - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; - - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw - - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM - - this._create(); - - this.setOptions(options); + return dest; } - ItemSet.prototype = new Component(); - - // available item types will be registered here - ItemSet.types = { - background: BackgroundItem, - box: BoxItem, - range: RangeItem, - point: PointItem - }; - /** - * Create the HTML DOM for the ItemSet + * 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 */ - ItemSet.prototype._create = function () { - var frame = document.createElement('div'); - frame.className = 'vis-itemset'; - frame['timeline-itemset'] = this; - this.dom.frame = frame; - - // create background panel - var background = document.createElement('div'); - background.className = 'vis-background'; - frame.appendChild(background); - this.dom.background = background; - - // 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(); + function merge(dest, src) { + return extend(dest, src, true); + } - // create background Group - var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); - backgroundGroup.show(); - this.groups[BACKGROUND] = backgroundGroup; + /** + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] + */ + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - // 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); + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - // drag items when selected - this.hammer.on('hammer.input', (function (event) { - if (event.isFirst) { - this._onTouch(event); + if (properties) { + extend(childP, properties); } - }).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)); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - - // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); - - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('press', this._onMultiSelectItem.bind(this)); - - // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); - - // attach to the DOM - this.show(); - }; + } /** - * 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. + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} */ - 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); - - 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; - } - } - - 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); - } - } - } - } + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - 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); - } + /** + * 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; + } - // 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); - - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); - } - }; + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } /** - * 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] + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - ItemSet.prototype.markDirty = function (options) { - this.groupIds = []; - this.stackDirty = true; + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } - if (options && options.refreshItems) { - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); + /** + * 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); }); - } - }; + } /** - * Destroy the ItemSet + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found */ - ItemSet.prototype.destroy = function () { - this.hide(); - this.setItems(null); - this.setGroups(null); - - this.hammer = null; - - this.body = null; - this.conversion = null; - }; + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } /** - * Hide the component from the DOM + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found */ - 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); - } - - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); - } - }; + function inStr(str, find) { + return str.indexOf(find) > -1; + } /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * split string on whitespace + * @param {String} str + * @returns {Array} words */ - ItemSet.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - - // 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); - } - }; + function splitStr(str) { + return str.trim().split(/\s+/g); + } /** - * 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. + * 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 */ - ItemSet.prototype.setSelection = function (ids) { - var i, ii, id, item; - - if (ids == undefined) ids = []; - if (!Array.isArray(ids)) ids = [ids]; - - // 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(); - } - - // 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(); + 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; } - } - }; + } /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} */ - ItemSet.prototype.getSelection = function () { - return this.selection.concat([]); - }; + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * 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}] */ - 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); + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + 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++; + } - // 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); + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); } - } } - } - return ids; - }; + return results; + } /** - * Deselect a selected item - * @param {String | Number} id - * @private + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - 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; + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); + + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; + + if (prop in obj) { + return prop; + } + i++; } - } - }; + return undefined; + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * get a unique id + * @returns {number} uniqueId */ - 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; - - // 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; - - // update class name - frame.className = 'vis-itemset'; - - // reorder the groups (if needed) - resized = this._orderGroups() || resized; + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } - // 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; + /** + * 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); + } - 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; + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + 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); - // 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; + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - // update frame height - frame.style.height = asSize(height); + var COMPUTE_INTERVAL = 25; - // calculate actual size - this.props.width = frame.offsetWidth; - this.props.height = height; + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - // 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'; + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; - // check if this component is resized - resized = this._isResized() || resized; + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; - return resized; - }; + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup - * @private + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor */ - 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]; + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - return firstGroup || null; - }; + // 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); + } + }; - /** - * 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; + this.init(); - 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; + Input.prototype = { + /** + * should handle the inputEvent data and trigger the callback + * @virtual + */ + handler: function() { }, - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - ungrouped.add(item); - } - } + /** + * 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); + }, - ungrouped.show(); + /** + * 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); } - } }; /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} */ - ItemSet.prototype.getLabelSet = function () { - return this.dom.labelSet; - }; + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; + + 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); + } /** - * Set items - * @param {vis.DataSet | null} items + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input */ - ItemSet.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + 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)); - // 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'); - } + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + if (isFirst) { + manager.session = {}; + } - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + // compute scale, rotation etc + computeInputData(manager, input); - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + // emit secret event + manager.emit('hammer.input', input); - // update the group holding all ungrouped items - this._updateUngrouped(); - } - }; + manager.recognize(input); + manager.session.prevInput = input; + } + + /** + * 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; + + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } + + // 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; + } + + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - /** - * Get the current items - * @returns {vis.DataSet | null} - */ - ItemSet.prototype.getItems = function () { - return this.itemsData; - }; + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - /** - * Set groups - * @param {vis.DataSet} groups - */ - ItemSet.prototype.setGroups = function (groups) { - var me = this, - ids; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.off(event, callback); - }); + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - // 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'); - } + computeIntervalInputData(session, input); - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; + } + input.target = target; + } - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - // update the group holding all ungrouped items - this._updateUngrouped(); + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; - // update the order of all items in each group - this._order(); + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; + } - this.body.emitter.emit('change', { queue: true }); - }; + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input */ - ItemSet.prototype.getGroups = function () { - return this.groupsData; - }; + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; + + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; + + 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); + + 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; + } + + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } /** - * Remove an item by its id - * @param {String | Number} id + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData */ - ItemSet.prototype.removeItem = function (id) { - var item = this.itemsData.get(id), - dataset = this.itemsData.getDataSet(); + 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++; + } - 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); - } - }); - } - }; + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } /** - * Get the time of an item based on it's data and options.type - * @param {Object} itemData - * @returns {string} Returns the type - * @private + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties */ - ItemSet.prototype._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); - }; + 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) + }; + } + + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; + } + + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } /** - * Get the group id for an item - * @param {Object} itemData - * @returns {string} Returns the groupId - * @private + * 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` */ - 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; - } - }; + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } /** - * Handle updated items - * @param {Number[]} ids - * @protected + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction */ - ItemSet.prototype._onUpdate = function (ids) { - var me = this; - - ids.forEach((function (id) { - var itemData = me.itemsData.get(id, me.itemOptions); - var item = me.items[id]; - var type = me._getType(itemData); - - var constructor = ItemSet.types[type]; - var selected; - - 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); - } + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; } - 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(); - } - } 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 + '"'); - } + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } - }).bind(this)); - - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', { queue: true }); - }; + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } /** - * Handle added items - * @param {Number[]} ids - * @protected + * 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 */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + 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]]; + + return Math.sqrt((x * x) + (y * y)); + } /** - * Handle removed items - * @param {Number[]} ids - * @protected + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle */ - 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); + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } - }); - - 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 x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } /** - * Update the order of item in all groups - * @private + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation */ - 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(); - }); - }; + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } /** - * Handle updated groups - * @param {Number[]} ids - * @private + * 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 */ - ItemSet.prototype._onUpdateGroups = function (ids) { - this._onAddGroups(ids); + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); + } + + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END }; + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + /** - * Handle changed groups (added or updated) - * @param {Number[]} ids - * @private + * Mouse events input + * @constructor + * @extends Input */ - ItemSet.prototype._onAddGroups = function (ids) { - var me = this; + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state - if (!group) { - // check for reserved ids - if (id == UNGROUPED || id == BACKGROUND) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); - } + Input.apply(this, arguments); + } - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - group = new Group(id, groupData, me); - me.groups[id] = group; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; + } - // 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); - } + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; } - } - group.order(); - group.show(); - } else { - // update group - group.setData(groupData); + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } + + if (eventType & INPUT_END) { + this.pressed = false; + } + + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } - }); + }); - this.body.emitter.emit('change', { queue: true }); + 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 }; + 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'; + } + /** - * Handle removed groups - * @param {Number[]} ids - * @private + * Pointer events input + * @constructor + * @extends Input */ - ItemSet.prototype._onRemoveGroups = function (ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - if (group) { - group.hide(); - delete groups[id]; - } - }); + Input.apply(this, arguments); - this.markDirty(); + this.store = (this.manager.session.pointerEvents = []); + } - this.body.emitter.emit('change', { queue: true }); - }; + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - /** - * Reorder the groups if needed - * @return {boolean} changed - * @private - */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + + var isTouch = (pointerType == INPUT_TYPE_TOUCH); + + // 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; + } - 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(); - }); + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); + // update the event in the store + store[storeIndex] = ev; - this.groupIds = groupIds; + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); + + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); + } } + }); - return changed; - } else { - return false; - } + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL }; - /** - * Add a new item - * @param {Item} item - * @private - */ - ItemSet.prototype._addItem = function (item) { - this.items[item.id] = item; - - // add to group - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); - }; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * Touch events input + * @constructor + * @extends Input */ - ItemSet.prototype._updateItem = function (item, itemData) { - var oldGroupId = item.data.group; - var oldSubGroupId = item.data.subgroup; + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - // update the items data (will redraw the item when displayed) - item.setData(itemData); + Input.apply(this, arguments); + } - // update group - if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); - } - }; + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } - /** - * 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 - */ - ItemSet.prototype._removeItem = function (item) { - // remove from DOM - item.hide(); + if (!this.started) { + return; + } - // remove from items - delete this.items[item.id]; + var touches = normalizeSingleTouches.call(this, ev, type); - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; + } - // remove from group - item.parent && item.parent.remove(item); - }; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] */ - ItemSet.prototype._constructByEndArray = function (array) { - var endArray = []; + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { - endArray.push(array[i]); + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } - } - return endArray; - }; - /** - * Register the clicked item on touch, before dragStart is initiated. - * - * 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. - * - * @param {Event} event - * @private - */ - 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; + return [all, changed]; + } + + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL }; + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; + /** - * Start dragging the selected events - * @param {Event} event - * @private + * Multi-user touch events input + * @constructor + * @extends Input */ - ItemSet.prototype._onDragStart = function (event) { - var item = this.touchParams.item || null; - var me = this; - var props; + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - if (item && item.selected) { + Input.apply(this, arguments); + } - if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { - return; + 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 + }); } + }); - // override options.editable - if (item.editable === false) { - return; + /** + * @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; + + // 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]; } - var dragLeftItem = this.touchParams.dragLeftItem; - var dragRightItem = this.touchParams.dragRightItem; + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - if (dragLeftItem) { - props = { - item: dragLeftItem, - initialX: event.center.x, - dragLeft: true, - data: util.extend({}, item.data) // clone the items data - }; + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); + }); - 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 - }; + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; + } + } - 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 - }; + // 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 props; - }); + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; } - 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); - } - }; + if (!changedTargetTouches.length) { + return; + } + + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } /** - * Start creating a new range item by dragging. - * @param {Event} event - * @private + * 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 */ - 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; - - var itemData = { - type: 'range', - start: start, - end: end, - content: 'new item' - }; - - var id = util.randomUUID(); - itemData[this.itemsData._fieldId] = id; + function TouchMouseInput() { + Input.apply(this, arguments); - var group = this.groupFromTarget(event); - if (group) { - itemData.group = group.groupId; - } + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - 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); + 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); - var props = { - item: newItem, - dragRight: true, - initialX: event.center.x, - data: util.extend({}, itemData) - }; - this.touchParams.itemProps = [props]; + // 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; + } - event.stopPropagation(); - }; + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; + } - /** - * Drag selected items - * @param {Event} event - * @private - */ - ItemSet.prototype._onDrag = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); + this.callback(manager, inputEvent, inputData); + }, - 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(); + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); + } + }); - // 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; + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - var itemData = util.extend({}, props.item.data); // clone the data + // 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'; - if (props.item.editable === false) { - return; - } + /** + * 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); + } - var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; + 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(); + } - if (updateTimeAllowed) { - 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); + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + } + this.actions = value.toLowerCase().trim(); + }, - if (itemData.end != undefined) { - var initialEnd = util.convert(props.data.end, 'Date'); - var duration = initialEnd.valueOf() - initialStart.valueOf(); + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); + }, - 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; + /** + * 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()); } - } - } - } - - var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; + }); + return cleanTouchActions(actions.join(' ')); + }, - if (updateGroupAllowed && (!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 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; } - } - // confirm moving the item - me.options.onMoving(itemData, function (itemData) { - if (itemData) { - props.item.setData(itemData); + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; + + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; } - }); - }); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); - } - }; + 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); - /** - * Move an item to another group - * @param {Item} item - * @param {String | Number} groupId - * @private - */ - 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(); + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); + } + }, - item.data.group = group.groupId; - } + /** + * 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(); + } }; /** - * End of dragging selected items - * @param {Event} event - * @private + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} */ - ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; + } - // prepare a change set for the changed items - var changes = []; - var me = this; - var dataset = this.itemsData.getDataSet(); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - 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; + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } - 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); - } + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + } - // 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); + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); - } - }); - } - }); + return TOUCH_ACTION_AUTO; + } - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); - } - } - }; + /** + * 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; /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; + function Recognizer(options) { + this.id = uniqueId(); - var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); - var shiftKey = event.srcEvent && event.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; - } + this.manager = null; + this.options = merge(options || {}, this.defaults); - var oldSelection = this.getSelection(); + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - var item = this.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + this.state = STATE_POSSIBLE; - var newSelection = this.getSelection(); + this.simultaneous = {}; + this.requireFail = []; + } - // 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, - event: event - }); - } - }; + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - /** - * 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; + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - var me = this; - var snap = this.options.snap || null; - var item = this.itemFromTarget(event); + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - event.stopPropagation(); + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; + } - if (item) { - // update item + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); + } + return this; + }, - // 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(); + /** + * 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 newItem = { - start: snap ? snap(start, scale, step) : start, - content: 'new item' - }; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - // 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; - } + /** + * 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; + } - newItem[this.itemsData._fieldId] = util.randomUUID(); + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; - } + /** + * 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; + } - // 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? - } - }); - } - }; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - /** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private - */ - ItemSet.prototype._onMultiSelectItem = function (event) { - if (!this.options.selectable) return; + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - var item = this.itemFromTarget(event); + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - if (item) { - // multi select items (if allowed) + /** + * 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; - var selection = this.options.multiselect ? this.getSelection() // take current selection - : []; // deselect current selection + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } - var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - if (shiftKey && this.options.multiselect) { - // select all items between the old selection and the tapped item + emit(); // simple 'eventName' events - // determine the selection range - selection.push(item.id); - var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - // 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; + /** + * 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 (start >= range.min && end <= range.max && !(_item instanceof BackgroundItem)) { - selection.push(_item.id); // do not use id but item.id, id itself is stringified - } + /** + * 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++; } - } - } 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); - } - } + return true; + }, - this.setSelection(selection); + /** + * 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); - this.body.emitter.emit('select', { - items: this.getSelection(), - event: event - }); - } - }; + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; + } - /** - * 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 - */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; + } - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } + this.state = this.process(inputDataClone); - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = data.start; - } - } - }); + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, - return { - min: min, - max: max - }; - }; + /** + * 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 - /** - * 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 the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - return null; + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } }; /** - * 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 + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state */ - 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; + 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 ''; + } - 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; - } + /** + * 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 null; - }; + return ''; + } /** - * 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 + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); } - target = target.parentNode; - } + return otherRecognizer; + } - return null; - }; + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - module.exports = ItemSet; + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; + }, - 'use strict'; + /** + * 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; + + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); - var Component = __webpack_require__(38); - var moment = __webpack_require__(20); - var locales = __webpack_require__(39); + // 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; + } + }); /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {number | string} id - * {string} locales - * {string} locale - * @constructor CustomTime - * @extends Component + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - function CustomTime(body, options) { - this.body = body; - - // default options - this.defaultOptions = { - locales: locales, - locale: 'en', - id: undefined - }; - this.options = util.extend({}, this.defaultOptions); + this.pX = null; + this.pY = null; + } - if (options && options.time) { - this.customTime = options.time; - } else { - this.customTime = new Date(); - } + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - this.eventParams = {}; // stores state parameters while dragging the bar + 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; + }, - this.setOptions(options); + 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; - // create the DOM - this._create(); - } + // 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; - CustomTime.prototype = new Component(); + return hasMoved && distance > options.threshold && direction & options.direction; + }, - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {number | string} id - * {string} locales - * {string} locale - */ - CustomTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['locale', 'locales', 'id'], this.options, options); - } - }; + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - /** - * Create the DOM for the custom time - * @private - */ - CustomTime.prototype._create = function () { - var bar = document.createElement('div'); - bar['custom-time'] = this; - bar.className = 'vis-custom-time ' + (this.options.id || ''); - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; - this.bar = bar; + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; - bar.appendChild(drag); + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - // attach event listeners - this.hammer = new Hammer(drag); - this.hammer.on('panstart', this._onDragStart.bind(this)); - this.hammer.on('panmove', this._onDrag.bind(this)); - this.hammer.on('panend', this._onDragEnd.bind(this)); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - // TODO: cleanup - //this.hammer.on('pan', function (event) { - // event.preventDefault(); - //}); - }; + this._super.emit.call(this, input); + } + }); /** - * Destroy the CustomTime bar + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer */ - CustomTime.prototype.destroy = function () { - this.hide(); + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - this.hammer.destroy(); - this.hammer = null; + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - this.body = null; - }; + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, + + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, + + 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); + } + } + }); /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer */ - CustomTime.prototype.redraw = function () { - var parent = this.body.dom.backgroundVertical; - if (this.bar.parentNode != parent) { - // attach to the dom - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - parent.appendChild(this.bar); - } + function PressRecognizer() { + Recognizer.apply(this, arguments); - var x = this.body.util.toScreen(this.customTime); + this._timer = null; + this._input = null; + } - var locale = this.options.locales[this.options.locale]; - if (!locale) { - if (!this.warned) { - console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); - this.warned = true; - } - locale = this.options.locales['en']; // fall back on english when not available - } - var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); + 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 + }, - this.bar.style.left = x + 'px'; - this.bar.title = title; + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - return false; - }; + 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; - /** - * Remove the CustomTime from the DOM - */ - CustomTime.prototype.hide = function () { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - }; + this._input = input; - /** - * Set custom time. - * @param {Date | number | string} time - */ - CustomTime.prototype.setCustomTime = function (time) { - this.customTime = util.convert(time, 'Date'); - this.redraw(); - }; + // 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; + }, - /** - * Retrieve the current custom time. - * @return {Date} customTime - */ - CustomTime.prototype.getCustomTime = function () { - return new Date(this.customTime.valueOf()); - }; + reset: function() { + clearTimeout(this._timer); + }, - /** - * Start moving horizontally - * @param {Event} event - * @private - */ - CustomTime.prototype._onDragStart = function (event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; + } - event.stopPropagation(); - }; + 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); + } + } + }); /** - * Perform moving operating. - * @param {Event} event - * @private + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; - - var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; - var time = this.body.util.toTime(x); + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - this.setCustomTime(time); + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - // fire a timechange event - this.body.emitter.emit('timechange', { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - event.stopPropagation(); - }; + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); /** - * Stop moving operating. - * @param {Event} event - * @private + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; - - // fire a timechanged event - this.body.emitter.emit('timechanged', { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } - event.stopPropagation(); - }; + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - /** - * Find a custom time from an event target: - * searches for the attribute 'custom-time' in the event target's element tree - * @param {Event} event - * @return {CustomTime | null} customTime - */ - CustomTime.customTimeFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('custom-time')) { - return target['custom-time']; - } - target = target.parentNode; - } + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - return null; - }; + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - module.exports = CustomTime; + 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; + } -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - 'use strict'; + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); + this.manager.emit(this.options.event, input); + } + }); /** - * @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 + * 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 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; + function TapRecognizer() { + Recognizer.apply(this, arguments); - this.top = null; - this.left = null; - this.width = null; - this.height = null; + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - this.editable = null; - if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { - this.editable = data.editable; - } + this._timer = null; + this._input = null; + this.count = 0; } - Item.prototype.stack = true; + 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 + }, - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - /** - * Unselect current item - */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + process: function(input) { + var options = this.options; - /** - * 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); - } + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { - this.editable = data.editable; - } + this.reset(); - this.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); + } - /** - * 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; - } - }; + // 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(); + } - /** - * 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; - }; + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed - */ - Item.prototype.show = function () { - return false; - }; + this.pTime = input.timeStamp; + this.pCenter = input.center; - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + 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; + } + } + } + return STATE_FAILED; + }, + + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, + + reset: function() { + clearTimeout(this._timer); + }, + + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } + } + }); /** - * Reposition the Item horizontally + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - Item.prototype.repositionX = function () {}; + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); + } /** - * Reposition the Item vertically + * @const {string} */ - Item.prototype.repositionY = function () {}; + Hammer.VERSION = '2.0.4'; /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected + * default settings + * @namespace */ - Item.prototype._repaintDeleteButton = function (anchor) { - var editable = (this.options.editable.remove || this.data.editable === true) && this.data.editable !== false; - - if (this.selected && editable && !this.dom.deleteButton) { - // create and show button - var me = this; + 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, - var deleteButton = document.createElement('div'); - deleteButton.className = 'vis-delete'; - deleteButton.title = 'Delete this item'; + /** + * 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, - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on('tap', function (event) { - event.stopPropagation(); - me.parent.removeFromDataSet(me); - }); + /** + * @type {Boolean} + * @default true + */ + enable: true, - 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; - } - }; + /** + * 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, - /** - * 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; - } + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - 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); - } - } + /** + * 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] + ], - this.content = 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', - /** - * 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'); - } - }; + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: '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 = []; + /** + * 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 (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == 'all') { - attributes = Object.keys(this.data); - } else { - return; - } + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', - if (value != null) { - element.setAttribute('data-' + name, value); - } else { - element.removeAttribute('data-' + name); - } + /** + * 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)' } - } - }; - - /** - * 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; - } - - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; - } - }; - - /** - * 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; }; - /** - * Return the width of the item left from its start date - * @return {number} - */ - Item.prototype.getWidthLeft = function () { - return 0; - }; + var STOP = 1; + var FORCED_STOP = 2; /** - * Return the width of the item right from the max of its start and end date - * @return {number} + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - Item.prototype.getWidthRight = function () { - return 0; - }; - - module.exports = Item; - - // should be implemented by the item + function Manager(element, options) { + options = options || {}; - // should be implemented by the item + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - // should be implemented by the item + this.handlers = {}; + this.session = {}; + this.recognizers = []; -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - 'use strict'; + toggleCssProps(this, true); - if (typeof window !== 'undefined') { - var propagating = __webpack_require__(16); - var Hammer = window['Hammer'] || __webpack_require__(17); - module.exports = propagating(Hammer, { - preventDefault: 'mouse' - }); - } else { - module.exports = function () { - throw Error('hammer.js is only available in a browser, not in node.js.'); - }; + 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); } -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { - - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - (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 + // 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; + }, - /** - * 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) { - var _options = options || { - preventDefault: false - }; + /** + * 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; + }, - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; + /** + * 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 PropagatingHammer = function(element, options) { - var o = Object.create(_options); - if (options) Hammer.extend(o, options); - return propagating(new Hammer(element, o), o); - }; - Hammer.extend(PropagatingHammer, Hammer); + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - PropagatingHammer.Manager = function (element, options) { - var o = Object.create(_options); - if (options) Hammer.extend(o, options); - return propagating(new Hammer.Manager(element, o), o); - }; + var recognizer; + var recognizers = this.recognizers; - return PropagatingHammer; - } + // 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; - // create a wrapper object which will override the functions - // `on`, `off`, `destroy`, and `emit` of the hammer instance - var wrapper = Object.create(hammer); + // 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; + } - // attach to DOM element - var element = hammer.element; - element.hammer = wrapper; + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - // 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; - } - }); + // 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(); + } - /** @type {Object.>} */ - wrapper._handlers = {}; + // 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++; + } + }, /** - * 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 + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} */ - wrapper.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = wrapper._handlers[event]; - if (!_handlers) { - wrapper._handlers[event] = _handlers = []; - - // register the static, propagated handler - hammer.on(event, propagatedHandler); + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; } - _handlers.push(handler); - }); - return wrapper; - }; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } + } + return null; + }, /** - * 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 + * add a recognizer to the manager + * existing recognizers with the same event name will be removed + * @param {Recognizer} recognizer + * @returns {Recognizer|Manager} */ - wrapper.off = function (events, handler) { - // unregister the handler - split(events).forEach(function (event) { - var _handlers = wrapper._handlers[event]; - if (_handlers) { - _handlers = handler ? _handlers.filter(function (h) { - return h !== handler; - }) : []; + add: function(recognizer) { + if (invokeArrayArg(recognizer, 'add', this)) { + return this; + } - if (_handlers.length > 0) { - wrapper._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer.off(event, propagatedHandler); - delete wrapper._handlers[event]; - } + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); } - }); - return wrapper; - }; + this.recognizers.push(recognizer); + recognizer.manager = this; + + this.touchAction.update(); + return recognizer; + }, /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} */ - wrapper.emit = function(eventType, event) { - _firstTarget = event.target; - hammer.emit(eventType, event); - }; + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - wrapper.destroy = function () { - // Detach from DOM element - delete hammer.element.hammer; + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - // clear all handlers - wrapper._handlers = {}; + this.touchAction.update(); + return this; + }, - // call original hammer destroy - hammer.destroy(); - }; + /** + * 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; + }, - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); - } + /** + * 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; + }, /** - * A static event handler, applying event propagation. - * @param {Object} event + * emit event to the listeners + * @param {String} event + * @param {Object} data */ - 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 = {}; + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); } - if (event.srcEvent._handled[event.type]) { - return; - } - else { - event.srcEvent._handled[event.type] = true; + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; } - } - - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - // 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); - } + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; } + }, - elem = elem.parentNode; - } + /** + * 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); + + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; } + }; - return wrapper; - }; - })); + /** + * 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 : ''; + }); + } + + /** + * 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); + } + + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, + + 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, + + 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, + + Manager: Manager, + Input: Input, + TouchAction: TouchAction, + + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, + + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, + + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); + + if ("function" == TYPE_FUNCTION && __webpack_require__(8)) { + !(__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; + } + + })(window, document, 'Hammer'); /***/ }, -/* 17 */ +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + + /* WEBPACK VAR INJECTION */}.call(exports, {})) + +/***/ }, +/* 9 */ /***/ function(module, exports, __webpack_require__) { - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ + // utility functions + + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + + 'use strict'; + + var moment = __webpack_require__(10); + var uuid = __webpack_require__(13); + + /** + * Test whether given object is a number + * @param {*} object + * @return {Boolean} isNumber + */ + exports.isNumber = function (object) { + return object instanceof Number || typeof object == 'number'; + }; + + /** + * 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 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. * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + * @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 VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + /** + * Test whether given object is a string + * @param {*} object + * @return {Boolean} isString + */ + exports.isString = function (object) { + return object instanceof String || typeof object == 'string'; + }; - var TYPE_FUNCTION = 'function'; + /** + * 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 round = Math.round; - var abs = Math.abs; - var now = Date.now; + return false; + }; /** - * set a timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} + * Create a semi UUID + * source: http://stackoverflow.com/a/105074/1262753 + * @return {String} uuid */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + exports.randomUUID = function () { + return uuid.v4(); + }; /** - * 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} + * assign all keys of an object that are not nested objects to a certain value (used for color objects). + * @param obj + * @param value */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; + exports.assignAllKeys = function (obj, value) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] !== 'object') { + obj[prop] = value; + } } - return false; - } + } + }; /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context + * 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 */ - function each(obj, iterator, context) { - var i; - - if (!obj) { - return; - } + exports.fillIfDefined = function (a, b) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - 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++; + 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 { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } else { + if (typeof a[prop] === 'object') { + exports.fillIfDefined(a[prop], b[prop], allowDeletion); } + } } - } + } + }; /** - * 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 + * 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 */ - 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++; + 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 dest; - } + } + return a; + }; /** - * 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 + * 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 */ - function merge(dest, src) { - return extend(dest, src, true); - } + 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; + }; /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] + * 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 */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; + exports.selectiveExtend = function (props, a, b) { + if (!Array.isArray(props)) { + throw new Error('Array with property names expected as first argument'); + } - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; - if (properties) { - extend(childP, properties); + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } } - } + } + return a; + }; /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} + * 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 */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + exports.selectiveDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - /** - * 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); + // 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 val; - } + } + return a; + }; /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} + * 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 */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + exports.selectiveNotDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + + // 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; + }; /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * 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} */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } + 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; + }; /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * 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. */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + 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; + } + + return true; + }; /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found + * 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 */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; + exports.convert = function (object, type) { + var match; + + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } + + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } + + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); + + case 'number': + case 'Number': + return Number(object.valueOf()); + + case 'string': + case 'String': + return String(object); + + 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 + } + } 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 (object instanceof Date) { + return moment(object.valueOf()); + } else if (moment.isMoment(object)) { + return moment(object); + } + 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'); + } + + 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'); + } + + 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 } - node = node.parentNode; - } - return false; - } + return '/Date(' + value + ')/'; + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); + } - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } + default: + throw new Error('Unknown type "' + type + '"'); + } + }; - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + // parse ASP.Net Date pattern, + // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' + // code from http://momentjs.com/ + var ASPDateRegex = /^\/?Date\((\-?\d+)/i; /** - * 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 + * Get the type of an object, for example exports.getType([]) returns 'Array' + * @param {*} object + * @return {String} type */ - 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; + exports.getType = function (object) { + var type = typeof object; + + 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'; + } + 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'; + } + + return type; + }; /** - * convert array-like objects to real arrays - * @param {Object} obj + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue * @returns {Array} */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } + exports.copyAndExtendArray = function (arr, newValue) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + newArr.push(newValue); + return newArr; + }; /** - * 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}] + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue + * @returns {Array} */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; - - 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++; - } - - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } - } - - return results; - } + exports.copyArray = function (arr) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + return newArr; + }; /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed + * 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. */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); - - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; - - if (prop in obj) { - return prop; - } - i++; - } - return undefined; - } + exports.getAbsoluteLeft = function (elem) { + return elem.getBoundingClientRect().left; + }; /** - * get a unique id - * @returns {number} uniqueId + * 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. */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + exports.getAbsoluteTop = function (elem) { + return elem.getBoundingClientRect().top; + }; /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} + * add a className to the given elements style + * @param {Element} elem + * @param {String} className */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } - - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - - 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); - - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; - - var COMPUTE_INTERVAL = 25; - - 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']; + 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(' '); + } + }; /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor + * add a className to the given elements style + * @param {Element} elem + * @param {String} className */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; - - // 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.init(); - - } - - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, - - /** - * 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); - }, - - /** - * 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); - } + 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(' '); + } }; /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} + * 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) */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; - - 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; + 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); } - return new (Type)(manager, inputHandler); - } + } else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } + } + } + }; /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input + * 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 */ - 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)); - - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; - - if (isFirst) { - manager.session = {}; - } - - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - - // compute scale, rotation etc - computeInputData(manager, input); + exports.toArray = function (object) { + var array = []; - // emit secret event - manager.emit('hammer.input', input); + for (var prop in object) { + if (object.hasOwnProperty(prop)) array.push(object[prop]); + } - manager.recognize(input); - manager.session.prevInput = input; - } + return array; + }; /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * Update a property in an object + * @param {Object} object + * @param {String} key + * @param {*} value + * @return {Boolean} changed */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + exports.updateProperty = function (object, key, value) { + if (object[key] !== value) { + object[key] = value; + return true; + } else { + return false; + } + }; - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } + /** + * 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; - // 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; + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - - 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; + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent('on' + action, listener); // IE browsers + } + }; - computeIntervalInputData(session, input); + /** + * 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; - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - input.target = target; - } - - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent('on' + action, listener); + } + }; - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + /** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ + exports.preventDefault = function (event) { + if (!event) event = window.event; - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } else { + event.returnValue = false; // IE browsers + } + }; /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input + * Get HTML element which is the target of the event + * @param {Event} event + * @return {Element} target element */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; + exports.getTarget = function (event) { + // code from http://www.quirksmode.org/js/events_properties.html + if (!event) { + event = window.event; + } - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + var target; - 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); + if (event.target) { + target = event.target; + } else if (event.srcElement) { + target = event.srcElement; + } - 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; - } + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + return target; + }; /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData + * Check if given element contains given parent somewhere in the DOM tree + * @param {Element} element + * @param {Element} parent */ - 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++; + exports.hasParent = function (element, parent) { + var e = element; + + while (e) { + if (e === parent) { + return true; } + e = e.parentNode; + } - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } + return false; + }; + + exports.option = {}; /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties + * Convert a value into a boolean + * @param {Boolean | function | undefined} value + * @param {Boolean} [defaultValue] + * @returns {Boolean} bool */ - 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) - }; - } + exports.option.asBoolean = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } + if (value != null) { + return value != false; + } - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } + return defaultValue || null; + }; /** - * 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` + * Convert a value into a number + * @param {Boolean | function | undefined} value + * @param {Number} [defaultValue] + * @returns {Number} number */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } + exports.option.asNumber = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + if (value != null) { + return Number(value) || defaultValue || null; + } + + return defaultValue || null; + }; /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction + * Convert a value into a string + * @param {String | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} str */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } + exports.option.asString = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } + if (value != null) { + return String(value); + } + + return defaultValue || null; + }; /** - * 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 + * 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 */ - 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]]; + exports.option.asSize = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - return Math.sqrt((x * x) + (y * y)); - } + if (exports.isString(value)) { + return value; + } else if (exports.isNumber(value)) { + return value + 'px'; + } else { + return defaultValue || null; + } + }; /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle + * Convert a value into a DOM element + * @param {HTMLElement | function | undefined} value + * @param {HTMLElement} [defaultValue] + * @returns {HTMLElement | null} dom */ - 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; - } + exports.option.asElement = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } + + return value || defaultValue || null; + }; /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation + * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + * + * @param {String} hex + * @returns {{r: *, g: *, b: *}} | 255 range */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } + 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; + }; /** - * 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 + * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. + * @param color + * @param opacity + * @returns {*} */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } - - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END + 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 + ')'; + } + } }; - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - /** - * Mouse events input + * + * @param red 0 -- 255 + * @param green 0 -- 255 + * @param blue 0 -- 255 + * @returns {string} * @constructor - * @extends Input */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; - - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state - - Input.apply(this, arguments); - } - - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; - - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } - - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + exports.RGBToHex = function (red, green, blue) { + return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); + }; - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; + /** + * 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 } - - if (eventType & INPUT_END) { - this.pressed = false; + }; + } 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; - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); + 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; } - }); - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; + 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; + } + } - // 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 + return c; }; - 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 + * http://www.javascripter.net/faq/rgb2hsv.htm + * + * @param red + * @param green + * @param blue + * @returns {*} * @constructor - * @extends Input */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; - - Input.apply(this, arguments); - - this.store = (this.manager.session.pointerEvents = []); - } + 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)); - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; + // Black-gray-white + if (minRGB == maxRGB) { + return { h: 0, s: 0, v: minRGB }; + } - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + // 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 isTouch = (pointerType == INPUT_TYPE_TOUCH); + var cssUtil = { + // split a string with css styles into an object with key/values + split: function split(cssText) { + var styles = {}; - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + 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; + } + }); - // 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 styles; + }, - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } + // 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('; '); + } + }; - // update the event in the store - store[storeIndex] = ev; + /** + * 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); - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + element.style.cssText = cssUtil.join(styles); + }; - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); - } + /** + * 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); + + for (var key in removeStyles) { + if (removeStyles.hasOwnProperty(key)) { + delete styles[key]; } - }); + } - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + element.style.cssText = cssUtil.join(styles); }; - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - /** - * Touch events input + * https://gist.github.com/mjijackson/5311256 + * @param h + * @param s + * @param v + * @returns {{r: number, g: number, b: number}} * @constructor - * @extends Input */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + exports.HSVToRGB = function (h, s, v) { + var r, g, b; - Input.apply(this, 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); - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + 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; + } - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } + return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; + }; - if (!this.started) { - return; - } + exports.HSVToHex = function (h, s, v) { + var rgb = exports.HSVToRGB(h, s, v); + return exports.RGBToHex(rgb.r, rgb.g, rgb.b); + }; - var touches = normalizeSingleTouches.call(this, ev, type); + exports.hexToHSV = function (hex) { + var rgb = exports.hexToRGB(hex); + return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); + }; - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; - } + exports.isValidHex = function (hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; + }; - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + 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; + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); - - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); + 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]]); + } + } } - - return [all, changed]; - } - - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + return objectTo; + } else { + return null; + } }; - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - /** - * Multi-user touch events input - * @constructor - * @extends Input + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} */ - 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; + 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]); } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + } } - }); + return objectTo; + } else { + return null; + } + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * 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 */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; - - // 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]; - } - - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; - - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); - - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } + exports.mergeOptions = function (mergeTarget, options, option) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - // 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]); + 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; } - - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; + for (var prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } } - i++; - } - - if (!changedTargetTouches.length) { - return; + } } - - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } + } + }; /** - * 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. + * 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. * - * @constructor - * @extends Input + * @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 */ - 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; - } + exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { + var maxIterations = 10000; + var iteration = 0; + var low = 0; + var high = orderedItems.length - 1; - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } + while (low <= high && iteration < maxIterations) { + var middle = Math.floor((low + high) / 2); - this.callback(manager, inputEvent, inputData); - }, + var item = orderedItems[middle]; + var value = field2 === undefined ? item[field] : item[field][field2]; - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); + 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; } - }); - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + iteration++; + } - // 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'; + return -1; + }; /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor + * 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 */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } + 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; - 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(); - } + 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]; - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; - } - this.actions = value.toLowerCase().trim(); - }, + 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++; + } - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, + // didnt find anything. Return -1. + return -1; + }; - /** - * 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(' ')); - }, + /* + * 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; + } + }; - /** - * 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; - } +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + 'use strict'; - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } + module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(11); - 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); +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); - } - }, + /* WEBPACK VAR INJECTION */(function(module) {//! moment.js + //! version : 2.10.3 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com - /** - * 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(); - } - }; + (function (global, factory) { + true ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() + }(this, function () { 'use strict'; - /** - * 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; - } + var hookCallback; - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); + } - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; } - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; } - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; } - return TOUCH_ACTION_AUTO; - } + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } - /** - * 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; + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + } - /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options - */ - function Recognizer(options) { - this.id = uniqueId(); + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } - this.manager = null; - this.options = merge(options || {}, this.defaults); + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } - this.state = STATE_POSSIBLE; + return a; + } - this.simultaneous = {}; - this.requireFail = []; - } + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); + } - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, + 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 + }; + } - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; + } - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, + 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; - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } } + return m._isValid; + } - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); } - return this; - }, - - /** - * 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; + else { + getParsingFlags(m).userInvalidated = true; } - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, + return m; + } - /** - * 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 momentProperties = utils_hooks__hooks.momentProperties = []; - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, + function copyConfig(to, from) { + var i, prop, val; - /** - * 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; + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; } - - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); + if (typeof from._i !== 'undefined') { + to._i = from._i; } - return this; - }, - - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, - - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, - - /** - * 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; - - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + if (typeof from._f !== 'undefined') { + to._f = from._f; } - - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); + if (typeof from._l !== 'undefined') { + to._l = from._l; } - - emit(); // simple 'eventName' events - - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; } - }, - - /** - * 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); + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; } - // it's failing anyway - this.state = STATE_FAILED; - }, - - /** - * 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++; + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; } - 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; + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; } - - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; + if (typeof from._pf !== 'undefined') { + to._pf = getParsingFlags(from); } - - 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); + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; } - }, - - /** - * 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() { } - }; + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - /** - * 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 to; } - return ''; - } - /** - * 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 ''; - } + var updateInProgress = false; - /** - * 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); + // 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; + } } - return otherRecognizer; - } - - /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer - */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != 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; - }, + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - /** - * 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; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + return value; + } - // 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; + 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 state | STATE_CHANGED; } - return STATE_FAILED; + return diffs + lengthDiff; } - }); - /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer - */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + function Locale() { + } - this.pX = null; - this.pY = null; - } + var locales = {}; + var globalLocale; - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); + // 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; + + 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++; } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); + return null; + } + + 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 actions; - }, + return locales[name]; + } - 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; + // 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); + } - // 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); + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; } } - input.direction = direction; - return hasMoved && distance > options.threshold && direction & options.direction; - }, + return globalLocale._abbr; + } - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + function defineLocale (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; } - - this._super.emit.call(this, input); } - }); - - /** - * 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); - } - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + // returns locale data + function locale_locales__getLocale (key) { + var locale; - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + if (!key) { + return globalLocale; + } - 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); + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; } + + return chooseLocale(key); } - }); - /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer - */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + var aliases = {}; - this._timer = null; - this._input = null; - } + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + } - 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 - }, + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; + } - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - 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; + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } - this._input = input; + return normalizedInput; + } - // 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 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); + } + }; + } - reset: function() { - clearTimeout(this._timer); - }, + function get_set__get (mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + function get_set__set (mom, unit, value) { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); + // MOMENTS + + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + return this[units](value); + } } + return this; } - }); - - /** - * 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]; - }, + function zeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; } - }); - /** - * 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); - } + 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; - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + var formatFunctions = {}; - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + var formatTokenFunctions = {}; - 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; + // 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](); + }; } - - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, - - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); + 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); + }; } + } - this.manager.emit(this.options.event, input); + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); } - }); - /** - * 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); + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } - this._timer = null; - this._input = null; - this.count = 0; - } + 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; + }; + } + + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - 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 - }, + format = expandFormat(format, m.localeData()); - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - process: function(input) { - var options = this.options; + return formatFunctions[format](m); + } - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + function expandFormat(format, locale) { + var i = 5; - this.reset(); + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; } - // 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(); - } + return format; + } - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + 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 - this.pTime = input.timeStamp; - this.pCenter = input.center; + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - this._input = input; + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 - // 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; - }, + // 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; - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + var regexes = {}; - reset: function() { - clearTimeout(this._timer); - }, + function addRegexToken (token, regex, strictRegex) { + regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; + } - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); } - } - }); - /** - * 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); - } + return regexes[token](config._strict, config._locale); + } - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; + // 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, '\\$&'); + } - /** - * 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, + var tokens = {}; - /** - * 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, + 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; + } + } - /** - * @type {Boolean} - * @default true - */ - enable: true, + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } - /** - * 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, + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } + } - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; - /** - * 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] - ], + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - /** - * 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', + // FORMATTING - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); - /** - * 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', + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + // ALIASES - /** - * 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)' - } - }; + addUnitAlias('month', 'M'); - var STOP = 1; - var FORCED_STOP = 2; + // PARSING - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Manager(element, options) { - options = options || {}; + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', matchWord); + addRegexToken('MMMM', matchWord); - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; + }); - this.handlers = {}; - this.session = {}; - this.recognizers = []; + 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; + } + }); - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + // LOCALES - toggleCssProps(this, true); + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m) { + return this._months[m.month()]; + } - 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); - } + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m) { + return this._monthsShort[m.month()]; + } - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); + + 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; + } } - return this; - }, + } - /** - * 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; - }, + // MOMENTS - /** - * 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; + 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; + } } - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } - var recognizer; - var recognizers = this.recognizers; + 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 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; + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); + } - // 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; - } + function checkOverflow (m) { + var overflow; + var a = m._a; - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + 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; - // 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(); + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; } - // 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++; + getParsingFlags(m).overflow = overflow; } - }, - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } + return m; + } - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); } - return null; - }, + } + + function deprecate(msg, fn) { + var firstTime = true, + msgWithStack = msg + '\n' + (new Error()).stack; + + return extend(function () { + if (firstTime) { + warn(msgWithStack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } - /** - * 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; - } + var deprecations = {}; - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; } + } - this.recognizers.push(recognizer); - recognizer.manager = this; + utils_hooks__hooks.suppressDeprecationWarnings = false; - this.touchAction.update(); - return recognizer; - }, + 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)?)?$/; - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', 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}/] + ]; - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); + // 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/] + ]; - this.touchAction.update(); - return this; - }, + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; - /** - * 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; - }, + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = from_string__isoRegex.exec(string); - /** - * 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); + 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; + } } - }); - return this; - }, - - /** - * 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); + 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; } + } - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { + // 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; } - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; - - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); } - }, - - /** - * 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); - - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; } - }; - - /** - * 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 : ''; - }); - } - /** - * 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); - } + 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' : '')); + } + ); - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, + 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); - 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, + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } - 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 createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } - Manager: Manager, - Input: Input, - TouchAction: TouchAction, + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, + // ALIASES - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); + addUnitAlias('year', 'y'); - if ("function" == TYPE_FUNCTION && __webpack_require__(18)) { - !(__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; - } + // PARSING - })(window, document, 'Hammer'); + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); + addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); + addParseToken('YY', function (input, array) { + array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); + }); -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + // HELPERS - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } - /* WEBPACK VAR INJECTION */}.call(exports, {})) + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + // HOOKS - // utility functions + utils_hooks__hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. + // MOMENTS - 'use strict'; + var getSetYear = makeGetSet('FullYear', false); - var moment = __webpack_require__(20); - var uuid = __webpack_require__(23); + function getIsLeapYear () { + return isLeapYear(this.year()); + } - /** - * Test whether given object is a number - * @param {*} object - * @return {Boolean} isNumber - */ - exports.isNumber = function (object) { - return object instanceof Number || typeof object == 'number'; - }; + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - /** - * 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); - } - } - }; + // ALIASES - /** - * 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); - } - }; + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); - /** - * Test whether given object is a string - * @param {*} object - * @return {Boolean} isString - */ - exports.isString = function (object) { - return object instanceof String || typeof object == 'string'; - }; + // PARSING - /** - * 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; - } - } + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); - return false; - }; + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + }); - /** - * Create a semi UUID - * source: http://stackoverflow.com/a/105074/1262753 - * @return {String} uuid - */ - exports.randomUUID = function () { - return uuid.v4(); - }; + // HELPERS - /** - * 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; - } - } - } - }; + // 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; - /** - * 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]; - 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); + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; } - } - } - } - }; - /** - * 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; - }; + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - /** - * 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]; - } + adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; } - } - 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'); - } - for (var i = 2; i < arguments.length; i++) { - var other = arguments[i]; + // LOCALES - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - a[prop] = other[prop]; - } + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; } - } - 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.selectiveDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + 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. + }; - // 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]; - } - } + function localeFirstDayOfWeek () { + return this._week.dow; } - } - 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.selectiveNotDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + function localeFirstDayOfYear () { + return this._week.doy; + } - // 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]; - } - } + // MOMENTS + + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); } - } - return a; - }; - /** - * 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]; - } + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); } - } - 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; + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - for (var i = 0, len = a.length; i < len; i++) { - if (a[i] != b[i]) return false; - } + // ALIASES - return true; - }; + addUnitAlias('dayOfYear', 'DDD'); - /** - * 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; + // PARSING - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); - } + // HELPERS - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); + //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; - case 'number': - case 'Number': - return Number(object.valueOf()); + 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; - case 'string': - case 'String': - return String(object); + return { + year : dayOfYear > 0 ? year : year - 1, + dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - 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 + // MOMENTS + + 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'); + } + + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { + return a; } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); - } + if (b != null) { + return b; + } + return c; + } - case 'Moment': - if (exports.isNumber(object)) { - return moment(object); - } - if (object instanceof Date) { - return moment(object.valueOf()); - } else if (moment.isMoment(object)) { - return moment(object); - } - 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 + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); - } + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } - 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 + // 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; } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); - } - 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 + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); } - 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 the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - // parse ASP.Net Date pattern, - // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' - // code from http://momentjs.com/ - var ASPDateRegex = /^\/?Date\((\-?\d+)/i; + if (config._dayOfYear > daysInYear(yearToUse)) { + getParsingFlags(config)._overflowDayOfYear = true; + } - /** - * 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; + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } - 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'; - } - 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'; - } + // 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]; + } - return type; - }; + // 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]; + } - /** - * 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 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; + } - /** - * 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; - }; + 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); + } - /** - * 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 (config._nextDay) { + config._a[HOUR] = 24; + } + } - /** - * 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; - }; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - /** - * 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(' '); - } - }; + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - /** - * 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(' '); - } - }; + // 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; - /** - * 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); - } + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); + + 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); + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; } - } - }; - /** - * 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 = []; + utils_hooks__hooks.ISO_8601 = function () {}; - for (var prop in object) { - if (object.hasOwnProperty(prop)) array.push(object[prop]); - } + // 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; + } - return array; - }; + config._a = []; + getParsingFlags(config).empty = true; - /** - * 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; - } - }; + // 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; - /** - * 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; + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox - } + 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); + } + } - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent('on' + action, listener); // IE browsers - } - }; + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } - /** - * 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; + // 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); - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + configFromArray(config); + checkOverflow(config); } - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent('on' + action, listener); - } - }; - /** - * Cancels the event if it is cancelable, without stopping further propagation of the event. - */ - exports.preventDefault = function (event) { - if (!event) event = window.event; + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } else { - event.returnValue = false; // IE browsers - } - }; + 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; + } + } - /** - * 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; - } + function configFromStringAndArray(config) { + var tempConfig, + bestMoment, - var target; + scoreToBeat, + i, + currentScore; - if (event.target) { - target = event.target; - } else if (event.srcElement) { - target = event.srcElement; - } + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } - if (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } + 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); - return target; - }; + if (!valid__isValid(tempConfig)) { + continue; + } - /** - * 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; + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; - while (e) { - if (e === parent) { - return true; - } - e = e.parentNode; - } + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - return false; - }; + getParsingFlags(tempConfig).score = currentScore; - exports.option = {}; + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - /** - * 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(); - } + extend(config, bestMoment || tempConfig); + } - if (value != null) { - return value != false; - } + function configFromObject(config) { + if (config._d) { + return; + } - return defaultValue || null; - }; + var i = normalizeObjectUnits(config._i); + config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - /** - * 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(); - } + configFromArray(config); + } - if (value != null) { - return Number(value) || defaultValue || null; - } + function createFromConfig (config) { + var input = config._i, + format = config._f, + res; - return defaultValue || null; - }; + config._locale = config._locale || locale_locales__getLocale(config._l); - /** - * 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 (input === null || (format === undefined && input === '')) { + return valid__createInvalid({nullInput: true}); + } - if (value != null) { - return String(value); - } + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - return defaultValue || null; - }; + 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); + } - /** - * 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(); - } + res = new Moment(checkOverflow(config)); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } - if (exports.isString(value)) { - return value; - } else if (exports.isNumber(value)) { - return value + 'px'; - } else { - return defaultValue || null; - } - }; + return res; + } - /** - * 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(); - } + 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); + } + } - return value || defaultValue || null; - }; + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; - /** - * 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; - }; + 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; - /** - * 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 + ')'; + return createFromConfig(c); } - } - }; - - /** - * - * @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); - }; - /** - * 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]); + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); } - 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 + + 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; + } + ); + + 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; + } + ); + + // 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 (!moments.length) { + return local__createLocal(); } - }; - } else { - c = { - background: color, - border: color, - highlight: { - background: color, - border: color - }, - hover: { - background: color, - border: color + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (moments[i][fn](res)) { + res = moments[i]; + } } - }; + return res; } - } 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; + // TODO: Use [].sort instead? + function min () { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); } - 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; + function max () { + var args = [].slice.call(arguments, 0); + + return pickBy('isAfter', args); } - } - return c; - }; + 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; - /** - * 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)); + // 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; - // Black-gray-white - if (minRGB == maxRGB) { - return { h: 0, s: 0, v: minRGB }; - } + this._data = {}; - // 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._locale = locale_locales__getLocale(); - var cssUtil = { - // split a string with css styles into an object with key/values - split: function split(cssText) { - var styles = {}; + this._bubble(); + } - 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; - } + function isDuration (obj) { + return obj instanceof Duration; + } + + 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('Z', matchOffset); + addRegexToken('ZZ', matchOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(input); }); - return styles; - }, + // HELPERS - // 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('; '); - } - }; + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; - /** - * 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); + 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]); - element.style.cssText = cssUtil.join(styles); - }; + return parts[0] === '+' ? minutes : -minutes; + } - /** - * 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); + // 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(); + } - for (var key in removeStyles) { - if (removeStyles.hasOwnProperty(key)) { - delete styles[key]; + 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; } - } - element.style.cssText = cssUtil.join(styles); - }; + // HOOKS - /** - * 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 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 () {}; - 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); + // MOMENTS - 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; - } + // 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); + } + } - return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; - }; + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } - exports.HSVToHex = function (h, s, v) { - var rgb = exports.HSVToRGB(h, s, v); - return exports.RGBToHex(rgb.r, rgb.g, rgb.b); - }; + this.utcOffset(input, keepLocalTime); - exports.hexToHSV = function (hex) { - var rgb = exports.hexToRGB(hex); - return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); - }; + return this; + } else { + return -this.utcOffset(); + } + } - exports.isValidHex = function (hex) { - var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); - return isOk; - }; + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } - 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; - }; + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - /** - * 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]]); + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } } - } + return this; } - 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 {*} - */ - 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]); + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(this._i)); } - } + return this; } - return objectTo; - } else { - return null; - } - }; - - /** - * 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 (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; + function hasAlignedHourOffset (input) { + if (!input) { + input = 0; } - for (var prop in options[option]) { - if (options[option].hasOwnProperty(prop)) { - mergeTarget[option][prop] = options[option][prop]; - } + else { + input = local__createLocal(input).utcOffset(); } - } + + return (this.utcOffset() - input) % 60 === 0; } - } - }; - /** - * 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; + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } + + 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; + } + + return false; + } + + function isLocal () { + return !this._isUTC; + } + + function isUtcOffset () { + return this._isUTC; + } + + function isUtc () { + return this._isUTC && this._offset === 0; + } + + var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; + + // 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 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; + } - while (low <= high && iteration < maxIterations) { - var middle = Math.floor((low + high) / 2); + ret = new Duration(duration); - var item = orderedItems[middle]; - var value = field2 === undefined ? item[field] : item[field][field2]; + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - 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; + return ret; } - iteration++; - } + create__createDuration.fn = Duration.prototype; - return -1; - }; + 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; + } - /** - * 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; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - 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]; + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - 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++; - } + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - // didnt find anything. Return -1. - return -1; - }; + return res; + } - /* - * 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; - } - }; + 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; + } -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { + return res; + } - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. - 'use strict'; + 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; + } - module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(21); + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); + return this; + }; + } -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - /* WEBPACK VAR INJECTION */(function(module) {//! moment.js - //! version : 2.10.3 - //! authors : Tim Wood, Iskren Chernev, Moment.js contributors - //! license : MIT - //! momentjs.com + 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); + } + } - (function (global, factory) { - true ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() - }(this, function () { 'use strict'; + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); - var hookCallback; + 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))); + } - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); + function clone () { + return new Moment(this); } - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; + 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); + } } - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + 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 isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + function isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); } - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); + 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 res; } - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); + function absFloor (number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } } - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; + function diff (input, units, asFloat) { + var that = cloneWithOffset(input, this), + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, + delta, output; + + units = normalizeUnits(units); + + 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); + } - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + 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; - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; + 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); } - return a; + return -(wholeMonthDiff + adjust); } - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - 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 - }; + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); } - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); + 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]'); } - return m._pf; } - 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; + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); + } - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; - } + function from (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); } - return m._isValid; + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); } - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; - } + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); + } - return m; + function to (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); } - var momentProperties = utils_hooks__hooks.momentProperties = []; + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); + } - function copyConfig(to, from) { - var i, prop, val; + function locale (key) { + var newLocaleData; - 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 (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; + } + + 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); + } } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; + ); + + function localeData () { + return this._locale; + } + + 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); } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); + + // weeks are a special case + if (units === 'week') { + this.weekday(0); } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; + if (units === 'isoWeek') { + this.isoWeekday(1); } - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); } - return to; + return this; } - var updateInProgress = false; - - // 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 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'); } - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + function to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; - - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + function unix () { + return Math.floor(+this / 1000); + } - return value; + function toDate () { + return this._offset ? new Date(+this) : this._d; } - 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 toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; } - function Locale() { + function moment_valid__isValid () { + return valid__isValid(this); } - var locales = {}; - var globalLocale; + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; + function invalidAt () { + return getParsingFlags(this).overflow; } - // 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; + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); - 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; - } + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); - 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]; + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); } - // 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); - } + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } - } + // ALIASES - return globalLocale._abbr; - } + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + // PARSING - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); + 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); - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - } + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + }); - // returns locale data - function locale_locales__getLocale (key) { - var locale; + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = utils_hooks__hooks.parseTwoDigitYear(input); + }); - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } + // HELPERS - if (!key) { - return globalLocale; - } + function weeksInYear(year, dow, doy) { + return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; + } - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } + // MOMENTS - return chooseLocale(key); + 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'); } - var aliases = {}; + function getSetISOWeekYear (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); + } - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); } - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; + function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + addFormatToken('Q', 0, 0, 'quarter'); - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } + // ALIASES - return normalizedInput; - } + addUnitAlias('quarter', 'Q'); - 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); - } - }; - } + // PARSING - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + // MOMENTS + + function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); } + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); + // MOMENTS - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } + var getSetDayOfMonth = makeGetSet('Date', true); + + addFormatToken('d', 0, 'do', 'day'); + + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); + + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); + + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); + + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); + + // 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 { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); - } + getParsingFlags(config).invalidWeekday = input; } - return this; - } + }); - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); - while (output.length < targetLength) { - output = '0' + output; + // 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 (sign ? (forceSign ? '+' : '') : '-') + output; + return input; } - 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; + // LOCALES - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m) { + return this._weekdays[m.day()]; + } - var formatFunctions = {}; + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } - var formatTokenFunctions = {}; + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } - // 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]); - }; + function localeWeekdaysParse (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; + + 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; + } } } - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); + // 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; } - return input.replace(/\\/g, ''); } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + function getSetLocaleDayOfWeek (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } + 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 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('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, function () { + return this.hours() % 12 || 12; + }); + + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); } - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } + meridiem('a', true); + meridiem('A', false); - format = expandFormat(format, m.localeData()); + // ALIASES - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + addUnitAlias('hour', 'h'); - return formatFunctions[format](m); + // PARSING + + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; } - function expandFormat(format, locale) { - var i = 5; + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + 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; + }); - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + // LOCALES - return format; + 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'; + } } - 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 + // MOMENTS - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + // 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); - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + addFormatToken('m', ['mm', 2], 0, 'minute'); - // 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; + // ALIASES - var regexes = {}; + addUnitAlias('minute', 'm'); - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; - } + // PARSING - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); - } + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - return regexes[token](config._strict, config._locale); - } + // MOMENTS - // 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, '\\$&'); - } + var getSetMinute = makeGetSet('Minutes', false); - var tokens = {}; + addFormatToken('s', ['ss', 2], 0, 'second'); - 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; - } - } + // ALIASES - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); - }); - } + addUnitAlias('second', 's'); - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); - } - } + // PARSING - var YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } + // MOMENTS - // FORMATTING + var getSetSecond = makeGetSet('Seconds', false); - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); }); - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); }); - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); + function millisecond__milliseconds (token) { + addFormatToken(0, [token, 3], 0, 'millisecond'); + } + + millisecond__milliseconds('SSS'); + millisecond__milliseconds('SSSS'); // ALIASES - addUnitAlias('month', 'M'); + addUnitAlias('millisecond', 'ms'); // PARSING - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); - - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; + 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); }); - 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; - } - }); + // MOMENTS - // LOCALES + var getSetMillisecond = makeGetSet('Milliseconds', false); - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; } - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; } - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; + var momentPrototype__proto = Moment.prototype; - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } + 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; - 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; - } - } - } + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; - // MOMENTS + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; - function setMonth (mom, value) { - var dayOfMonth; + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - // 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; - } - } + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); - } + // 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; + + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + + // 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; + + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; + + // 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); + + var momentPrototype = momentPrototype__proto; + + function moment__createUnix (input) { + return local__createLocal(input * 1000); } - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); } - function checkOverflow (m) { - var overflow; - var a = m._a; + 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' + }; - 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; + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.call(mom, now) : output; + } - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + 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' + }; - getParsingFlags(m).overflow = overflow; + 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; + } - return m; + var defaultInvalidDate = 'Invalid date'; + + function invalidDate () { + return this._invalidDate; } - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); } - function deprecate(msg, fn) { - var firstTime = true, - msgWithStack = msg + '\n' + (new Error()).stack; + function preParsePostFormat (string) { + return string; + } - return extend(function () { - if (firstTime) { - warn(msgWithStack); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); + 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' + }; + + 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); } - var deprecations = {}; + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + } - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; + 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); } - utils_hooks__hooks.suppressDeprecationWarnings = false; + var prototype__proto = Locale.prototype; - 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)?)?$/; + 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; - 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}/] - ]; + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; - // 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/] - ]; + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + // 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; - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; - 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 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 from iso format or fallback - function configFromString(config) { - var matched = aspNetJsonRegex.exec(config._i); + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; + } + + format = format || ''; - if (matched !== null) { - config._d = new Date(+matched[1]); - return; + if (index != null) { + return lists__get(format, index, field, setter); } - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); } + return out; } - 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' : '')); - } - ); + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } - 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); + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); } - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); } - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); + } + + 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; + } }); - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + // 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); - // ALIASES + var mathAbs = Math.abs; - addUnitAlias('year', 'y'); + function duration_abs__abs () { + var data = this._data; - // PARSING + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); + 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); - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + return this; + } - // HELPERS + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); } - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + // 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); } - // HOOKS + // 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); + } - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years = 0; - // MOMENTS + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - var getSetYear = makeGetSet('FullYear', false); + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - function getIsLeapYear () { - return isLeapYear(this.year()); - } + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + hours = absFloor(minutes / 60); + data.hours = hours % 24; - // ALIASES + days += absFloor(hours / 24); - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); + // Accurately convert days to years, assume start from year 0. + years = absFloor(daysToYears(days)); + days -= absFloor(yearsToDays(years)); - // PARSING + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absFloor(days / 30); + days %= 30; - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); + // 12 months -> 1 year + years += absFloor(months / 12); + months %= 12; - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); + data.days = days; + data.months = months; + data.years = years; - // HELPERS + return this; + } - // 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; + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; + } + + function yearsToDays (years) { + // years * 365 + absFloor(years / 4) - + // absFloor(years / 100) + absFloor(years / 400); + return years * 146097 / 400; + } + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } + units = normalizeUnits(units); - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + 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); + } } + } - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } + + function makeAs (alias) { + return function () { + return this.as(alias); }; } - // LOCALES + 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 localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); } - 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. - }; + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } - function localeFirstDayOfWeek () { - return this._week.dow; + 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 localeFirstDayOfYear () { - return this._week.doy; + 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 + }; + + // 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); } - // MOMENTS + 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 getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); + 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]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); } - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); + // 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; } - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); - // ALIASES + if (withSuffix) { + output = locale.pastFuture(+this, output); + } - addUnitAlias('dayOfYear', 'DDD'); + return locale.postformat(output); + } - // PARSING + var iso_string__abs = Math.abs; - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); + 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(); - // HELPERS + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - //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; + 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' : ''); + } - 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; + var duration_prototype__proto = Duration.prototype; - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + 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; - // MOMENTS + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; - 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'); - } + // Side effect imports - // 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; - } + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; - } - return [now.getFullYear(), now.getMonth(), now.getDate()]; - } + // PARSING - // 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; + 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)); + }); - if (config._d) { - return; - } + // Side effect imports - currentDate = currentDateArray(config); - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + utils_hooks__hooks.version = '2.10.3'; - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + setHookCallback(local__createLocal); + + 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; + + var _moment = utils_hooks__hooks; + + return _moment; + + })); + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(12)(module))) + +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; - } - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - // 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]; - } + /* WEBPACK VAR INJECTION */(function(global) {'use strict'; - // 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]; - } + var _rng; - // 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 globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; - 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); - } + 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 (config._nextDay) { - config._a[HOUR] = 24; - } + 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 dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; + return _rnds; + }; + } - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; + // uuid.js + // + // Copyright (c) 2010-2012 Robert Kieffer + // MIT License - http://opensource.org/licenses/mit-license.php - // 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; + // 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 - weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); - week = defaults(w.w, 1); + //var _rng = require('./rng'); - 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); + // 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; + } - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = buf && offset || 0, + ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { + if (ii < 16) { + // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; } + }); - utils_hooks__hooks.ISO_8601 = function () {}; + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } - // 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; - } + return buf; + } - config._a = []; - getParsingFlags(config).empty = true; + // **`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++]]; + } - // 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; + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); - 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); - } - } + // 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]]; - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; - // 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); + // Previous uuid creation time + var _lastMSecs = 0, + _lastNSecs = 0; - configFromArray(config); - checkOverflow(config); - } + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + options = options || {}; - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; - 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; - } - } + // 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(); - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, + // 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; - scoreToBeat, - i, - currentScore; + // Time since last uuid creation (in msecs) + var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 16383; + } - 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); + // 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; + } - if (!valid__isValid(tempConfig)) { - continue; - } + // 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'); + } - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; - getParsingFlags(tempConfig).score = currentScore; + // `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; - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } + // `time_mid` + var tmh = msecs / 4294967296 * 10000 & 268435455; + b[i++] = tmh >>> 8 & 255; + b[i++] = tmh & 255; - extend(config, bestMoment || tempConfig); - } + // `time_high_and_version` + b[i++] = tmh >>> 24 & 15 | 16; // include version + b[i++] = tmh >>> 16 & 255; - function configFromObject(config) { - if (config._d) { - return; - } + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 128; - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; + // `clock_seq_low` + b[i++] = clockseq & 255; - configFromArray(config); - } + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; + return buf ? buf : unparse(b); + } - config._locale = config._locale || locale_locales__getLocale(config._l); + // **`v4()` - Generate random UUID** - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); - } + // 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 input === 'string') { - config._i = input = config._locale.preparse(input); - } + if (typeof options == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; + } + options = options || {}; - 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 rnds = options.random || (options.rng || _rng)(); - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } + // 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; - return res; + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; } + } - 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); - } - } + return buf || unparse(rnds); + } - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; - 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; + module.exports = uuid; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - return createFromConfig(c); - } +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } + // DOM utility methods - 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; - } - ); + /** + * this prepares the JSON container for allocating SVG elements + * @param JSONcontainer + * @private + */ + 'use strict'; - 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; - } - ); + 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 = []; + } + } + }; - // 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 (!moments.length) { - return local__createLocal(); - } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } + /** + * 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]); } - return res; + JSONcontainer[elementType].redundant = []; + } + } + } + }; + + /** + * 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); } + } 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; + }; - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); - - return pickBy('isBefore', args); + /** + * 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 max () { - var args = [].slice.call(arguments, 0); - - return pickBy('isAfter', args); + } 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; + }; - 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; - - // 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; - - this._data = {}; + /** + * Draw a point object. This is a separate 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 groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' } + * @param JSONcontainer + * @param svgContainer + * @param labelObj + * @returns {*} + */ + exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) { + var point; + if (groupTemplate.style == 'circle') { + point = exports.getSVGElement('circle', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'cx', x); + point.setAttributeNS(null, 'cy', y); + point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size); + } else { + point = exports.getSVGElement('rect', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size); + point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size); + point.setAttributeNS(null, 'width', groupTemplate.size); + point.setAttributeNS(null, 'height', groupTemplate.size); + } - this._locale = locale_locales__getLocale(); + if (groupTemplate.style !== undefined) { + point.setAttributeNS(null, 'style', groupTemplate.style); + } + point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point'); + //handle label - this._bubble(); + if (labelObj) { + var label = exports.getSVGElement('text', JSONcontainer, svgContainer); + if (labelObj.xOffset) { + x = x + labelObj.xOffset; } - function isDuration (obj) { - return obj instanceof Duration; + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; } - 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); - }); + if (labelObj.className) { + label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); } + label.setAttributeNS(null, 'x', x); + label.setAttributeNS(null, 'y', y); + } - offset('Z', ':'); - offset('ZZ', ''); + return point; + }; - // PARSING + /** + * 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; + } + 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); + } + } + }; - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); - }); +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - // HELPERS + 'use strict'; - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; + var util = __webpack_require__(9); + var Queue = __webpack_require__(16); - 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]); + /** + * 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. - // 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); - } - } + /** + * @param {Object} [options] Available options: + * {Object} queue Queue changes to the DataSet, + * flush them all at once. + * Queue options: + * - {number} delay Delay in ms, null by default + * - {number} max Maximum number of entries in the queue, Infinity by default + * @param options + */ + DataSet.prototype.setOptions = function (options) { + if (options && options.queue !== undefined) { + if (options.queue === false) { + // delete queue if loaded + if (this._queue) { + this._queue.destroy(); + delete this._queue; + } + } else { + // create queue and update its options + if (!this._queue) { + this._queue = Queue.extend(this, { + replace: ['add', 'update', 'remove'] + }); + } - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } + if (typeof options.queue === 'object') { + this._queue.setOptions(options.queue); + } + } + } + }; - this.utcOffset(input, keepLocalTime); + /** + * Subscribe to an event, add an event listener + * @param {String} event Event name. Available events: 'put', 'update', + * 'remove' + * @param {function} callback Callback method. Called with three parameters: + * {String} event + * {Object | null} params + * {String | Number} senderId + */ + DataSet.prototype.on = function (event, callback) { + var subscribers = this._subscribers[event]; + if (!subscribers) { + subscribers = []; + this._subscribers[event] = subscribers; + } - return this; - } else { - return -this.utcOffset(); - } - } + subscribers.push({ + callback: callback + }); + }; - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } + // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) + DataSet.prototype.subscribe = function () { + throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.'); + }; - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + /** + * Unsubscribe from an event, remove an event listener + * @param {String} event + * @param {function} callback + */ + DataSet.prototype.off = function (event, callback) { + var subscribers = this._subscribers[event]; + if (subscribers) { + this._subscribers[event] = subscribers.filter(function (listener) { + return listener.callback != callback; + }); + } + }; - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; - } + // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) + DataSet.prototype.unsubscribe = function () { + throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.'); + }; - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); - } - return this; - } + /** + * Trigger an event + * @param {String} event + * @param {Object | null} params + * @param {String} [senderId] Optional id of the sender. + * @private + */ + DataSet.prototype._trigger = function (event, params, senderId) { + if (event == '*') { + throw new Error('Cannot trigger event *'); + } - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); - } + var subscribers = []; + if (event in this._subscribers) { + subscribers = subscribers.concat(this._subscribers[event]); + } + if ('*' in this._subscribers) { + subscribers = subscribers.concat(this._subscribers['*']); + } - return (this.utcOffset() - input) % 60 === 0; + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); } + } + }; - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); + /** + * Add data. + * Adding an item will fail when there already is an item with the same id. + * @param {Object | Array} data + * @param {String} [senderId] Optional sender id + * @return {Array} addedIds Array with the ids of the added items + */ + DataSet.prototype.add = function (data, senderId) { + var addedIds = [], + id, + me = this; + + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + id = me._addItem(data[i]); + addedIds.push(id); } + } else if (data instanceof Object) { + // Single item + id = me._addItem(data); + addedIds.push(id); + } else { + throw new Error('Unknown dataType'); + } - 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; - } + if (addedIds.length) { + this._trigger('add', { items: addedIds }, senderId); + } - return false; - } + return addedIds; + }; - function isLocal () { - return !this._isUTC; - } + /** + * Update existing items. When an item does not exist, it will be created + * @param {Object | Array} data + * @param {String} [senderId] Optional sender id + * @return {Array} updatedIds The ids of the added or updated items + */ + DataSet.prototype.update = function (data, senderId) { + var addedIds = []; + var updatedIds = []; + var updatedData = []; + var me = this; + var fieldId = me._fieldId; - function isUtcOffset () { - return this._isUTC; + var addOrUpdate = function addOrUpdate(item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); + updatedData.push(item); + } else { + // add new item + id = me._addItem(item); + addedIds.push(id); } + }; - function isUtc () { - return this._isUTC && this._offset === 0; + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); } + } else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } else { + throw new Error('Unknown dataType'); + } - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - - // 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)$/; + if (addedIds.length) { + this._trigger('add', { items: addedIds }, senderId); + } + if (updatedIds.length) { + this._trigger('update', { items: updatedIds, data: updatedData }, senderId); + } - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; + return addedIds.concat(updatedIds); + }; - 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)); + /** + * Get a data item or multiple items. + * + * Usage: + * + * get() + * get(options: Object) + * + * get(id: Number | String) + * get(id: Number | String, options: Object) + * + * get(ids: Number[] | String[]) + * get(ids: Number[] | String[], options: Object) + * + * 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} [returnType] Type of data to be returned. + * Can be 'Array' (default) or 'Object'. + * {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; - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } + // 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]; + } - ret = new Duration(duration); + // 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 (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - return ret; + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; } - - 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; + } 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); + } } - - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; + } 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); } - - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; + } } + } - 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; - } + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - return res; + // 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); + } } + } - 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; - }; + // 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; + } + } + }; - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + /** + * 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 = []; - 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); + 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); + } } - } + } - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); + this._sort(items, order); - 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))); + 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 { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } - function clone () { - return new Moment(this); - } + this._sort(items, order); - 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); + 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 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; - } - } + return ids; + }; - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } + /** + * 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 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)); - } - } + /** + * 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; - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); + + 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); } + } } + } + }; - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; - - units = normalizeUnits(units); + /** + * 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; - 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); + // 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)); + } } + } - 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; + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } - 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); - } + return mappedItems; + }; - return -(wholeMonthDiff + adjust); - } + /** + * 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; + } - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + var filteredItem = {}; - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + if (Array.isArray(fields)) { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; + } } - - 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]'); - } + } else { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { + filteredItem[fields[field]] = item[field]; + } } + } - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); - } + return filteredItem; + }; - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - } + /** + * 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; + }); + } 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'); + } + }; - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); - } + /** + * 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; - function to (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } } - - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); + } else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); } + } - function locale (key) { - var newLocaleData; + if (removedIds.length) { + this._trigger('remove', { items: removedIds }, senderId); + } - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } + return removedIds; + }; + + /** + * 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; + } + } 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; + }; - 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); - } - } - ); + /** + * 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); - function localeData () { - return this._locale; - } + this._data = {}; + this.length = 0; - 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); - } + this._trigger('remove', { items: ids }, senderId); - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } + return ids; + }; - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } + /** + * 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; - return this; + 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; + } } + } - 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'); - } + return max; + }; - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); - } + /** + * 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; - function unix () { - return Math.floor(+this / 1000); + 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 toDate () { - return this._offset ? new Date(+this) : this._d; - } + return min; + }; - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; + /** + * 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; + + 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 moment_valid__isValid () { - return valid__isValid(this); + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); } + } - function parsingFlags () { - return extend({}, getParsingFlags(this)); + return values; + }; + + /** + * 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'); } + } else { + // generate an id + id = util.randomUUID(); + item[this._fieldId] = id; + } - function invalidAt () { - return getParsingFlags(this).overflow; + 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); } + } + this._data[id] = d; + this.length++; - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); + return id; + }; - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); + /** + * 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; - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } + + // 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; + }; - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + /** + * 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'); + } - // ALIASES + // 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); + } + } - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); + return id; + }; - // PARSING + module.exports = DataSet; - 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); +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); + /** + * 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'; - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; - // HELPERS + // properties + this._queue = []; + this._timeout = null; + this._extended = null; - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; - } + this.setOptions(options); + } + + /** + * 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; + } - // MOMENTS + this._flushIfNeeded(); + }; - 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'); - } + /** + * 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 getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - } + if (object.flush !== undefined) { + throw new Error('Target object already has a property flush'); + } + object.flush = function () { + queue.flush(); + }; - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); - } + var methods = [{ + name: 'flush', + original: undefined + }]; - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + 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); } + } - addFormatToken('Q', 0, 0, 'quarter'); - - // ALIASES - - addUnitAlias('quarter', 'Q'); - - // PARSING + queue._extended = { + object: object, + methods: methods + }; - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); + return queue; + }; - // MOMENTS + /** + * 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(); - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + 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; + } + }; - addFormatToken('D', ['DD', 2], 'Do', 'date'); - - // ALIASES - - addUnitAlias('date', 'D'); - - // PARSING + /** + * 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'); + } - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; - }); + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this }); + }; + }; - // MOMENTS - - var getSetDayOfMonth = makeGetSet('Date', true); - - addFormatToken('d', 0, 'do', 'day'); - - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); + /** + * 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); + } - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); + this._flushIfNeeded(); + }; - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); + /** + * 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(); + } - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); + // 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); + } + }; - // ALIASES + /** + * 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 || []); + } + }; - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); + module.exports = Queue; - // PARSING +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + 'use strict'; - 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; - } - }); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); + /** + * 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 - // HELPERS + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - 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; - } + this.setData(data); + } - // LOCALES + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } + /** + * Set a data source for the view + * @param {DataSet | DataView} data + */ + DataView.prototype.setData = function (data) { + var ids, i, len; - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; + if (this._data) { + // unsubscribe from current dataset + if (this._data.off) { + this._data.off('*', this.listener); } - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; + // 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 }); + } - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; + this._data = data; - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; - 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; - } - } + // 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 }); - // 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; - } + // subscribe to new dataset + if (this._data.on) { + this._data.on('*', this.listener); } + } + }; - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - } + /** + * 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 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); + // 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++; } + } - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); - - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); + // 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--; + } } + } - meridiem('a', true); - meridiem('A', false); + // trigger events + if (added.length) { + this._trigger('add', { items: added }); + } + if (removed.length) { + this._trigger('remove', { items: removed }); + } + }; - // ALIASES + /** + * 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; - addUnitAlias('hour', 'h'); + // 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]; + } - // PARSING + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } + // 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); + }; + } - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); - 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; - }); + return this._data && this._data.get.apply(this._data, getArguments); + }; - // LOCALES + /** + * 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; - 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'); - } + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; } + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); + } else { + ids = []; + } - // MOMENTS + return ids; + }; - // 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); + /** + * 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; + }; - addFormatToken('m', ['mm', 2], 0, 'minute'); + /** + * 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 = []; - // ALIASES + 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); + } + } - addUnitAlias('minute', 'm'); + break; - // PARSING + 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); - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); + 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 {} + } + } - // MOMENTS + break; - var getSetMinute = makeGetSet('Minutes', false); + 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); + } + } - addFormatToken('s', ['ss', 2], 0, 'second'); + break; + } - // ALIASES + this.length += added.length - removed.length; - addUnitAlias('second', 's'); + 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); + } + } + }; - // PARSING + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); + // 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; - // MOMENTS + module.exports = DataView; - var getSetSecond = makeGetSet('Seconds', false); + // nothing interesting for me :-( - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); +/***/ }, +/* 18 */ +/***/ function(module, exports, __webpack_require__) { - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); + 'use strict'; - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); - } + var Emitter = __webpack_require__(20); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var util = __webpack_require__(9); + var Point3d = __webpack_require__(21); + var Point2d = __webpack_require__(19); + var Camera = __webpack_require__(22); + var Filter = __webpack_require__(23); + var Slider = __webpack_require__(24); + var StepNumber = __webpack_require__(25); - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); + /** + * @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'); + } - // ALIASES + // 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%'; - addUnitAlias('millisecond', 'ms'); + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; - // PARSING + var passValueFn = function passValueFn(v) { + return v; + }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - 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); - }); + this.filterLabel = 'time'; + this.legendLabel = 'value'; - // MOMENTS + 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' - var getSetMillisecond = makeGetSet('Milliseconds', false); + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); + this.camera = new Camera(); + this.camera.setArmRotation(1, 0.5); + this.camera.setArmLength(1.7); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - // MOMENTS + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; - } + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } + 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 momentPrototype__proto = Moment.prototype; + // colors + this.axisColor = '#4D4D4D'; + this.gridColor = '#D3D3D3'; + this.dataColor = { + fill: '#7DC1FF', + stroke: '#3267D2', + strokeWidth: 1 // px + }; - 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; + // create a frame and canvas + this.create(); - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; + // apply options (also when undefined) + this.setOptions(options); - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + // apply data + if (data) { + this.setData(data); + } + } - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; + /** + * 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)); - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + // 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; + } + } - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + // 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); + }; - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + /** + * 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); + }; - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + /** + * 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, - // 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; + // 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), - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; + // 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)); - // 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); + return new Point3d(dx, dy, dz); + }; - var momentPrototype = momentPrototype__proto; + /** + * 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; - function moment__createUnix (input) { - return local__createLocal(input * 1000); - } + // 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()); + } - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); - } + // 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); + }; - 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' - }; + /** + * 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; - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; - } + 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 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' - }; + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + 'px'; + this.frame.style.borderStyle = 'solid'; + }; - 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; - } + /// 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 + }; - var defaultInvalidDate = 'Invalid date'; + /** + * 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; + } - function invalidDate () { - return this._invalidDate; - } + return -1; + }; - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; + /** + * 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; - function ordinal (number) { - return this._ordinal.replace('%d', number); + 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; - function preParsePostFormat (string) { - return string; + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; } + } else { + throw 'Unknown style "' + this.style + '"'; + } + }; - 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' - }; + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - 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); + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; } + } + return counter; + }; - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + 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; + }; - 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); + 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; + }; - var prototype__proto = Locale.prototype; - - 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; - - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + /** + * 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; - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); + } - // 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 (rawData === undefined) return; - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - 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); - } + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error('Array, DataSet, or DataView expected'); + } - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; - } + if (data.length == 0) return; - format = format || ''; + this.dataSet = rawData; + this.dataTable = data; - if (index != null) { - return lists__get(format, index, field, setter); - } + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on('*', this._onChange); - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); - } - return out; - } + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); - } + // 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'; - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); + // 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(); + }); } + } - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); - } + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); + // 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; } - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 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; - } - }); - - // 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); - - var mathAbs = Math.abs; + // calculate minimums and maximums + var xRange = this.getColumnRange(data, this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; + } + 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; - function duration_abs__abs () { - var data = this._data; + 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; - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + 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; - 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); + 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; + } - return this; - } + // set the scale dependent on the ranges. + this._setScale(); + }; - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); + /** + * 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; - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; + var dataPoints = []; - return duration._bubble(); - } + 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 - // 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); - } + // 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; - // 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); + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } } - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; + var sortNumber = function sortNumber(a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + // 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; - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - hours = absFloor(minutes / 60); - data.hours = hours % 24; + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - days += absFloor(hours / 24); + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); + dataMatrix[xIndex][yIndex] = obj; - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; + dataPoints.push(obj); + } - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; + // 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; - data.days = days; - data.months = months; - data.years = years; + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } - return this; - } + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; + dataPoints.push(obj); } + } - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; - } + return dataPoints; + }; - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; + /** + * 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); + } - units = normalizeUnits(units); + this.frame = document.createElement('div'); + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; - 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); - } - } - } + // 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); + } - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); - } + 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); - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } + // 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' - 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'); + 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); - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); - } + // add the new graph to the container element + this.containerElement.appendChild(this.frame); + }; - function makeGetter(name) { - return function () { - return this._data[name]; - }; - } + /** + * 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%') + */ + Graph3d.prototype.setSize = function (width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - 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'); + this._resizeCanvas(); + }; - function weeks () { - return absFloor(this.days() / 7); - } + /** + * Resize the canvas to the current size of the frame + */ + Graph3d.prototype._resizeCanvas = function () { + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - 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 - }; + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - // 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); - } + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; + }; - 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')); + /** + * Start animation + */ + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; - 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]; + this.frame.filter.slider.play(); + }; - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } + /** + * Stop animation + */ + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - // 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; - } + this.frame.filter.slider.stop(); + }; - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); + /** + * 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 + */ + 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 + } - if (withSuffix) { - output = locale.pastFuture(+this, output); - } + // 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 + } + }; - return locale.postformat(output); - } + /** + * 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. + */ + Graph3d.prototype.setCameraPosition = function (pos) { + if (pos === undefined) { + return; + } - var iso_string__abs = Math.abs; + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } - 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(); + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + this.redraw(); + }; - 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' : ''); - } + /** + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance + */ + Graph3d.prototype.getCameraPosition = function () { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; + }; - var duration_prototype__proto = Duration.prototype; + /** + * Load data into the 3D Graph + */ + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - 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 (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; + // draw the filter + this._redrawFilter(); + }; - // Side effect imports + /** + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data + */ + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; - // PARSING + /** + * Update the options. Options will be merged with current options + * @param {Object} options + */ + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - 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)); - }); + this.animationStop(); - // Side effect imports + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - utils_hooks__hooks.version = '2.10.3'; + 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; - setHookCallback(local__createLocal); + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - 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; + 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; - var _moment = utils_hooks__hooks; + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - return _moment; + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - })); - /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22)(module))) + 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; -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } + // colors + if (options.axisColor !== undefined) this.axisColor = options.axisColor; + if (options.gridColor !== undefined) this.gridColor = options.gridColor; + if (options.dataColor) { + if (typeof options.dataColor === 'string') { + this.dataColor.fill = options.dataColor; + this.dataColor.stroke = options.dataColor; + } else { + if (options.dataColor.fill) { + this.dataColor.fill = options.dataColor.fill; + } + if (options.dataColor.stroke) { + this.dataColor.stroke = options.dataColor.stroke; + } + if (options.dataColor.strokeWidth !== undefined) { + this.dataColor.strokeWidth = options.dataColor.strokeWidth; + } + } + } + this._setBackgroundColor(options.backgroundColor); + } -/***/ }, -/* 23 */ -/***/ function(module, exports, __webpack_require__) { + this.setSize(this.width, this.height); - /* WEBPACK VAR INJECTION */(function(global) {'use strict'; + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); + } - var _rng; + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; - var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; + /** + * Redraw the Graph. + */ + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } - 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._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); - 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; - } + 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 _rnds; - }; - } + this._redrawInfo(); + this._redrawLegend(); + }; - // uuid.js - // - // Copyright (c) 2010-2012 Robert Kieffer - // MIT License - http://opensource.org/licenses/mit-license.php + /** + * Clear the canvas before redrawing + */ + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - // 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 + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; - //var _rng = require('./rng'); + /** + * Redraw the legend showing the colors + */ + Graph3d.prototype._redrawLegend = function () { + var y; - // 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; - } + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = buf && offset || 0, - ii = 0; + var dotSize = this.frame.clientWidth * 0.02; - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { - if (ii < 16) { - // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; + 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 } - }); - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; + 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; } - return buf; - } - - // **`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++]]; - } - - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html - - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options - // 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]]; + 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); - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; + //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); - // Previous uuid creation time - var _lastMSecs = 0, - _lastNSecs = 0; + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; + ctx.strokeStyle = this.axisColor; + ctx.strokeRect(left, top, widthMax, height); + } - options = options || {}; + if (this.style === Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.axisColor; + ctx.fillStyle = this.dataColor.fill; + 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(); + } - var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + 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; - // 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(); + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - // 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; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - // Time since last uuid creation (in msecs) - var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; + step.next(); + } - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq === undefined) { - clockseq = clockseq + 1 & 16383; + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); } + }; - // 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; - } + /** + * Redraw the filter + */ + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ''; - // 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'); - } + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; + // TODO: css here is not nice here... + this.frame.filter.style.padding = '10px'; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); - // `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; + // create an event handler + var me = this; + var onchange = function onchange() { + var index = slider.getIndex(); - // `time_mid` - var tmh = msecs / 4294967296 * 10000 & 268435455; - b[i++] = tmh >>> 8 & 255; - b[i++] = tmh & 255; + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); - // `time_high_and_version` - b[i++] = tmh >>> 24 & 15 | 16; // include version - b[i++] = tmh >>> 16 & 255; + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; + } + }; - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 128; + /** + * Redraw the slider + */ + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } + }; - // `clock_seq_low` - b[i++] = clockseq & 255; + /** + * Redraw common information + */ + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; + ctx.font = '14px arial'; // TODO: put in options + ctx.lineStyle = 'gray'; + ctx.fillStyle = 'gray'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); } + }; - return buf ? buf : unparse(b); - } + /** + * Redraw the axis + */ + 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; - // **`v4()` - Generate random UUID** + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + 'px arial'; - // 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; + // 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; - if (typeof options == 'string') { - buf = options == 'binary' ? new Array(16) : null; - options = null; + // 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(); } - options = options || {}; + while (!step.end()) { + var x = step.getCurrent(); - var rnds = options.random || (options.rng || _rng)(); + 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.gridColor; + 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.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - // 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; + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; + 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'; } + ctx.fillStyle = this.axisColor; + ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); + + step.next(); } - return buf || unparse(rnds); - } + // 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.gridColor; + 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.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; + 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.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - module.exports = uuid; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) + 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'; + } + ctx.fillStyle = this.axisColor; + ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + step.next(); + } - // DOM utility methods + // 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.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); - /** - * this prepares the JSON container for allocating SVG elements - * @param JSONcontainer - * @private - */ - 'use strict'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); - 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 = []; - } + step.next(); } - }; + ctx.lineWidth = 1; + from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - /** - * 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 = []; - } - } - } - }; + // 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.axisColor; + 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.axisColor; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); - /** - * 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(); + // 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.axisColor; + 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.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + // 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 { - // create a new element and add it to the SVG - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - svgContainer.appendChild(element); + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; } - } 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); + ctx.fillStyle = this.axisColor; + ctx.fillText(xLabel, text.x, text.y); } - 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); - } - } - } 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); + // 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 { - DOMContainer.appendChild(element); + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; } + ctx.fillStyle = this.axisColor; + ctx.fillText(yLabel, text.x, text.y); + } + + // 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.axisColor; + ctx.fillText(zLabel, text.x - offset, text.y); } - JSONcontainer[elementType].used.push(element); - return element; }; /** - * Draw a point object. This is a separate 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 groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' } - * @param JSONcontainer - * @param svgContainer - * @param labelObj - * @returns {*} + * 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 */ - exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) { - var point; - if (groupTemplate.style == 'circle') { - point = exports.getSVGElement('circle', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'cx', x); - point.setAttributeNS(null, 'cy', y); - point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size); - } else { - point = exports.getSVGElement('rect', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size); - point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size); - point.setAttributeNS(null, 'width', groupTemplate.size); - point.setAttributeNS(null, 'height', groupTemplate.size); - } - - if (groupTemplate.style !== undefined) { - point.setAttributeNS(null, 'style', groupTemplate.style); - } - point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point'); - //handle label + Graph3d.prototype._hsv2rgb = function (H, S, V) { + var R, G, B, C, Hi, X; - if (labelObj) { - var label = exports.getSVGElement('text', JSONcontainer, svgContainer); - if (labelObj.xOffset) { - x = x + labelObj.xOffset; - } + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); - if (labelObj.yOffset) { - y = y + labelObj.yOffset; - } - if (labelObj.content) { - label.textContent = labelObj.content; - } + 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; - if (labelObj.className) { - label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); - } - label.setAttributeNS(null, 'x', x); - label.setAttributeNS(null, 'y', y); + default: + R = 0;G = 0;B = 0;break; } - return point; + return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; }; /** - * draw a bar SVG element centered on the X coordinate - * - * @param x - * @param y - * @param className + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' */ - 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); - } - } - }; + 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; -/***/ }, -/* 25 */ -/***/ function(module, exports, __webpack_require__) { + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; - 'use strict'; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - var util = __webpack_require__(19); - var Queue = __webpack_require__(26); + // 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); - /** - * 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. 0; + } else { + topSideVisible = true; + } - this.setOptions(options); - } + 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 - /** - * @param {Object} [options] Available options: - * {Object} queue Queue changes to the DataSet, - * flush them all at once. - * Queue options: - * - {number} delay Delay in ms, null by default - * - {number} max Maximum number of entries in the queue, Infinity by default - * @param options - */ - DataSet.prototype.setOptions = function (options) { - if (options && options.queue !== undefined) { - if (options.queue === false) { - // delete queue if loaded - if (this._queue) { - this._queue.destroy(); - delete this._queue; - } - } else { - // create queue and update its options - if (!this._queue) { - this._queue = Queue.extend(this, { - replace: ['add', 'update', 'remove'] - }); - } + 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.axisColor; + } + } else { + fillStyle = 'gray'; + strokeStyle = this.axisColor; + } - if (typeof options.queue === 'object') { - this._queue.setOptions(options.queue); + ctx.lineWidth = this._getStrokeWidth(point); + 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(); } } - } - }; - - /** - * Subscribe to an event, add an event listener - * @param {String} event Event name. Available events: 'put', 'update', - * 'remove' - * @param {function} callback Callback method. Called with three parameters: - * {String} event - * {Object | null} params - * {String | Number} senderId - */ - DataSet.prototype.on = function (event, callback) { - var subscribers = this._subscribers[event]; - if (!subscribers) { - subscribers = []; - this._subscribers[event] = subscribers; - } - - subscribers.push({ - callback: callback - }); - }; - - // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) - DataSet.prototype.subscribe = function () { - throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.'); - }; - - /** - * Unsubscribe from an event, remove an event listener - * @param {String} event - * @param {function} callback - */ - DataSet.prototype.off = function (event, callback) { - var subscribers = this._subscribers[event]; - if (subscribers) { - this._subscribers[event] = subscribers.filter(function (listener) { - return listener.callback != callback; - }); - } - }; + } 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; - // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) - DataSet.prototype.unsubscribe = function () { - throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.'); - }; + 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; - /** - * Trigger an event - * @param {String} event - * @param {Object | null} params - * @param {String} [senderId] Optional id of the sender. - * @private - */ - DataSet.prototype._trigger = function (event, params, senderId) { - if (event == '*') { - throw new Error('Cannot trigger event *'); - } + ctx.lineWidth = this._getStrokeWidth(point) * 2; + 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(); + } - var subscribers = []; - if (event in this._subscribers) { - subscribers = subscribers.concat(this._subscribers[event]); - } - if ('*' in this._subscribers) { - subscribers = subscribers.concat(this._subscribers['*']); - } + 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; - for (var i = 0; i < subscribers.length; i++) { - var subscriber = subscribers[i]; - if (subscriber.callback) { - subscriber.callback(event, params, senderId || null); + ctx.lineWidth = this._getStrokeWidth(point) * 2; + 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(); + } } } }; - /** - * Add data. - * Adding an item will fail when there already is an item with the same id. - * @param {Object | Array} data - * @param {String} [senderId] Optional sender id - * @return {Array} addedIds Array with the ids of the added items - */ - DataSet.prototype.add = function (data, senderId) { - var addedIds = [], - id, - me = this; - - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - id = me._addItem(data[i]); - addedIds.push(id); - } - } else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } else { - throw new Error('Unknown dataType'); - } - - if (addedIds.length) { - this._trigger('add', { items: addedIds }, senderId); + Graph3d.prototype._getStrokeWidth = function (point) { + if (point !== undefined) { + if (this.showPerspective) { + return 1 / -point.trans.z * this.dataColor.strokeWidth; + } else { + return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; + } } - return addedIds; + return this.dataColor.strokeWidth; }; /** - * Update existing items. When an item does not exist, it will be created - * @param {Object | Array} data - * @param {String} [senderId] Optional sender id - * @return {Array} updatedIds The ids of the added or updated items + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - DataSet.prototype.update = function (data, senderId) { - var addedIds = []; - var updatedIds = []; - var updatedData = []; - var me = this; - var fieldId = me._fieldId; + Graph3d.prototype._redrawDataDot = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i; - var addOrUpdate = function addOrUpdate(item) { - var id = item[fieldId]; - if (me._data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - updatedData.push(item); + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + + // 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; + + // 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; + } + + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + 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]; + + 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.gridColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } + + // 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 { - // add new item - id = me._addItem(item); - addedIds.push(id); + size = dotSize; } - }; - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; } - } else if (data instanceof Object) { - // Single item - addOrUpdate(data); - } else { - throw new Error('Unknown dataType'); - } - if (addedIds.length) { - this._trigger('add', { items: addedIds }, senderId); - } - if (updatedIds.length) { - this._trigger('update', { items: updatedIds, data: updatedData }, senderId); - } + 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.dataColor.fill; + borderColor = this.dataColor.stroke; + } 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); + } - return addedIds.concat(updatedIds); + // draw the circle + ctx.lineWidth = this._getStrokeWidth(point); + 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(); + } }; /** - * Get a data item or multiple items. - * - * Usage: - * - * get() - * get(options: Object) - * - * get(id: Number | String) - * get(id: Number | String, options: Object) - * - * get(ids: Number[] | String[]) - * get(ids: Number[] | String[], options: Object) - * - * 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} [returnType] Type of data to be returned. - * Can be 'Array' (default) or 'Object'. - * {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 + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - DataSet.prototype.get = function (args) { - var me = this; + Graph3d.prototype._redrawDataBar = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i, j, surface, corners; - // 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 (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // 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'; + // 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; + + // 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; } - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // 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); - } - } - } - } + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } + // 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]; - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); + // 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.dataColor.fill; + borderColor = this.dataColor.stroke; } else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } + // 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); } - } - // return the results - if (returnType == 'Object') { - var result = {}; - for (i = 0; i < items.length; i++) { - result[items[i].id] = items[i]; + // 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); } - return result; - } else { - if (id != undefined) { - // a single item - return item; - } else { - // just return our array - return items; + + // 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) }]; + + // 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); + }); + + // 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; + + // 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}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = this._getStrokeWidth(point); + 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(); } } }; /** - * 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 + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - 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 = []; + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, + i; - 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.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - this._sort(items, order); + // 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); - 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 { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); - } - } + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + } - this._sort(items, order); + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; - 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]); - } - } + ctx.lineWidth = this._getStrokeWidth(point); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + ctx.strokeStyle = this.dataColor.stroke; + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + + // 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); } - } - return ids; + // finish the line + ctx.stroke(); + } }; /** - * Returns the DataSet itself. Is overwritten for example by the DataView, - * which returns the DataSet it is connected to instead. + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - DataSet.prototype.getDataSet = function () { - return this; - }; + Graph3d.prototype._onMouseDown = function (event) { + event = event || window.event; - /** - * 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; + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); + } - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + // only react on left mouse button down + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!this.leftButtonDown && !this.touchDown) return; - 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); - } - } - } - } + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); + + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); + + this.frame.style.cursor = 'move'; + + // 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); }; /** - * 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 + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - // 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)); - } - } - } + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - return mappedItems; - }; + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - /** - * 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; + // 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; } - var filteredItem = {}; - - 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]; - } - } + // 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; } - return filteredItem; + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); + + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + util.preventDefault(event); }; /** - * Sort the provided array with items - * @param {Object[]} items - * @param {String | function} order A field name or custom sort function. - * @private + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - 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'); - } + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = false; + + // remove event listeners here + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); }; /** - * 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 + * 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 */ - DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, - len, - removedId; + 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; - 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); - } + if (!this.showTooltip) { + return; } - if (removedIds.length) { - this._trigger('remove', { items: removedIds }, senderId); + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); } - return removedIds; - }; + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } - /** - * 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; - } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; + 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); } - return null; }; /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items + * Event handler for touchstart event on mobile devices */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); - - this._data = {}; - this.length = 0; + Graph3d.prototype._onTouchStart = function (event) { + this.touchDown = true; - this._trigger('remove', { items: ids }, senderId); + 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); - return ids; + this._onMouseDown(event); }; /** - * 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 + * Event handler for touchmove event on mobile devices */ - DataSet.prototype.max = function (field) { - var data = this._data, - max = null, - maxField = null; - - 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; - } - } - } - - return max; + Graph3d.prototype._onTouchMove = function (event) { + this._onMouseMove(event); }; /** - * 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 + * Event handler for touchend event on mobile devices */ - DataSet.prototype.min = function (field) { - var data = this._data, - min = null, - minField = null; + Graph3d.prototype._onTouchEnd = function (event) { + this.touchDown = false; - 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; - } - } - } + util.removeEventListener(document, 'touchmove', this.ontouchmove); + util.removeEventListener(document, 'touchend', this.ontouchend); - return min; + this._onMouseUp(event); }; /** - * 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. + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event */ - 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; - - 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++; - } - } - } + Graph3d.prototype._onWheel = function (event) { + if (!event) /* For IE. */ + event = window.event; - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } + // 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; } - return values; - }; + // 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); - /** - * 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]; + this.camera.setArmLength(newLength); + this.redraw(); - 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'); - } - } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; + this._hideTooltip(); } - 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); - } - } - this._data[id] = d; - this.length++; + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); - return id; + // 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); }; /** - * 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 + * 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 */ - DataSet.prototype._getItem = function (id, types) { - var field, value; + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; + function sign(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } - // 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; + 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)); + + // 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); }; /** - * 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 + * 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 */ - 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'); - } + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); - // 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); + 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; + } + } + } } - } - - return id; - }; - - module.exports = DataSet; - -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * 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'; - - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; - - // properties - this._queue = []; - this._timeout = null; - this._extended = null; - - this.setOptions(options); - } + } 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); - /** - * 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 ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } } - this._flushIfNeeded(); + return closestDataPoint; }; /** - * 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 + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - Queue.extend = function (object, options) { - var queue = new Queue(options); + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - if (object.flush !== undefined) { - throw new Error('Target object already has a property flush'); + 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'; + + 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'; + + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot + } + }; + } else { + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } - object.flush = function () { - queue.flush(); - }; - var methods = [{ - name: 'flush', - original: undefined - }]; + this._hideTooltip(); - 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); - } + 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 + '
'; } - queue._extended = { - object: object, - methods: methods - }; + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); - return queue; + // 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'; }; /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. + * Hide the tooltip when displayed + * @private */ - Queue.prototype.destroy = function () { - this.flush(); + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - 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]; + 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); + } } } - this._extended = null; } }; + /**--------------------------------------------------------------------------**/ + /** - * Replace a method on an object with a queued version - * @param {Object} object Object having the method - * @param {string} method The method name + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - Queue.prototype.replace = function (object, method) { - var me = this; - var original = object[method]; - if (!original) { - throw new Error('Method ' + method + ' undefined'); - } - - 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 - }); - }; - }; + function getMouseX(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } /** - * Queue a call - * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y */ - Queue.prototype.queue = function (entry) { - if (typeof entry === 'function') { - this._queue.push({ fn: entry }); - } else { - this._queue.push(entry); - } + function getMouseY(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } - this._flushIfNeeded(); - }; + module.exports = Graph3d; - /** - * 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(); - } + // use use defaults - // 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); - } - }; +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { /** - * Flush all queued calls + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] */ - Queue.prototype.flush = function () { - while (this._queue.length > 0) { - var entry = this._queue.shift(); - entry.fn.apply(entry.context || entry.fn, entry.args || []); - } - }; + "use strict"; - module.exports = Queue; + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } + + module.exports = Point2d; /***/ }, -/* 27 */ +/* 20 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + + /** + * Expose `Emitter`. + */ - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); + module.exports = Emitter; /** - * 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 + * Initialize a new `Emitter`. * - * @constructor DataView + * @api public */ - 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.setData(data); - } - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + function Emitter(obj) { + if (obj) return mixin(obj); + }; /** - * Set a data source for the view - * @param {DataSet | DataView} data + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private */ - DataView.prototype.setData = function (data) { - var ids, i, len; - - if (this._data) { - // unsubscribe from current dataset - if (this._data.off) { - this._data.off('*', this.listener); - } - // 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 }); + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; } + return obj; + } - 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 }); + /** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); - } - } + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; }; /** - * Refresh the DataView. Useful when the DataView has a filter function - * containing a variable parameter. + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - DataView.prototype.refresh = function () { - var id; - var ids = this._data.getIds({ filter: this._options && this._options.filter }); - var newIds = {}; - var added = []; - var removed = []; - // 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++; - } - } + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - // 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 on() { + self.off(event, on); + fn.apply(this, arguments); } - // trigger events - if (added.length) { - this._trigger('add', { items: added }); - } - if (removed.length) { - this._trigger('remove', { items: removed }); - } + on.fn = fn; + this.on(event, on); + return this; }; /** - * 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: + * Remove the given callback for `event` or all + * registered callbacks. * - * {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 + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - DataView.prototype.get = function (args) { - var me = this; - // 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]; + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; } - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - // 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); - }; + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; } - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); + // 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; + } } - getArguments.push(viewOptions); - getArguments.push(data); - - return this._data && this._data.get.apply(this._data, getArguments); + return this; }; /** - * 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 + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} */ - DataView.prototype.getIds = function (options) { - var ids; - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); } - - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; } - return ids; + return this; }; /** - * 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 + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public */ - DataView.prototype.getDataSet = function () { - var dataSet = this; - while (dataSet instanceof DataView) { - dataSet = dataSet._data; - } - return dataSet || null; + + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; }; /** - * 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. + * Check if this emitter has `event` handlers. + * * @param {String} event - * @param {Object | null} params - * @param {String} senderId - * @private + * @return {Boolean} + * @api public */ - 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 (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); - } - } - break; + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; + }; - 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); - 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 {} - } - } +/***/ }, +/* 21 */ +/***/ function(module, exports, __webpack_require__) { - break; + /** + * @prototype Point3d + * @param {Number} [x] + * @param {Number} [y] + * @param {Number} [z] + */ + "use strict"; - 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); - } - } + function Point3d(x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; + }; - break; - } + /** + * 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; + }; - this.length += added.length - removed.length; + /** + * Add the two provided points, returns a+b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a+b + */ + 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; + }; - 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); - } - } + /** + * 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); }; - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; + /** + * 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 + */ + Point3d.crossProduct = function (a, b) { + var crossproduct = new Point3d(); - // 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; + 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; - module.exports = DataView; + return crossproduct; + }; - // nothing interesting for me :-( + /** + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length + */ + Point3d.prototype.length = function () { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + }; + + module.exports = Point3d; /***/ }, -/* 28 */ +/* 22 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Emitter = __webpack_require__(30); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var util = __webpack_require__(19); - var Point3d = __webpack_require__(31); - var Point2d = __webpack_require__(29); - var Camera = __webpack_require__(32); - var Filter = __webpack_require__(33); - var Slider = __webpack_require__(34); - var StepNumber = __webpack_require__(35); + var Point3d = __webpack_require__(21); /** - * @constructor Graph3d - * Graph3d displays data in 3d. - * - * Graph3d is developed in javascript as a Google Visualization Chart. + * @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. * - * @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] + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection */ - 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%'; - - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; + function Camera() { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; - var passValueFn = function passValueFn(v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - this.filterLabel = 'time'; - this.legendLabel = 'value'; + this.calculateCameraOrientation(); + } - 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' + /** + * 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 + */ + Camera.prototype.setArmLocation = function (x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + this.calculateCameraOrientation(); + }; - this.camera = new Camera(); - this.camera.setArmRotation(1, 0.5); - this.camera.setArmLength(1.7); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + /** + * 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. + */ + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; + } - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + 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; + } - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } + }; - 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 + /** + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical + */ + Camera.prototype.getArmRotation = function () { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; - // colors - this.axisColor = '#4D4D4D'; - this.gridColor = '#D3D3D3'; - this.dataColor = { - fill: '#7DC1FF', - stroke: '#3267D2', - strokeWidth: 1 // px - }; + return rot; + }; - // create a frame and canvas - this.create(); + /** + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 + */ + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; - // apply options (also when undefined) - this.setOptions(options); + this.armLength = length; - // apply data - if (data) { - this.setData(data); - } - } + // 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; - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + this.calculateCameraOrientation(); + }; /** - * Calculate the scaling values, dependent on the range in x, y, and z direction + * Retrieve the arm length + * @return {Number} length */ - Graph3d.prototype._setScale = function () { - this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); - - // 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; - } - } - - // 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); + Camera.prototype.getArmLength = function () { + return this.armLength; + }; - // 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); + /** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; }; /** - * 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 + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - Graph3d.prototype._convert3Dto2D = function (point3d) { - var translation = this._convertPointToTranslation(point3d); - return this._convertTranslationToScreen(translation); + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; }; /** - * 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 + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm */ - 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, + 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); - // 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), + // calculate rotation of the camera + this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; + }; - // 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)); + module.exports = Camera; - return new Point3d(dx, dy, dz); - }; +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var DataView = __webpack_require__(17); /** - * 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 + * @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 */ - 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; + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph - // calculate position on screen from translation - var bx; - var by; - if (this.showPerspective) { - bx = (dx - ex) * (ez / dz); - by = (dy - ey) * (ez / dz); + this.index = undefined; + this.value = undefined; + + // read all distinct values and select the first one + this.values = graph.getDistinctValues(data.get(), this.column); + + // sort both numeric and string values correctly + this.values.sort(function (a, b) { + return a > b ? 1 : a < b ? -1 : 0; + }); + + if (this.values.length > 0) { + this.selectValue(0); + } + + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; + + this.loaded = false; + this.onLoadCallback = undefined; + + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); } else { - bx = dx * -(ez / this.camera.getArmLength()); - by = dy * -(ez / this.camera.getArmLength()); + this.loaded = true; } + }; - // 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); + /** + * Return the label + * @return {string} label + */ + Filter.prototype.isLoaded = function () { + return this.loaded; }; /** - * Set the background styling for the graph - * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor + * Return the loaded progress + * @return {Number} percentage between 0 and 100 */ - Graph3d.prototype._setBackgroundColor = function (backgroundColor) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; + Filter.prototype.getLoadedProgress = function () { + var len = this.values.length; - 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 i = 0; + while (this.dataPoints[i]) { + i++; } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; + return Math.round(i / len * 100); }; - /// 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 + /** + * Return the label + * @return {string} label + */ + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; }; /** - * 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 + * Return the columnIndex of the filter + * @return {Number} columnIndex */ - 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 -1; + Filter.prototype.getColumn = function () { + return this.column; }; /** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value */ - 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; - - 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; - - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } else { - throw 'Unknown style "' + this.style + '"'; - } - }; + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; + return this.values[this.index]; }; - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; - } - } - return counter; + /** + * Retrieve all values of the filter + * @return {Array} values + */ + Filter.prototype.getValues = function () { + return this.values; }; - 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; - }; + /** + * 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'; - 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 this.values[index]; }; /** - * 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 + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); - } + if (index === undefined) return []; - if (rawData === undefined) return; + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + var dataView = new DataView(this.data, { filter: function filter(item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error('Array, DataSet, or DataView expected'); + this.dataPoints[index] = dataPoints; } - if (data.length == 0) return; + return dataPoints; + }; - this.dataSet = rawData; - this.dataTable = data; + /** + * Set a callback function when the filter is fully loaded. + */ + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; + }; - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); + /** + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {Number} index + */ + Filter.prototype.selectValue = function (index) { + if (index >= this.values.length) throw 'Error: index out of range'; - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + this.index = index; + this.value = this.values[index]; + }; - // 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'; + /** + * Load all filtered rows in the background one by one + * Start this method without providing an index! + */ + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; - // 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(); - }); - } - } + var frame = this.graph.frame; - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat - // 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; + // create a progress box + if (frame.progress === undefined) { + frame.progress = document.createElement('DIV'); + frame.progress.style.position = 'absolute'; + frame.progress.style.color = 'gray'; + frame.appendChild(frame.progress); } + var progress = this.getLoadedProgress(); + frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; + // TODO: this is no nice solution... + frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider + frame.progress.style.left = 10 + 'px'; - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; + var me = this; + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); + this.loaded = false; + } else { + this.loaded = true; + + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; } - } - // calculate minimums and maximums - var xRange = this.getColumnRange(data, this.colX); - if (withBars) { - xRange.min -= this.xBarWidth / 2; - xRange.max += this.xBarWidth / 2; + if (this.onLoadCallback) this.onLoadCallback(); } - 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; - } - 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; + module.exports = Filter; - 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; +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { - 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; - } + 'use strict'; - // set the scale dependent on the ranges. - this._setScale(); - }; + var util = __webpack_require__(9); /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen + * @constructor Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. */ - 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); - } - } + function Slider(container, options) { + if (container === undefined) { + throw 'Error: No container element defined'; + } + this.container = container; + this.visible = options && options.visible != undefined ? options.visible : true; - var sortNumber = function sortNumber(a, b) { - return a - b; - }; - dataX.sort(sortNumber); - dataY.sort(sortNumber); + if (this.visible) { + this.frame = document.createElement('DIV'); + //this.frame.style.backgroundColor = '#E5E5E5'; + this.frame.style.width = '100%'; + this.frame.style.position = 'relative'; + this.container.appendChild(this.frame); - // 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; + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + this.frame.bar = document.createElement('INPUT'); + this.frame.bar.type = 'BUTTON'; + this.frame.bar.style.position = 'absolute'; + this.frame.bar.style.border = '1px solid red'; + this.frame.bar.style.width = '100px'; + this.frame.bar.style.height = '6px'; + this.frame.bar.style.borderRadius = '2px'; + this.frame.bar.style.MozBorderRadius = '2px'; + this.frame.bar.style.border = '1px solid #7F7F7F'; + this.frame.bar.style.backgroundColor = '#E5E5E5'; + this.frame.appendChild(this.frame.bar); - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + this.frame.slide = document.createElement('INPUT'); + this.frame.slide.type = 'BUTTON'; + this.frame.slide.style.margin = '0px'; + this.frame.slide.value = ' '; + this.frame.slide.style.position = 'relative'; + this.frame.slide.style.left = '-100px'; + this.frame.appendChild(this.frame.slide); - dataMatrix[xIndex][yIndex] = obj; + // create events + var me = this; + this.frame.slide.onmousedown = function (event) { + me._onMouseDown(event); + }; + this.frame.prev.onclick = function (event) { + me.prev(event); + }; + this.frame.play.onclick = function (event) { + me.togglePlay(event); + }; + this.frame.next.onclick = function (event) { + me.next(event); + }; + } - dataPoints.push(obj); - } + this.onChangeCallback = undefined; - // 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; + this.values = []; + this.index = undefined; - if (this.colValue !== undefined) { - point.value = data[i][this.colValue] || 0; - } + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + /** + * Select the previous index + */ + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); + } + }; - dataPoints.push(obj); - } + /** + * Select the next index + */ + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } - - return dataPoints; }; /** - * 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. + * Select the next index */ - Graph3d.prototype.create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } - - this.frame = document.createElement('div'); - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + Slider.prototype.playNext = function () { + var start = new Date(); - // 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); + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } else if (this.playLoop) { + // jump to the start + index = 0; + this.setIndex(index); } - 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); - - // 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' + var end = new Date(); + var diff = end - start; - 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); + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(this.playInterval - diff, 0); + // document.title = diff // TODO: cleanup - // add the new graph to the container element - this.containerElement.appendChild(this.frame); + var me = this; + this.playTimeout = setTimeout(function () { + me.playNext(); + }, interval); }; /** - * 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%') + * Toggle start or stop playing */ - Graph3d.prototype.setSize = function (width, height) { - this.frame.style.width = width; - this.frame.style.height = height; - - this._resizeCanvas(); + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } }; /** - * Resize the canvas to the current size of the frame + * Start playing */ - Graph3d.prototype._resizeCanvas = function () { - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + this.playNext(); - // adjust with for margin - this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; + if (this.frame) { + this.frame.play.value = 'Stop'; + } }; /** - * Start animation + * Stop playing */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; - this.frame.filter.slider.play(); + if (this.frame) { + this.frame.play.value = 'Play'; + } }; /** - * Stop animation + * Set a callback function which will be triggered when the value of the + * slider bar has changed. */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; - - this.frame.filter.slider.stop(); + Slider.prototype.setOnChangeCallback = function (callback) { + this.onChangeCallback = callback; }; /** - * 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 + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds */ - 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 - } - - // 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 - } + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; }; /** - * 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. + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds */ - Graph3d.prototype.setCameraPosition = function (pos) { - if (pos === undefined) { - return; - } - - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } - - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } - - this.redraw(); + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; }; /** - * Retrieve the current camera rotation - * @return {object} An object with parameters horizontal, vertical, and - * distance + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. */ - Graph3d.prototype.getCameraPosition = function () { - var pos = this.camera.getArmRotation(); - pos.distance = this.camera.getArmLength(); - return pos; + Slider.prototype.setPlayLoop = function (doLoop) { + this.playLoop = doLoop; }; /** - * Load data into the 3D Graph + * Execute the onchange callback function */ - Graph3d.prototype._readData = function (data) { - // read the data - this._dataInitialize(data, this.style); - - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); } - - // draw the filter - this._redrawFilter(); }; /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data + * redraw the slider on the correct place */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + Slider.prototype.redraw = function () { + if (this.frame) { + // resize the bar + this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px'; + this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px'; - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + 'px'; } }; /** - * Update the options. Options will be merged with current options - * @param {Object} options + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) */ - 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; - - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - - 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; - - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - - 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; - - 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); - } - - // colors - if (options.axisColor !== undefined) this.axisColor = options.axisColor; - if (options.gridColor !== undefined) this.gridColor = options.gridColor; - if (options.dataColor) { - if (typeof options.dataColor === 'string') { - this.dataColor.fill = options.dataColor; - this.dataColor.stroke = options.dataColor; - } else { - if (options.dataColor.fill) { - this.dataColor.fill = options.dataColor.fill; - } - if (options.dataColor.stroke) { - this.dataColor.stroke = options.dataColor.stroke; - } - if (options.dataColor.strokeWidth !== undefined) { - this.dataColor.strokeWidth = options.dataColor.strokeWidth; - } - } - } - this._setBackgroundColor(options.backgroundColor); - } - - this.setSize(this.width, this.height); - - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } + Slider.prototype.setValues = function (values) { + this.values = values; - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** - * Redraw the Graph. + * Select a value by its index + * @param {Number} index */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } - - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; - 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(); + this.redraw(); + this.onChange(); } else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); + throw 'Error: index out of range'; } - - this._redrawInfo(); - this._redrawLegend(); }; /** - * Clear the canvas before redrawing + * retrieve the index of the currently selected vaue + * @return {Number} index */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, canvas.width, canvas.height); + Slider.prototype.getIndex = function () { + return this.index; }; /** - * Redraw the legend showing the colors + * retrieve the currently selected value + * @return {*} value */ - Graph3d.prototype._redrawLegend = function () { - var y; - - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + Slider.prototype.get = function () { + return this.values[this.index]; + }; - var dotSize = this.frame.clientWidth * 0.02; + Slider.prototype._onMouseDown = function (event) { + // only react on left mouse button down + var leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!leftButtonDown) return; - 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 - } + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - 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; - } + this.frame.style.cursor = 'move'; - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options + // 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', this.onmousemove); + util.addEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); + }; - 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); + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - //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); + var index = Math.round(x / width * (this.values.length - 1)); + if (index < 0) index = 0; + if (index > this.values.length - 1) index = this.values.length - 1; - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); - } + return index; + }; - ctx.strokeStyle = this.axisColor; - ctx.strokeRect(left, top, widthMax, height); - } + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - if (this.style === Graph3d.STYLE.DOTSIZE) { - // draw border around color bar - ctx.strokeStyle = this.axisColor; - ctx.fillStyle = this.dataColor.fill; - 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(); - } + var x = index / (this.values.length - 1) * width; + var left = x + 3; - 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 left; + }; - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + var index = this.leftToIndex(x); - step.next(); - } + this.setIndex(index); - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } + util.preventDefault(); }; - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ''; + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + // remove event listeners + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + util.preventDefault(); + }; - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + module.exports = Slider; - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - // create an event handler - var me = this; - var onchange = function onchange() { - var index = slider.getIndex(); + /** + * @prototype StepNumber + * The class StepNumber is an iterator for Numbers. You provide a start and end + * value, and a best step size. StepNumber itself rounds to fixed values and + * a finds the step that best fits the provided step. + * + * If prettyStep is true, the step size is chosen as close as possible to the + * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... + * + * Example usage: + * var step = new StepNumber(0, 10, 2.5, true); + * step.start(); + * while (!step.end()) { + * alert(step.getCurrent()); + * step.next(); + * } + * + * Version: 1.0 + * + * @param {Number} start The start value + * @param {Number} end The end value + * @param {Number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + */ + "use strict"; - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + function StepNumber(start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; - } + this._current = 0; + this.setRange(start, end, step, prettyStep); }; /** - * Redraw the slider + * Set a new range: start, end and step. + * + * @param {Number} start The start value + * @param {Number} end The end value + * @param {Number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; + + this.setStep(step, prettyStep); }; /** - * Redraw common information + * Set a new step size + * @param {Number} step New step size. Must be a positive value + * @param {boolean} prettyStep Optional. If true, the provided step is rounded + * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + StepNumber.prototype.setStep = function (step, prettyStep) { + if (step === undefined || step <= 0) return; - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + if (prettyStep !== undefined) this.prettyStep = prettyStep; - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); - } + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** - * Redraw the axis + * Calculate a nice step size, closest to the desired step size. + * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an + * integer Number. For example 1, 2, 5, 10, 20, 50, etc... + * @param {Number} step Desired step size + * @return {Number} Nice step size */ - 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; + StepNumber.calculatePrettyStep = function (step) { + var log10 = function log10(x) { + return Math.log(x) / Math.LN10; + }; - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + // try three steps (multiple of 1, 2, or 5 + var step1 = Math.pow(10, Math.round(log10(step))), + step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), + step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); - // 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; + // choose the best step (closest to minimum step) + var prettyStep = step1; + if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; + if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; - // 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(); + // for safety + if (prettyStep <= 0) { + prettyStep = 1; } - while (!step.end()) { - var x = step.getCurrent(); - 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.gridColor; - 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.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + return prettyStep; + }; - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } + /** + * returns the current value of the step + * @return {Number} current value + */ + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); + }; - 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'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); + /** + * returns the current step size + * @return {Number} current step size + */ + StepNumber.prototype.getStep = function () { + return this._step; + }; - step.next(); - } + /** + * Set the current value to the largest value smaller than start, which + * is a multiple of the step size + */ + StepNumber.prototype.start = function () { + this._current = this._start - this._start % this._step; + }; - // 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.gridColor; - 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.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + /** + * Do a step, add the step size to the current value + */ + StepNumber.prototype.next = function () { + this._current += this._step; + }; - 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.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } + /** + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. + */ + StepNumber.prototype.end = function () { + return this._current > this._end; + }; - 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'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); + module.exports = StepNumber; - step.next(); - } +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { - // 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.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); + 'use strict'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + var Emitter = __webpack_require__(20); + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var Range = __webpack_require__(30); + var Core = __webpack_require__(33); + var TimeAxis = __webpack_require__(42); + var CurrentTime = __webpack_require__(27); + var CustomTime = __webpack_require__(45); + var ItemSet = __webpack_require__(34); + + var Configurator = __webpack_require__(46); + var Validator = __webpack_require__(48)['default']; + var printStyle = __webpack_require__(48).printStyle; + var allOptions = __webpack_require__(49).allOptions; + var configureOptions = __webpack_require__(49).configureOptions; - step.next(); + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | vis.DataView | Array} [items] + * @param {vis.DataSet | vis.DataView | Array} [groups] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor + * @extends Core + */ + function Timeline(container, items, groups, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); } - ctx.lineWidth = 1; - from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); - ctx.strokeStyle = this.axisColor; - 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.axisColor; - 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.axisColor; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); - - // 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.axisColor; - 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.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // 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'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(xLabel, text.x, text.y); + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; } - // 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'; - } - ctx.fillStyle = this.axisColor; - ctx.fillText(yLabel, text.x, text.y); - } + var me = this; + this.defaultOptions = { + start: null, + end: null, - // 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.axisColor; - ctx.fillText(zLabel, text.x - offset, text.y); - } - }; + autoResize: true, - /** - * 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 - */ - Graph3d.prototype._hsv2rgb = function (H, S, V) { - var R, G, B, C, Hi, X; + orientation: { + axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' + item: 'bottom' // not relevant + }, - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - 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; + // Create the DOM, props, and emitter + this._create(container); - default: - R = 0;G = 0;B = 0;break; - } + // all components listed here will be repainted automatically + this.components = []; - return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; - }; + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + hiddenDates: [], + util: { + getScale: function getScale() { + return me.timeAxis.step.scale; + }, + getStep: function getStep() { + return me.timeAxis.step.step; + }, - /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' - */ - 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; + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime: me._toGlobalTime.bind(me) + } + }; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // time axis + this.timeAxis = new TimeAxis(this.body); + this.timeAxis2 = null; // used in case of orientation option 'both' + this.components.push(this.timeAxis); - // 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); + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - // 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; - } + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - // 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.on('tap', function (event) { + me.emit('click', me.getEventProperties(event)); + }); + this.on('doubletap', function (event) { + me.emit('doubleClick', me.getEventProperties(event)); + }); + this.dom.root.oncontextmenu = function (event) { + me.emit('contextmenu', me.getEventProperties(event)); }; - this.dataPoints.sort(sortDepth); - - 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; - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + // apply options + if (options) { + this.setOptions(options); + } - 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) + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; - } + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } - 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 + // Extend the functionality from Core + Timeline.prototype = new Core(); - 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.axisColor; - } - } else { - fillStyle = 'gray'; - strokeStyle = this.axisColor; - } + /** + * Load a configurator + * @return {Object} + * @private + */ + Timeline.prototype._createConfigurator = function () { + return new Configurator(this, this.dom.container, configureOptions); + }; - ctx.lineWidth = this._getStrokeWidth(point); - 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(); - } - } - } 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; + /** + * Force a redraw. The size of all items will be recalculated. + * Can be useful to manually redraw when option autoResize=false and the window + * has been resized, or when the items CSS has been changed. + */ + Timeline.prototype.redraw = function () { + this.itemSet && this.itemSet.markDirty({ refreshItems: true }); + this._redraw(); + }; - 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; + Timeline.prototype.setOptions = function (options) { + // validate options + var errorFound = Validator.validate(options, allOptions); + if (errorFound === true) { + console.log('%cErrors have been found in the supplied options object.', printStyle); + } - ctx.lineWidth = this._getStrokeWidth(point) * 2; - 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(); - } + Core.prototype.setOptions.call(this, options); - 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; + if ('type' in options) { + if (options.type !== this.options.type) { + this.options.type = options.type; - ctx.lineWidth = this._getStrokeWidth(point) * 2; - 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(); + // force recreation of all items + var itemsData = this.itemsData; + if (itemsData) { + var selection = this.getSelection(); + this.setItems(null); // remove all + this.setItems(itemsData); // add all + this.setSelection(selection); // restore selection } } } }; - Graph3d.prototype._getStrokeWidth = function (point) { - if (point !== undefined) { - if (this.showPerspective) { - return 1 / -point.trans.z * this.dataColor.strokeWidth; - } else { - return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; - } - } - - return this.dataColor.strokeWidth; - }; - /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' + * Set items + * @param {vis.DataSet | Array | null} items */ - Graph3d.prototype._redrawDataDot = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i; - - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - - // 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; + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - // 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; + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: 'Date', + end: 'Date' + } + }); } - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - 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]; - - 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.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); - } + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - // 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; - } + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var range = this.getItemRange(); + } - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; - } + var start = this.options.start != undefined ? this.options.start : range.min; + var end = this.options.end != undefined ? this.options.end : range.max; - 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.dataColor.fill; - borderColor = this.dataColor.stroke; + this.setWindow(start, end, { animation: false }); } 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.fit({ animation: false }); } - - // draw the circle - ctx.lineWidth = this._getStrokeWidth(point); - 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(); } }; /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' + * Set groups + * @param {vis.DataSet | Array} groups */ - 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? + Timeline.prototype.setGroups = function (groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - // 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; + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); + }; - // 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; + /** + * Set both items and groups in one go + * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data + */ + Timeline.prototype.setData = function (data) { + if (data && data.groups) { + this.setGroups(data.groups); } - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + if (data && data.items) { + this.setItems(data.items); + } + }; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + /** + * 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. If ids is an empty array, all items will be + * unselected. + * @param {Object} [options] Available options: + * `focus: boolean` + * If true, focus will be set to the selected item(s) + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + * Only applicable when option focus is true. + */ + Timeline.prototype.setSelection = function (ids, options) { + this.itemSet && this.itemSet.setSelection(ids); - // 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]; + if (options && options.focus) { + this.focus(ids, options); + } + }; - // 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.dataColor.fill; - borderColor = this.dataColor.stroke; - } 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); - } + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + Timeline.prototype.getSelection = function () { + return this.itemSet && this.itemSet.getSelection() || []; + }; - // 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); - } + /** + * Adjust the visible window such that the selected item (or multiple items) + * are centered on screen. + * @param {String | String[]} id An item id or array with item ids + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + */ + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - // 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) }]; + var ids = Array.isArray(id) ? id : [id]; - // 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); - }); + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: 'Date', + end: 'Date' + } + }); - // 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; + // calculate minimum start and maximum end of specified items + var start = null; + var end = null; + itemsData.forEach(function (itemData) { + var s = itemData.start.valueOf(); + var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); - // 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}) + if (start === null || s < start) { + start = s; } - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; - - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + if (end === null || e > end) { + end = e; + } + }); - // both are equal - return 0; - }); + if (start !== null && end !== null) { + // calculate the new middle and interval for the window + var middle = (start + end) / 2; + var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); - // draw the ordered surfaces - ctx.lineWidth = this._getStrokeWidth(point); - 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(); - } + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animation); } }; /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' + * Set Timeline window such that it fits all items + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + */ + Timeline.prototype.fit = function (options) { + var animation = options && options.animation !== undefined ? options.animation : true; + var range = this.getItemRange(); + this.range.setRange(range.min, range.max, animation); + }; + + /** + * Determine the range of the items, taking into account their actual width + * and a margin of 10 pixels on both sides. + * @return {{min: Date | null, max: Date | null}} */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - i; + Timeline.prototype.getItemRange = function () { + var _this = this; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // get a rough approximation for the range based on the items start and end dates + var range = this.getDataRange(); + var min = range.min; + var max = range.max; + var minItem = null; + var maxItem = null; - // 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); + if (min != null && max != null) { + var interval; + var factor; + var lhs; + var rhs; + var delta; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + (function () { + var getStart = function (item) { + return util.convert(item.data.start, 'Date').valueOf(); + }; - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + var getEnd = function (item) { + var end = item.data.end != undefined ? item.data.end : item.data.start; + return util.convert(end, 'Date').valueOf(); + }; - ctx.lineWidth = this._getStrokeWidth(point); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - ctx.strokeStyle = this.dataColor.stroke; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); + interval = max - min; + // ms + if (interval <= 0) { + interval = 10; + } + factor = interval / _this.props.center.width; - // 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); - } + // calculate the date of the left side and right side of the items given + util.forEach(_this.itemSet.items, (function (item) { + item.show(); - // finish the line - ctx.stroke(); + var start = getStart(item); + var end = getEnd(item); + + var left = new Date(start - (item.getWidthLeft() + 10) * factor); + var right = new Date(end + (item.getWidthRight() + 10) * factor); + + if (left < min) { + min = left; + minItem = item; + } + if (right > max) { + max = right; + maxItem = item; + } + }).bind(_this)); + + if (minItem && maxItem) { + lhs = minItem.getWidthLeft() + 10; + rhs = maxItem.getWidthRight() + 10; + delta = _this.props.center.width - lhs - rhs; + // px + + if (delta > 0) { + min = getStart(minItem) - lhs * interval / delta; // ms + max = getEnd(maxItem) + rhs * interval / delta; // ms + } + } + })(); } + + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) + * Calculate the data range of the items start and end dates + * @returns {{min: Date | null, max: Date | null}} */ - Graph3d.prototype._onMouseDown = function (event) { - event = event || window.event; + Timeline.prototype.getDataRange = function () { + var min = null; + var max = null; - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); + var dataset = this.itemsData && this.itemsData.getDataSet(); + if (dataset) { + dataset.forEach(function (item) { + var start = util.convert(item.start, 'Date').valueOf(); + var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf(); + if (min === null || start < min) { + min = start; + } + if (max === null || end > max) { + max = start; + } + }); } - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; - - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); - - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); - - this.frame.style.cursor = 'move'; - - // 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); + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null }; - util.addEventListener(document, 'mousemove', me.onmousemove); - util.addEventListener(document, 'mouseup', me.onmouseup); - util.preventDefault(event); }; /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * Generate Timeline related information from an event + * @param {Event} event + * @return {Object} An object with related information, like on which area + * The event happened, whether clicked on an item, etc. */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; - - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + Timeline.prototype.getEventProperties = function (event) { + var clientX = event.center ? event.center.x : event.clientX; + var clientY = event.center ? event.center.y : event.clientY; + var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); + var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + var item = this.itemSet.itemFromTarget(event); + var group = this.itemSet.groupFromTarget(event); + var customTime = CustomTime.customTimeFromTarget(event); - // 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; - } + var snap = this.itemSet.options.snap || null; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var time = this._toTime(x); + var snappedTime = snap ? snap(time, scale, step) : time; - // 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; + var element = util.getTarget(event); + var what = null; + if (item != null) { + what = 'item'; + } else if (customTime != null) { + what = 'custom-time'; + } else if (util.hasParent(element, this.timeAxis.dom.foreground)) { + what = 'axis'; + } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { + what = 'axis'; + } else if (util.hasParent(element, this.itemSet.dom.labelSet)) { + what = 'group-label'; + } else if (util.hasParent(element, this.currentTime.bar)) { + what = 'current-time'; + } else if (util.hasParent(element, this.dom.center)) { + what = 'background'; } - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + return { + event: event, + item: item ? item.id : null, + group: group ? group.groupId : null, + what: what, + pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, + pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, + x: x, + y: y, + time: time, + snappedTime: snappedTime + }; + }; - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + module.exports = Timeline; - util.preventDefault(event); - }; +/***/ }, +/* 27 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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; + 'use strict'; - // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + var util = __webpack_require__(9); + var Component = __webpack_require__(28); + var moment = __webpack_require__(10); + var locales = __webpack_require__(29); /** - * 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 + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ - 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; + function CurrentTime(body, options) { + this.body = body; - if (!this.showTooltip) { - return; - } + // default options + this.defaultOptions = { + showCurrentTime: true, - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } + locales: locales, + locale: 'en' + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; - } + this._create(); - 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; + this.setOptions(options); + } - // show a tooltip if we have a data point - var dataPoint = me._dataPointFromXY(mouseX, mouseY); - if (dataPoint) { - me._showTooltip(dataPoint); - } - }, delay); - } - }; + CurrentTime.prototype = new Component(); /** - * Event handler for touchstart event on mobile devices + * Create the HTML DOM for the current time bar + * @private */ - Graph3d.prototype._onTouchStart = function (event) { - this.touchDown = true; - - 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); + CurrentTime.prototype._create = function () { + var bar = document.createElement('div'); + bar.className = 'vis-current-time'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - this._onMouseDown(event); + this.bar = bar; }; /** - * Event handler for touchmove event on mobile devices + * Destroy the CurrentTime bar */ - Graph3d.prototype._onTouchMove = function (event) { - this._onMouseMove(event); + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; }; /** - * Event handler for touchend event on mobile devices + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - Graph3d.prototype._onTouchEnd = function (event) { - this.touchDown = false; - - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); - - this._onMouseUp(event); + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); + } }; /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Graph3d.prototype._onWheel = function (event) { - if (!event) /* For IE. */ - event = window.event; + CurrentTime.prototype.redraw = function () { + if (this.options.showCurrentTime) { + var parent = this.body.dom.backgroundVertical; + if (this.bar.parentNode != parent) { + // attach to the dom + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + parent.appendChild(this.bar); - // 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; - } + this.start(); + } - // 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); + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); - this.camera.setArmLength(newLength); - this.redraw(); + var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; + } + locale = this.options.locales['en']; // fall back on english when not available + } + var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); + title = title.charAt(0).toUpperCase() + title.substring(1); - this._hideTooltip(); + this.bar.style.left = x + 'px'; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + this.stop(); } - // 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); + return false; }; /** - * 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 + * Start auto refreshing the current time bar */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; - - function sign(x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; - } - - 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)); + CurrentTime.prototype.start = function () { + var me = this; - // 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); - }; + function update() { + me.stop(); - /** - * 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 - */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, - // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + // determine interval to refresh + var scale = me.body.range.conversion(me.body.domProps.center.width).scale; + var interval = 1 / scale / 10; + if (interval < 30) interval = 30; + if (interval > 1000) interval = 1000; - 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); + me.redraw(); - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); } - return closestDataPoint; + update(); }; /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private + * Stop auto refreshing the current time bar */ - 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'; - - 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'; - - this.tooltip = { - dataPoint: null, - dom: { - content: content, - line: line, - dot: dot - } - }; - } else { - content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; } + }; - this._hideTooltip(); + /** + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. + */ + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, 'Date').valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); + }; - 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 + '
'; - } + /** + * Get the current time. + * @return {Date} Returns the current time. + */ + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); + }; - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); + module.exports = CurrentTime; - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; +/***/ }, +/* 28 */ +/***/ function(module, exports, __webpack_require__) { - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + /** + * Prototype for visual components + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] + * @param {Object} [options] + */ + "use strict"; - 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'; - }; + function Component(body, options) { + this.options = null; + this.props = null; + } /** - * Hide the tooltip when displayed - * @private + * Set options for the component. The new options will be merged into the + * current options. + * @param {Object} options */ - 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); - } - } - } + Component.prototype.setOptions = function (options) { + if (options) { + util.extend(this.options, options); } }; - /**--------------------------------------------------------------------------**/ + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + Component.prototype.redraw = function () { + // should be implemented by the component + return false; + }; /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x + * Destroy the component. Cleanup DOM and event listeners */ - function getMouseX(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - } + Component.prototype.destroy = function () {}; /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected */ - function getMouseY(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - } + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - module.exports = Graph3d; + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - // use use defaults + return resized; + }; + + module.exports = Component; + + // should be implemented by the component /***/ }, /* 29 */ /***/ function(module, exports, __webpack_require__) { - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - "use strict"; + // English + 'use strict'; - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - } + exports['en'] = { + current: 'current', + time: 'time' + }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; - module.exports = Point2d; + // Dutch + exports['nl'] = { + current: 'huidige', + time: 'tijd' + }; + exports['nl_NL'] = exports['nl']; + exports['nl_BE'] = exports['nl']; /***/ }, /* 30 */ /***/ function(module, exports, __webpack_require__) { - + 'use strict'; + + var util = __webpack_require__(9); + var hammerUtil = __webpack_require__(31); + var moment = __webpack_require__(10); + var Component = __webpack_require__(28); + var DateUtil = __webpack_require__(32); + /** - * Expose `Emitter`. + * @constructor Range + * A Range controls a numeric range with a start and end value. + * The Range adjusts the range based on mouse events or programmatic changes, + * and triggers events when the range is changing or has been changed. + * @param {{dom: Object, domProps: Object, emitter: Emitter}} body + * @param {Object} [options] See description at Range.setOptions */ + function Range(body, options) { + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.start = now.clone().add(-3, 'days').valueOf(); // Number + this.end = now.clone().add(4, 'days').valueOf(); // Number - module.exports = Emitter; + this.body = body; + this.deltaDifference = 0; + this.scaleOffset = 0; + this.startToFront = false; + this.endToFront = true; + + // default options + this.defaultOptions = { + start: null, + end: null, + direction: 'horizontal', // 'horizontal' or 'vertical' + moveable: true, + zoomable: true, + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + }; + this.options = util.extend({}, this.defaultOptions); + + this.props = { + touch: {} + }; + this.animationTimer = null; + + // drag listeners for dragging + this.body.emitter.on('panstart', this._onDragStart.bind(this)); + this.body.emitter.on('panmove', this._onDrag.bind(this)); + this.body.emitter.on('panend', this._onDragEnd.bind(this)); + + // mouse wheel for zooming + this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + + // pinch to zoom + this.body.emitter.on('touch', this._onTouch.bind(this)); + this.body.emitter.on('pinch', this._onPinch.bind(this)); + + this.setOptions(options); + } + + Range.prototype = new Component(); /** - * Initialize a new `Emitter`. - * - * @api public + * Set options for the range controller + * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default */ + Range.prototype.setOptions = function (options) { + if (options) { + // copy the options that we know + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; + util.selectiveExtend(fields, this.options, options); - function Emitter(obj) { - if (obj) return mixin(obj); + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } + } }; /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; + function validateDirection(direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); } - return obj; } /** - * Listen on the given `event` with `fn`. + * Set a new start and end range + * @param {Date | Number | String} [start] + * @param {Date | Number | String} [end] + * @param {boolean | {duration: number, easingFunction: string}} [animation=false] + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + * @param {Boolean} [byUser=false] * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public */ + Range.prototype.setRange = function (start, end, animation, byUser) { + if (byUser !== true) { + byUser = false; + } + var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; + var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; + this._cancelAnimation(); - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; - }; + if (animation) { + // true or an Object + var me = this; + var initStart = this.start; + var initEnd = this.end; + var duration = typeof animation === 'object' && 'duration' in animation ? animation.duration : 500; + var easingName = typeof animation === 'object' && 'easingFunction' in animation ? animation.easingFunction : 'easeInOutQuad'; + var easingFunction = util.easingFunctions[easingName]; + if (!easingFunction) { + throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + 'Choose from: ' + Object.keys(util.easingFunctions).join(', ')); + } - /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ + var initTime = new Date().valueOf(); + var anyChanged = false; - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; + var next = function next() { + if (!me.props.touch.dragging) { + var now = new Date().valueOf(); + var time = now - initTime; + var ease = easingFunction(time / duration); + var done = time > duration; + var s = done || finalStart === null ? finalStart : initStart + (finalStart - initStart) * ease; + var e = done || finalEnd === null ? finalEnd : initEnd + (finalEnd - initEnd) * ease; - function on() { - self.off(event, on); - fn.apply(this, arguments); - } + changed = me._applyRange(s, e); + DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); + anyChanged = anyChanged || changed; + if (changed) { + me.body.emitter.emit('rangechange', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } - on.fn = fn; - this.on(event, on); - return this; + if (done) { + if (anyChanged) { + me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } + } else { + // animate with as high as possible frame rate, leave 20 ms in between + // each to prevent the browser from blocking + me.animationTimer = setTimeout(next, 20); + } + } + }; + + return next(); + } else { + var changed = this._applyRange(finalStart, finalEnd); + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + if (changed) { + var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); + } + } }; /** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Stop an animation + * @private */ + Range.prototype._cancelAnimation = function () { + if (this.animationTimer) { + clearTimeout(this.animationTimer); + this.animationTimer = null; + } + }; - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; + /** + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed + * @private + */ + Range.prototype._applyRange = function (start, end) { + var newStart = start != null ? util.convert(start, 'Date').valueOf() : this.start, + newEnd = end != null ? util.convert(end, 'Date').valueOf() : this.end, + max = this.options.max != null ? util.convert(this.options.max, 'Date').valueOf() : null, + min = this.options.min != null ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); } - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; } - // 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; + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = min - newStart; + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } } } - return this; - }; - /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } + } - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if (newEnd - newStart < zoomMin) { + if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the minimum + diff = zoomMin - (newEnd - newStart); + newStart -= diff / 2; + newEnd += diff / 2; + } } } - return this; - }; + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } - /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ + if (newEnd - newStart > zoomMax) { + if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the maximum + diff = newEnd - newStart - zoomMax; + newStart += diff / 2; + newEnd -= diff / 2; + } + } + } - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; - }; + var changed = this.start != newStart || this.end != newEnd; - /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ + // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) + if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { + this.body.emitter.emit('checkRangedItems'); + } - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; + this.start = newStart; + this.end = newEnd; + return changed; }; - -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { - /** - * @prototype Point3d - * @param {Number} [x] - * @param {Number} [y] - * @param {Number} [z] + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - "use strict"; - - function Point3d(x, y, z) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - this.z = z !== undefined ? z : 0; + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end + }; }; /** - * Subtract the two provided points, returns a-b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a-b + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ - 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; + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); }; /** - * Add the two provided points, returns a+b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a+b + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ - 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; + Range.conversion = function (start, end, width, totalHidden) { + if (totalHidden === undefined) { + totalHidden = 0; + } + if (width != 0 && end - start != 0) { + return { + offset: start, + scale: width / (end - start - totalHidden) + }; + } else { + return { + offset: 0, + scale: 1 + }; + } }; /** - * Calculate the average of two 3d points - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} The average, (a+b)/2 + * Start dragging horizontally or vertically + * @param {Event} event + * @private */ - Point3d.avg = function (a, b) { - return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); - }; + Range.prototype._onDragStart = function (event) { + this.deltaDifference = 0; + this.previousDelta = 0; + // only allow dragging when configured as movable + if (!this.options.moveable) return; - /** - * 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 - */ - Point3d.crossProduct = function (a, b) { - var crossproduct = new Point3d(); + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - 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.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.dragging = true; - return crossproduct; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; + } }; /** - * Rtrieve the length of the vector (or the distance from this point to the origin - * @return {Number} length + * Perform dragging operation + * @param {Event} event + * @private */ - Point3d.prototype.length = function () { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - }; - - module.exports = Point3d; - -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - 'use strict'; + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - var Point3d = __webpack_require__(31); + var direction = this.options.direction; + validateDirection(direction); + var delta = direction == 'horizontal' ? event.deltaX : event.deltaY; + delta -= this.deltaDifference; + var interval = this.props.touch.end - this.props.touch.start; - /** - * @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 - */ - function Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; + // normalize dragging speed if cutout is in between. + var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + interval -= duration; - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); + var width = direction == 'horizontal' ? this.body.domProps.center.width : this.body.domProps.center.height; + var diffRange = -delta / width * interval; + var newStart = this.props.touch.start + diffRange; + var newEnd = this.props.touch.end + diffRange; - this.calculateCameraOrientation(); - } + // snapping times away from hidden zones + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.deltaDifference += delta; + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this._onDrag(event); + return; + } - /** - * 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 - */ - Camera.prototype.setArmLocation = function (x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; + this.previousDelta = delta; + this._applyRange(newStart, newEnd); - this.calculateCameraOrientation(); + // fire a rangechange event + this.body.emitter.emit('rangechange', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * 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. + * Stop dragging operation + * @param {event} event + * @private */ - Camera.prototype.setArmRotation = function (horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; - } + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) 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; - } + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; } + + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - return rot; - }; + // 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; + } - /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 - */ - Camera.prototype.setArmLength = function (length) { - if (length === undefined) return; + // 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) { + // perform the zoom action. Delta is normally 1 or -1 - this.armLength = length; + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - delta / 5; + } else { + scale = 1 / (1 + delta / 5); + } - // 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; + // calculate center, the date to zoom around + var pointer = getPointer({ x: event.clientX, y: event.clientY }, this.body.dom.center); + var pointerDate = this._pointerToDate(pointer); - this.calculateCameraOrientation(); - }; + this.zoom(scale, pointerDate, delta); + } - /** - * Retrieve the arm length - * @return {Number} length - */ - Camera.prototype.getArmLength = function () { - return this.armLength; + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * Retrieve the camera location - * @return {Point3d} cameraLocation + * Start of a touch gesture + * @private */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; + Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; + this.scaleOffset = 0; + this.deltaDifference = 0; }; /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation + * Handle pinch event + * @param {Event} event + * @private */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; - }; + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - /** - * 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.props.touch.allowDragging = false; - // 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 (!this.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); + } - module.exports = Camera; + var scale = 1 / (event.scale + this.scaleOffset); + var centerDate = this._pointerToDate(this.props.touch.center); -/***/ }, -/* 33 */ -/***/ function(module, exports, __webpack_require__) { + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - 'use strict'; + // calculate new start and end + var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; + var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + + // snapping times away from hidden zones + this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times + this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this.scaleOffset = 1 - event.scale; + newStart = safeStart; + newEnd = safeEnd; + } + + this.setRange(newStart, newEnd, false, true); - var DataView = __webpack_require__(27); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default + }; /** - * @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 + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private */ - function Filter(data, column, graph) { - this.data = data; - this.column = column; - this.graph = graph; // the parent graph + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - this.index = undefined; - this.value = undefined; + validateDirection(direction); - // read all distinct values and select the first one - this.values = graph.getDistinctValues(data.get(), this.column); + if (direction == 'horizontal') { + return this.body.util.toTime(pointer.x).valueOf(); + } else { + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; + } + }; - // sort both numeric and string values correctly - this.values.sort(function (a, b) { - return a > b ? 1 : a < b ? -1 : 0; - }); + /** + * Get the pointer location relative to the location of the dom element + * @param {{x: Number, y: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer + * @private + */ + function getPointer(touch, element) { + return { + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) + }; + } - if (this.values.length > 0) { - this.selectValue(0); + /** + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. + */ + Range.prototype.zoom = function (scale, center, delta) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; } - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - this.loaded = false; - this.onLoadCallback = undefined; + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); - } else { - this.loaded = true; + // snapping times away from hidden zones + this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + newStart = safeStart; + newEnd = safeEnd; } + + this.setRange(newStart, newEnd, false, true); + + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; /** - * Return the label - * @return {string} label + * Move the range with a given delta to the left or right. Start and end + * value will be adjusted. For example, try delta = 0.1 or -0.1 + * @param {Number} delta Moving amount. Positive value will move right, + * negative value will move left */ - Filter.prototype.isLoaded = function () { - return this.loaded; - }; + Range.prototype.move = function (delta) { + // zoom start Date and end Date relative to the centerDate + var diff = this.end - this.start; - /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 - */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - var i = 0; - while (this.dataPoints[i]) { - i++; - } + // TODO: reckon with min and max range - return Math.round(i / len * 100); + this.start = newStart; + this.end = newEnd; }; /** - * Return the label - * @return {string} label + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; - }; + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function () { - return this.column; + var diff = center - moveTo; + + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); }; + module.exports = Range; + +/***/ }, +/* 31 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Hammer = __webpack_require__(5); + /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * Register a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; + exports.onTouch = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFirst && !isTouching) { + callback(event); - return this.values[this.index]; + isTouching = true; + setTimeout(function () { + isTouching = false; + }, 0); + } + }; + + hammer.on('hammer.input', callback.inputHandler); }; + // isTouching is true while a touch action is being emitted + // this is a hack to prevent `touch` from being fired twice + var isTouching = false; + /** - * Retrieve all values of the filter - * @return {Array} values + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - Filter.prototype.getValues = function () { - return this.values; + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal && !isReleasing) { + callback(event); + + isReleasing = true; + setTimeout(function () { + isReleasing = false; + }, 0); + } + }; + + return hammer.on('hammer.input', callback.inputHandler); }; + // isReleasing is true while a release action is being emitted + // this is a hack to prevent `release` from being fired twice + var isReleasing = false; + /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value + * Unregister a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; - - return this.values[index]; + exports.offTouch = function (hammer, callback) { + hammer.off('hammer.input', callback.inputHandler); }; /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataPoints + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - Filter.prototype._getDataPoints = function (index) { - if (index === undefined) index = this.index; - - if (index === undefined) return []; - - var dataPoints; - if (this.dataPoints[index]) { - dataPoints = this.dataPoints[index]; - } else { - var f = {}; - f.column = this.column; - f.value = this.values[index]; + exports.offRelease = exports.offTouch; - var dataView = new DataView(this.data, { filter: function filter(item) { - return item[f.column] == f.value; - } }).get(); - dataPoints = this.graph._getDataPoints(dataView); +/***/ }, +/* 32 */ +/***/ function(module, exports, __webpack_require__) { - this.dataPoints[index] = dataPoints; - } + "use strict"; - return dataPoints; - }; + var moment = __webpack_require__(10); /** - * Set a callback function when the filter is fully loaded. + * used in Core to convert the options into a volatile variable + * + * @param Core */ - Filter.prototype.setOnLoadCallback = function (callback) { - this.onLoadCallback = callback; + exports.convertHiddenOptions = function (body, hiddenDates) { + body.hiddenDates = []; + if (hiddenDates) { + if (Array.isArray(hiddenDates) == true) { + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat === undefined) { + var dateItem = {}; + dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); + dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); + body.hiddenDates.push(dateItem); + } + } + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + } + } }; /** - * Add a value to the list with available values for this filter - * No double entries will be created. - * @param {Number} index + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates */ - Filter.prototype.selectValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - this.index = index; - this.value = this.values[index]; - }; + var start = moment(body.range.start); + var end = moment(body.range.end); - /** - * Load all filtered rows in the background one by one - * Start this method without providing an index! - */ - Filter.prototype.loadInBackground = function (index) { - if (index === undefined) index = 0; + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - var frame = this.graph.frame; + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat !== undefined) { + var startDate = moment(hiddenDates[i].start); + var endDate = moment(hiddenDates[i].end); - if (index < this.values.length) { - var dataPointsTemp = this._getDataPoints(index); - //this.graph.redrawInfo(); // TODO: not neat + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); + } + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); + } - // create a progress box - if (frame.progress === undefined) { - frame.progress = document.createElement('DIV'); - frame.progress.style.position = 'absolute'; - frame.progress.style.color = 'gray'; - frame.appendChild(frame.progress); - } - var progress = this.getLoadedProgress(); - frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; - // TODO: this is no nice solution... - frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider - frame.progress.style.left = 10 + 'px'; + var duration = endDate - startDate; + if (duration >= 4 * pixelTime) { - var me = this; - setTimeout(function () { - me.loadInBackground(index + 1); - }, 10); - this.loaded = false; - } else { - this.loaded = true; + var offset = 0; + var runUntil = end.clone(); + switch (hiddenDates[i].repeat) { + case "daily": + // case of time + if (startDate.day() != endDate.day()) { + offset = 1; + } + startDate.dayOfYear(start.dayOfYear()); + startDate.year(start.year()); + startDate.subtract(7, "days"); - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = undefined; - } + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); - if (this.onLoadCallback) this.onLoadCallback(); - } - }; + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); - module.exports = Filter; + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { + // force + startDate.day(day); + endDate.day(day); + endDate.add(dayOffset, "days"); - 'use strict'; + startDate.subtract(1, "weeks"); + endDate.subtract(1, "weeks"); - var util = __webpack_require__(19); + runUntil.add(1, "weeks"); + break; + case "monthly": + if (startDate.month() != endDate.month()) { + offset = 1; + } + startDate.month(start.month()); + startDate.year(start.year()); + startDate.subtract(1, "months"); - /** - * @constructor Slider - * - * An html slider control with start/stop/prev/next buttons - * @param {Element} container The element where the slider will be created - * @param {Object} options Available options: - * {boolean} visible If true (default) the - * slider is visible. - */ - function Slider(container, options) { - if (container === undefined) { - throw 'Error: No container element defined'; - } - this.container = container; - this.visible = options && options.visible != undefined ? options.visible : true; + endDate.month(start.month()); + endDate.year(start.year()); + endDate.subtract(1, "months"); + endDate.add(offset, "months"); - if (this.visible) { - this.frame = document.createElement('DIV'); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = '100%'; - this.frame.style.position = 'relative'; - this.container.appendChild(this.frame); + runUntil.add(1, "months"); + break; + case "yearly": + if (startDate.year() != endDate.year()) { + offset = 1; + } + startDate.year(start.year()); + startDate.subtract(1, "years"); + endDate.year(start.year()); + endDate.subtract(1, "years"); + endDate.add(offset, "years"); - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); + runUntil.add(1, "years"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + while (startDate < runUntil) { + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + switch (hiddenDates[i].repeat) { + case "daily": + startDate.add(1, "days"); + endDate.add(1, "days"); + break; + case "weekly": + startDate.add(1, "weeks"); + endDate.add(1, "weeks"); + break; + case "monthly": + startDate.add(1, "months"); + endDate.add(1, "months"); + break; + case "yearly": + startDate.add(1, "y"); + endDate.add(1, "y"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + } + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + } + } + } + // remove duplicates, merge where possible + exports.removeDuplicates(body); + // ensure the new positions are not on hidden dates + var startHidden = exports.isHidden(body.range.start, body.hiddenDates); + var endHidden = exports.isHidden(body.range.end, body.hiddenDates); + var rangeStart = body.range.start; + var rangeEnd = body.range.end; + if (startHidden.hidden == true) { + rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; + } + if (endHidden.hidden == true) { + rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; + } + if (startHidden.hidden == true || endHidden.hidden == true) { + body.range._applyRange(rangeStart, rangeEnd); + } + } + }; - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); + /** + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body + */ + exports.removeDuplicates = function (body) { + var hiddenDates = body.hiddenDates; + var safeDates = []; + for (var i = 0; i < hiddenDates.length; i++) { + for (var j = 0; j < hiddenDates.length; j++) { + if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { + // j inside i + if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[j].remove = true; + } + // j start inside i + else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { + hiddenDates[i].end = hiddenDates[j].end; + hiddenDates[j].remove = true; + } + // j end inside i + else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[i].start = hiddenDates[j].start; + hiddenDates[j].remove = true; + } + } + } + } - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); + } + } - this.frame.bar = document.createElement('INPUT'); - this.frame.bar.type = 'BUTTON'; - this.frame.bar.style.position = 'absolute'; - this.frame.bar.style.border = '1px solid red'; - this.frame.bar.style.width = '100px'; - this.frame.bar.style.height = '6px'; - this.frame.bar.style.borderRadius = '2px'; - this.frame.bar.style.MozBorderRadius = '2px'; - this.frame.bar.style.border = '1px solid #7F7F7F'; - this.frame.bar.style.backgroundColor = '#E5E5E5'; - this.frame.appendChild(this.frame.bar); + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + }; - this.frame.slide = document.createElement('INPUT'); - this.frame.slide.type = 'BUTTON'; - this.frame.slide.style.margin = '0px'; - this.frame.slide.value = ' '; - this.frame.slide.style.position = 'relative'; - this.frame.slide.style.left = '-100px'; - this.frame.appendChild(this.frame.slide); + exports.printDates = function (dates) { + for (var i = 0; i < dates.length; i++) { + console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); + } + }; - // create events - var me = this; - this.frame.slide.onmousedown = function (event) { - me._onMouseDown(event); - }; - this.frame.prev.onclick = function (event) { - me.prev(event); - }; - this.frame.play.onclick = function (event) { - me.togglePlay(event); - }; - this.frame.next.onclick = function (event) { - me.next(event); - }; + /** + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime + */ + exports.stepOverHiddenDates = function (timeStep, previousTime) { + var stepInHidden = false; + var currentValue = timeStep.current.valueOf(); + for (var i = 0; i < timeStep.hiddenDates.length; i++) { + var startDate = timeStep.hiddenDates[i].start; + var endDate = timeStep.hiddenDates[i].end; + if (currentValue >= startDate && currentValue < endDate) { + stepInHidden = true; + break; + } } - this.onChangeCallback = undefined; + if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { + var prevValue = moment(previousTime); + var newValue = moment(endDate); + //check if the next step should be major + if (prevValue.year() != newValue.year()) { + timeStep.switchedYear = true; + } else if (prevValue.month() != newValue.month()) { + timeStep.switchedMonth = true; + } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { + timeStep.switchedDay = true; + } - this.values = []; - this.index = undefined; + timeStep.current = newValue.toDate(); + } + }; - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } + ///** + // * Used in TimeStep to avoid the hidden times. + // * @param timeStep + // * @param previousTime + // */ + //exports.checkFirstStep = function(timeStep) { + // var stepInHidden = false; + // var currentValue = timeStep.current.valueOf(); + // for (var i = 0; i < timeStep.hiddenDates.length; i++) { + // var startDate = timeStep.hiddenDates[i].start; + // var endDate = timeStep.hiddenDates[i].end; + // if (currentValue >= startDate && currentValue < endDate) { + // stepInHidden = true; + // break; + // } + // } + // + // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { + // var newValue = moment(endDate); + // timeStep.current = newValue.toDate(); + // } + //}; /** - * Select the previous index + * replaces the Core toScreen methods + * @param Core + * @param time + * @param width + * @returns {number} */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); - } - }; + exports.toScreen = function (Core, time, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return (time.valueOf() - conversion.offset) * conversion.scale; + } else { + var hidden = exports.isHidden(time, Core.body.hiddenDates); + if (hidden.hidden == true) { + time = hidden.startDate; + } - /** - * Select the next index - */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + + var conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; } }; /** - * Select the next index + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} */ - Slider.prototype.playNext = function () { - var start = new Date(); + exports.toTime = function (Core, x, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return new Date(x / conversion.scale + conversion.offset); + } else { + var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + var totalDuration = Core.range.end - Core.range.start - hiddenDuration; + var partialDuration = totalDuration * x / width; + var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } else if (this.playLoop) { - // jump to the start - index = 0; - this.setIndex(index); + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; } - - var end = new Date(); - var diff = end - start; - - // calculate how much time it to to set the index and to execute the callback - // function. - var interval = Math.max(this.playInterval - diff, 0); - // document.title = diff // TODO: cleanup - - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); }; /** - * Toggle start or stop playing + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} */ - Slider.prototype.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { + var duration = 0; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= start && endDate < end) { + duration += endDate - startDate; + } } + return duration; }; /** - * Start playing + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; + }; - this.playNext(); + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - if (this.frame) { - this.frame.play.value = 'Stop'; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= range.start && endDate < range.end) { + if (time >= endDate) { + timeOffset += endDate - startDate; + } + } } + return timeOffset; }; /** - * Stop playing + * sum the duration from start to finish, including the hidden duration, + * until the required amount has been reached, return the accumulated hidden duration + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - Slider.prototype.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; - - if (this.frame) { - this.frame.play.value = 'Play'; + exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { + var hiddenDuration = 0; + var duration = 0; + var previousPoint = range.start; + //exports.printDates(hiddenDates) + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= range.start && endDate < range.end) { + duration += startDate - previousPoint; + previousPoint = endDate; + if (duration >= requiredDuration) { + break; + } else { + hiddenDuration += endDate - startDate; + } + } } - }; - /** - * Set a callback function which will be triggered when the value of the - * slider bar has changed. - */ - Slider.prototype.setOnChangeCallback = function (callback) { - this.onChangeCallback = callback; + return hiddenDuration; }; /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds + * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true + * @param hiddenDates + * @param time + * @param direction + * @param correctionEnabled + * @returns {*} */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; + exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { + var isHidden = exports.isHidden(time, hiddenDates); + if (isHidden.hidden == true) { + if (direction < 0) { + if (correctionEnabled == true) { + return isHidden.startDate - (isHidden.endDate - time) - 1; + } else { + return isHidden.startDate - 1; + } + } else { + if (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; + } + } + } else { + return time; + } }; /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds + * Check if a time is hidden + * + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - Slider.prototype.getPlayInterval = function (interval) { - return this.playInterval; - }; + exports.isHidden = function (time, hiddenDates) { + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; - /** - * Set looping on or off - * @pararm {boolean} doLoop If true, the slider will jump to the start when - * the end is passed, and will jump to the end - * when the start is passed. - */ - Slider.prototype.setPlayLoop = function (doLoop) { - this.playLoop = doLoop; + if (time >= startDate && time < endDate) { + // if the start is entering a hidden zone + return { hidden: true, startDate: startDate, endDate: endDate }; + break; + } + } + return { hidden: false, startDate: startDate, endDate: endDate }; }; +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Emitter = __webpack_require__(20); + var Hammer = __webpack_require__(5); + var hammerUtil = __webpack_require__(31); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var Range = __webpack_require__(30); + var ItemSet = __webpack_require__(34); + var TimeAxis = __webpack_require__(42); + var Activator = __webpack_require__(43); + var DateUtil = __webpack_require__(32); + var CustomTime = __webpack_require__(45); + /** - * Execute the onchange callback function + * Create a timeline visualization + * @constructor */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); - } - }; + function Core() {} + + // turn Core into an event emitter + Emitter(Core.prototype); /** - * redraw the slider on the correct place + * Create the main DOM for the Core: a root panel containing left, right, + * top, bottom, content, and background panel. + * @param {Element} container The container element where the Core will + * be attached. + * @protected */ - Slider.prototype.redraw = function () { - if (this.frame) { - // resize the bar - this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px'; - this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px'; + Core.prototype._create = function (container) { + this.dom = {}; - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = left + 'px'; + this.dom.container = container; + + this.dom.root = document.createElement('div'); + this.dom.background = document.createElement('div'); + this.dom.backgroundVertical = document.createElement('div'); + this.dom.backgroundHorizontal = document.createElement('div'); + this.dom.centerContainer = document.createElement('div'); + this.dom.leftContainer = document.createElement('div'); + this.dom.rightContainer = document.createElement('div'); + this.dom.center = document.createElement('div'); + this.dom.left = document.createElement('div'); + this.dom.right = document.createElement('div'); + this.dom.top = document.createElement('div'); + this.dom.bottom = document.createElement('div'); + this.dom.shadowTop = document.createElement('div'); + this.dom.shadowBottom = document.createElement('div'); + this.dom.shadowTopLeft = document.createElement('div'); + this.dom.shadowBottomLeft = document.createElement('div'); + this.dom.shadowTopRight = document.createElement('div'); + this.dom.shadowBottomRight = document.createElement('div'); + + this.dom.root.className = 'vis-timeline'; + this.dom.background.className = 'vis-panel vis-background'; + this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical'; + this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal'; + this.dom.centerContainer.className = 'vis-panel vis-center'; + this.dom.leftContainer.className = 'vis-panel vis-left'; + this.dom.rightContainer.className = 'vis-panel vis-right'; + this.dom.top.className = 'vis-panel vis-top'; + this.dom.bottom.className = 'vis-panel vis-bottom'; + this.dom.left.className = 'vis-content'; + this.dom.center.className = 'vis-content'; + this.dom.right.className = 'vis-content'; + this.dom.shadowTop.className = 'vis-shadow vis-top'; + this.dom.shadowBottom.className = 'vis-shadow vis-bottom'; + this.dom.shadowTopLeft.className = 'vis-shadow vis-top'; + this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; + this.dom.shadowTopRight.className = 'vis-shadow vis-top'; + this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; + + this.dom.root.appendChild(this.dom.background); + this.dom.root.appendChild(this.dom.backgroundVertical); + this.dom.root.appendChild(this.dom.backgroundHorizontal); + this.dom.root.appendChild(this.dom.centerContainer); + this.dom.root.appendChild(this.dom.leftContainer); + this.dom.root.appendChild(this.dom.rightContainer); + this.dom.root.appendChild(this.dom.top); + this.dom.root.appendChild(this.dom.bottom); + + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); + + this.dom.centerContainer.appendChild(this.dom.shadowTop); + this.dom.centerContainer.appendChild(this.dom.shadowBottom); + this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); + this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); + this.dom.rightContainer.appendChild(this.dom.shadowTopRight); + this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + + this.on('rangechange', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pan', this._onDrag.bind(this)); + + var me = this; + this.on('change', function (properties) { + if (properties && properties.queue == true) { + // redraw once on next tick + if (!me._redrawTimer) { + me._redrawTimer = setTimeout(function () { + me._redrawTimer = null; + me._redraw(); + }, 0); + } + } else { + // redraw immediately + me._redraw(); + } + }); + + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = new Hammer(this.dom.root); + this.hammer.get('pinch').set({ enable: true }); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + this.listeners = {}; + + var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend' + // TODO: cleanup + //'touch', 'pinch', + //'tap', 'doubletap', 'hold', + //'dragstart', 'drag', 'dragend', + //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (type) { + var listener = function listener(event) { + if (me.isActive()) { + me.emit(type, event); + } + }; + me.hammer.on(type, listener); + me.listeners[type] = listener; + }); + + // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) + hammerUtil.onTouch(this.hammer, (function (event) { + me.emit('touch', event); + }).bind(this)); + + // emulate a release event (emitted after a pan, pinch, tap, or press) + hammerUtil.onRelease(this.hammer, (function (event) { + me.emit('release', event); + }).bind(this)); + + function onMouseWheel(event) { + if (me.isActive()) { + me.emit('mousewheel', event); + } } + this.dom.root.addEventListener('mousewheel', onMouseWheel); + this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); + + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + + this.customTimes = []; + + // store state information needed for touch events + this.touch = {}; + + this.redrawCount = 0; + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); }; /** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) + * Set options. Options will be passed to all components loaded in the Timeline. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Timeline, + * can be 'bottom' (default) or 'top'. + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Timeline will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window */ - Slider.prototype.setValues = function (values) { - this.values = values; + Core.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; + util.selectiveExtend(fields, this.options, options); + + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation = { + item: options.orientation, + axis: options.orientation + }; + } else if (typeof options.orientation === 'object') { + if ('item' in options.orientation) { + this.options.orientation.item = options.orientation.item; + } + if ('axis' in options.orientation) { + this.options.orientation.axis = options.orientation.axis; + } + } + } + + if (this.options.orientation.axis === 'both') { + if (!this.timeAxis2) { + var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); + timeAxis2.setOptions = function (options) { + var _options = options ? util.extend({}, options) : {}; + _options.orientation = 'top'; // override the orientation option, always top + TimeAxis.prototype.setOptions.call(timeAxis2, _options); + }; + this.components.push(timeAxis2); + } + } else { + if (this.timeAxis2) { + var index = this.components.indexOf(this.timeAxis2); + if (index !== -1) { + this.components.splice(index, 1); + } + this.timeAxis2.destroy(); + this.timeAxis2 = null; + } + } + + // if the graph2d's drawPoints is a function delegate the callback to the onRender property + if (typeof options.drawPoints == 'function') { + options.drawPoints = { + onRender: options.drawPoints + }; + } + + if ('hiddenDates' in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + } + + if ('clickToUse' in options) { + if (options.clickToUse) { + if (!this.activator) { + this.activator = new Activator(this.dom.root); + } + } else { + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } + } + } + + if ('showCustomTime' in options) { + throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); + } + + // enable/disable autoResize + this._initAutoResize(); + } - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; - }; + // propagate options to all components + this.components.forEach(function (component) { + return component.setOptions(options); + }); - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; + // enable/disable configure + if ('configure' in options) { + if (!this.configurator) { + this.configurator = this._createConfigurator(); + } - this.redraw(); - this.onChange(); - } else { - throw 'Error: index out of range'; + this.configurator.setOptions(options.configure); + + // collect the settings of all components, and pass them to the configuration system + var appliedOptions = util.deepExtend({}, this.options); + this.components.forEach(function (component) { + util.deepExtend(appliedOptions, component.options); + }); + this.configurator.setModuleOptions({ global: appliedOptions }); } + + // redraw everything + this._redraw(); }; /** - * retrieve the index of the currently selected vaue - * @return {Number} index + * Returns true when the Timeline is active. + * @returns {boolean} */ - Slider.prototype.getIndex = function () { - return this.index; + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; }; /** - * retrieve the currently selected value - * @return {*} value + * Destroy the Core, clean up all DOM elements and event listeners. */ - Slider.prototype.get = function () { - return this.values[this.index]; - }; - - Slider.prototype._onMouseDown = function (event) { - // only react on left mouse button down - var leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!leftButtonDown) return; - - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); - - this.frame.style.cursor = 'move'; + Core.prototype.destroy = function () { + // unbind datasets + this.setItems(null); + this.setGroups(null); - // 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', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + // remove all event listeners + this.off(); - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - var x = left - 3; + // stop checking for changed size + this._stopAutoResize(); - var index = Math.round(x / width * (this.values.length - 1)); - if (index < 0) index = 0; - if (index > this.values.length - 1) index = this.values.length - 1; + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); + } + this.dom = null; - return index; - }; + // remove Activator + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; + } + } + this.listeners = null; + this.hammer = null; - var x = index / (this.values.length - 1) * width; - var left = x + 3; + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + return component.destroy(); + }); - return left; + this.body = null; }; - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; - - var index = this.leftToIndex(x); + /** + * Set a custom time bar + * @param {Date} time + * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. + */ + Core.prototype.setCustomTime = function (time, id) { + var customTimes = this.customTimes.filter(function (component) { + return id === component.options.id; + }); - this.setIndex(index); + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + } - util.preventDefault(); + if (customTimes.length > 0) { + customTimes[0].setCustomTime(time); + } }; - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - - // remove event listeners - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); + /** + * Retrieve the current custom time. + * @param {number} [id=undefined] Id of the custom time bar. + * @return {Date | undefined} customTime + */ + Core.prototype.getCustomTime = function (id) { + var customTimes = this.customTimes.filter(function (component) { + return component.options.id === id; + }); - util.preventDefault(); + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + } + return customTimes[0].getCustomTime(); }; - module.exports = Slider; - -/***/ }, -/* 35 */ -/***/ function(module, exports, __webpack_require__) { - /** - * @prototype StepNumber - * The class StepNumber is an iterator for Numbers. You provide a start and end - * value, and a best step size. StepNumber itself rounds to fixed values and - * a finds the step that best fits the provided step. - * - * If prettyStep is true, the step size is chosen as close as possible to the - * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... - * - * Example usage: - * var step = new StepNumber(0, 10, 2.5, true); - * step.start(); - * while (!step.end()) { - * alert(step.getCurrent()); - * step.next(); - * } - * - * Version: 1.0 - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Add custom vertical bar + * @param {Date | String | Number} [time] A Date, unix timestamp, or + * ISO date string. Time point where + * the new bar should be placed. + * If not provided, `new Date()` will + * be used. + * @param {Number | String} [id=undefined] Id of the new bar. Optional + * @return {Number | String} Returns the id of the new bar */ - "use strict"; + Core.prototype.addCustomTime = function (time, id) { + var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); - function StepNumber(start, end, step, prettyStep) { - // set default values - this._start = 0; - this._end = 0; - this._step = 1; - this.prettyStep = true; - this.precision = 5; + var exists = this.customTimes.some(function (customTime) { + return customTime.options.id === id; + }); + if (exists) { + throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists'); + } - this._current = 0; - this.setRange(start, end, step, prettyStep); - }; + var customTime = new CustomTime(this.body, { + time: timestamp, + id: id + }); - /** - * Set a new range: start, end and step. - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) - */ - StepNumber.prototype.setRange = function (start, end, step, prettyStep) { - this._start = start ? start : 0; - this._end = end ? end : 0; + this.customTimes.push(customTime); + this.components.push(customTime); + this.redraw(); - this.setStep(step, prettyStep); + return id; }; /** - * Set a new step size - * @param {Number} step New step size. Must be a positive value - * @param {boolean} prettyStep Optional. If true, the provided step is rounded - * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Remove previously added custom bar + * @param {int} id ID of the custom bar to be removed + * @return {boolean} True if the bar exists and is removed, false otherwise */ - StepNumber.prototype.setStep = function (step, prettyStep) { - if (step === undefined || step <= 0) return; + Core.prototype.removeCustomTime = function (id) { + var customTimes = this.customTimes.filter(function (bar) { + return bar.options.id === id; + }); - if (prettyStep !== undefined) this.prettyStep = prettyStep; + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + } - if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; + customTimes.forEach((function (customTime) { + this.customTimes.splice(this.customTimes.indexOf(customTime), 1); + this.components.splice(this.components.indexOf(customTime), 1); + customTime.destroy(); + }).bind(this)); }; /** - * Calculate a nice step size, closest to the desired step size. - * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an - * integer Number. For example 1, 2, 5, 10, 20, 50, etc... - * @param {Number} step Desired step size - * @return {Number} Nice step size + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function log10(x) { - return Math.log(x) / Math.LN10; - }; - - // try three steps (multiple of 1, 2, or 5 - var step1 = Math.pow(10, Math.round(log10(step))), - step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), - step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); + Core.prototype.getVisibleItems = function () { + return this.itemSet && this.itemSet.getVisibleItems() || []; + }; - // choose the best step (closest to minimum step) - var prettyStep = step1; - if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; - if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + /** + * Set Core window such that it fits all items + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + */ + Core.prototype.fit = function (options) { + var range = this.getDataRange(); - // for safety - if (prettyStep <= 0) { - prettyStep = 1; + // skip range set if there is no min and max date + if (range.min === null && range.max === null) { + return; } - return prettyStep; + // apply a margin of 1% left and right of the data + var interval = range.max - range.min; + var min = new Date(range.min.valueOf() - interval * 0.01); + var max = new Date(range.max.valueOf() + interval * 0.01); + + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(min, max, animation); }; /** - * returns the current value of the step - * @return {Number} current value + * Calculate the data range of the items start and end dates + * @returns {{min: Date | null, max: Date | null}} + * @protected */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); + Core.prototype.getDataRange = function () { + // must be implemented by Timeline and Graph2d + throw new Error('Cannot invoke abstract method getDataRange'); }; /** - * returns the current step size - * @return {Number} current step size + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(start, end, options) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - StepNumber.prototype.getStep = function () { - return this._step; + Core.prototype.setWindow = function (start, end, options) { + var animation; + if (arguments.length == 1) { + var range = arguments[0]; + animation = range.animation !== undefined ? range.animation : true; + this.range.setRange(range.start, range.end, animation); + } else { + animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(start, end, animation); + } }; /** - * Set the current value to the largest value smaller than start, which - * is a multiple of the step size + * Move the window such that given time is centered on screen. + * @param {Date | Number | String} time + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - StepNumber.prototype.start = function () { - this._current = this._start - this._start % this._step; + Core.prototype.moveTo = function (time, options) { + var interval = this.range.end - this.range.start; + var t = util.convert(time, 'Date').valueOf(); + + var start = t - interval / 2; + var end = t + interval / 2; + var animation = options && options.animation !== undefined ? options.animation : true; + + this.range.setRange(start, end, animation); }; /** - * Do a step, add the step size to the current value + * Get the visible window + * @return {{start: Date, end: Date}} Visible range */ - StepNumber.prototype.next = function () { - this._current += this._step; + Core.prototype.getWindow = function () { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; /** - * Returns true whether the end is reached - * @return {boolean} True if the current value has passed the end value. + * Force a redraw. Can be overridden by implementations of Core */ - StepNumber.prototype.end = function () { - return this._current > this._end; + Core.prototype.redraw = function () { + this._redraw(); }; - module.exports = StepNumber; - -/***/ }, -/* 36 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Emitter = __webpack_require__(30); - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var Range = __webpack_require__(40); - var Core = __webpack_require__(11); - var TimeAxis = __webpack_require__(50); - var CurrentTime = __webpack_require__(37); - var CustomTime = __webpack_require__(13); - var ItemSet = __webpack_require__(12); - - var Configurator = __webpack_require__(53); - var Validator = __webpack_require__(55)['default']; - var printStyle = __webpack_require__(55).printStyle; - var allOptions = __webpack_require__(56).allOptions; - var configureOptions = __webpack_require__(56).configureOptions; - /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | vis.DataView | Array} [items] - * @param {vis.DataSet | vis.DataView | Array} [groups] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor - * @extends Core + * Redraw for internal use. Redraws all components. See also the public + * method redraw. + * @protected */ - function Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError('Constructor must be called with the new operator'); - } - - // if the third element is options, the forth is groups (optionally); - if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { - var forthArgument = options; - options = groups; - groups = forthArgument; - } - - var me = this; - this.defaultOptions = { - start: null, - end: null, - - autoResize: true, - - orientation: { - axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' - item: 'bottom' // not relevant - }, - - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); - - // Create the DOM, props, and emitter - this._create(container); + Core.prototype._redraw = function () { + var resized = false; + var options = this.options; + var props = this.props; + var dom = this.dom; - // all components listed here will be repainted automatically - this.components = []; + if (!dom) return; // when destroyed - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - hiddenDates: [], - util: { - getScale: function getScale() { - return me.timeAxis.step.scale; - }, - getStep: function getStep() { - return me.timeAxis.step.step; - }, + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime: me._toGlobalTime.bind(me) - } - }; + // update class names + if (options.orientation == 'top') { + util.addClassName(dom.root, 'vis-top'); + util.removeClassName(dom.root, 'vis-bottom'); + } else { + util.removeClassName(dom.root, 'vis-top'); + util.addClassName(dom.root, 'vis-bottom'); + } - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + // update root width and height options + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); + dom.root.style.width = util.option.asSize(options.width, ''); - // time axis - this.timeAxis = new TimeAxis(this.body); - this.timeAxis2 = null; // used in case of orientation option 'both' - this.components.push(this.timeAxis); + // calculate border widths + props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; + props.border.right = props.border.left; + props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; + props.border.bottom = props.border.top; + var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; + var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + // workaround for a bug in IE: the clientWidth of an element with + // a height:0px and overflow:hidden is not calculated and always has value 0 + if (dom.centerContainer.clientHeight === 0) { + props.border.left = props.border.top; + props.border.right = props.border.left; + } + if (dom.root.clientHeight === 0) { + borderRootWidth = borderRootHeight; + } - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + // calculate the heights. If any of the side panels is empty, we set the height to + // minus the border width, such that the border will be invisible + props.center.height = dom.center.offsetHeight; + props.left.height = dom.left.offsetHeight; + props.right.height = dom.right.offsetHeight; + props.top.height = dom.top.clientHeight || -props.border.top; + props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + // TODO: compensate borders when any of the panels is empty. - this.on('tap', function (event) { - me.emit('click', me.getEventProperties(event)); - }); - this.on('doubletap', function (event) { - me.emit('doubleClick', me.getEventProperties(event)); - }); - this.dom.root.oncontextmenu = function (event) { - me.emit('contextmenu', me.getEventProperties(event)); - }; + // apply auto height + // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) + var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); + var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; + dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); - // apply options - if (options) { - this.setOptions(options); - } + // calculate heights of the content panels + props.root.height = dom.root.offsetHeight; + props.background.height = props.root.height - borderRootHeight; + var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; + props.centerContainer.height = containerHeight; + props.leftContainer.height = containerHeight; + props.rightContainer.height = props.leftContainer.height; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + // calculate the widths of the panels + props.root.width = dom.root.offsetWidth; + props.background.width = props.root.width - borderRootWidth; + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.leftContainer.width = props.left.width; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + props.rightContainer.width = props.right.width; + var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; + props.center.width = centerWidth; + props.centerContainer.width = centerWidth; + props.top.width = centerWidth; + props.bottom.width = centerWidth; - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } + // resize the panels + dom.background.style.height = props.background.height + 'px'; + dom.backgroundVertical.style.height = props.background.height + 'px'; + dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; + dom.centerContainer.style.height = props.centerContainer.height + 'px'; + dom.leftContainer.style.height = props.leftContainer.height + 'px'; + dom.rightContainer.style.height = props.rightContainer.height + 'px'; - // Extend the functionality from Core - Timeline.prototype = new Core(); + dom.background.style.width = props.background.width + 'px'; + dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; + dom.backgroundHorizontal.style.width = props.background.width + 'px'; + dom.centerContainer.style.width = props.center.width + 'px'; + dom.top.style.width = props.top.width + 'px'; + dom.bottom.style.width = props.bottom.width + 'px'; - /** - * Load a configurator - * @return {Object} - * @private - */ - Timeline.prototype._createConfigurator = function () { - return new Configurator(this, this.dom.container, configureOptions); - }; + // reposition the panels + dom.background.style.left = '0'; + dom.background.style.top = '0'; + dom.backgroundVertical.style.left = props.left.width + props.border.left + 'px'; + dom.backgroundVertical.style.top = '0'; + dom.backgroundHorizontal.style.left = '0'; + dom.backgroundHorizontal.style.top = props.top.height + 'px'; + dom.centerContainer.style.left = props.left.width + 'px'; + dom.centerContainer.style.top = props.top.height + 'px'; + dom.leftContainer.style.left = '0'; + dom.leftContainer.style.top = props.top.height + 'px'; + dom.rightContainer.style.left = props.left.width + props.center.width + 'px'; + dom.rightContainer.style.top = props.top.height + 'px'; + dom.top.style.left = props.left.width + 'px'; + dom.top.style.top = '0'; + dom.bottom.style.left = props.left.width + 'px'; + dom.bottom.style.top = props.top.height + props.centerContainer.height + 'px'; - /** - * Force a redraw. The size of all items will be recalculated. - * Can be useful to manually redraw when option autoResize=false and the window - * has been resized, or when the items CSS has been changed. - */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); - }; + // update the scrollTop, feasible range for the offset can be changed + // when the height of the Core or of the contents of the center changed + this._updateScrollTop(); - Timeline.prototype.setOptions = function (options) { - // validate options - var errorFound = Validator.validate(options, allOptions); - if (errorFound === true) { - console.log('%cErrors have been found in the supplied options object.', printStyle); + // reposition the scrollable contents + var offset = this.props.scrollTop; + if (options.orientation.item != 'top') { + offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); } + dom.center.style.left = '0'; + dom.center.style.top = offset + 'px'; + dom.left.style.left = '0'; + dom.left.style.top = offset + 'px'; + dom.right.style.left = '0'; + dom.right.style.top = offset + 'px'; - Core.prototype.setOptions.call(this, options); - - if ('type' in options) { - if (options.type !== this.options.type) { - this.options.type = options.type; + // show shadows when vertical scrolling is available + var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; - // force recreation of all items - var itemsData = this.itemsData; - if (itemsData) { - var selection = this.getSelection(); - this.setItems(null); // remove all - this.setItems(itemsData); // add all - this.setSelection(selection); // restore selection - } + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + var MAX_REDRAWS = 3; // maximum number of consecutive redraws + if (this.redrawCount < MAX_REDRAWS) { + this.redrawCount++; + this._redraw(); + } else { + console.log('WARNING: infinite loop in redraw?'); } + this.redrawCount = 0; } }; + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error('Function repaint is deprecated. Use redraw instead.'); + }; + /** - * Set items - * @param {vis.DataSet | Array | null} items + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * Only applicable when option `showCurrentTime` is true. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; - - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; - } else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' - } - }); + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); } - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); - - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - if (this.options.start == undefined || this.options.end == undefined) { - var range = this.getItemRange(); - } - - var start = this.options.start != undefined ? this.options.start : range.min; - var end = this.options.end != undefined ? this.options.end : range.max; - - this.setWindow(start, end, { animation: false }); - } else { - this.fit({ animation: false }); - } - } + this.currentTime.setCurrentTime(time); }; /** - * Set groups - * @param {vis.DataSet | Array} groups + * Get the current time. + * Only applicable when option `showCurrentTime` is true. + * @return {Date} Returns the current time. */ - Timeline.prototype.setGroups = function (groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); + Core.prototype.getCurrentTime = function () { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); } - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); + return this.currentTime.getCurrentTime(); }; /** - * Set both items and groups in one go - * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @protected */ - Timeline.prototype.setData = function (data) { - if (data && data.groups) { - this.setGroups(data.groups); - } + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); + }; - if (data && data.items) { - this.setItems(data.items); - } + /** + * Convert a position on the global screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @protected + */ + // TODO: move this function to Range + Core.prototype._toGlobalTime = function (x) { + return DateUtil.toTime(this, x, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return new Date(x / conversion.scale + conversion.offset); }; /** - * 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. If ids is an empty array, all items will be - * unselected. - * @param {Object} [options] Available options: - * `focus: boolean` - * If true, focus will be set to the selected item(s) - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - * Only applicable when option focus is true. + * Convert a datetime (Date object) into a position on the screen + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + * @protected */ - Timeline.prototype.setSelection = function (ids, options) { - this.itemSet && this.itemSet.setSelection(ids); + // TODO: move this function to Range + Core.prototype._toScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.center.width); + }; - if (options && options.focus) { - this.focus(ids, options); - } + /** + * Convert a datetime (Date object) into a position on the root + * This is used to get the pixel density estimate for the screen, not the center panel + * @param {Date} time A date + * @return {int} x The position on root in pixels which corresponds + * with the given date. + * @protected + */ + // TODO: move this function to Range + Core.prototype._toGlobalScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return (time.valueOf() - conversion.offset) * conversion.scale; }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Initialize watching when option autoResize is true + * @private */ - Timeline.prototype.getSelection = function () { - return this.itemSet && this.itemSet.getSelection() || []; + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); + } }; /** - * Adjust the visible window such that the selected item (or multiple items) - * are centered on screen. - * @param {String | String[]} id An item id or array with item ids - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - Timeline.prototype.focus = function (id, options) { - if (!this.itemsData || id == undefined) return; + Core.prototype._startAutoResize = function () { + var me = this; - var ids = Array.isArray(id) ? id : [id]; + this._stopAutoResize(); - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: 'Date', - end: 'Date' + this._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; } - }); - // calculate minimum start and maximum end of specified items - var start = null; - var end = null; - itemsData.forEach(function (itemData) { - var s = itemData.start.valueOf(); - var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); + if (me.dom.root) { + // check whether the frame is resized + // Note: we compare offsetWidth here, not clientWidth. For some reason, + // IE does not restore the clientWidth from 0 to the actual width after + // changing the timeline's container display style from none to visible + if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { + me.props.lastWidth = me.dom.root.offsetWidth; + me.props.lastHeight = me.dom.root.offsetHeight; - if (start === null || s < start) { - start = s; + me.emit('change'); + } } + }; - if (end === null || e > end) { - end = e; - } - }); + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); - if (start !== null && end !== null) { - // calculate the new middle and interval for the window - var middle = (start + end) / 2; - var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); + this.watchTimer = setInterval(this._onResize, 1000); + }; - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, animation); + /** + * Stop watching for a resize of the frame. + * @private + */ + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; } + + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; }; /** - * Set Timeline window such that it fits all items - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Start moving the timeline vertically + * @param {Event} event + * @private */ - Timeline.prototype.fit = function (options) { - var animation = options && options.animation !== undefined ? options.animation : true; - var range = this.getItemRange(); - this.range.setRange(range.min, range.max, animation); + Core.prototype._onTouch = function (event) { + this.touch.allowDragging = true; + this.touch.initialScrollTop = this.props.scrollTop; }; /** - * Determine the range of the items, taking into account their actual width - * and a margin of 10 pixels on both sides. - * @return {{min: Date | null, max: Date | null}} + * Start moving the timeline vertically + * @param {Event} event + * @private */ - Timeline.prototype.getItemRange = function () { - var _this = this; - - // get a rough approximation for the range based on the items start and end dates - var range = this.getDataRange(); - var min = range.min; - var max = range.max; - var minItem = null; - var maxItem = null; - - if (min != null && max != null) { - var interval; - var factor; - var lhs; - var rhs; - var delta; - - (function () { - var getStart = function (item) { - return util.convert(item.data.start, 'Date').valueOf(); - }; - - var getEnd = function (item) { - var end = item.data.end != undefined ? item.data.end : item.data.start; - return util.convert(end, 'Date').valueOf(); - }; - - interval = max - min; - // ms - if (interval <= 0) { - interval = 10; - } - factor = interval / _this.props.center.width; - - // calculate the date of the left side and right side of the items given - util.forEach(_this.itemSet.items, (function (item) { - item.show(); - - var start = getStart(item); - var end = getEnd(item); + Core.prototype._onPinch = function (event) { + this.touch.allowDragging = false; + }; - var left = new Date(start - (item.getWidthLeft() + 10) * factor); - var right = new Date(end + (item.getWidthRight() + 10) * factor); + /** + * Move the timeline vertically + * @param {Event} event + * @private + */ + Core.prototype._onDrag = function (event) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.touch.allowDragging) return; - if (left < min) { - min = left; - minItem = item; - } - if (right > max) { - max = right; - maxItem = item; - } - }).bind(_this)); + var delta = event.deltaY; - if (minItem && maxItem) { - lhs = minItem.getWidthLeft() + 10; - rhs = maxItem.getWidthRight() + 10; - delta = _this.props.center.width - lhs - rhs; - // px + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - if (delta > 0) { - min = getStart(minItem) - lhs * interval / delta; // ms - max = getEnd(maxItem) + rhs * interval / delta; // ms - } - } - })(); + if (newScrollTop != oldScrollTop) { + this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + this.emit('verticalDrag'); } + }; - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + /** + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - Timeline.prototype.getDataRange = function () { - var min = null; - var max = null; - - var dataset = this.itemsData && this.itemsData.getDataSet(); - if (dataset) { - dataset.forEach(function (item) { - var start = util.convert(item.start, 'Date').valueOf(); - var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf(); - if (min === null || start < min) { - min = start; - } - if (max === null || end > max) { - max = start; - } - }); + Core.prototype._updateScrollTop = function () { + // recalculate the scrollTopMin + var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero + if (scrollTopMin != this.props.scrollTopMin) { + // in case of bottom orientation, change the scrollTop such that the contents + // do not move relative to the time axis at the bottom + if (this.options.orientation.item != 'top') { + this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; + } + this.props.scrollTopMin = scrollTopMin; } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + // limit the scrollTop to the feasible scroll range + if (this.props.scrollTop > 0) this.props.scrollTop = 0; + if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; + + return this.props.scrollTop; }; /** - * Generate Timeline related information from an event - * @param {Event} event - * @return {Object} An object with related information, like on which area - * The event happened, whether clicked on an item, etc. + * Get the current scrollTop + * @returns {number} scrollTop + * @private */ - Timeline.prototype.getEventProperties = function (event) { - var clientX = event.center ? event.center.x : event.clientX; - var clientY = event.center ? event.center.y : event.clientY; - var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); - var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); - - var item = this.itemSet.itemFromTarget(event); - var group = this.itemSet.groupFromTarget(event); - var customTime = CustomTime.customTimeFromTarget(event); - - var snap = this.itemSet.options.snap || null; - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); - var time = this._toTime(x); - var snappedTime = snap ? snap(time, scale, step) : time; - - var element = util.getTarget(event); - var what = null; - if (item != null) { - what = 'item'; - } else if (customTime != null) { - what = 'custom-time'; - } else if (util.hasParent(element, this.timeAxis.dom.foreground)) { - what = 'axis'; - } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { - what = 'axis'; - } else if (util.hasParent(element, this.itemSet.dom.labelSet)) { - what = 'group-label'; - } else if (util.hasParent(element, this.currentTime.bar)) { - what = 'current-time'; - } else if (util.hasParent(element, this.dom.center)) { - what = 'background'; - } + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - return { - event: event, - item: item ? item.id : null, - group: group ? group.groupId : null, - what: what, - pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, - pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, - x: x, - y: y, - time: time, - snappedTime: snappedTime - }; + /** + * Load a configurator + * @return {Object} + * @private + */ + Core.prototype._createConfigurator = function () { + throw new Error('Cannot invoke abstract method _createConfigurator'); }; - module.exports = Timeline; + module.exports = Core; /***/ }, -/* 37 */ +/* 34 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var Component = __webpack_require__(38); - var moment = __webpack_require__(20); - var locales = __webpack_require__(39); + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var TimeStep = __webpack_require__(38); + var Component = __webpack_require__(28); + var Group = __webpack_require__(35); + var BackgroundGroup = __webpack_require__(39); + var BoxItem = __webpack_require__(40); + var PointItem = __webpack_require__(2); + var RangeItem = __webpack_require__(37); + var BackgroundItem = __webpack_require__(41); + + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var BACKGROUND = '__background__'; // reserved group id for background items without group /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime + * 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 CurrentTime(body, options) { + function ItemSet(body, options) { this.body = body; - // default options this.defaultOptions = { - showCurrentTime: true, + type: null, // 'box', 'point', 'range', 'background' + orientation: { + item: 'bottom' // item orientation: 'top' or 'bottom' + }, + align: 'auto', // alignment of box items + stack: true, + groupOrder: null, - locales: locales, - locale: 'en' + selectable: true, + multiselect: false, + + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, + + snap: TimeStep.snap, + + 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); + }, + + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 + } }; + + // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: { start: 'Date', end: 'Date' } + }; + + this.conversion = { + toScreen: body.util.toScreen, + toTime: body.util.toTime + }; + this.dom = {}; + this.props = {}; + this.hammer = null; + + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // 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); + } + }; + + // 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); + } + }; + + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; + + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw + + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM this._create(); this.setOptions(options); } - CurrentTime.prototype = new Component(); - - /** - * Create the HTML DOM for the current time bar - * @private - */ - CurrentTime.prototype._create = function () { - var bar = document.createElement('div'); - bar.className = 'vis-current-time'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + ItemSet.prototype = new Component(); - this.bar = bar; + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem }; /** - * Destroy the CurrentTime bar + * Create the HTML DOM for the ItemSet */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + ItemSet.prototype._create = function () { + var frame = document.createElement('div'); + frame.className = 'vis-itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; - this.body = null; - }; + // create background panel + var background = document.createElement('div'); + background.className = 'vis-background'; + frame.appendChild(background); + this.dom.background = background; - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] - */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); - } - }; + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'vis-foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - CurrentTime.prototype.redraw = function () { - if (this.options.showCurrentTime) { - var parent = this.body.dom.backgroundVertical; - if (this.bar.parentNode != parent) { - // attach to the dom - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - parent.appendChild(this.bar); + // create axis panel + var axis = document.createElement('div'); + axis.className = 'vis-axis'; + this.dom.axis = axis; - this.start(); - } + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'vis-labelset'; + this.dom.labelSet = labelSet; - var now = new Date(new Date().valueOf() + this.offset); - var x = this.body.util.toScreen(now); + // create ungrouped Group + this._updateUngrouped(); - var locale = this.options.locales[this.options.locale]; - if (!locale) { - if (!this.warned) { - console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); - this.warned = true; - } - locale = this.options.locales['en']; // fall back on english when not available - } - var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; - this.bar.style.left = x + 'px'; - this.bar.title = title; - } else { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); + // 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); } - this.stop(); - } + }).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)); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - return false; + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on('press', this._onMultiSelectItem.bind(this)); + + // add item on doubletap + this.hammer.on('doubletap', this._onAddItem.bind(this)); + + // attach to the DOM + this.show(); }; /** - * Start auto refreshing the current time bar + * 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. */ - CurrentTime.prototype.start = function () { - var me = this; - - function update() { - me.stop(); + 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); - // determine interval to refresh - var scale = me.body.range.conversion(me.body.domProps.center.width).scale; - var interval = 1 / scale / 10; - if (interval < 30) interval = 30; - if (interval > 1000) interval = 1000; + 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; + } + } - me.redraw(); + 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); + } + } + } + } - // start a renderTimer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); - } + 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); + } + } - update(); - }; + // 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); - /** - * Stop auto refreshing the current time bar - */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); } }; /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. - */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, 'Date').valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); - }; - - /** - * Get the current time. - * @return {Date} Returns the current time. - */ - CurrentTime.prototype.getCurrentTime = function () { - return new Date(new Date().valueOf() + this.offset); - }; - - module.exports = CurrentTime; - -/***/ }, -/* 38 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] + * 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] */ - "use strict"; - - function Component(body, options) { - this.options = null; - this.props = null; - } + ItemSet.prototype.markDirty = function (options) { + this.groupIds = []; + this.stackDirty = true; - /** - * Set options for the component. The new options will be merged into the - * current options. - * @param {Object} options - */ - Component.prototype.setOptions = function (options) { - if (options) { - util.extend(this.options, options); + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Destroy the ItemSet */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; - }; + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - /** - * Destroy the component. Cleanup DOM and event listeners - */ - Component.prototype.destroy = function () {}; + this.hammer = null; + + this.body = null; + this.conversion = null; + }; /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * Hide the component from the DOM */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - return resized; + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } }; - module.exports = Component; - - // should be implemented by the component - -/***/ }, -/* 39 */ -/***/ function(module, exports, __webpack_require__) { - - // English - 'use strict'; + /** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ + ItemSet.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } - exports['en'] = { - current: 'current', - time: 'time' - }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); + } - // Dutch - exports['nl'] = { - current: 'huidige', - time: 'tijd' + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; - -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var util = __webpack_require__(19); - var hammerUtil = __webpack_require__(41); - var moment = __webpack_require__(20); - var Component = __webpack_require__(38); - var DateUtil = __webpack_require__(42); /** - * @constructor Range - * A Range controls a numeric range with a start and end value. - * The Range adjusts the range based on mouse events or programmatic changes, - * and triggers events when the range is changing or has been changed. - * @param {{dom: Object, domProps: Object, emitter: Emitter}} body - * @param {Object} [options] See description at Range.setOptions + * 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 Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, 'days').valueOf(); // Number - this.end = now.clone().add(4, 'days').valueOf(); // Number - - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; - - // default options - this.defaultOptions = { - start: null, - end: null, - direction: 'horizontal', // 'horizontal' or 'vertical' - moveable: true, - zoomable: true, - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds - }; - this.options = util.extend({}, this.defaultOptions); - - this.props = { - touch: {} - }; - this.animationTimer = null; - - // drag listeners for dragging - this.body.emitter.on('panstart', this._onDragStart.bind(this)); - this.body.emitter.on('panmove', this._onDrag.bind(this)); - this.body.emitter.on('panend', this._onDragEnd.bind(this)); - - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); - - this.setOptions(options); - } + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; - Range.prototype = new Component(); + if (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; - /** - * Set options for the range controller - * @param {Object} options Available options: - * {Number | Date | String} start Start date for the range - * {Number | Date | String} end End date for the range - * {Number} min Minimum value for start - * {Number} max Maximum value for end - * {Number} zoomMin Set a minimum value for - * (end - start). - * {Number} zoomMax Set a maximum value for - * (end - start). - * {Boolean} moveable Enable moving of the range - * by dragging. True by default - * {Boolean} zoomable Enable zooming of the range - * by pinching/scrolling. True by default - */ - Range.prototype.setOptions = function (options) { - if (options) { - // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; - util.selectiveExtend(fields, this.options, options); + // 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(); + } - if ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + // 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(); } } }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - function validateDirection(direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); - } - } + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); + }; /** - * Set a new start and end range - * @param {Date | Number | String} [start] - * @param {Date | Number | String} [end] - * @param {boolean | {duration: number, easingFunction: string}} [animation=false] - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - * @param {Boolean} [byUser=false] - * + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - Range.prototype.setRange = function (start, end, animation, byUser) { - if (byUser !== true) { - byUser = false; - } - var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; - var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; - this._cancelAnimation(); - - if (animation) { - // true or an Object - var me = this; - var initStart = this.start; - var initEnd = this.end; - var duration = typeof animation === 'object' && 'duration' in animation ? animation.duration : 500; - var easingName = typeof animation === 'object' && 'easingFunction' in animation ? animation.easingFunction : 'easeInOutQuad'; - var easingFunction = util.easingFunctions[easingName]; - if (!easingFunction) { - throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + 'Choose from: ' + Object.keys(util.easingFunctions).join(', ')); - } - - var initTime = new Date().valueOf(); - var anyChanged = false; - - var next = function next() { - if (!me.props.touch.dragging) { - var now = new Date().valueOf(); - var time = now - initTime; - var ease = easingFunction(time / duration); - var done = time > duration; - var s = done || finalStart === null ? finalStart : initStart + (finalStart - initStart) * ease; - var e = done || finalEnd === null ? finalEnd : initEnd + (finalEnd - initEnd) * ease; + 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); - changed = me._applyRange(s, e); - DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); - anyChanged = anyChanged || changed; - if (changed) { - me.body.emitter.emit('rangechange', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - if (done) { - if (anyChanged) { - me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } - } else { - // animate with as high as possible frame rate, leave 20 ms in between - // each to prevent the browser from blocking - me.animationTimer = setTimeout(next, 20); + // 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); } } - }; - - return next(); - } else { - var changed = this._applyRange(finalStart, finalEnd); - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - if (changed) { - var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); } } + + return ids; }; /** - * Stop an animation + * Deselect a selected item + * @param {String | Number} id * @private */ - Range.prototype._cancelAnimation = function () { - if (this.animationTimer) { - clearTimeout(this.animationTimer); - this.animationTimer = null; + 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; + } } }; /** - * Set a new start and end range. This method is the same as setRange, but - * does not trigger a range change and range changed event, and it returns - * true when the range is changed - * @param {Number} [start] - * @param {Number} [end] - * @return {Boolean} changed - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Range.prototype._applyRange = function (start, end) { - var newStart = start != null ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = end != null ? util.convert(end, 'Date').valueOf() : this.end, - max = this.options.max != null ? util.convert(this.options.max, 'Date').valueOf() : null, - min = this.options.min != null ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; + 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; - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } + // 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; - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } + // update class name + frame.className = 'vis-itemset'; - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + // reorder the groups (if needed) + resized = this._orderGroups() || resized; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } - } + // 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; - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; + 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; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } - } + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if (newEnd - newStart < zoomMin) { - if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the minimum - diff = zoomMin - (newEnd - newStart); - newStart -= diff / 2; - newEnd += diff / 2; - } - } - } + // 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; - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } + // update frame height + frame.style.height = asSize(height); - if (newEnd - newStart > zoomMax) { - if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the maximum - diff = newEnd - newStart - zoomMax; - newStart += diff / 2; - newEnd -= diff / 2; + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; + + // 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; - var changed = this.start != newStart || this.end != newEnd; + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + ungrouped.add(item); + } + } - // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) - if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { - this.body.emitter.emit('checkRangedItems'); + ungrouped.show(); + } } - - this.start = newStart; - this.end = newEnd; - return changed; }; /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end - }; + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; }; /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Set items + * @param {vis.DataSet | null} items */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); - }; + ItemSet.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.conversion = function (start, end, width, totalHidden) { - if (totalHidden === undefined) { - totalHidden = 0; - } - if (width != 0 && end - start != 0) { - return { - offset: start, - scale: width / (end - start - totalHidden) - }; + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; } else { - return { - offset: 0, - scale: 1 - }; + throw new TypeError('Data must be an instance of DataSet or DataView'); } - }; - /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private - */ - Range.prototype._onDragStart = function (event) { - this.deltaDifference = 0; - this.previousDelta = 0; - // only allow dragging when configured as movable - if (!this.options.moveable) return; + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.dragging = true; + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + + // update the group holding all ungrouped items + this._updateUngrouped(); } }; /** - * Perform dragging operation - * @param {Event} event - * @private + * Get the current items + * @returns {vis.DataSet | null} */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + ItemSet.prototype.getItems = function () { + return this.itemsData; + }; - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; - var direction = this.options.direction; - validateDirection(direction); - var delta = direction == 'horizontal' ? event.deltaX : event.deltaY; - delta -= this.deltaDifference; - var interval = this.props.touch.end - this.props.touch.start; + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.off(event, callback); + }); - // normalize dragging speed if cutout is in between. - var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - interval -= duration; + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - var width = direction == 'horizontal' ? this.body.domProps.center.width : this.body.domProps.center.height; - var diffRange = -delta / width * interval; - var newStart = this.props.touch.start + diffRange; - var newEnd = this.props.touch.end + diffRange; + // 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'); + } - // snapping times away from hidden zones - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.deltaDifference += delta; - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this._onDrag(event); - return; + 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); } - this.previousDelta = delta; - this._applyRange(newStart, newEnd); + // update the group holding all ungrouped items + this._updateUngrouped(); - // fire a rangechange event - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + // update the order of all items in each group + this._order(); + + this.body.emitter.emit('change', { queue: true }); }; /** - * Stop dragging operation - * @param {event} event - * @private + * Get the current groups + * @returns {vis.DataSet | null} groups */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; - - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; - - this.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; - } - - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + ItemSet.prototype.getGroups = function () { + return this.groupsData; }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @private + * Remove an item by its id + * @param {String | Number} id */ - Range.prototype._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - // 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) { - // perform the zoom action. Delta is normally 1 or -1 - - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - delta / 5; - } else { - scale = 1 / (1 + delta / 5); - } - - // calculate center, the date to zoom around - var pointer = getPointer({ x: event.clientX, y: event.clientY }, this.body.dom.center); - var pointerDate = this._pointerToDate(pointer); + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - this.zoom(scale, pointerDate, delta); + 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); + } + }); } - - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); }; /** - * Start of a touch gesture + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type * @private */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; - this.scaleOffset = 0; - this.deltaDifference = 0; + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); }; /** - * Handle pinch event - * @param {Event} event + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId * @private */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - this.props.touch.allowDragging = false; - - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.center, this.body.dom.center); + 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; } + }; - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); - - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + /** + * Handle updated items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - // calculate new start and end - var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; - var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + ids.forEach((function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - // snapping times away from hidden zones - this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times - this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + var constructor = ItemSet.types[type]; + var selected; - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this.scaleOffset = 1 - event.scale; - newStart = safeStart; - newEnd = safeEnd; - } + 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); + } + } - this.setRange(newStart, newEnd, false, true); + 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(); + } + } 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.startToFront = false; // revert to default - this.endToFront = true; // revert to default + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); }; /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private + * Handle added items + * @param {Number[]} ids + * @protected */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; - validateDirection(direction); + /** + * 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); + } + }); - if (direction == 'horizontal') { - return this.body.util.toTime(pointer.x).valueOf(); - } else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); } }; /** - * Get the pointer location relative to the location of the dom element - * @param {{x: Number, y: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer + * Update the order of item in all groups * @private */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) - }; - } + 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(); + }); + }; /** - * Zoom the range the given scale in or out. Start and end date will - * be adjusted, and the timeline will be redrawn. You can optionally give a - * date around which to zoom. - * For example, try scale = 0.9 or 1.1 - * @param {Number} scale Scaling factor. Values above 1 will zoom out, - * values below 1 will zoom in. - * @param {Number} [center] Value representing a date around which will - * be zoomed. + * Handle updated groups + * @param {Number[]} ids + * @private */ - Range.prototype.zoom = function (scale, center, delta) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - // snapping times away from hidden zones - this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - newStart = safeStart; - newEnd = safeEnd; - } + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } + + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); + + group = new Group(id, groupData, me); + me.groups[id] = group; + + // 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); + } + } + } - this.setRange(newStart, newEnd, false, true); + group.order(); + group.show(); + } else { + // update group + group.setData(groupData); + } + }); - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + this.body.emitter.emit('change', { queue: true }); }; /** - * Move the range with a given delta to the left or right. Start and end - * value will be adjusted. For example, try delta = 0.1 or -0.1 - * @param {Number} delta Moving amount. Positive value will move right, - * negative value will move left + * Handle removed groups + * @param {Number[]} ids + * @private */ - Range.prototype.move = function (delta) { - // zoom start Date and end Date relative to the centerDate - var diff = this.end - this.start; + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + if (group) { + group.hide(); + delete groups[id]; + } + }); - // TODO: reckon with min and max range + this.markDirty(); - this.start = newStart; - this.end = newEnd; + this.body.emitter.emit('change', { queue: true }); }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Reorder the groups if needed + * @return {boolean} changed + * @private */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); - var diff = center - moveTo; + 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(); + }); - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); - this.setRange(newStart, newEnd); + this.groupIds = groupIds; + } + + return changed; + } else { + return false; + } }; - module.exports = Range; + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; -/***/ }, -/* 41 */ -/***/ function(module, exports, __webpack_require__) { + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + }; - 'use strict'; + /** + * 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; + + // update the items data (will redraw the item when displayed) + item.setData(itemData); + + // update group + if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - var Hammer = __webpack_require__(15); + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + } + }; /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * 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 */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst && !isTouching) { - callback(event); + ItemSet.prototype._removeItem = function (item) { + // remove from DOM + item.hide(); - isTouching = true; - setTimeout(function () { - isTouching = false; - }, 0); - } - }; + // remove from items + delete this.items[item.id]; - hammer.on('hammer.input', callback.inputHandler); - }; + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - // isTouching is true while a touch action is being emitted - // this is a hack to prevent `touch` from being fired twice - var isTouching = false; + // remove from group + item.parent && item.parent.remove(item); + }; /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal && !isReleasing) { - callback(event); + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - isReleasing = true; - setTimeout(function () { - isReleasing = false; - }, 0); + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); } - }; - - return hammer.on('hammer.input', callback.inputHandler); + } + return endArray; }; - // isReleasing is true while a release action is being emitted - // this is a hack to prevent `release` from being fired twice - var isReleasing = false; - /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Register the clicked item on touch, before dragStart is initiated. + * + * 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. + * + * @param {Event} event + * @private */ - exports.offTouch = function (hammer, callback) { - hammer.off('hammer.input', callback.inputHandler); + 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; }; /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Start dragging the selected events + * @param {Event} event + * @private */ - exports.offRelease = exports.offTouch; + ItemSet.prototype._onDragStart = function (event) { + var item = this.touchParams.item || null; + var me = this; + var props; -/***/ }, -/* 42 */ -/***/ function(module, exports, __webpack_require__) { + if (item && item.selected) { - "use strict"; + if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { + return; + } - var moment = __webpack_require__(20); + // override options.editable + if (item.editable === false) { + return; + } - /** - * used in Core to convert the options into a volatile variable - * - * @param Core - */ - exports.convertHiddenOptions = function (body, hiddenDates) { - body.hiddenDates = []; - if (hiddenDates) { - if (Array.isArray(hiddenDates) == true) { - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat === undefined) { - var dateItem = {}; - dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); - dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); - body.hiddenDates.push(dateItem); - } - } - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; + + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x, + dragLeft: true, + data: util.extend({}, item.data) // clone the items data + }; + + 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 + }; + + 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 + }; + + return props; + }); } + + 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); } }; /** - * create new entrees for the repeating hidden dates - * @param body - * @param hiddenDates + * Start creating a new range item by dragging. + * @param {Event} event + * @private */ - exports.updateHiddenDates = function (body, hiddenDates) { - if (hiddenDates && body.domProps.centerContainer.width !== undefined) { - exports.convertHiddenOptions(body, hiddenDates); - - var start = moment(body.range.start); - var end = moment(body.range.end); - - var totalRange = body.range.end - body.range.start; - var pixelTime = totalRange / body.domProps.centerContainer.width; + 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; - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat !== undefined) { - var startDate = moment(hiddenDates[i].start); - var endDate = moment(hiddenDates[i].end); + var itemData = { + type: 'range', + start: start, + end: end, + content: 'new item' + }; - if (startDate._d == "Invalid Date") { - throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); - } - if (endDate._d == "Invalid Date") { - throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); - } + var id = util.randomUUID(); + itemData[this.itemsData._fieldId] = id; - var duration = endDate - startDate; - if (duration >= 4 * pixelTime) { + var group = this.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } - var offset = 0; - var runUntil = end.clone(); - switch (hiddenDates[i].repeat) { - case "daily": - // case of time - if (startDate.day() != endDate.day()) { - offset = 1; - } - startDate.dayOfYear(start.dayOfYear()); - startDate.year(start.year()); - startDate.subtract(7, "days"); + 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); - endDate.dayOfYear(start.dayOfYear()); - endDate.year(start.year()); - endDate.subtract(7 - offset, "days"); + var props = { + item: newItem, + dragRight: true, + initialX: event.center.x, + data: util.extend({}, itemData) + }; + this.touchParams.itemProps = [props]; - runUntil.add(1, "weeks"); - break; - case "weekly": - var dayOffset = endDate.diff(startDate, "days"); - var day = startDate.day(); + event.stopPropagation(); + }; - // set the start date to the range.start - startDate.date(start.date()); - startDate.month(start.month()); - startDate.year(start.year()); - endDate = startDate.clone(); + /** + * Drag selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - // force - startDate.day(day); - endDate.day(day); - endDate.add(dayOffset, "days"); + 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(); - startDate.subtract(1, "weeks"); - endDate.subtract(1, "weeks"); + // 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; - runUntil.add(1, "weeks"); - break; - case "monthly": - if (startDate.month() != endDate.month()) { - offset = 1; - } - startDate.month(start.month()); - startDate.year(start.year()); - startDate.subtract(1, "months"); + var itemData = util.extend({}, props.item.data); // clone the data - endDate.month(start.month()); - endDate.year(start.year()); - endDate.subtract(1, "months"); - endDate.add(offset, "months"); + if (props.item.editable === false) { + return; + } - runUntil.add(1, "months"); - break; - case "yearly": - if (startDate.year() != endDate.year()) { - offset = 1; - } - startDate.year(start.year()); - startDate.subtract(1, "years"); - endDate.year(start.year()); - endDate.subtract(1, "years"); - endDate.add(offset, "years"); + var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; - runUntil.add(1, "years"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; + if (updateTimeAllowed) { + 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; } - while (startDate < runUntil) { - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - switch (hiddenDates[i].repeat) { - case "daily": - startDate.add(1, "days"); - endDate.add(1, "days"); - break; - case "weekly": - startDate.add(1, "weeks"); - endDate.add(1, "weeks"); - break; - case "monthly": - startDate.add(1, "months"); - endDate.add(1, "months"); - break; - case "yearly": - startDate.add(1, "y"); - endDate.add(1, "y"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; - } + } 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; } - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - } - } - } - // remove duplicates, merge where possible - exports.removeDuplicates(body); - // ensure the new positions are not on hidden dates - var startHidden = exports.isHidden(body.range.start, body.hiddenDates); - var endHidden = exports.isHidden(body.range.end, body.hiddenDates); - var rangeStart = body.range.start; - var rangeEnd = body.range.end; - if (startHidden.hidden == true) { - rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; - } - if (endHidden.hidden == true) { - rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; - } - if (startHidden.hidden == true || endHidden.hidden == true) { - body.range._applyRange(rangeStart, rangeEnd); - } - } - }; + } 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); - /** - * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. - * Scales with N^2 - * @param body - */ - exports.removeDuplicates = function (body) { - var hiddenDates = body.hiddenDates; - var safeDates = []; - for (var i = 0; i < hiddenDates.length; i++) { - for (var j = 0; j < hiddenDates.length; j++) { - if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { - // j inside i - if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[j].remove = true; - } - // j start inside i - else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { - hiddenDates[i].end = hiddenDates[j].end; - hiddenDates[j].remove = true; + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var duration = initialEnd.valueOf() - initialStart.valueOf(); + + 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; + } + } } - // j end inside i - else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[i].start = hiddenDates[j].start; - hiddenDates[j].remove = true; + } + + var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; + + if (updateGroupAllowed && (!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; + } } } - } - } - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].remove !== true) { - safeDates.push(hiddenDates[i]); - } - } + // confirm moving the item + me.options.onMoving(itemData, function (itemData) { + if (itemData) { + props.item.setData(itemData); + } + }); + }); - body.hiddenDates = safeDates; - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + } }; - exports.printDates = function (dates) { - for (var i = 0; i < dates.length; i++) { - console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); + /** + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId + * @private + */ + 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(); + + item.data.group = group.groupId; } }; /** - * Used in TimeStep to avoid the hidden times. - * @param timeStep - * @param previousTime + * End of dragging selected items + * @param {Event} event + * @private */ - exports.stepOverHiddenDates = function (timeStep, previousTime) { - var stepInHidden = false; - var currentValue = timeStep.current.valueOf(); - for (var i = 0; i < timeStep.hiddenDates.length; i++) { - var startDate = timeStep.hiddenDates[i].start; - var endDate = timeStep.hiddenDates[i].end; - if (currentValue >= startDate && currentValue < endDate) { - stepInHidden = true; - break; - } - } + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { - var prevValue = moment(previousTime); - var newValue = moment(endDate); - //check if the next step should be major - if (prevValue.year() != newValue.year()) { - timeStep.switchedYear = true; - } else if (prevValue.month() != newValue.month()) { - timeStep.switchedMonth = true; - } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { - timeStep.switchedDay = true; - } + // prepare a change set for the changed items + var changes = []; + var me = this; + var dataset = this.itemsData.getDataSet(); - timeStep.current = newValue.toDate(); + 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; + + 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); + } + + // 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); + + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit('change'); + } + }); + } + }); + + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); + } } }; - ///** - // * Used in TimeStep to avoid the hidden times. - // * @param timeStep - // * @param previousTime - // */ - //exports.checkFirstStep = function(timeStep) { - // var stepInHidden = false; - // var currentValue = timeStep.current.valueOf(); - // for (var i = 0; i < timeStep.hiddenDates.length; i++) { - // var startDate = timeStep.hiddenDates[i].start; - // var endDate = timeStep.hiddenDates[i].end; - // if (currentValue >= startDate && currentValue < endDate) { - // stepInHidden = true; - // break; - // } - // } - // - // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { - // var newValue = moment(endDate); - // timeStep.current = newValue.toDate(); - // } - //}; - /** - * replaces the Core toScreen methods - * @param Core - * @param time - * @param width - * @returns {number} + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ - exports.toScreen = function (Core, time, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return (time.valueOf() - conversion.offset) * conversion.scale; - } else { - var hidden = exports.isHidden(time, Core.body.hiddenDates); - if (hidden.hidden == true) { - time = hidden.startDate; - } + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); + var shiftKey = event.srcEvent && event.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; + } - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; + var oldSelection = this.getSelection(); + + var item = this.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); + + var newSelection = this.getSelection(); + + // 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, + event: event + }); } }; /** - * Replaces the core toTime methods - * @param body - * @param range - * @param x - * @param width - * @returns {Date} + * Handle creation and updates of an item on double tap + * @param event + * @private */ - exports.toTime = function (Core, x, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return new Date(x / conversion.scale + conversion.offset); + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; + + var me = this; + var snap = this.options.snap || null; + var item = this.itemFromTarget(event); + + event.stopPropagation(); + + if (item) { + // update item + + // 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 { - var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - var totalDuration = Core.range.end - Core.range.start - hiddenDuration; - var partialDuration = totalDuration * x / width; - var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); + // 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(); - var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: 'new item' + }; + + // 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; + } + + newItem[this.itemsData._fieldId] = util.randomUUID(); + + 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? + } + }); } }; /** - * Support function - * - * @param hiddenDates - * @param range - * @returns {number} + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private */ - exports.getHiddenDurationBetween = function (hiddenDates, start, end) { - var duration = 0; - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= start && endDate < end) { - duration += endDate - startDate; + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; + + var item = this.itemFromTarget(event); + + if (item) { + // multi select items (if allowed) + + var selection = this.options.multiselect ? this.getSelection() // take current selection + : []; // deselect current selection + + var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; + + if (shiftKey && this.options.multiselect) { + // select all items between the old selection and the tapped item + + // determine the selection range + selection.push(item.id); + var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + + // 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 (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); + } } + + this.setSelection(selection); + + this.body.emitter.emit('select', { + items: this.getSelection(), + event: event + }); } - return duration; }; /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * 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 */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; - }; + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= range.start && endDate < range.end) { - if (time >= endDate) { - timeOffset += endDate - startDate; + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; } } - } - return timeOffset; + }); + + return { + min: min, + max: max + }; }; /** - * sum the duration from start to finish, including the hidden duration, - * until the required amount has been reached, return the accumulated hidden duration - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * 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 */ - exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { - var hiddenDuration = 0; - var duration = 0; - var previousPoint = range.start; - //exports.printDates(hiddenDates) - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= range.start && endDate < range.end) { - duration += startDate - previousPoint; - previousPoint = endDate; - if (duration >= requiredDuration) { - break; - } else { - hiddenDuration += endDate - startDate; - } + ItemSet.prototype.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; } + target = target.parentNode; } - return hiddenDuration; + return null; }; /** - * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true - * @param hiddenDates - * @param time - * @param direction - * @param correctionEnabled - * @returns {*} + * 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 */ - exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { - var isHidden = exports.isHidden(time, hiddenDates); - if (isHidden.hidden == true) { - if (direction < 0) { - if (correctionEnabled == true) { - return isHidden.startDate - (isHidden.endDate - time) - 1; - } else { - return isHidden.startDate - 1; + 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; + } + + if (this.options.orientation.item === 'top') { + if (i === this.groupIds.length - 1 && clientY > top) { + return group; } } else { - if (correctionEnabled == true) { - return isHidden.endDate + (time - isHidden.startDate) + 1; - } else { - return isHidden.endDate + 1; + if (i === 0 && clientY < top + foreground.offset) { + return group; } } - } else { - return time; } + + return null; }; /** - * Check if a time is hidden - * - * @param time - * @param hiddenDates - * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} + * 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 */ - exports.isHidden = function (time, hiddenDates) { - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - - if (time >= startDate && time < endDate) { - // if the start is entering a hidden zone - return { hidden: true, startDate: startDate, endDate: endDate }; - break; + ItemSet.itemSetFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; } + target = target.parentNode; } - return { hidden: false, startDate: startDate, endDate: endDate }; + + return null; }; + module.exports = ItemSet; + /***/ }, -/* 43 */ +/* 35 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var stack = __webpack_require__(44); - var RangeItem = __webpack_require__(45); + var util = __webpack_require__(9); + var stack = __webpack_require__(36); + var RangeItem = __webpack_require__(37); /** * @constructor Group @@ -21256,7 +19041,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Group; /***/ }, -/* 44 */ +/* 36 */ /***/ function(module, exports, __webpack_require__) { // Utility functions for ordering and stacking of items @@ -21380,13 +19165,13 @@ return /******/ (function(modules) { // webpackBootstrap }; /***/ }, -/* 45 */ +/* 37 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(15); - var Item = __webpack_require__(14); + var Hammer = __webpack_require__(5); + var Item = __webpack_require__(4); /** * @constructor RangeItem @@ -21676,14 +19461,14 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = RangeItem; /***/ }, -/* 46 */ +/* 38 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var moment = __webpack_require__(20); - var DateUtil = __webpack_require__(42); - var util = __webpack_require__(19); + var moment = __webpack_require__(10); + var DateUtil = __webpack_require__(32); + var util = __webpack_require__(9); /** * @constructor TimeStep @@ -22366,13 +20151,13 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = TimeStep; /***/ }, -/* 47 */ +/* 39 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var Group = __webpack_require__(43); + var util = __webpack_require__(9); + var Group = __webpack_require__(35); /** * @constructor BackgroundGroup @@ -22430,13 +20215,13 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundGroup; /***/ }, -/* 48 */ +/* 40 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Item = __webpack_require__(14); - var util = __webpack_require__(19); + var Item = __webpack_require__(4); + var util = __webpack_require__(9); /** * @constructor BoxItem @@ -22670,15 +20455,15 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BoxItem; /***/ }, -/* 49 */ +/* 41 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(15); - var Item = __webpack_require__(14); - var BackgroundGroup = __webpack_require__(47); - var RangeItem = __webpack_require__(45); + var Hammer = __webpack_require__(5); + var Item = __webpack_require__(4); + var BackgroundGroup = __webpack_require__(39); + var RangeItem = __webpack_require__(37); /** * @constructor BackgroundItem @@ -22891,16 +20676,16 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundItem; /***/ }, -/* 50 */ +/* 42 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var Component = __webpack_require__(38); - var TimeStep = __webpack_require__(46); - var DateUtil = __webpack_require__(42); - var moment = __webpack_require__(20); + var util = __webpack_require__(9); + var Component = __webpack_require__(28); + var TimeStep = __webpack_require__(38); + var DateUtil = __webpack_require__(32); + var moment = __webpack_require__(10); /** * A horizontal time axis @@ -23330,15 +21115,15 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = TimeAxis; /***/ }, -/* 51 */ +/* 43 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var keycharm = __webpack_require__(52); - var Emitter = __webpack_require__(30); - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); + var keycharm = __webpack_require__(44); + var Emitter = __webpack_require__(20); + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); /** * Turn an element into an clickToUse element. @@ -23489,7 +21274,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Activator; /***/ }, -/* 52 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; @@ -23688,7 +21473,247 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 53 */ +/* 45 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); + var Component = __webpack_require__(28); + var moment = __webpack_require__(10); + var locales = __webpack_require__(29); + + /** + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {number | string} id + * {string} locales + * {string} locale + * @constructor CustomTime + * @extends Component + */ + + function CustomTime(body, options) { + this.body = body; + + // default options + this.defaultOptions = { + locales: locales, + locale: 'en', + id: undefined + }; + this.options = util.extend({}, this.defaultOptions); + + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); + } + + this.eventParams = {}; // stores state parameters while dragging the bar + + this.setOptions(options); + + // create the DOM + this._create(); + } + + CustomTime.prototype = new Component(); + + /** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {number | string} id + * {string} locales + * {string} locale + */ + CustomTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['locale', 'locales', 'id'], this.options, options); + } + }; + + /** + * Create the DOM for the custom time + * @private + */ + CustomTime.prototype._create = function () { + var bar = document.createElement('div'); + bar['custom-time'] = this; + bar.className = 'vis-custom-time ' + (this.options.id || ''); + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + this.bar = bar; + + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); + + // attach event listeners + this.hammer = new Hammer(drag); + this.hammer.on('panstart', this._onDragStart.bind(this)); + this.hammer.on('panmove', this._onDrag.bind(this)); + this.hammer.on('panend', this._onDragEnd.bind(this)); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + // TODO: cleanup + //this.hammer.on('pan', function (event) { + // event.preventDefault(); + //}); + }; + + /** + * Destroy the CustomTime bar + */ + CustomTime.prototype.destroy = function () { + this.hide(); + + this.hammer.destroy(); + this.hammer = null; + + this.body = null; + }; + + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + CustomTime.prototype.redraw = function () { + var parent = this.body.dom.backgroundVertical; + if (this.bar.parentNode != parent) { + // attach to the dom + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + parent.appendChild(this.bar); + } + + var x = this.body.util.toScreen(this.customTime); + + var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; + } + locale = this.options.locales['en']; // fall back on english when not available + } + var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); + title = title.charAt(0).toUpperCase() + title.substring(1); + + this.bar.style.left = x + 'px'; + this.bar.title = title; + + return false; + }; + + /** + * Remove the CustomTime from the DOM + */ + CustomTime.prototype.hide = function () { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + }; + + /** + * Set custom time. + * @param {Date | number | string} time + */ + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, 'Date'); + this.redraw(); + }; + + /** + * Retrieve the current custom time. + * @return {Date} customTime + */ + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); + }; + + /** + * Start moving horizontally + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; + + event.stopPropagation(); + }; + + /** + * Perform moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; + + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); + + this.setCustomTime(time); + + // fire a timechange event + this.body.emitter.emit('timechange', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); + }; + + /** + * Stop moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; + + // fire a timechanged event + this.body.emitter.emit('timechanged', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); + }; + + /** + * Find a custom time from an event target: + * searches for the attribute 'custom-time' in the event target's element tree + * @param {Event} event + * @return {CustomTime | null} customTime + */ + CustomTime.customTimeFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('custom-time')) { + return target['custom-time']; + } + target = target.parentNode; + } + + return null; + }; + + module.exports = CustomTime; + +/***/ }, +/* 46 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -23703,11 +21728,11 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _ColorPicker = __webpack_require__(54); + var _ColorPicker = __webpack_require__(47); var _ColorPicker2 = _interopRequireDefault(_ColorPicker); - var util = __webpack_require__(19); + var util = __webpack_require__(9); /** * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. @@ -24371,7 +22396,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 54 */ +/* 47 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -24384,9 +22409,9 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var Hammer = __webpack_require__(15); - var hammerUtil = __webpack_require__(41); - var util = __webpack_require__(19); + var Hammer = __webpack_require__(5); + var hammerUtil = __webpack_require__(31); + var util = __webpack_require__(9); var ColorPicker = (function () { function ColorPicker() { @@ -24955,7 +22980,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 55 */ +/* 48 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -24968,7 +22993,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(19); + var util = __webpack_require__(9); var errorFound = false; var allOptions = undefined; @@ -25225,9 +23250,9 @@ return /******/ (function(modules) { // webpackBootstrap // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript /* Copyright (c) 2011 Andrei Mackenzie - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ value: function levenshteinDistance(a, b) { if (a.length === 0) return b.length; @@ -25271,7 +23296,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.printStyle = printStyle; /***/ }, -/* 56 */ +/* 49 */ /***/ function(module, exports, __webpack_require__) { /** @@ -25485,28 +23510,28 @@ return /******/ (function(modules) { // webpackBootstrap exports.configureOptions = configureOptions; /***/ }, -/* 57 */ +/* 50 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Emitter = __webpack_require__(30); - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var Range = __webpack_require__(40); - var Core = __webpack_require__(11); - var TimeAxis = __webpack_require__(50); - var CurrentTime = __webpack_require__(37); - var CustomTime = __webpack_require__(13); - var LineGraph = __webpack_require__(58); - - var Configurator = __webpack_require__(53); - var Validator = __webpack_require__(55)['default']; - var printStyle = __webpack_require__(55).printStyle; - var allOptions = __webpack_require__(65).allOptions; - var configureOptions = __webpack_require__(65).configureOptions; + var Emitter = __webpack_require__(20); + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var Range = __webpack_require__(30); + var Core = __webpack_require__(33); + var TimeAxis = __webpack_require__(42); + var CurrentTime = __webpack_require__(27); + var CustomTime = __webpack_require__(45); + var LineGraph = __webpack_require__(51); + + var Configurator = __webpack_require__(46); + var Validator = __webpack_require__(48)['default']; + var printStyle = __webpack_require__(48).printStyle; + var allOptions = __webpack_require__(59).allOptions; + var configureOptions = __webpack_require__(59).configureOptions; /** * Create a timeline visualization @@ -25821,21 +23846,21 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Graph2d; /***/ }, -/* 58 */ +/* 51 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var DOMutil = __webpack_require__(24); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - var Component = __webpack_require__(38); - var DataAxis = __webpack_require__(59); - var GraphGroup = __webpack_require__(61); - var Legend = __webpack_require__(64); - var BarFunctions = __webpack_require__(63); - var LineFunctions = __webpack_require__(62); + var util = __webpack_require__(9); + var DOMutil = __webpack_require__(14); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + var Component = __webpack_require__(28); + var DataAxis = __webpack_require__(52); + var GraphGroup = __webpack_require__(54); + var Legend = __webpack_require__(58); + var BarFunctions = __webpack_require__(57); + var LineFunctions = __webpack_require__(55); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -26797,15 +24822,15 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = LineGraph; /***/ }, -/* 59 */ +/* 52 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var DOMutil = __webpack_require__(24); - var Component = __webpack_require__(38); - var DataStep = __webpack_require__(60); + var util = __webpack_require__(9); + var DOMutil = __webpack_require__(14); + var Component = __webpack_require__(28); + var DataStep = __webpack_require__(53); /** * A horizontal time axis @@ -27401,7 +25426,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = DataAxis; /***/ }, -/* 60 */ +/* 53 */ /***/ function(module, exports, __webpack_require__) { /** @@ -27628,16 +25653,16 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = DataStep; /***/ }, -/* 61 */ +/* 54 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var DOMutil = __webpack_require__(24); - var Line = __webpack_require__(62); - var Bar = __webpack_require__(63); - var Points = __webpack_require__(10); + var util = __webpack_require__(9); + var DOMutil = __webpack_require__(14); + var Line = __webpack_require__(55); + var Bar = __webpack_require__(57); + var Points = __webpack_require__(56); /** * /** @@ -27838,13 +25863,13 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = GraphGroup; /***/ }, -/* 62 */ +/* 55 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var DOMutil = __webpack_require__(24); - var Points = __webpack_require__(10); + var DOMutil = __webpack_require__(14); + var Points = __webpack_require__(56); function Line(groupId, options) { this.groupId = groupId; @@ -28133,13 +26158,92 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Line; /***/ }, -/* 63 */ +/* 56 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var DOMutil = __webpack_require__(14); + + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } + + Points.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; + + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); + }; + + /** + * draw the data points + * + * @param {Array} dataset + * @param {Object} JSONcontainer + * @param {Object} svg | SVG DOM element + * @param {GraphGroup} group + * @param {Number} [offset] + */ + Points.draw = function (dataset, group, framework, offset) { + offset = offset || 0; + var callback = getCallback(); + + for (var i = 0; i < dataset.length; i++) { + if (!callback) { + // draw the point the simple way. + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label); + } else { + var callbackResult = callback(dataset[i], group, framework); // result might be true, false or an object + if (callbackResult === true || typeof callbackResult === 'object') { + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label); + } + } + } + + function getGroupTemplate(callbackResult) { + callbackResult = typeof callbackResult === 'undefined' ? {} : callbackResult; + return { + style: callbackResult.style || group.options.drawPoints.style, + size: callbackResult.size || group.options.drawPoints.size, + className: callbackResult.className || group.className + }; + } + + function getCallback() { + var callback = undefined; + // check for the graph2d onRender + if (framework.options.drawPoints.onRender && typeof framework.options.drawPoints.onRender == 'function') { + callback = framework.options.drawPoints.onRender; + } + + // override it with the group onRender if defined + if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') { + callback = group.group.options.drawPoints.onRender; + } + + return callback; + } + }; + + module.exports = Points; + +/***/ }, +/* 57 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var DOMutil = __webpack_require__(24); - var Points = __webpack_require__(10); + var DOMutil = __webpack_require__(14); + var Points = __webpack_require__(56); function Bargraph(groupId, options) { this.groupId = groupId; @@ -28381,14 +26485,14 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Bargraph; /***/ }, -/* 64 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(19); - var DOMutil = __webpack_require__(24); - var Component = __webpack_require__(38); + var util = __webpack_require__(9); + var DOMutil = __webpack_require__(14); + var Component = __webpack_require__(28); /** * Legend for Graph2d @@ -28595,7 +26699,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Legend; /***/ }, -/* 65 */ +/* 59 */ /***/ function(module, exports, __webpack_require__) { /** @@ -28864,7 +26968,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.configureOptions = configureOptions; /***/ }, -/* 66 */ +/* 60 */ /***/ function(module, exports, __webpack_require__) { // Load custom shapes into CanvasRenderingContext2D @@ -28872,59 +26976,59 @@ return /******/ (function(modules) { // webpackBootstrap function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - var _modulesGroups = __webpack_require__(67); + var _modulesGroups = __webpack_require__(61); var _modulesGroups2 = _interopRequireDefault(_modulesGroups); - var _modulesNodesHandler = __webpack_require__(68); + var _modulesNodesHandler = __webpack_require__(62); var _modulesNodesHandler2 = _interopRequireDefault(_modulesNodesHandler); - var _modulesEdgesHandler = __webpack_require__(86); + var _modulesEdgesHandler = __webpack_require__(82); var _modulesEdgesHandler2 = _interopRequireDefault(_modulesEdgesHandler); - var _modulesPhysicsEngine = __webpack_require__(90); + var _modulesPhysicsEngine = __webpack_require__(89); var _modulesPhysicsEngine2 = _interopRequireDefault(_modulesPhysicsEngine); - var _modulesClustering = __webpack_require__(99); + var _modulesClustering = __webpack_require__(98); var _modulesClustering2 = _interopRequireDefault(_modulesClustering); - var _modulesCanvasRenderer = __webpack_require__(101); + var _modulesCanvasRenderer = __webpack_require__(100); var _modulesCanvasRenderer2 = _interopRequireDefault(_modulesCanvasRenderer); - var _modulesCanvas = __webpack_require__(102); + var _modulesCanvas = __webpack_require__(101); var _modulesCanvas2 = _interopRequireDefault(_modulesCanvas); - var _modulesView = __webpack_require__(103); + var _modulesView = __webpack_require__(102); var _modulesView2 = _interopRequireDefault(_modulesView); - var _modulesInteractionHandler = __webpack_require__(104); + var _modulesInteractionHandler = __webpack_require__(103); var _modulesInteractionHandler2 = _interopRequireDefault(_modulesInteractionHandler); - var _modulesSelectionHandler = __webpack_require__(106); + var _modulesSelectionHandler = __webpack_require__(3); var _modulesSelectionHandler2 = _interopRequireDefault(_modulesSelectionHandler); - var _modulesLayoutEngine = __webpack_require__(107); + var _modulesLayoutEngine = __webpack_require__(106); var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); - var _modulesManipulationSystem = __webpack_require__(8); + var _modulesManipulationSystem = __webpack_require__(107); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); - var _sharedConfigurator = __webpack_require__(53); + var _sharedConfigurator = __webpack_require__(46); var _sharedConfigurator2 = _interopRequireDefault(_sharedConfigurator); - var _sharedValidator = __webpack_require__(55); + var _sharedValidator = __webpack_require__(48); var _sharedValidator2 = _interopRequireDefault(_sharedValidator); @@ -28932,15 +27036,15 @@ return /******/ (function(modules) { // webpackBootstrap __webpack_require__(109); - var Emitter = __webpack_require__(30); - var Hammer = __webpack_require__(15); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); + var Emitter = __webpack_require__(20); + var Hammer = __webpack_require__(5); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); var dotparser = __webpack_require__(110); var gephiParser = __webpack_require__(111); var Images = __webpack_require__(112); - var Activator = __webpack_require__(51); + var Activator = __webpack_require__(43); var locales = __webpack_require__(113); /** @@ -29474,7 +27578,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Network; /***/ }, -/* 67 */ +/* 61 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -29487,7 +27591,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var util = __webpack_require__(19); + var util = __webpack_require__(9); /** * @class Groups @@ -29616,7 +27720,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 68 */ +/* 62 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29631,17 +27735,17 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNode = __webpack_require__(69); + var _componentsNode = __webpack_require__(63); var _componentsNode2 = _interopRequireDefault(_componentsNode); - var _componentsSharedLabel = __webpack_require__(70); + var _componentsSharedLabel = __webpack_require__(64); var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); var NodesHandler = (function () { function NodesHandler(body, images, groups, layoutEngine) { @@ -30094,7 +28198,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 69 */ +/* 63 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30109,71 +28213,71 @@ 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__(70); + var _sharedLabel = __webpack_require__(64); var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - var _nodesShapesBox = __webpack_require__(71); + var _nodesShapesBox = __webpack_require__(65); var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); - var _nodesShapesCircle = __webpack_require__(72); + var _nodesShapesCircle = __webpack_require__(67); var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); - var _nodesShapesCircularImage = __webpack_require__(74); + var _nodesShapesCircularImage = __webpack_require__(69); var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); - var _nodesShapesDatabase = __webpack_require__(75); + var _nodesShapesDatabase = __webpack_require__(70); var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); - var _nodesShapesDiamond = __webpack_require__(76); + var _nodesShapesDiamond = __webpack_require__(71); var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); - var _nodesShapesDot = __webpack_require__(78); + var _nodesShapesDot = __webpack_require__(73); var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); - var _nodesShapesEllipse = __webpack_require__(79); + var _nodesShapesEllipse = __webpack_require__(74); var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); - var _nodesShapesIcon = __webpack_require__(5); + var _nodesShapesIcon = __webpack_require__(75); var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); - var _nodesShapesImage = __webpack_require__(80); + var _nodesShapesImage = __webpack_require__(76); var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); - var _nodesShapesSquare = __webpack_require__(81); + var _nodesShapesSquare = __webpack_require__(77); var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); - var _nodesShapesStar = __webpack_require__(82); + var _nodesShapesStar = __webpack_require__(78); var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); - var _nodesShapesText = __webpack_require__(83); + var _nodesShapesText = __webpack_require__(79); var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); - var _nodesShapesTriangle = __webpack_require__(84); + var _nodesShapesTriangle = __webpack_require__(80); var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); - var _nodesShapesTriangleDown = __webpack_require__(85); + var _nodesShapesTriangleDown = __webpack_require__(81); var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); - var _sharedValidator = __webpack_require__(55); + var _sharedValidator = __webpack_require__(48); var _sharedValidator2 = _interopRequireDefault(_sharedValidator); - var util = __webpack_require__(19); + var util = __webpack_require__(9); /** * @class Node @@ -30614,7 +28718,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 70 */ +/* 64 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30629,7 +28733,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(19); + var util = __webpack_require__(9); var Label = (function () { function Label(body, options) { @@ -30904,33 +29008,550 @@ return /******/ (function(modules) { // webpackBootstrap this.lines = lines; this.lineCount = lineCount; - return width; - } - }], [{ - key: 'parseOptions', - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + return width; + } + }], [{ + key: 'parseOptions', + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + + if (typeof newOptions.font === 'string') { + var newOptionsArray = newOptions.font.split(' '); + parentOptions.size = newOptionsArray[0].replace('px', ''); + parentOptions.face = newOptionsArray[1]; + parentOptions.color = newOptionsArray[2]; + } else if (typeof newOptions.font === 'object') { + util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); + } + parentOptions.size = Number(parentOptions.size); + } + }]); + + return Label; + })(); + + exports['default'] = Label; + module.exports = exports['default']; + +/***/ }, +/* 65 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilNodeBase = __webpack_require__(66); + + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + + var Box = (function (_NodeBase) { + function Box(options, body, labelModule) { + _classCallCheck(this, Box); + + _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Box, _NodeBase); + + _createClass(Box, [{ + key: 'resize', + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + this.radius = 0.5 * this.width; + } + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + + var borderRadius = 6; + ctx.roundRect(this.left, this.top, this.width, this.height, borderRadius); + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + + ctx.stroke(); + + this.updateBoundingBox(x, y); + this.labelModule.draw(ctx, x, y, selected); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + } + }]); + + return Box; + })(_utilNodeBase2['default']); + + exports['default'] = Box; + module.exports = exports['default']; + +/***/ }, +/* 66 */ +/***/ 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 NodeBase = (function () { + function NodeBase(options, body, labelModule) { + _classCallCheck(this, NodeBase); + + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.top = undefined; + this.left = undefined; + this.height = undefined; + this.width = undefined; + this.radius = undefined; + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + } + + _createClass(NodeBase, [{ + key: 'setOptions', + value: function setOptions(options) { + this.options = options; + } + }, { + key: '_distanceToBorder', + value: function _distanceToBorder(angle) { + var borderWidth = 1; + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + } + }, { + key: 'enableShadow', + value: function enableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = this.options.shadow.size; + ctx.shadowOffsetX = this.options.shadow.x; + ctx.shadowOffsetY = this.options.shadow.y; + } + } + }, { + key: 'disableShadow', + value: function disableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0)'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } + } + }]); + + return NodeBase; + })(); + + exports['default'] = NodeBase; + module.exports = exports['default']; + +/***/ }, +/* 67 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilCircleImageBase = __webpack_require__(68); + + var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); + + var Circle = (function (_CircleImageBase) { + function Circle(options, body, labelModule) { + _classCallCheck(this, Circle); + + _get(Object.getPrototypeOf(Circle.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Circle, _CircleImageBase); + + _createClass(Circle, [{ + key: 'resize', + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.size = diameter / 2; + + this.width = diameter; + this.height = diameter; + this.radius = 0.5 * this.width; + } + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + this.updateBoundingBox(x, y); + this.labelModule.draw(ctx, x, y, selected); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + } + }]); + + return Circle; + })(_utilCircleImageBase2['default']); + + exports['default'] = Circle; + module.exports = exports['default']; + +/***/ }, +/* 68 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilNodeBase = __webpack_require__(66); + + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + + var CircleImageBase = (function (_NodeBase) { + function CircleImageBase(options, body, labelModule) { + _classCallCheck(this, CircleImageBase); + + _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); + this.labelOffset = 0; + this.imageLoaded = false; + } + + _inherits(CircleImageBase, _NodeBase); + + _createClass(CircleImageBase, [{ + key: '_resizeImage', + + /** + * This function resizes the image by the options size when the image has not yet loaded. If the image has loaded, we + * force the update of the size again. + * + * @private + */ + value: function _resizeImage() { + var force = false; + if (!this.imageObj.width || !this.imageObj.height) { + // undefined or 0 + this.imageLoaded = false; + } else if (this.imageLoaded === false) { + this.imageLoaded = true; + force = true; + } + + if (!this.width || !this.height || force === true) { + // undefined or 0 + var width, height, ratio; + if (this.imageObj.width && this.imageObj.height) { + // not undefined or 0 + width = 0; + height = 0; + } + if (this.imageObj.width > this.imageObj.height) { + ratio = this.imageObj.width / this.imageObj.height; + width = this.options.size * 2 * ratio || this.imageObj.width; + height = this.options.size * 2 || this.imageObj.height; + } else { + if (this.imageObj.width && this.imageObj.height) { + // not undefined or 0 + ratio = this.imageObj.height / this.imageObj.width; + } else { + ratio = 1; + } + width = this.options.size * 2 || this.imageObj.width; + height = this.options.size * 2 * ratio || this.imageObj.height; + } + this.width = width; + this.height = height; + this.radius = 0.5 * this.width; + } + } + }, { + key: '_drawRawCircle', + value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(x, y, size); + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + + ctx.stroke(); + } + }, { + key: '_drawImageAtPosition', + value: function _drawImageAtPosition(ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + + // disable shadows for other elements. + this.disableShadow(ctx); + } + } + }, { + key: '_drawImageLabel', + value: function _drawImageLabel(ctx, x, y, selected) { + var yLabel; + var offset = 0; + + if (this.height !== undefined) { + offset = this.height * 0.5; + var labelDimensions = this.labelModule.getTextSize(ctx); + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + } + } + + yLabel = y + offset; + + if (this.options.label) { + this.labelOffset = offset; + } + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + } + }]); + + return CircleImageBase; + })(_utilNodeBase2['default']); + + exports['default'] = CircleImageBase; + module.exports = exports['default']; + +/***/ }, +/* 69 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilCircleImageBase = __webpack_require__(68); + + var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); + + var CircularImage = (function (_CircleImageBase) { + function CircularImage(options, body, labelModule, imageObj) { + _classCallCheck(this, CircularImage); + + _get(Object.getPrototypeOf(CircularImage.prototype), 'constructor', this).call(this, options, body, labelModule); + this.imageObj = imageObj; + this._swapToImageResizeWhenImageLoaded = true; + } + + _inherits(CircularImage, _CircleImageBase); + + _createClass(CircularImage, [{ + key: 'resize', + value: function resize() { + if (this.imageObj.src === undefined || this.imageObj.width === undefined || this.imageObj.height === undefined) { + if (!this.width) { + var diameter = this.options.size * 2; + this.width = diameter; + this.height = diameter; + this._swapToImageResizeWhenImageLoaded = true; + this.radius = 0.5 * this.width; + } + } else { + if (this._swapToImageResizeWhenImageLoaded) { + this.width = undefined; + this.height = undefined; + this._swapToImageResizeWhenImageLoaded = false; + } + this._resizeImage(); + } + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(); + + this.left = x - this.width / 2; + this.top = y - this.height / 2; - if (typeof newOptions.font === 'string') { - var newOptionsArray = newOptions.font.split(' '); - parentOptions.size = newOptionsArray[0].replace('px', ''); - parentOptions.face = newOptionsArray[1]; - parentOptions.color = newOptionsArray[2]; - } else if (typeof newOptions.font === 'object') { - util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); - } - parentOptions.size = Number(parentOptions.size); + var size = Math.min(0.5 * this.height, 0.5 * this.width); + + this._drawRawCircle(ctx, x, y, selected, hover, size); + + ctx.save(); + ctx.circle(x, y, size); + ctx.stroke(); + ctx.clip(); + + this._drawImageAtPosition(ctx); + + ctx.restore(); + + this._drawImageLabel(ctx, x, y, selected); + + this.updateBoundingBox(x, y); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); } }]); - return Label; - })(); + return CircularImage; + })(_utilCircleImageBase2['default']); - exports['default'] = Label; + exports['default'] = CircularImage; module.exports = exports['default']; /***/ }, -/* 71 */ +/* 70 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30949,27 +29570,28 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(3); + var _utilNodeBase = __webpack_require__(66); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var Box = (function (_NodeBase) { - function Box(options, body, labelModule) { - _classCallCheck(this, Box); + var Database = (function (_NodeBase) { + function Database(options, body, labelModule) { + _classCallCheck(this, Database); - _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Database.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Box, _NodeBase); + _inherits(Database, _NodeBase); - _createClass(Box, [{ + _createClass(Database, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; this.radius = 0.5 * this.width; } } @@ -30984,14 +29606,12 @@ return /******/ (function(modules) { // webpackBootstrap var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - - var borderRadius = 6; - ctx.roundRect(this.left, this.top, this.width, this.height, borderRadius); + ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); // draw shadow if enabled this.enableShadow(ctx); @@ -31002,12 +29622,15 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); - this.updateBoundingBox(x, y); + this.updateBoundingBox(x, y, ctx); + this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { + value: function updateBoundingBox(x, y, ctx) { + this.resize(ctx); + this.left = x - this.width * 0.5; this.top = y - this.height * 0.5; @@ -31028,14 +29651,14 @@ return /******/ (function(modules) { // webpackBootstrap } }]); - return Box; + return Database; })(_utilNodeBase2['default']); - exports['default'] = Box; + exports['default'] = Database; module.exports = exports['default']; /***/ }, -/* 72 */ +/* 71 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31054,49 +29677,118 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(73); + var _utilShapeBase = __webpack_require__(72); - var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Circle = (function (_CircleImageBase) { - function Circle(options, body, labelModule) { - _classCallCheck(this, Circle); + var Diamond = (function (_ShapeBase) { + function Diamond(options, body, labelModule) { + _classCallCheck(this, Diamond); - _get(Object.getPrototypeOf(Circle.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Diamond.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Circle, _CircleImageBase); + _inherits(Diamond, _ShapeBase); - _createClass(Circle, [{ + _createClass(Diamond, [{ key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.size = diameter / 2; + value: function resize(ctx) { + this._resizeShape(); + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'diamond', 4, x, y, selected, hover); + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + } + }]); - this.width = diameter; - this.height = diameter; + return Diamond; + })(_utilShapeBase2['default']); + + exports['default'] = Diamond; + module.exports = exports['default']; + +/***/ }, +/* 72 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilNodeBase = __webpack_require__(66); + + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + + var ShapeBase = (function (_NodeBase) { + function ShapeBase(options, body, labelModule) { + _classCallCheck(this, ShapeBase); + + _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(ShapeBase, _NodeBase); + + _createClass(ShapeBase, [{ + key: '_resizeShape', + value: function _resizeShape() { + if (this.width === undefined) { + var size = 2 * this.options.size; + this.width = size; + this.height = size; this.radius = 0.5 * this.width; } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); + key: '_drawShape', + value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { + this._resizeShape(); + this.left = x - this.width / 2; this.top = y - this.height / 2; - this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](x, y, this.options.size); + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + + ctx.stroke(); + + if (this.options.label !== undefined) { + var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + } this.updateBoundingBox(x, y); - this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', @@ -31105,27 +29797,79 @@ return /******/ (function(modules) { // webpackBootstrap this.boundingBox.left = x - this.options.size; this.boundingBox.right = x + this.options.size; this.boundingBox.bottom = y + this.options.size; + + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); + } + } + }]); + + return ShapeBase; + })(_utilNodeBase2['default']); + + exports['default'] = ShapeBase; + module.exports = exports['default']; + +/***/ }, +/* 73 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilShapeBase = __webpack_require__(72); + + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + + var Dot = (function (_ShapeBase) { + function Dot(options, body, labelModule) { + _classCallCheck(this, Dot); + + _get(Object.getPrototypeOf(Dot.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Dot, _ShapeBase); + + _createClass(Dot, [{ + key: 'resize', + value: function resize(ctx) { + this._resizeShape(); + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'circle', 2, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + return this.options.size + this.options.borderWidth; } }]); - return Circle; - })(_utilCircleImageBase2['default']); + return Dot; + })(_utilShapeBase2['default']); - exports['default'] = Circle; + exports['default'] = Dot; module.exports = exports['default']; /***/ }, -/* 73 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31144,81 +29888,51 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(3); + var _utilNodeBase = __webpack_require__(66); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var CircleImageBase = (function (_NodeBase) { - function CircleImageBase(options, body, labelModule) { - _classCallCheck(this, CircleImageBase); + var Ellipse = (function (_NodeBase) { + function Ellipse(options, body, labelModule) { + _classCallCheck(this, Ellipse); - _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); - this.labelOffset = 0; - this.imageLoaded = false; + _get(Object.getPrototypeOf(Ellipse.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(CircleImageBase, _NodeBase); - - _createClass(CircleImageBase, [{ - key: '_resizeImage', + _inherits(Ellipse, _NodeBase); - /** - * This function resizes the image by the options size when the image has not yet loaded. If the image has loaded, we - * force the update of the size again. - * - * @private - */ - value: function _resizeImage() { - var force = false; - if (!this.imageObj.width || !this.imageObj.height) { - // undefined or 0 - this.imageLoaded = false; - } else if (this.imageLoaded === false) { - this.imageLoaded = true; - force = true; - } + _createClass(Ellipse, [{ + key: 'resize', + value: function resize(ctx, selected) { + if (this.width === undefined) { + var textSize = this.labelModule.getTextSize(ctx, selected); - if (!this.width || !this.height || force === true) { - // undefined or 0 - var width, height, ratio; - if (this.imageObj.width && this.imageObj.height) { - // not undefined or 0 - width = 0; - height = 0; - } - if (this.imageObj.width > this.imageObj.height) { - ratio = this.imageObj.width / this.imageObj.height; - width = this.options.size * 2 * ratio || this.imageObj.width; - height = this.options.size * 2 || this.imageObj.height; - } else { - if (this.imageObj.width && this.imageObj.height) { - // not undefined or 0 - ratio = this.imageObj.height / this.imageObj.width; - } else { - ratio = 1; - } - width = this.options.size * 2 || this.imageObj.width; - height = this.options.size * 2 * ratio || this.imageObj.height; + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; } - this.width = width; - this.height = height; this.radius = 0.5 * this.width; } } }, { - key: '_drawRawCircle', - value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + var borderWidth = this.options.borderWidth; var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth /= this.body.view.scale; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(x, y, size); + ctx.ellipse(this.left, this.top, this.width, this.height); // draw shadow if enabled this.enableShadow(ctx); @@ -31228,53 +29942,159 @@ return /******/ (function(modules) { // webpackBootstrap this.disableShadow(ctx); ctx.stroke(); + + this.updateBoundingBox(x, y); + this.labelModule.draw(ctx, x, y, selected); } }, { - key: '_drawImageAtPosition', - value: function _drawImageAtPosition(ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y, ctx) { + this.resize(ctx, false); // just in case - // draw shadow if enabled - this.enableShadow(ctx); - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; - // disable shadows for other elements. - this.disableShadow(ctx); + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width * 0.5; + var b = this.height * 0.5; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + } + }]); + + return Ellipse; + })(_utilNodeBase2['default']); + + exports['default'] = Ellipse; + module.exports = exports['default']; + +/***/ }, +/* 75 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilNodeBase = __webpack_require__(66); + + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + + var Icon = (function (_NodeBase) { + function Icon(options, body, labelModule) { + _classCallCheck(this, Icon); + + _get(Object.getPrototypeOf(Icon.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Icon, _NodeBase); + + _createClass(Icon, [{ + key: 'resize', + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var iconSize = { + width: Number(this.options.icon.size), + height: Number(this.options.icon.size) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; + this.radius = 0.5 * this.width; } } }, { - key: '_drawImageLabel', - value: function _drawImageLabel(ctx, x, y, selected) { - var yLabel; - var offset = 0; + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.options.icon.size = this.options.icon.size || 50; - if (this.height !== undefined) { - offset = this.height * 0.5; - var labelDimensions = this.labelModule.getTextSize(ctx); - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - } + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + this._icon(ctx, x, y, selected); + + if (this.options.label !== undefined) { + var iconTextSpacing = 5; + this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); } - yLabel = y + offset; + this.updateBoundingBox(x, y); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.boundingBox.top = y - this.options.icon.size * 0.5; + this.boundingBox.left = x - this.options.icon.size * 0.5; + this.boundingBox.right = x + this.options.icon.size * 0.5; + this.boundingBox.bottom = y + this.options.icon.size * 0.5; - if (this.options.label) { - this.labelOffset = offset; + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + var iconTextSpacing = 5; + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing); } - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + } + }, { + key: '_icon', + value: function _icon(ctx, x, y, selected) { + var iconSize = Number(this.options.icon.size); + + if (this.options.icon.code !== undefined) { + ctx.font = (selected ? 'bold ' : '') + iconSize + 'px ' + this.options.icon.face; + + // draw icon + ctx.fillStyle = this.options.icon.color || 'black'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fillText(this.options.icon.code, x, y); + + // disable shadows for other elements. + this.disableShadow(ctx); + } else { + console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.'); + } + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); } }]); - return CircleImageBase; + return Icon; })(_utilNodeBase2['default']); - exports['default'] = CircleImageBase; + exports['default'] = Icon; module.exports = exports['default']; /***/ }, -/* 74 */ +/* 76 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31293,93 +30113,76 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(73); + var _utilCircleImageBase = __webpack_require__(68); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); - var CircularImage = (function (_CircleImageBase) { - function CircularImage(options, body, labelModule, imageObj) { - _classCallCheck(this, CircularImage); + var Image = (function (_CircleImageBase) { + function Image(options, body, labelModule, imageObj) { + _classCallCheck(this, Image); - _get(Object.getPrototypeOf(CircularImage.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Image.prototype), 'constructor', this).call(this, options, body, labelModule); this.imageObj = imageObj; - this._swapToImageResizeWhenImageLoaded = true; } - _inherits(CircularImage, _CircleImageBase); + _inherits(Image, _CircleImageBase); - _createClass(CircularImage, [{ + _createClass(Image, [{ key: 'resize', value: function resize() { - if (this.imageObj.src === undefined || this.imageObj.width === undefined || this.imageObj.height === undefined) { - if (!this.width) { - var diameter = this.options.size * 2; - this.width = diameter; - this.height = diameter; - this._swapToImageResizeWhenImageLoaded = true; - this.radius = 0.5 * this.width; - } - } else { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = undefined; - this.height = undefined; - this._swapToImageResizeWhenImageLoaded = false; - } - this._resizeImage(); - } + this._resizeImage(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(); - this.left = x - this.width / 2; this.top = y - this.height / 2; - var size = Math.min(0.5 * this.height, 0.5 * this.width); - - this._drawRawCircle(ctx, x, y, selected, hover, size); - - ctx.save(); - ctx.circle(x, y, size); - ctx.stroke(); - ctx.clip(); - this._drawImageAtPosition(ctx); - ctx.restore(); - - this._drawImageLabel(ctx, x, y, selected); + this._drawImageLabel(ctx, x, y, selected || hover); this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + this.resize(); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + } } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); - return this._distanceToBorder(angle); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); } }]); - return CircularImage; + return Image; })(_utilCircleImageBase2['default']); - exports['default'] = CircularImage; + exports['default'] = Image; module.exports = exports['default']; /***/ }, -/* 75 */ +/* 77 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31398,95 +30201,45 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(3); + var _utilShapeBase = __webpack_require__(72); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Database = (function (_NodeBase) { - function Database(options, body, labelModule) { - _classCallCheck(this, Database); + var Square = (function (_ShapeBase) { + function Square(options, body, labelModule) { + _classCallCheck(this, Square); - _get(Object.getPrototypeOf(Database.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Square.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Database, _NodeBase); + _inherits(Square, _ShapeBase); - _createClass(Database, [{ + _createClass(Square, [{ key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; - this.radius = 0.5 * this.width; - } + value: function resize() { + this._resizeShape(); } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); - - // disable shadows for other elements. - this.disableShadow(ctx); - - ctx.stroke(); - - this.updateBoundingBox(x, y, ctx); - - this.labelModule.draw(ctx, x, y, selected); - } - }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y, ctx) { - this.resize(ctx); - - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; + this._drawShape(ctx, 'square', 2, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + this.resize(); + return this._distanceToBorder(angle); } }]); - return Database; - })(_utilNodeBase2['default']); + return Square; + })(_utilShapeBase2['default']); - exports['default'] = Database; + exports['default'] = Square; module.exports = exports['default']; /***/ }, -/* 76 */ +/* 78 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31505,20 +30258,20 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(77); + var _utilShapeBase = __webpack_require__(72); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Diamond = (function (_ShapeBase) { - function Diamond(options, body, labelModule) { - _classCallCheck(this, Diamond); + var Star = (function (_ShapeBase) { + function Star(options, body, labelModule) { + _classCallCheck(this, Star); - _get(Object.getPrototypeOf(Diamond.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Star.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Diamond, _ShapeBase); + _inherits(Star, _ShapeBase); - _createClass(Diamond, [{ + _createClass(Star, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); @@ -31526,7 +30279,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'diamond', 4, x, y, selected, hover); + this._drawShape(ctx, 'star', 4, x, y, selected, hover); } }, { key: 'distanceToBorder', @@ -31535,14 +30288,14 @@ return /******/ (function(modules) { // webpackBootstrap } }]); - return Diamond; + return Star; })(_utilShapeBase2['default']); - exports['default'] = Diamond; + exports['default'] = Star; module.exports = exports['default']; /***/ }, -/* 77 */ +/* 79 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31561,87 +30314,75 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(3); + var _utilNodeBase = __webpack_require__(66); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var ShapeBase = (function (_NodeBase) { - function ShapeBase(options, body, labelModule) { - _classCallCheck(this, ShapeBase); + var Text = (function (_NodeBase) { + function Text(options, body, labelModule) { + _classCallCheck(this, Text); - _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Text.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(ShapeBase, _NodeBase); + _inherits(Text, _NodeBase); - _createClass(ShapeBase, [{ - key: '_resizeShape', - value: function _resizeShape() { + _createClass(Text, [{ + key: 'resize', + value: function resize(ctx, selected) { if (this.width === undefined) { - var size = 2 * this.options.size; - this.width = size; - this.height = size; + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; this.radius = 0.5 * this.width; } } }, { - key: '_drawShape', - value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { - this._resizeShape(); - + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected || hover); this.left = x - this.width / 2; this.top = y - this.height / 2; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](x, y, this.options.size); - // draw shadow if enabled this.enableShadow(ctx); - ctx.fill(); + this.labelModule.draw(ctx, x, y, selected || hover); // disable shadows for other elements. this.disableShadow(ctx); - ctx.stroke(); - - if (this.options.label !== undefined) { - var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); - } - this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + this.resize(); - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); - } + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); } }]); - return ShapeBase; + return Text; })(_utilNodeBase2['default']); - exports['default'] = ShapeBase; + exports['default'] = Text; module.exports = exports['default']; /***/ }, -/* 78 */ +/* 80 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31660,20 +30401,20 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(77); + var _utilShapeBase = __webpack_require__(72); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Dot = (function (_ShapeBase) { - function Dot(options, body, labelModule) { - _classCallCheck(this, Dot); + var Triangle = (function (_ShapeBase) { + function Triangle(options, body, labelModule) { + _classCallCheck(this, Triangle); - _get(Object.getPrototypeOf(Dot.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Triangle.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Dot, _ShapeBase); + _inherits(Triangle, _ShapeBase); - _createClass(Dot, [{ + _createClass(Triangle, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); @@ -31681,220 +30422,514 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'circle', 2, x, y, selected, hover); + this._drawShape(ctx, 'triangle', 3, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { - return this.options.size + this.options.borderWidth; + return this._distanceToBorder(angle); } }]); - return Dot; + return Triangle; })(_utilShapeBase2['default']); - exports['default'] = Dot; + exports['default'] = Triangle; module.exports = exports['default']; /***/ }, -/* 79 */ +/* 81 */ +/***/ 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; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilShapeBase = __webpack_require__(72); + + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + + var TriangleDown = (function (_ShapeBase) { + function TriangleDown(options, body, labelModule) { + _classCallCheck(this, TriangleDown); + + _get(Object.getPrototypeOf(TriangleDown.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(TriangleDown, _ShapeBase); + + _createClass(TriangleDown, [{ + key: 'resize', + value: function resize(ctx) { + this._resizeShape(); + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover); + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + } + }]); + + return TriangleDown; + })(_utilShapeBase2['default']); + + exports['default'] = TriangleDown; + module.exports = exports['default']; + +/***/ }, +/* 82 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); + 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 _componentsEdge = __webpack_require__(83); + + var _componentsEdge2 = _interopRequireDefault(_componentsEdge); + + var _componentsSharedLabel = __webpack_require__(64); + + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + + var util = __webpack_require__(9); + var DataSet = __webpack_require__(15); + var DataView = __webpack_require__(17); + + var EdgesHandler = (function () { + function EdgesHandler(body, images, groups) { + var _this = this; + + _classCallCheck(this, EdgesHandler); + + this.body = body; + this.images = images; + this.groups = groups; + + // create the edge API in the body container + this.body.functions.createEdge = this.create.bind(this); + + this.edgesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items); + }, + remove: function remove(event, params) { + _this.remove(params.items); + } + }; + + this.options = {}; + this.defaultOptions = { + arrows: { + to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: 1 }, + from: { enabled: false, scaleFactor: 1 } + }, + color: { + color: '#848484', + highlight: '#848484', + hover: '#848484', + inherit: 'from', + opacity: 1 + }, + dashes: false, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 2, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + hidden: false, + hoverWidth: 1.5, + label: undefined, + length: undefined, + physics: true, + scaling: { + min: 1, + max: 15, + label: { + enabled: true, + 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); + } + } + }, + selectionWidth: 1.5, + selfReferenceSize: 20, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + smooth: { + enabled: true, + type: 'dynamic', + roundness: 0.5 + }, + title: undefined, + width: 1, + value: undefined + }; + + util.extend(this.options, this.defaultOptions); + + this.bindEventListeners(); + } + + _createClass(EdgesHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this2 = this; + + // this allows external modules to force all dynamic curves to turn static. + this.body.emitter.on('_forceDisableDynamicCurves', function (type) { + if (type === 'dynamic') { + type = 'continuous'; + } + var emitChange = false; + for (var edgeId in _this2.body.edges) { + if (_this2.body.edges.hasOwnProperty(edgeId)) { + var edge = _this2.body.edges[edgeId]; + var edgeData = _this2.body.data.edges._data[edgeId]; + + // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. + // this is because a change in the global would not affect these curves. + if (edgeData !== undefined) { + var edgeOptions = edgeData.smooth; + if (edgeOptions !== undefined) { + if (edgeOptions.enabled === true && edgeOptions.type === 'dynamic') { + if (type === undefined) { + edge.setOptions({ smooth: false }); + } else { + edge.setOptions({ smooth: { type: type } }); + } + emitChange = true; + } + } + } + } + } + if (emitChange === true) { + _this2.body.emitter.emit('_dataChanged'); + } + }); + + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on('_dataUpdated', function () { + _this2.reconnectEdges(); + _this2.markAllEdgesAsDirty(); + }); + + // refresh the edges. Used when reverting from hierarchical layout + this.body.emitter.on('refreshEdges', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); + this.body.emitter.on('destroy', function () { + delete _this2.body.functions.createEdge; + delete _this2.edgesListeners.add; + delete _this2.edgesListeners.update; + delete _this2.edgesListeners.remove; + delete _this2.edgesListeners; + }); + } + }, { + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + // use the parser from the Edge class to fill in all shorthand notations + _componentsEdge2['default'].parseOptions(this.options, options); + + // hanlde multiple input cases for color + if (options.color !== undefined) { + this.markAllEdgesAsDirty(); + } - 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; }; })(); + // update smooth settings in all edges + var dataChanged = false; + if (options.smooth !== undefined) { + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; + } + } + } - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + // update fonts in all edges + if (options.font !== undefined) { + // use the parser from the Label class to fill in all shorthand notations + _componentsSharedLabel2['default'].parseOptions(this.options.font, options); + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].updateLabelModule(); + } + } + } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + // update the state of the variables if needed + if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { + this.body.emitter.emit('_dataChanged'); + } + } + } + }, { + key: 'setData', - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ + value: function setData(edges) { + var _this3 = this; - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var _utilNodeBase = __webpack_require__(3); + var oldEdgesData = this.body.data.edges; - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + if (edges instanceof DataSet || edges instanceof DataView) { + this.body.data.edges = edges; + } else if (Array.isArray(edges)) { + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); + } else if (!edges) { + this.body.data.edges = new DataSet(); + } else { + throw new TypeError('Array or DataSet expected'); + } - var Ellipse = (function (_NodeBase) { - function Ellipse(options, body, labelModule) { - _classCallCheck(this, Ellipse); + // TODO: is this null or undefined or false? + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); + } - _get(Object.getPrototypeOf(Ellipse.prototype), 'constructor', this).call(this, options, body, labelModule); - } + // remove drawn edges + this.body.edges = {}; - _inherits(Ellipse, _NodeBase); + // TODO: is this null or undefined or false? + if (this.body.data.edges) { + // subscribe to new dataset + util.forEach(this.edgesListeners, function (callback, event) { + _this3.body.data.edges.on(event, callback); + }); - _createClass(Ellipse, [{ - key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var textSize = this.labelModule.getTextSize(ctx, selected); + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this.add(ids, true); + } - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } - this.radius = 0.5 * this.width; + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; + key: 'add', - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + var edges = this.body.edges; + var edgesData = this.body.data.edges; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.ellipse(this.left, this.top, this.width, this.height); + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + var data = edgesData.get(id, { 'showInternalIds': true }); + edges[id] = this.create(data); + } - // disable shadows for other elements. - this.disableShadow(ctx); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); + } + } + }, { + key: 'update', - ctx.stroke(); + /** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var data = edgesData.get(id); + var edge = edges[id]; + if (edge === null) { + // update edge + edge.disconnect(); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. + edge.connect(); + } else { + // create edge + this.body.edges[id] = this.create(data); + dataChanged = true; + } + } - this.updateBoundingBox(x, y); - this.labelModule.draw(ctx, x, y, selected); + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); + } else { + this.body.emitter.emit('_dataUpdated'); + } } }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y, ctx) { - this.resize(ctx, false); // just in case + key: 'remove', - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; + /** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var edges = this.body.edges; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge !== undefined) { + edge.edgeType.cleanup(); + edge.disconnect(); + delete edges[id]; + } + } - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; + this.body.emitter.emit('_dataChanged'); } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width * 0.5; - var b = this.height * 0.5; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + key: 'refresh', + value: function refresh() { + var edges = this.body.edges; + for (var edgeId in edges) { + var edge = undefined; + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + } + var data = this.body.data.edges._data[edgeId]; + if (edge !== undefined && data !== undefined) { + edge.setOptions(data); + } + } } - }]); - - return Ellipse; - })(_utilNodeBase2['default']); - - exports['default'] = Ellipse; - module.exports = exports['default']; - -/***/ }, -/* 80 */ -/***/ 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; }; })(); - - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - - 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'); } } - - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - - var _utilCircleImageBase = __webpack_require__(73); - - var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); - - var Image = (function (_CircleImageBase) { - function Image(options, body, labelModule, imageObj) { - _classCallCheck(this, Image); - - _get(Object.getPrototypeOf(Image.prototype), 'constructor', this).call(this, options, body, labelModule); - this.imageObj = imageObj; - } - - _inherits(Image, _CircleImageBase); - - _createClass(Image, [{ - key: 'resize', - value: function resize() { - this._resizeImage(); + }, { + key: 'create', + value: function create(properties) { + return new _componentsEdge2['default'](properties, this.body, this.options); } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - this._drawImageAtPosition(ctx); - - this._drawImageLabel(ctx, x, y, selected || hover); - - this.updateBoundingBox(x, y); + key: 'markAllEdgesAsDirty', + value: function markAllEdgesAsDirty() { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].edgeType.colorDirty = true; + } } }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.resize(); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + key: 'reconnectEdges', - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + /** + * Reconnect all edges + * @private + */ + value: function reconnectEdges() { + var id; + var nodes = this.body.nodes; + var edges = this.body.edges; - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } + + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } } } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + key: 'getConnectedNodes', + value: function getConnectedNodes(edgeId) { + var nodeList = []; + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + if (edge.fromId) { + nodeList.push(edge.fromId); + } + if (edge.toId) { + nodeList.push(edge.toId); + } + } + return nodeList; } }]); - return Image; - })(_utilCircleImageBase2['default']); + return EdgesHandler; + })(); - exports['default'] = Image; + exports['default'] = EdgesHandler; module.exports = exports['default']; /***/ }, -/* 81 */ +/* 83 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31905,308 +30940,560 @@ 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; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - 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'); } } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - - var _utilShapeBase = __webpack_require__(77); - - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + var _sharedLabel = __webpack_require__(64); - var Square = (function (_ShapeBase) { - function Square(options, body, labelModule) { - _classCallCheck(this, Square); + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - _get(Object.getPrototypeOf(Square.prototype), 'constructor', this).call(this, options, body, labelModule); - } + var _edgesBezierEdgeDynamic = __webpack_require__(84); - _inherits(Square, _ShapeBase); + var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); - _createClass(Square, [{ - key: 'resize', - value: function resize() { - this._resizeShape(); - } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'square', 2, x, y, selected, hover); - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(); - return this._distanceToBorder(angle); - } - }]); + var _edgesBezierEdgeStatic = __webpack_require__(87); - return Square; - })(_utilShapeBase2['default']); + var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); - exports['default'] = Square; - module.exports = exports['default']; + var _edgesStraightEdge = __webpack_require__(88); -/***/ }, -/* 82 */ -/***/ function(module, exports, __webpack_require__) { + var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); - 'use strict'; + var util = __webpack_require__(9); - Object.defineProperty(exports, '__esModule', { - value: true - }); + /** + * @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 _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 Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + if (body === undefined) { + throw 'No body provided'; + } + this.options = util.bridgeObject(globalOptions); + this.body = body; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + this.baseWidth = this.options.width; + this.baseFontSize = this.options.font.size; - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + this.from = undefined; // a node + this.to = undefined; // a node - var _utilShapeBase = __webpack_require__(77); + this.edgeType = undefined; - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + this.connected = false; - var Star = (function (_ShapeBase) { - function Star(options, body, labelModule) { - _classCallCheck(this, Star); + this.labelModule = new _sharedLabel2['default'](this.body, this.options); - _get(Object.getPrototypeOf(Star.prototype), 'constructor', this).call(this, options, body, labelModule); + this.setOptions(options); } - _inherits(Star, _ShapeBase); - - _createClass(Star, [{ - key: 'resize', - value: function resize(ctx) { - this._resizeShape(); - } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'star', 4, x, y, selected, hover); - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - } - }]); - - return Star; - })(_utilShapeBase2['default']); - - exports['default'] = Star; - module.exports = exports['default']; - -/***/ }, -/* 83 */ -/***/ 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; }; })(); + _createClass(Edge, [{ + key: 'setOptions', - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + /** + * 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; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + Edge.parseOptions(this.options, options, true); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + 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 = parseFloat(options.value); + } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + // update label Module + this.updateLabelModule(); - var _utilNodeBase = __webpack_require__(3); + var dataChanged = this.updateEdgeType(); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + // if anything has been updates, reset the selection width and the hover width + this._setInteractionWidths(); - var Text = (function (_NodeBase) { - function Text(options, body, labelModule) { - _classCallCheck(this, Text); + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); - _get(Object.getPrototypeOf(Text.prototype), 'constructor', this).call(this, options, body, labelModule); - } + if (options.hidden !== undefined || options.physics !== undefined) { + dataChanged = true; + } - _inherits(Text, _NodeBase); + return dataChanged; + } + }, { + key: 'updateLabelModule', - _createClass(Text, [{ - key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - this.radius = 0.5 * this.width; + /** + * 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: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected || hover); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + key: 'updateEdgeType', - // draw shadow if enabled - this.enableShadow(ctx); - this.labelModule.draw(ctx, x, y, selected || hover); + /** + * 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; + } - // disable shadows for other elements. - this.disableShadow(ctx); + if (changeInType === true) { + dataChanged = this.edgeType.cleanup(); + } + } - this.updateBoundingBox(x, y); + 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: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.resize(); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; + key: 'togglePhysics', - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; + this.edgeType.togglePhysics(status); } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - } - }]); - - return Text; - })(_utilNodeBase2['default']); - - exports['default'] = Text; - module.exports = exports['default']; - -/***/ }, -/* 84 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; + key: 'connect', - Object.defineProperty(exports, '__esModule', { - value: true - }); + /** + * Connect an edge to its nodes + */ + value: function connect() { + this.disconnect(); - 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; }; })(); + 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 _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + 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); + } + } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + this.edgeType.connect(); + } + }, { + key: 'disconnect', - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * 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; + } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + this.connected = false; + } + }, { + key: 'getTitle', - var _utilShapeBase = __webpack_require__(77); + /** + * 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', - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + /** + * 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 Triangle = (function (_ShapeBase) { - function Triangle(options, body, labelModule) { - _classCallCheck(this, Triangle); + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; + } + }, { + key: 'setValueRange', - _get(Object.getPrototypeOf(Triangle.prototype), 'constructor', this).call(this, options, body, labelModule); - } + /** + * 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; + } + this.options.width = this.options.scaling.min + scale * widthDiff; + } else { + this.options.width = this.baseWidth; + this.options.font.size = this.baseFontSize; + } - _inherits(Triangle, _ShapeBase); + this._setInteractionWidths(); + } + }, { + 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; + } - _createClass(Triangle, [{ - key: 'resize', - value: function resize(ctx) { - this._resizeShape(); + 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', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'triangle', 3, x, y, selected, hover); + + /** + * 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: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + 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) { + this.labelModule.pointToSelf = false; + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); - return Triangle; - })(_utilShapeBase2['default']); + // 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); + } - exports['default'] = Triangle; - module.exports = exports['default']; + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + // Ignore the orientations. + this.labelModule.pointToSelf = true; + 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); + } + } + } + }, { + key: 'isOverlappingWith', -/***/ }, -/* 85 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; - 'use strict'; + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - Object.defineProperty(exports, '__esModule', { - value: true - }); + return dist < distMax; + } else { + return false; + } + } + }, { + key: '_rotateForLabelAlignment', - 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; }; })(); + /** + * 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); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; + } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + ctx.rotate(angleInDegrees); + } + }, { + key: '_pointOnCircle', - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * 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]; - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; - var _utilShapeBase = __webpack_require__(77); + // only deep extend the items in the field array. These do not have shorthand. + util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + util.mergeOptions(parentOptions, newOptions, 'smooth'); + util.mergeOptions(parentOptions, newOptions, 'shadow'); - var TriangleDown = (function (_ShapeBase) { - function TriangleDown(options, body, labelModule) { - _classCallCheck(this, TriangleDown); + if (newOptions.dashes !== undefined && newOptions.dashes !== null) { + parentOptions.dashes = newOptions.dashes; + } else if (allowDeletion === true && newOptions.dashes === null) { + parentOptions.dashes = undefined; + delete parentOptions.dashes; + } - _get(Object.getPrototypeOf(TriangleDown.prototype), 'constructor', this).call(this, options, body, labelModule); - } + // 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; + } - _inherits(TriangleDown, _ShapeBase); + // 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; + } - _createClass(TriangleDown, [{ - key: 'resize', - value: function resize(ctx) { - this._resizeShape(); - } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover); - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + // 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; + } + } + } 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); + } } }]); - return TriangleDown; - })(_utilShapeBase2['default']); + return Edge; + })(); - exports['default'] = TriangleDown; + exports['default'] = Edge; module.exports = exports['default']; /***/ }, -/* 86 */ +/* 84 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -32217,431 +31504,308 @@ 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; }; })(); + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + 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 _componentsEdge = __webpack_require__(87); - - var _componentsEdge2 = _interopRequireDefault(_componentsEdge); - - var _componentsSharedLabel = __webpack_require__(70); - - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - - var util = __webpack_require__(19); - var DataSet = __webpack_require__(25); - var DataView = __webpack_require__(27); - - var EdgesHandler = (function () { - function EdgesHandler(body, images, groups) { - var _this = this; - - _classCallCheck(this, EdgesHandler); - - this.body = body; - this.images = images; - this.groups = groups; - - // create the edge API in the body container - this.body.functions.createEdge = this.create.bind(this); + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - this.edgesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; + var _utilBezierEdgeBase = __webpack_require__(85); - this.options = {}; - this.defaultOptions = { - arrows: { - to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: 1 }, - from: { enabled: false, scaleFactor: 1 } - }, - color: { - color: '#848484', - highlight: '#848484', - hover: '#848484', - inherit: 'from', - opacity: 1 - }, - dashes: false, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 2, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - hidden: false, - hoverWidth: 1.5, - label: undefined, - length: undefined, - physics: true, - scaling: { - min: 1, - max: 15, - label: { - enabled: true, - 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); - } - } - }, - selectionWidth: 1.5, - selfReferenceSize: 20, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - smooth: { - enabled: true, - type: 'dynamic', - roundness: 0.5 - }, - title: undefined, - width: 1, - value: undefined - }; + var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); - util.extend(this.options, this.defaultOptions); + var BezierEdgeDynamic = (function (_BezierEdgeBase) { + function BezierEdgeDynamic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeDynamic); - this.bindEventListeners(); + //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below } - _createClass(EdgesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; - - // this allows external modules to force all dynamic curves to turn static. - this.body.emitter.on('_forceDisableDynamicCurves', function (type) { - if (type === 'dynamic') { - type = 'continuous'; - } - var emitChange = false; - for (var edgeId in _this2.body.edges) { - if (_this2.body.edges.hasOwnProperty(edgeId)) { - var edge = _this2.body.edges[edgeId]; - var edgeData = _this2.body.data.edges._data[edgeId]; - - // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. - // this is because a change in the global would not affect these curves. - if (edgeData !== undefined) { - var edgeOptions = edgeData.smooth; - if (edgeOptions !== undefined) { - if (edgeOptions.enabled === true && edgeOptions.type === 'dynamic') { - if (type === undefined) { - edge.setOptions({ smooth: false }); - } else { - edge.setOptions({ smooth: { type: type } }); - } - emitChange = true; - } - } - } - } - } - if (emitChange === true) { - _this2.body.emitter.emit('_dataChanged'); - } - }); - - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on('_dataUpdated', function () { - _this2.reconnectEdges(); - _this2.markAllEdgesAsDirty(); - }); + _inherits(BezierEdgeDynamic, _BezierEdgeBase); - // refresh the edges. Used when reverting from hierarchical layout - this.body.emitter.on('refreshEdges', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createEdge; - delete _this2.edgesListeners.add; - delete _this2.edgesListeners.update; - delete _this2.edgesListeners.remove; - delete _this2.edgesListeners; - }); - } - }, { + _createClass(BezierEdgeDynamic, [{ key: 'setOptions', value: function setOptions(options) { - if (options !== undefined) { - // use the parser from the Edge class to fill in all shorthand notations - _componentsEdge2['default'].parseOptions(this.options, options); - - // hanlde multiple input cases for color - if (options.color !== undefined) { - this.markAllEdgesAsDirty(); - } - - // update smooth settings in all edges - var dataChanged = false; - if (options.smooth !== undefined) { - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; - } - } - } - - // update fonts in all edges - if (options.font !== undefined) { - // use the parser from the Label class to fill in all shorthand notations - _componentsSharedLabel2['default'].parseOptions(this.options.font, options); - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - this.body.edges[edgeId].updateLabelModule(); - } - } - } - - // update the state of the variables if needed - if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } - } + this.options = options; + this.id = this.options.id; + this.setupSupportNode(); + this.connect(); } }, { - key: 'setData', - - /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ - value: function setData(edges) { - var _this3 = this; - - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var oldEdgesData = this.body.data.edges; - - if (edges instanceof DataSet || edges instanceof DataView) { - this.body.data.edges = edges; - } else if (Array.isArray(edges)) { - this.body.data.edges = new DataSet(); - this.body.data.edges.add(edges); - } else if (!edges) { - this.body.data.edges = new DataSet(); + key: 'connect', + value: function connect() { + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + if (this.from === undefined || this.to === undefined || this.options.physics === false) { + this.via.setOptions({ physics: false }); } else { - throw new TypeError('Array or DataSet expected'); - } - - // TODO: is this null or undefined or false? - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); - } - - // remove drawn edges - this.body.edges = {}; - - // TODO: is this null or undefined or false? - if (this.body.data.edges) { - // subscribe to new dataset - util.forEach(this.edgesListeners, function (callback, event) { - _this3.body.data.edges.on(event, callback); - }); - - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this.add(ids, true); + // fix weird behaviour where a selfreferencing node has physics enabled + if (this.from.id === this.to.id) { + this.via.setOptions({ physics: false }); + } else { + this.via.setOptions({ physics: true }); + } } - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); + } + }, { + key: 'cleanup', + value: function cleanup() { + if (this.via !== undefined) { + delete this.body.nodes[this.via.id]; + this.via = undefined; + return true; } + return false; } }, { - key: 'add', + key: 'togglePhysics', + value: function togglePhysics(status) { + this.via.setOptions({ physics: status }); + this.positionBezierNode(); + } + }, { + key: 'setupSupportNode', /** - * Add edges - * @param {Number[] | String[]} ids + * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but + * are used for the force calculation. + * + * The changed data is not called, if needed, it is returned by the main edge constructor. * @private */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - - var edges = this.body.edges; - var edgesData = this.body.data.edges; - - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } - - var data = edgesData.get(id, { 'showInternalIds': true }); - edges[id] = this.create(data); + value: function setupSupportNode() { + if (this.via === undefined) { + var nodeId = 'edgeId:' + this.id; + var node = this.body.functions.createNode({ + id: nodeId, + shape: 'circle', + physics: true, + hidden: true + }); + this.body.nodes[nodeId] = node; + this.via = node; + this.via.parentEdgeId = this.id; + this.positionBezierNode(); } - - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); + } + }, { + key: 'positionBezierNode', + value: function positionBezierNode() { + if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== undefined) { + this.via.x = 0; + this.via.y = 0; } } }, { - key: 'update', + key: '_line', /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function update(ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var data = edgesData.get(id); - var edge = edges[id]; - if (edge === null) { - // update edge - edge.disconnect(); - dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. - edge.connect(); - } else { - // create edge - this.body.edges[id] = this.create(data); - dataChanged = true; - } - } - - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } else { - this.body.emitter.emit('_dataUpdated'); - } + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + // draw shadow if enabled + this.enableShadow(ctx); + ctx.stroke(); + this.disableShadow(ctx); + return this.via; } }, { - key: 'remove', + key: 'getPoint', /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function remove(ids) { - var edges = this.body.edges; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge !== undefined) { - edge.edgeType.cleanup(); - edge.disconnect(); - delete edges[id]; - } - } + value: function getPoint(percentage) { + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; - this.body.emitter.emit('_dataChanged'); + return { x: x, y: y }; } }, { - key: 'refresh', - value: function refresh() { - var edges = this.body.edges; - for (var edgeId in edges) { - var edge = undefined; - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - } - var data = this.body.data.edges._data[edgeId]; - if (edge !== undefined && data !== undefined) { - edge.setOptions(data); - } - } + key: '_findBorderPosition', + value: function _findBorderPosition(nearNode, ctx) { + return this._findBorderPositionBezier(nearNode, ctx, this.via); } }, { - key: 'create', - value: function create(properties) { - return new _componentsEdge2['default'](properties, this.body, this.options); + key: '_getDistanceToEdge', + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); } - }, { - key: 'markAllEdgesAsDirty', - value: function markAllEdgesAsDirty() { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].edgeType.colorDirty = true; + }]); + + return BezierEdgeDynamic; + })(_utilBezierEdgeBase2['default']); + + exports['default'] = BezierEdgeDynamic; + module.exports = exports['default']; + +/***/ }, +/* 85 */ +/***/ 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; }; })(); + + var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + 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'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _EdgeBase2 = __webpack_require__(86); + + var _EdgeBase3 = _interopRequireDefault(_EdgeBase2); + + var BezierEdgeBase = (function (_EdgeBase) { + function BezierEdgeBase(options, body, labelModule) { + _classCallCheck(this, BezierEdgeBase); + + _get(Object.getPrototypeOf(BezierEdgeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(BezierEdgeBase, _EdgeBase); + + _createClass(BezierEdgeBase, [{ + key: '_findBorderPositionBezier', + + /** + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + */ + value: function _findBorderPositionBezier(nearNode, ctx) { + var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToPoint, difference; + var threshold = 0.2; + var node = this.to; + var from = false; + if (nearNode.id === this.from.id) { + node = this.from; + from = true; + } + + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; + + pos = this.getPoint(middle, viaNode); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference < 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (from === false) { + low = middle; + } else { + high = middle; + } + } else { + if (from === false) { + high = middle; + } else { + low = middle; + } + } + + iteration++; } + pos.t = middle; + + return pos; } }, { - key: 'reconnectEdges', + key: '_getDistanceToBezierEdge', /** - * Reconnect all edges + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 * @private */ - value: function reconnectEdges() { - var id; - var nodes = this.body.nodes; - var edges = this.body.edges; - - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; + value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var xVia = undefined, + yVia = undefined; + xVia = via.x; + yVia = via.y; + var minDistance = 1000000000; + var distance = undefined; + var i = undefined, + t = undefined, + x = undefined, + y = undefined; + var lastX = x1; + var lastY = y1; + for (i = 1; i < 10; i++) { + t = 0.1 * i; + x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; + y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; + if (i > 0) { + distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); + minDistance = distance < minDistance ? distance : minDistance; } + lastX = x; + lastY = y; } - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } - } - }, { - key: 'getConnectedNodes', - value: function getConnectedNodes(edgeId) { - var nodeList = []; - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - if (edge.fromId) { - nodeList.push(edge.fromId); - } - if (edge.toId) { - nodeList.push(edge.toId); - } - } - return nodeList; + return minDistance; } }]); - return EdgesHandler; - })(); + return BezierEdgeBase; + })(_EdgeBase3['default']); - exports['default'] = EdgesHandler; + exports['default'] = BezierEdgeBase; module.exports = exports['default']; /***/ }, -/* 87 */ +/* 86 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -32652,560 +31816,593 @@ 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 _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _sharedLabel = __webpack_require__(70); - - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - - var _edgesBezierEdgeDynamic = __webpack_require__(4); - - var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); - - var _edgesBezierEdgeStatic = __webpack_require__(88); - - var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); - - var _edgesStraightEdge = __webpack_require__(89); - - var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); - - var util = __webpack_require__(19); - - /** - * @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 util = __webpack_require__(9); - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + var EdgeBase = (function () { + function EdgeBase(options, body, labelModule) { + _classCallCheck(this, EdgeBase); - 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.labelModule = labelModule; this.setOptions(options); + this.colorDirty = true; + this.color = {}; + this.selectionWidth = 2; + this.hoverWidth = 1.5; } - _createClass(Edge, [{ + _createClass(EdgeBase, [{ + key: 'connect', + value: function connect() { + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + } + }, { + key: 'cleanup', + value: function cleanup() { + return false; + } + }, { key: 'setOptions', + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + } + }, { + key: 'togglePhysics', /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit + * overloadable if the shape has to toggle the via node to disabled + * @param status */ - value: function setOptions(options) { - if (!options) { - return; + value: function togglePhysics(status) {} + }, { + key: 'drawLine', + + /** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function drawLine(ctx, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx, selected, hover); + ctx.lineWidth = this.getLineWidth(selected, hover); + var via = undefined; + if (this.options.dashes !== false) { + via = this._drawDashedLine(ctx); + } else { + via = this._drawLine(ctx); } - this.colorDirty = true; + return via; + } + }, { + key: '_drawLine', + value: function _drawLine(ctx) { + var via = undefined; + if (this.from != this.to) { + // draw line + via = this._line(ctx); + } else { + var _getCircleData2 = this._getCircleData(ctx); - Edge.parseOptions(this.options, options, true); + var _getCircleData22 = _slicedToArray(_getCircleData2, 3); - 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; + var x = _getCircleData22[0]; + var y = _getCircleData22[1]; + var radius = _getCircleData22[2]; + + this._circle(ctx, x, y, radius); } - if (options.value !== undefined) { - options.value = parseFloat(options.value); + return via; + } + }, { + key: '_drawDashedLine', + value: function _drawDashedLine(ctx) { + var via = undefined; + ctx.lineCap = 'round'; + var pattern = [5, 5]; + if (Array.isArray(this.options.dashes) === true) { + pattern = this.options.dashes; } - // update label Module - this.updateLabelModule(); - - var dataChanged = this.updateEdgeType(); - - // if anything has been updates, reset the selection width and the hover width - this._setInteractionWidths(); + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.setLineDash !== undefined) { + ctx.save(); - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - if (options.hidden !== undefined || options.physics !== undefined) { - dataChanged = true; - } + // draw the line + if (this.from != this.to) { + // draw line + via = this._line(ctx); + } else { + var _getCircleData3 = this._getCircleData(ctx); - return dataChanged; - } - }, { - key: 'updateLabelModule', + var _getCircleData32 = _slicedToArray(_getCircleData3, 3); - /** - * 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', + var x = _getCircleData32[0]; + var y = _getCircleData32[1]; + var radius = _getCircleData32[2]; - /** - * 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; + this._circle(ctx, x, y, radius); } - if (changeInType === true) { - dataChanged = this.edgeType.cleanup(); - } - } + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines - 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); - } + if (this.from != this.to) { + // draw line + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern); } else { - this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); + var _getCircleData4 = this._getCircleData(ctx); + + var _getCircleData42 = _slicedToArray(_getCircleData4, 3); + + var x = _getCircleData42[0]; + var y = _getCircleData42[1]; + var radius = _getCircleData42[2]; + + this._circle(ctx, x, y, radius); } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); - } + // draw shadow if enabled + this.enableShadow(ctx); - return dataChanged; + ctx.stroke(); + + // disable shadows for other elements. + this.disableShadow(ctx); + } + return via; } }, { - key: 'togglePhysics', - - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - this.options.physics = status; - this.edgeType.togglePhysics(status); + key: 'findBorderPosition', + value: function findBorderPosition(nearNode, ctx, options) { + if (this.from != this.to) { + return this._findBorderPosition(nearNode, ctx, options); + } else { + return this._findBorderPositionCircle(nearNode, ctx, options); + } } }, { - key: 'connect', + key: 'findBorderPositions', + value: function findBorderPositions(ctx) { + var from = {}; + var to = {}; + if (this.from != this.to) { + from = this._findBorderPosition(this.from, ctx); + to = this._findBorderPosition(this.to, ctx); + } else { + var _getCircleData5 = this._getCircleData(ctx); - /** - * Connect an edge to its nodes - */ - value: function connect() { - this.disconnect(); + var _getCircleData52 = _slicedToArray(_getCircleData5, 3); - 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 x = _getCircleData52[0]; + var y = _getCircleData52[1]; + var radius = _getCircleData52[2]; - 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); - } + from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); } - - this.edgeType.connect(); + return { from: from, to: to }; } }, { - key: 'disconnect', + key: '_getCircleData', + value: function _getCircleData(ctx) { + var x = undefined, + y = undefined; + var node = this.from; + var radius = this.options.selfReferenceSize; - /** - * 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; + if (ctx !== undefined) { + if (node.shape.width === undefined) { + node.shape.resize(ctx); + } } - this.connected = false; - } - }, { - key: 'getTitle', - - /** - * 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; + // get circle coordinates + if (node.shape.width > node.shape.height) { + x = node.x + node.shape.width * 0.5; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.shape.height * 0.5; + } + return [x, y, radius]; } }, { - key: 'isSelected', + key: '_pointOnCircle', /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false + * 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 isSelected() { - return this.selected; + 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: 'getValue', + key: '_findBorderPositionCircle', /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value + * This function uses binary search to look for the point where the circle crosses the border of the node. + * @param node + * @param ctx + * @param options + * @returns {*} + * @private */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', + value: function _findBorderPositionCircle(node, ctx, options) { + var x = options.x; + var y = options.y; + var low = options.low; + var high = options.high; + var direction = options.direction; - /** - * 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; - } - this.options.width = this.options.scaling.min + scale * widthDiff; - } else { - this.options.width = this.baseWidth; - this.options.font.size = this.baseFontSize; - } + var maxIterations = 10; + var iteration = 0; + var radius = this.options.selfReferenceSize; + var pos = undefined, + angle = undefined, + distanceToBorder = undefined, + distanceToPoint = undefined, + difference = undefined; + var threshold = 0.05; + var middle = (low + high) * 0.5; - this._setInteractionWidths(); - } - }, { - 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; - } + while (low <= high && iteration < maxIterations) { + middle = (low + high) * 0.5; - 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; + pos = this._pointOnCircle(x, y, radius, middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference > 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (direction > 0) { + low = middle; + } else { + high = middle; + } + } else { + if (direction > 0) { + high = middle; + } else { + low = middle; + } + } + iteration++; } + pos.t = middle; + + return pos; } }, { - key: 'draw', + key: 'getLineWidth', /** - * 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 + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private */ - 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); + value: function getLineWidth(selected, hover) { + if (selected === true) { + return Math.max(this.selectionWidth, 0.3 / this.body.view.scale); + } else { + if (hover === true) { + return Math.max(this.hoverWidth, 0.3 / this.body.view.scale); + } else { + return Math.max(this.options.width, 0.3 / this.body.view.scale); + } } } }, { - 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) { - this.labelModule.pointToSelf = false; - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); + key: 'getColor', + value: function getColor(ctx, selected, hover) { + var colorOptions = this.options.color; + if (colorOptions.inherit !== false) { + // when this is a loop edge, just use the 'from' method + if (colorOptions.inherit === 'both' && this.from.id !== this.to.id) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor = undefined, + toColor = undefined; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; - // 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); + if (this.from.selected === false && this.to.selected === false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else if (this.from.selected === true && this.to.selected === false) { + toColor = this.to.options.color.border; + } else if (this.from.selected === false && this.to.selected === true) { + fromColor = this.from.options.color.border; } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - // Ignore the orientations. - this.labelModule.pointToSelf = true; - 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; + // -------------------- this returns -------------------- // + return grd; + } + + if (this.colorDirty === true) { + if (colorOptions.inherit === 'to') { + this.color.highlight = this.to.options.color.highlight.border; + this.color.hover = this.to.options.color.hover.border; + this.color.color = util.overrideOpacity(this.to.options.color.border, colorOptions.opacity); } else { - x = node1.x + radius; - y = node1.y - node1.shape.height * 0.5; + // (this.options.color.inherit.source === "from") { + this.color.highlight = this.from.options.color.highlight.border; + this.color.hover = this.from.options.color.hover.border; + this.color.color = util.overrideOpacity(this.from.options.color.border, colorOptions.opacity); } - point = this._pointOnCircle(x, y, radius, 0.125); - this.labelModule.draw(ctx, point.x, point.y, selected); } + } else if (this.colorDirty === true) { + this.color.highlight = colorOptions.highlight; + this.color.hover = colorOptions.hover; + this.color.color = util.overrideOpacity(colorOptions.color, colorOptions.opacity); } - } - }, { - key: 'isOverlappingWith', - - /** - * 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; - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + // if color inherit is on and gradients are used, the function has already returned by now. + this.colorDirty = false; - return dist < distMax; + if (selected === true) { + return this.color.highlight; + } else if (hover === true) { + return this.color.hover; } else { - return false; + return this.color.color; } } }, { - key: '_rotateForLabelAlignment', + key: '_circle', /** - * Rotates the canvas so the text is most readable + * Draw a line from a node to itself, a circle * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius * @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); + value: function _circle(ctx, x, y, radius) { + // draw shadow if enabled + this.enableShadow(ctx); - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; - } + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); - ctx.rotate(angleInDegrees); + // disable shadows for other elements. + this.disableShadow(ctx); } }, { - key: '_pointOnCircle', + key: 'getDistanceToEdge', /** - * 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 + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 * @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; + value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); + } else { + var _getCircleData6 = this._getCircleData(); + + var _getCircleData62 = _slicedToArray(_getCircleData6, 3); + + var x = _getCircleData62[0]; + var y = _getCircleData62[1]; + var radius = _getCircleData62[2]; + + var dx = x - x3; + var dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + } + + if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { + return 0; + } else { + return returnValue; + } } }, { - key: 'unselect', - value: function unselect() { - this.selected = false; - } - }], [{ - key: 'parseOptions', - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + key: '_getDistanceToLine', + value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { + var px = x2 - x1; + var py = y2 - y1; + var something = px * px + py * py; + var u = ((x3 - x1) * px + (y3 - y1) * py) / something; - var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; + } - // only deep extend the items in the field array. These do not have shorthand. - util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); + var x = x1 + u * px; + var y = y1 + u * py; + var dx = x - x3; + var dy = y - y3; - util.mergeOptions(parentOptions, newOptions, 'smooth'); - util.mergeOptions(parentOptions, newOptions, 'shadow'); + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance - if (newOptions.dashes !== undefined && newOptions.dashes !== null) { - parentOptions.dashes = newOptions.dashes; - } else if (allowDeletion === true && newOptions.dashes === null) { - parentOptions.dashes = undefined; - delete parentOptions.dashes; - } + return Math.sqrt(dx * dx + dy * dy); + } + }, { + key: 'drawArrowHead', - // 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; + /** + * + * @param ctx + * @param position + * @param viaNode + */ + value: function drawArrowHead(ctx, position, viaNode, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx, selected, hover); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this.getLineWidth(selected, hover); + + // set lets + var angle = undefined; + var length = undefined; + var arrowPos = undefined; + var node1 = undefined; + var node2 = undefined; + var guideOffset = undefined; + var scaleFactor = undefined; + + if (position === 'from') { + node1 = this.from; + node2 = this.to; + guideOffset = 0.1; + scaleFactor = this.options.arrows.from.scaleFactor; + } else if (position === 'to') { + node1 = this.to; + node2 = this.from; + guideOffset = -0.1; + scaleFactor = this.options.arrows.to.scaleFactor; + } else { + node1 = this.to; + node2 = this.from; + scaleFactor = this.options.arrows.middle.scaleFactor; } - // 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; + // if not connected to itself + if (node1 != node2) { + if (position !== 'middle') { + // draw arrow head + if (this.options.smooth.enabled === true) { + arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); + var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.findBorderPosition(node1, ctx); } - } 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)); + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. } - } else if (allowDeletion === true && newOptions.arrows === null) { - parentOptions.arrows = undefined; - delete parentOptions.arrows; - } + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - // 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)); - } + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); - if (newOptions.color.inherit === undefined && colorsDefined === true) { - parentOptions.color.inherit = false; - } + // disable shadows for other elements. + this.disableShadow(ctx); + ctx.stroke(); + } else { + // draw circle + var _angle = undefined, + point = undefined; + + var _getCircleData7 = this._getCircleData(ctx); + + var _getCircleData72 = _slicedToArray(_getCircleData7, 3); + + var x = _getCircleData72[0]; + var y = _getCircleData72[1]; + var radius = _getCircleData72[2]; + + if (position === 'from') { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + } else if (position === 'to') { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; + } else { + point = this._pointOnCircle(x, y, radius, 0.175); + _angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } - } 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); + // draw the arrowhead + var _length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(point.x, point.y, _angle, _length); + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + ctx.stroke(); + } + } + }, { + key: 'enableShadow', + value: function enableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = this.options.shadow.size; + ctx.shadowOffsetX = this.options.shadow.x; + ctx.shadowOffsetY = this.options.shadow.y; + } + } + }, { + key: 'disableShadow', + value: function disableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0)'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; } } }]); - return Edge; + return EdgeBase; })(); - exports['default'] = Edge; + exports['default'] = EdgeBase; module.exports = exports['default']; /***/ }, -/* 88 */ +/* 87 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -33224,7 +32421,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilBezierEdgeBase = __webpack_require__(2); + var _utilBezierEdgeBase = __webpack_require__(85); var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); @@ -33464,7 +32661,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 89 */ +/* 88 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -33483,7 +32680,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilEdgeBase = __webpack_require__(6); + var _utilEdgeBase = __webpack_require__(86); var _utilEdgeBase2 = _interopRequireDefault(_utilEdgeBase); @@ -33569,7 +32766,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 90 */ +/* 89 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -33584,39 +32781,39 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsPhysicsBarnesHutSolver = __webpack_require__(91); + var _componentsPhysicsBarnesHutSolver = __webpack_require__(90); var _componentsPhysicsBarnesHutSolver2 = _interopRequireDefault(_componentsPhysicsBarnesHutSolver); - var _componentsPhysicsRepulsionSolver = __webpack_require__(92); + var _componentsPhysicsRepulsionSolver = __webpack_require__(91); var _componentsPhysicsRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsRepulsionSolver); - var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(93); + var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(92); var _componentsPhysicsHierarchicalRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalRepulsionSolver); - var _componentsPhysicsSpringSolver = __webpack_require__(94); + var _componentsPhysicsSpringSolver = __webpack_require__(93); var _componentsPhysicsSpringSolver2 = _interopRequireDefault(_componentsPhysicsSpringSolver); - var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(95); + var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(94); var _componentsPhysicsHierarchicalSpringSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalSpringSolver); - var _componentsPhysicsCentralGravitySolver = __webpack_require__(96); + var _componentsPhysicsCentralGravitySolver = __webpack_require__(95); var _componentsPhysicsCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsCentralGravitySolver); - var _componentsPhysicsFA2BasedRepulsionSolver = __webpack_require__(97); + var _componentsPhysicsFA2BasedRepulsionSolver = __webpack_require__(96); var _componentsPhysicsFA2BasedRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedRepulsionSolver); - var _componentsPhysicsFA2BasedCentralGravitySolver = __webpack_require__(98); + var _componentsPhysicsFA2BasedCentralGravitySolver = __webpack_require__(97); var _componentsPhysicsFA2BasedCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedCentralGravitySolver); - var util = __webpack_require__(19); + var util = __webpack_require__(9); var PhysicsEngine = (function () { function PhysicsEngine(body) { @@ -34209,7 +33406,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 91 */ +/* 90 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34708,7 +33905,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 92 */ +/* 91 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34803,7 +34000,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 93 */ +/* 92 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34894,7 +34091,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 94 */ +/* 93 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35004,7 +34201,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 95 */ +/* 94 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35133,7 +34330,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 96 */ +/* 95 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35202,7 +34399,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 97 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35221,7 +34418,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _BarnesHutSolver2 = __webpack_require__(91); + var _BarnesHutSolver2 = __webpack_require__(90); var _BarnesHutSolver3 = _interopRequireDefault(_BarnesHutSolver2); @@ -35276,7 +34473,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 98 */ +/* 97 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35295,7 +34492,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _CentralGravitySolver2 = __webpack_require__(96); + var _CentralGravitySolver2 = __webpack_require__(95); var _CentralGravitySolver3 = _interopRequireDefault(_CentralGravitySolver2); @@ -35332,7 +34529,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 99 */ +/* 98 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -35347,11 +34544,11 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNodesCluster = __webpack_require__(100); + var _componentsNodesCluster = __webpack_require__(99); var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); - var util = __webpack_require__(19); + var util = __webpack_require__(9); var ClusterEngine = (function () { function ClusterEngine(body) { @@ -36087,7 +35284,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 100 */ +/* 99 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36104,7 +35301,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _Node2 = __webpack_require__(69); + var _Node2 = __webpack_require__(63); var _Node3 = _interopRequireDefault(_Node2); @@ -36132,7 +35329,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 101 */ +/* 100 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36149,7 +35346,7 @@ return /******/ (function(modules) { // webpackBootstrap window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } - var util = __webpack_require__(19); + var util = __webpack_require__(9); var CanvasRenderer = (function () { function CanvasRenderer(body, canvas) { @@ -36520,7 +35717,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 102 */ +/* 101 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36533,10 +35730,10 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var Hammer = __webpack_require__(15); - var hammerUtil = __webpack_require__(41); + var Hammer = __webpack_require__(5); + var hammerUtil = __webpack_require__(31); - var util = __webpack_require__(19); + var util = __webpack_require__(9); /** * Create the main frame for the Network. @@ -36898,7 +36095,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 103 */ +/* 102 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -36911,7 +36108,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var util = __webpack_require__(19); + var util = __webpack_require__(9); var View = (function () { function View(body, canvas) { @@ -37303,7 +36500,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 104 */ +/* 103 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37318,7 +36515,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNavigationHandler = __webpack_require__(7); + var _componentsNavigationHandler = __webpack_require__(104); var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); @@ -37326,7 +36523,7 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsPopup2 = _interopRequireDefault(_componentsPopup); - var util = __webpack_require__(19); + var util = __webpack_require__(9); var InteractionHandler = (function () { function InteractionHandler(body, canvas, selectionHandler) { @@ -38066,6 +37263,332 @@ return /******/ (function(modules) { // webpackBootstrap exports['default'] = InteractionHandler; module.exports = exports['default']; +/***/ }, +/* 104 */ +/***/ 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__(9); + var Hammer = __webpack_require__(5); + var hammerUtil = __webpack_require__(31); + var keycharm = __webpack_require__(44); + + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; + + _classCallCheck(this, NavigationHandler); + + this.body = body; + this.canvas = canvas; + + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; + + this.body.emitter.on('activate', function () { + _this.activated = true;_this.configureKeyboardBindings(); + }); + this.body.emitter.on('deactivate', function () { + _this.activated = false;_this.configureKeyboardBindings(); + }); + this.body.emitter.on('destroy', function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); + } + }); + + this.options = {}; + } + + _createClass(NavigationHandler, [{ + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + this.options = options; + this.create(); + } + } + }, { + key: 'create', + value: function create() { + if (this.options.navigationButtons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); + } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); + } + + this.configureKeyboardBindings(); + } + }, { + key: 'cleanNavigation', + value: function cleanNavigation() { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); + } + this.navigationHammers = []; + } + + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) { + this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']); + } + + this.iconsCreated = false; + } + }, { + key: 'loadNavigationElements', + + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + value: function loadNavigationElements() { + var _this2 = this; + + this.cleanNavigation(); + + this.navigationDOM = {}; + var navigationDivs = ['up', 'down', 'left', 'right', 'zoomIn', 'zoomOut', 'zoomExtends']; + var navigationDivActions = ['_moveUp', '_moveDown', '_moveLeft', '_moveRight', '_zoomIn', '_zoomOut', '_fit']; + + this.navigationDOM['wrapper'] = document.createElement('div'); + this.navigationDOM['wrapper'].className = 'vis-navigation'; + this.canvas.frame.appendChild(this.navigationDOM['wrapper']); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement('div'); + this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i]; + this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]); + + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); + if (navigationDivActions[i] === '_fit') { + hammerUtil.onTouch(hammer, this._fit.bind(this)); + } else { + hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); + } + + this.navigationHammers.push(hammer); + } + + // use a hammer for the release so we do not require the one used in the rest of the network + // the one the rest uses can be overloaded by the manipulation system. + var hammerFrame = new Hammer(this.canvas.frame); + hammerUtil.onRelease(hammerFrame, function () { + _this2._stopMovement(); + }); + this.navigationHammers.push(hammerFrame); + + this.iconsCreated = true; + } + }, { + key: 'bindToRedraw', + value: function bindToRedraw(action) { + if (this.boundFunctions[action] === undefined) { + this.boundFunctions[action] = this[action].bind(this); + this.body.emitter.on('initRedraw', this.boundFunctions[action]); + this.body.emitter.emit('_startRendering'); + } + } + }, { + key: 'unbindFromRedraw', + value: function unbindFromRedraw(action) { + if (this.boundFunctions[action] !== undefined) { + this.body.emitter.off('initRedraw', this.boundFunctions[action]); + this.body.emitter.emit('_stopRendering'); + delete this.boundFunctions[action]; + } + } + }, { + key: '_fit', + + /** + * this stops all movement induced by the navigation buttons + * + * @private + */ + value: function _fit() { + if (new Date().valueOf() - this.touchTime > 700) { + // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) + this.body.emitter.emit('fit', { duration: 700 }); + this.touchTime = new Date().valueOf(); + } + } + }, { + key: '_stopMovement', + + /** + * this stops all movement induced by the navigation buttons + * + * @private + */ + value: function _stopMovement() { + for (var boundAction in this.boundFunctions) { + if (this.boundFunctions.hasOwnProperty(boundAction)) { + this.body.emitter.off('initRedraw', this.boundFunctions[boundAction]); + this.body.emitter.emit('_stopRendering'); + } + } + this.boundFunctions = {}; + } + }, { + key: '_moveUp', + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; + } + }, { + key: '_moveDown', + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; + } + }, { + key: '_moveLeft', + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; + } + }, { + key: '_moveRight', + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; + } + }, { + key: '_zoomIn', + value: function _zoomIn() { + this.body.view.scale *= 1 + this.options.keyboard.speed.zoom; + this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale }); + } + }, { + key: '_zoomOut', + value: function _zoomOut() { + this.body.view.scale /= 1 + this.options.keyboard.speed.zoom; + this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale }); + } + }, { + key: 'configureKeyboardBindings', + + /** + * bind all keys using keycharm. + */ + value: function configureKeyboardBindings() { + var _this3 = this; + + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + + if (this.options.keyboard.enabled === true) { + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: true }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: true }); + } + + this.keycharm.reset(); + + if (this.activated === true) { + this.keycharm.bind('up', function () { + _this3.bindToRedraw('_moveUp'); + }, 'keydown'); + this.keycharm.bind('down', function () { + _this3.bindToRedraw('_moveDown'); + }, 'keydown'); + this.keycharm.bind('left', function () { + _this3.bindToRedraw('_moveLeft'); + }, 'keydown'); + this.keycharm.bind('right', function () { + _this3.bindToRedraw('_moveRight'); + }, 'keydown'); + this.keycharm.bind('=', function () { + _this3.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('num+', function () { + _this3.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('num-', function () { + _this3.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind('-', function () { + _this3.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind('[', function () { + _this3.bindToRedraw('_zoomOut'); + }, 'keydown'); + this.keycharm.bind(']', function () { + _this3.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('pageup', function () { + _this3.bindToRedraw('_zoomIn'); + }, 'keydown'); + this.keycharm.bind('pagedown', function () { + _this3.bindToRedraw('_zoomOut'); + }, 'keydown'); + + this.keycharm.bind('up', function () { + _this3.unbindFromRedraw('_moveUp'); + }, 'keyup'); + this.keycharm.bind('down', function () { + _this3.unbindFromRedraw('_moveDown'); + }, 'keyup'); + this.keycharm.bind('left', function () { + _this3.unbindFromRedraw('_moveLeft'); + }, 'keyup'); + this.keycharm.bind('right', function () { + _this3.unbindFromRedraw('_moveRight'); + }, 'keyup'); + this.keycharm.bind('=', function () { + _this3.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('num+', function () { + _this3.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('num-', function () { + _this3.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind('-', function () { + _this3.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind('[', function () { + _this3.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + this.keycharm.bind(']', function () { + _this3.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('pageup', function () { + _this3.unbindFromRedraw('_zoomIn'); + }, 'keyup'); + this.keycharm.bind('pagedown', function () { + _this3.unbindFromRedraw('_zoomOut'); + }, 'keyup'); + } + } + } + }]); + + return NavigationHandler; + })(); + + exports['default'] = NavigationHandler; + module.exports = exports['default']; + /***/ }, /* 105 */ /***/ function(module, exports, __webpack_require__) { @@ -38196,1239 +37719,1722 @@ return /******/ (function(modules) { // webpackBootstrap /* 106 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; + 'use strict'; - Object.defineProperty(exports, "__esModule", { + 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 _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 Node = __webpack_require__(69); - var Edge = __webpack_require__(87); - var util = __webpack_require__(19); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; + var util = __webpack_require__(9); - _classCallCheck(this, SelectionHandler); + var LayoutEngine = (function () { + function LayoutEngine(body) { + _classCallCheck(this, LayoutEngine); this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; - this.hoverObj = { nodes: {}, edges: {} }; + this.initialRandomSeed = Math.round(Math.random() * 1000000); + this.randomSeed = this.initialRandomSeed; this.options = {}; + this.optionsBackup = {}; + this.defaultOptions = { - multiselect: false, - selectable: true, - selectConnectedEdges: true, - hoverConnectedEdges: true + randomSeed: undefined, + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: 'UD', // UD, DU, LR, RL + sortMethod: 'hubsize' // hubsize, directed + } }; util.extend(this.options, this.defaultOptions); - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); + this.hierarchicalLevels = {}; + + this.bindEventListeners(); } - _createClass(SelectionHandler, [{ - key: "setOptions", - value: function setOptions(options) { - if (options !== undefined) { - var fields = ["multiselect", "hoverConnectedEdges", "selectable", "selectConnectedEdges"]; - util.selectiveDeepExtend(fields, this.options, options); - } + _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: "selectOnPoint", + key: 'setOptions', + value: function setOptions(options, allOptions) { + if (options !== undefined) { + var prevHierarchicalState = this.options.hierarchical.enabled; - /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.selectable === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + util.mergeOptions(this.options, options, 'hierarchical'); + if (options.randomSeed !== undefined) { + this.initialRandomSeed = options.randomSeed; + } - // unselect after getting the objects in order to restore width and height. - this.unselectAll(); + if (this.options.hierarchical.enabled === true) { + // make sure the level seperation is the right way up + if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { + if (this.options.hierarchical.levelSeparation > 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } else { + if (this.options.hierarchical.levelSeparation < 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } - if (obj !== undefined) { - selected = this.selectObject(obj); + this.body.emitter.emit('_resetHierarchicalLayout'); + // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + return this.adaptAllOptions(allOptions); + } else { + if (prevHierarchicalState === true) { + // refresh the overridden options for nodes and edges. + this.body.emitter.emit('refresh'); + return util.deepExtend(allOptions, this.optionsBackup); + } } - this.body.emitter.emit("_requestRedraw"); } - return selected; + return allOptions; } }, { - key: "selectAdditionalOnPoint", - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.selectable === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + key: 'adaptAllOptions', + value: function adaptAllOptions(allOptions) { + if (this.options.hierarchical.enabled === true) { + // set the physics + if (allOptions.physics === undefined || allOptions.physics === true) { + allOptions.physics = { solver: 'hierarchicalRepulsion' }; + this.optionsBackup.physics = { solver: 'barnesHut' }; + } else if (typeof allOptions.physics === 'object') { + this.optionsBackup.physics = { solver: 'barnesHut' }; + if (allOptions.physics.solver !== undefined) { + this.optionsBackup.physics = { solver: allOptions.physics.solver }; + } + allOptions.physics['solver'] = 'hierarchicalRepulsion'; + } else if (allOptions.physics !== false) { + this.optionsBackup.physics = { solver: 'barnesHut' }; + allOptions.physics['solver'] = 'hierarchicalRepulsion'; + } - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); + // get the type of static smooth curve in case it is required + var type = 'horizontal'; + if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { + type = 'vertical'; + } + + // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + if (allOptions.edges === undefined) { + this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; + allOptions.edges = { smooth: false }; + } else if (allOptions.edges.smooth === undefined) { + this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; + allOptions.edges.smooth = false; + } else { + if (typeof allOptions.edges.smooth === 'boolean') { + this.optionsBackup.edges = { smooth: allOptions.edges.smooth }; + allOptions.edges.smooth = { enabled: allOptions.edges.smooth, type: type }; } else { - this.selectObject(obj); - } + // allow custom types except for dynamic + if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') { + type = allOptions.edges.smooth.type; + } - this.body.emitter.emit("_requestRedraw"); + this.optionsBackup.edges = { + smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, + type: allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type, + roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness + }; + allOptions.edges.smooth = { + enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, + type: type, + roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness + }; + } } + + // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. + this.body.emitter.emit('_forceDisableDynamicCurves', type); } - return selectionChanged; + return allOptions; } }, { - key: "_generateClickEvent", - value: function _generateClickEvent(eventType, event, pointer, oldSelection) { - var emptySelection = arguments[4] === undefined ? false : arguments[4]; - - var properties = undefined; - if (emptySelection === true) { - properties = { nodes: [], edges: [] }; - } else { - properties = this.getSelection(); - } - properties["pointer"] = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - properties["event"] = event; - - if (oldSelection !== undefined) { - properties["previousSelection"] = oldSelection; - } - this.body.emitter.emit(eventType, properties); + key: 'seededRandom', + value: function seededRandom() { + var x = Math.sin(this.randomSeed++) * 10000; + return x - Math.floor(x); } }, { - key: "selectObject", - value: function selectObject(obj) { - var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; - - if (obj !== undefined) { - if (obj instanceof Node) { - if (highlightEdges === true) { - this._selectConnectedEdges(obj); + key: 'positionInitially', + value: function positionInitially(nodesArray) { + if (this.options.hierarchical.enabled !== true) { + this.randomSeed = this.initialRandomSeed; + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + var radius = 10 * 0.1 * nodesArray.length + 10; + var angle = 2 * Math.PI * this.seededRandom(); + if (node.x === undefined) { + node.x = radius * Math.cos(angle); + } + if (node.y === undefined) { + node.y = radius * Math.sin(angle); } } - obj.select(); - this._addToSelection(obj); - return true; } - return false; } }, { - key: "deselectObject", - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); - } + key: 'getSeed', + value: function getSeed() { + return this.initialRandomSeed; } }, { - key: "_getAllNodesOverlappingWith", + key: 'setupHierarchicalLayout', /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * * @private */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); + value: function setupHierarchicalLayout() { + if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var node = undefined, + nodeId = undefined; + var definedLevel = false; + var undefinedLevel = false; + this.hierarchicalLevels = {}; + this.nodeSpacing = 100; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.options.level !== undefined) { + definedLevel = true; + this.hierarchicalLevels[nodeId] = node.options.level; + } else { + undefinedLevel = true; + } + } + } + + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel === true && definedLevel === true) { + throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); + return; + } else { + // setup the system to use hierarchical method. + //this._changeConstants(); + + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel === true) { + if (this.options.hierarchical.sortMethod === 'hubsize') { + this._determineLevelsByHubsize(); + } else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') { + this._determineLevelsDirected(); + } + } + + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); + + // place the nodes on the canvas. + this._placeNodesByHierarchy(distribution); } } - return overlappingNodes; } }, { - key: "_pointerToPositionObject", + key: '_placeNodesByHierarchy', /** - * Return a position object in canvasspace from a single point in screenspace + * This function places the nodes on the canvas based on the hierarchial distribution. * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} + * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x - 1, - top: canvasPos.y + 1, - right: canvasPos.x + 1, - bottom: canvasPos.y - 1 - }; + value: function _placeNodesByHierarchy(distribution) { + var nodeId = undefined, + node = undefined; + this.positionedNodes = {}; + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + + node = distribution[level].nodes[nodeId]; + + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if (node.x === undefined) { + node.x = distribution[level].distance; + } + distribution[level].distance = node.x + this.nodeSpacing; + } else { + if (node.y === undefined) { + node.y = distribution[level].distance; + } + distribution[level].distance = node.y + this.nodeSpacing; + } + + this.positionedNodes[nodeId] = true; + this._placeBranchNodes(node.edges, node.id, distribution, level); + } + } + } + } } }, { - key: "getNodeAt", + key: '_getDistribution', /** - * Get the top node at the a specific point (like a click) + * This function get the distribution of levels based on hubsize * - * @param {{x: Number, y: Number}} pointer - * @return {Node | undefined} node + * @returns {Object} * @private */ - value: function getNodeAt(pointer) { - var returnNode = arguments[1] === undefined ? true : arguments[1]; + value: function _getDistribution() { + var distribution = {}; + var nodeId = undefined, + node = undefined; - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - if (returnNode === true) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return overlappingNodes[overlappingNodes.length - 1]; + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + var level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + node.y = this.options.hierarchical.levelSeparation * level; + node.options.fixed.y = true; + } else { + node.x = this.options.hierarchical.levelSeparation * level; + node.options.fixed.x = true; + } + if (distribution[level] === undefined) { + distribution[level] = { amount: 0, nodes: {}, distance: 0 }; + } + distribution[level].amount += 1; + distribution[level].nodes[nodeId] = node; } - } else { - return undefined; } + return distribution; } }, { - key: "_getEdgesOverlappingWith", + key: '_getHubSize', /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * Get the hubsize from all remaining unlevelled nodes. + * + * @returns {number} * @private */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + value: function _getHubSize() { + var hubSize = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.hierarchicalLevels[nodeId] === undefined) { + hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + } } } + return hubSize; } }, { - key: "_getAllEdgesOverlappingWith", + key: '_determineLevelsByHubsize', /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize * @private */ - value: function _getAllEdgesOverlappingWith(object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; + value: function _determineLevelsByHubsize() { + var nodeId = undefined, + node = undefined; + var hubSize = 1; + + while (hubSize > 0) { + // determine hubs + hubSize = this._getHubSize(); + if (hubSize === 0) break; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length === hubSize) { + this._setLevelByHubsize(0, node); + } + } + } + } } }, { - key: "getEdgeAt", + key: '_setLevelByHubsize', /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * - * @param pointer - * @returns {undefined} + * @param level + * @param edges + * @param parentId * @private */ - value: function getEdgeAt(pointer) { - var returnEdge = arguments[1] === undefined ? true : arguments[1]; - - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + value: function _setLevelByHubsize(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) return; - if (overlappingEdges.length > 0) { - if (returnEdge === true) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId === node.id) { + childNode = node.edges[i].from; } else { - return overlappingEdges[overlappingEdges.length - 1]; + childNode = node.edges[i].to; } - } else { - return undefined; + this._setLevelByHubsize(level + 1, childNode); } } }, { - key: "_addToSelection", + key: '_determineLevelsDirected', /** - * Add object to the selection array. + * this function allocates nodes in levels based on the direction of the edges * - * @param obj + * @param hubsize * @private */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + value: function _determineLevelsDirected() { + var nodeId = undefined, + node = undefined; + var minLevel = 10000; + + // set first node to source + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + this._setLevelDirected(minLevel, node); + } + } + + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.hierarchicalLevels[nodeId] -= minLevel; + } } } }, { - key: "_addToHover", + key: '_setLevelDirected', /** - * Add object to the selection array. + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * - * @param obj + * @param level + * @param edges + * @param parentId * @private */ - value: function _addToHover(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } else { - this.hoverObj.edges[obj.id] = obj; + value: function _setLevelDirected(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) return; + + var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId === node.id) { + childNode = node.edges[i].from; + this._setLevelDirected(level - 1, childNode); + } else { + childNode = node.edges[i].to; + this._setLevelDirected(level + 1, childNode); + } } } }, { - key: "_removeFromSelection", + key: '_placeBranchNodes', /** - * Remove a single option from selection. + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. * - * @param {Object} obj + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @private */ - value: function _removeFromSelection(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = undefined; + var parentNode = undefined; + if (edges[i].toId === parentId) { + childNode = edges[i].from; + parentNode = edges[i].to; + } else { + childNode = edges[i].to; + parentNode = edges[i].from; + } + var childNodeLevel = this.hierarchicalLevels[childNode.id]; + + if (this.positionedNodes[childNode.id] === undefined) { + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + if (childNodeLevel > parentLevel) { + if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if (childNode.x === undefined) { + childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); + } + distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; + this.positionedNodes[childNode.id] = true; + } else { + if (childNode.y === undefined) { + childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); + } + distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; + } + this.positionedNodes[childNode.id] = true; + + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + } + } + } } } - }, { - key: "unselectAll", + }]); + + return LayoutEngine; + })(); + + exports['default'] = LayoutEngine; + module.exports = exports['default']; + +/***/ }, +/* 107 */ +/***/ 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__(9); + var Hammer = __webpack_require__(5); + var hammerUtil = __webpack_require__(31); + + /** + * clears the toolbar div element of children + * + * @private + */ + + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { + var _this = this; + + _classCallCheck(this, ManipulationSystem); + + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + + this.editMode = false; + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + + this.manipulationHammers = []; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; + + this.touchTime = 0; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.inMode = false; + this.selectedControlNode = undefined; + + 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); + + 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)); + } + + _createClass(ManipulationSystem, [{ + key: '_restore', /** - * Unselect all. The selectionObj is useful for this. - * + * If something changes in the data during editing, switch back to the initial datamanipulation state and close all edit modes. * @private */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); + value: function _restore() { + if (this.inMode !== false) { + if (this.options.initiallyActive === true) { + this.enableEditMode(); + } else { + this.disableEditMode(); } } - - this.selectionObj = { nodes: {}, edges: {} }; } }, { - key: "_getSelectedNodeCount", + key: 'setOptions', /** - * return the number of selected nodes - * - * @returns {number} - * @private + * Set the Options + * @param options */ - value: function _getSelectedNodeCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; + 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; } } - return count; + + 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: "_getSelectedNode", + key: 'toggleEditMode', /** - * return the selected node + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. * - * @returns {number} * @private */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } + value: function toggleEditMode() { + if (this.editMode === true) { + this.disableEditMode(); + } else { + this.enableEditMode(); } - return undefined; } }, { - key: "_getSelectedEdge", + key: 'enableEditMode', + value: function enableEditMode() { + this.editMode = true; - /** - * return the selected edge - * - * @returns {number} - * @private - */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'block'; + this.closeDiv.style.display = 'block'; + this.editModeDiv.style.display = 'none'; + this.showManipulatorToolbar(); } - return undefined; } }, { - key: "_getSelectedEdgeCount", + key: 'disableEditMode', + value: function disableEditMode() { + this.editMode = false; - /** - * return the number of selected edges - * - * @returns {number} - * @private - */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } + this._clean(); + if (this.guiEnabled === true) { + this.manipulationDiv.style.display = 'none'; + this.closeDiv.style.display = 'none'; + this.editModeDiv.style.display = 'block'; + this._createEditButton(); } - return count; } }, { - key: "_getSelectedObjectCount", + key: 'showManipulatorToolbar', /** - * return the number of selected objects. + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. * - * @returns {number} * @private */ - value: function _getSelectedObjectCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + // reset global letiables + this.manipulationDOM = {}; + + // 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'; + + 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; + + if (this.options.addNode !== false) { + this._createAddNodeButton(locale); + needSeperator = true; } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + if (this.options.addEdge !== false) { + if (needSeperator === true) { + this._createSeperator(1); + } else { + needSeperator = true; + } + this._createAddEdgeButton(locale); + } + + 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); + } + + // 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); + } } + + // 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)); } - return count; + + // redraw to show any possible changes + this.body.emitter.emit('_redraw'); } }, { - key: "_selectionIsEmpty", + key: 'addNodeMode', /** - * Check if anything is selected + * Create the toolbar for adding Nodes * - * @returns {boolean} * @private */ - value: function _selectionIsEmpty() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } + value: function addNodeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } + + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + 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']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } - return true; + + this._temporaryBindEvent('click', this._performAddNode.bind(this)); } }, { - key: "_clusterInSelection", + key: 'editNode', /** - * check if one of the selected nodes is a cluster. + * call the bound function to handle the editing of the node. The node has to be selected. * - * @returns {boolean} * @private */ - value: function _clusterInSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; + 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(); + var node = this.selectionHandler._getSelectedNode(); + if (node !== undefined) { + this.inMode = 'editNode'; + if (typeof this.options.editNode === 'function') { + 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.getDataSet().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.'); } + } else { + this.showManipulatorToolbar(); } - return false; } }, { - key: "_selectConnectedEdges", + key: 'addEdgeMode', /** - * select the edges connected to the node that is being selected + * create the toolbar to connect nodes * - * @param {Node} node * @private */ - value: function _selectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); + value: function addEdgeMode() { + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - } - }, { - key: "_hoverConnectedEdges", - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _hoverConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); + // 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: "_unselectConnectedEdges", + key: 'editEdgeMode', /** - * unselect the edges connected to the node that is being selected + * create the toolbar to edit edges * - * @param {Node} node * @private */ - value: function _unselectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); + value: function editEdgeMode() { + var _this3 = this; + + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - } - }, { - key: "blurObject", - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function blurObject(object) { - if (object.hover === true) { - object.hover = false; - this.body.emitter.emit("blurNode", { node: object.id }); + // 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]; + if (this.edgeBeingEditedId !== undefined) { + (function () { + var edge = _this3.body.edges[_this3.edgeBeingEditedId]; + + // create control nodes + var controlNodeFrom = _this3._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = _this3._getNewTargetNode(edge.to.x, edge.to.y); + + _this3.temporaryIds.nodes.push(controlNodeFrom.id); + _this3.temporaryIds.nodes.push(controlNodeTo.id); + + _this3.body.nodes[controlNodeFrom.id] = controlNodeFrom; + _this3.body.nodeIndices.push(controlNodeFrom.id); + _this3.body.nodes[controlNodeTo.id] = controlNodeTo; + _this3.body.nodeIndices.push(controlNodeTo.id); + + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + _this3._temporaryBindUI('onTouch', _this3._controlNodeTouch.bind(_this3)); // used to get the position + _this3._temporaryBindUI('onTap', function () {}); // disabled + _this3._temporaryBindUI('onHold', function () {}); // disabled + _this3._temporaryBindUI('onDragStart', _this3._controlNodeDragStart.bind(_this3)); // used to select control node + _this3._temporaryBindUI('onDrag', _this3._controlNodeDrag.bind(_this3)); // used to drag control node + _this3._temporaryBindUI('onDragEnd', _this3._controlNodeDragEnd.bind(_this3)); // used to connect or revert control nodes + _this3._temporaryBindUI('onMouseMove', function () {}); // disabled + + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + _this3._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; + } + }); + + _this3.body.emitter.emit('_redraw'); + })(); + } else { + this.showManipulatorToolbar(); } } }, { - key: "hoverObject", + key: 'deleteSelected', /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection + * delete everything in the selection * - * @param {Node || Edge} object * @private */ - value: function hoverObject(object) { - var hoverChanged = false; - // remove all node hover highlights - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (object === undefined) { - this.blurObject(this.hoverObj.nodes[nodeId]); - hoverChanged = true; - } else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) { - this.blurObject(this.hoverObj.nodes[nodeId]); - hoverChanged = true; - delete this.hoverObj.nodes[nodeId]; - } - } - } + value: function deleteSelected() { + var _this4 = this; - // removing all edge hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } + // when using the gui, enable edit mode if it wasnt already. + if (this.editMode !== true) { + this.enableEditMode(); } - if (object !== undefined) { - if (object.hover === false) { - object.hover = true; - this._addToHover(object); - hoverChanged = true; - if (object instanceof Node) { - this.body.emitter.emit("hoverNode", { node: object.id }); + // 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; } } - if (object instanceof Node && this.options.hoverConnectedEdges === true) { - this._hoverConnectedEdges(object); + + 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 (hoverChanged === true) { - this.body.emitter.emit("_requestRedraw"); + if (typeof deleteFunction === 'function') { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length === 2) { + deleteFunction(data, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this4.inMode === 'delete') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this4.body.data.edges.getDataSet().remove(finalizedData.edges); + _this4.body.data.nodes.getDataSet().remove(finalizedData.nodes); + _this4.body.emitter.emit('startSimulation'); + _this4.showManipulatorToolbar(); + } else { + _this4.body.emitter.emit('startSimulation'); + _this4.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for delete does not support two arguments (data, callback)'); + } + } else { + this.body.data.edges.getDataSet().remove(selectedEdges); + this.body.data.nodes.getDataSet().remove(selectedNodes); + this.body.emitter.emit('startSimulation'); + this.showManipulatorToolbar(); } } }, { - key: "getSelection", + key: '_setup', - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - value: function getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; - } - }, { - key: "getSelectedNodes", + //********************************************** PRIVATE ***************************************// /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. + * draw or remove the DOM + * @private */ - value: function getSelectedNodes() { - var idArray = []; - if (this.options.selectable === true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; + + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); } + } else { + this._removeManipulationDOM(); + + // disable the gui + this.guiEnabled = false; } - return idArray; } }, { - key: "getSelectedEdges", + key: '_createWrappers', /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * create the div overlays that contain the DOM + * @private */ - value: function getSelectedEdges() { - var idArray = []; - if (this.options.selectable === true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } + 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); + } + + // 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); } - return idArray; } }, { - key: "selectNodes", + key: '_getNewTargetNode', /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} + * @private */ - value: function selectNodes(selection) { - var highlightEdges = arguments[1] === undefined ? true : arguments[1]; - - var i = undefined, - id = undefined; - - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - - // first unselect any selected node - this.unselectAll(); + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); - for (i = 0; i < selection.length; i++) { - id = selection[i]; + controlNodeStyle.id = 'targetNode' + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); - } - this.selectObject(node, highlightEdges); - } - this.body.emitter.emit("_requestRedraw"); + return this.body.functions.createNode(controlNodeStyle); } }, { - key: "selectEdges", + key: '_createEditButton', /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. + * Create the edit button */ - value: function selectEdges(selection) { - var i = undefined, - id = undefined; + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; + // reset the manipulationDOM + this.manipulationDOM = {}; - // first unselect any selected objects - this.unselectAll(); + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); - for (i = 0; i < selection.length; i++) { - id = selection[i]; + // 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 edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); - } - this.selectObject(edge); - } - this.body.emitter.emit("_requestRedraw"); + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); } }, { - key: "updateSelection", + key: '_clean', /** - * Validate the selection: remove ids of nodes which no longer exist + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. * @private */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; - } - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; - } - } - } - } - }]); - - return SelectionHandler; - })(); - - exports["default"] = SelectionHandler; - module.exports = exports["default"]; + value: function _clean() { + // not in mode + this.inMode = false; -/***/ }, -/* 107 */ -/***/ function(module, exports, __webpack_require__) { + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - 'use strict'; + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - Object.defineProperty(exports, '__esModule', { - value: true - }); + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - 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; }; })(); + // restore overloaded UI functions + this._unbindTemporaryUIs(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - var util = __webpack_require__(19); + // restore the physics if required + this.body.emitter.emit('restorePhysics'); + } + }, { + key: '_cleanManipulatorHammers', - var LayoutEngine = (function () { - function LayoutEngine(body) { - _classCallCheck(this, LayoutEngine); + /** + * 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(); + } + this.manipulationHammers = []; + } + } + }, { + key: '_removeManipulationDOM', - this.body = body; + /** + * Remove all DOM elements created by this module. + * @private + */ + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - this.initialRandomSeed = Math.round(Math.random() * 1000000); - this.randomSeed = this.initialRandomSeed; - this.options = {}; - this.optionsBackup = {}; + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); - this.defaultOptions = { - randomSeed: undefined, - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: 'UD', // UD, DU, LR, RL - sortMethod: 'hubsize' // hubsize, directed + // 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); } - }; - util.extend(this.options, this.defaultOptions); - - this.hierarchicalLevels = {}; - this.bindEventListeners(); - } + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + } + }, { + key: '_createSeperator', - _createClass(LayoutEngine, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this = this; + /** + * 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.body.emitter.on('_dataChanged', function () { - _this.setupHierarchicalLayout(); - }); - this.body.emitter.on('_resetHierarchicalLayout', function () { - _this.setupHierarchicalLayout(); - }); + this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); + this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line'; + this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); } }, { - 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; - } + key: '_createAddNodeButton', - if (this.options.hierarchical.enabled === true) { - // make sure the level seperation is the right way up - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } + // ---------------------- DOM functions for buttons --------------------------// - this.body.emitter.emit('_resetHierarchicalLayout'); - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. - return this.adaptAllOptions(allOptions); - } else { - if (prevHierarchicalState === true) { - // refresh the overridden options for nodes and edges. - this.body.emitter.emit('refresh'); - return util.deepExtend(allOptions, this.optionsBackup); - } - } - } - return allOptions; + 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: 'adaptAllOptions', - value: function adaptAllOptions(allOptions) { - if (this.options.hierarchical.enabled === true) { - // set the physics - if (allOptions.physics === undefined || allOptions.physics === true) { - allOptions.physics = { solver: 'hierarchicalRepulsion' }; - this.optionsBackup.physics = { solver: 'barnesHut' }; - } else if (typeof allOptions.physics === 'object') { - this.optionsBackup.physics = { solver: 'barnesHut' }; - if (allOptions.physics.solver !== undefined) { - this.optionsBackup.physics = { solver: allOptions.physics.solver }; - } - allOptions.physics['solver'] = 'hierarchicalRepulsion'; - } else if (allOptions.physics !== false) { - this.optionsBackup.physics = { solver: 'barnesHut' }; - allOptions.physics['solver'] = 'hierarchicalRepulsion'; - } + 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]; - // get the type of static smooth curve in case it is required - var type = 'horizontal'; - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { - type = 'vertical'; - } + 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', - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. - if (allOptions.edges === undefined) { - this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; - allOptions.edges = { smooth: false }; - } else if (allOptions.edges.smooth === undefined) { - this.optionsBackup.edges = { smooth: { enabled: true, type: 'dynamic' } }; - allOptions.edges.smooth = false; - } else { - if (typeof allOptions.edges.smooth === 'boolean') { - this.optionsBackup.edges = { smooth: allOptions.edges.smooth }; - allOptions.edges.smooth = { enabled: allOptions.edges.smooth, type: type }; - } else { - // allow custom types except for dynamic - if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') { - type = allOptions.edges.smooth.type; - } + // -------------------------- End of DOM functions for buttons ------------------------------// - this.optionsBackup.edges = { - smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, - type: allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type, - roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness - }; - allOptions.edges.smooth = { - enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, - type: type, - roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness - }; - } - } + /** + * 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', - // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. - this.body.emitter.emit('_forceDisableDynamicCurves', type); + /** + * 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))); } - return allOptions; } }, { - key: 'seededRandom', - value: function seededRandom() { - var x = Math.sin(this.randomSeed++) * 10000; - return x - Math.floor(x); + 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: 'positionInitially', - value: function positionInitially(nodesArray) { - if (this.options.hierarchical.enabled !== true) { - this.randomSeed = this.initialRandomSeed; - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - var radius = 10 * 0.1 * nodesArray.length + 10; - var angle = 2 * Math.PI * this.seededRandom(); - if (node.x === undefined) { - node.x = radius * Math.cos(angle); - } - if (node.y === undefined) { - node.y = radius * Math.sin(angle); - } - } + 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: 'getSeed', - value: function getSeed() { - return this.initialRandomSeed; + 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: 'setupHierarchicalLayout', + key: '_cleanupTemporaryNodesAndEdges', /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * + * Neatly clean up temporary edges and nodes * @private */ - value: function setupHierarchicalLayout() { - if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var node = undefined, - nodeId = undefined; - var definedLevel = false; - var undefinedLevel = false; - this.hierarchicalLevels = {}; - this.nodeSpacing = 100; - - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.options.level !== undefined) { - definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; - } else { - undefinedLevel = true; - } - } + 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); } + } - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel === true && definedLevel === true) { - throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); - return; - } else { - // setup the system to use hierarchical method. - //this._changeConstants(); - - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel === true) { - if (this.options.hierarchical.sortMethod === 'hubsize') { - this._determineLevelsByHubsize(); - } else if (this.options.hierarchical.sortMethod === 'directed' || 'direction') { - this._determineLevelsDirected(); - } - } - - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - - // place the nodes on the canvas. - this._placeNodesByHierarchy(distribution); + // _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: '_placeNodesByHierarchy', + key: '_controlNodeTouch', + + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() + * the touch is used to get the position of the initial click + * @param event * @private */ - value: function _placeNodesByHierarchy(distribution) { - var nodeId = undefined, - node = undefined; - this.positionedNodes = {}; - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { + 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', - node = distribution[level].nodes[nodeId]; + /** + * 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; - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - if (node.x === undefined) { - node.x = distribution[level].distance; - } - distribution[level].distance = node.x + this.nodeSpacing; - } else { - if (node.y === undefined) { - node.y = distribution[level].distance; - } - distribution[level].distance = node.y + this.nodeSpacing; - } + var fromSelect = from.isOverlappingWith(pointerObj); + var toSelect = to.isOverlappingWith(pointerObj); - this.positionedNodes[nodeId] = true; - this._placeBranchNodes(node.edges, node.id, distribution, level); - } - } - } + 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: '_getDistribution', + key: '_controlNodeDrag', /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} + * dragging the control nodes or the canvas + * @param event * @private */ - value: function _getDistribution() { - var distribution = {}; - var nodeId = undefined, - node = undefined; + value: function _controlNodeDrag(event) { + this.body.emitter.emit('disablePhysics'); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - var level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - node.y = this.options.hierarchical.levelSeparation * level; - node.options.fixed.y = true; - } else { - node.x = this.options.hierarchical.levelSeparation * level; - node.options.fixed.x = true; - } - if (distribution[level] === undefined) { - distribution[level] = { amount: 0, nodes: {}, distance: 0 }; - } - distribution[level].amount += 1; - distribution[level].nodes[nodeId] = node; - } + 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 }; } - return distribution; + this.body.emitter.emit('_redraw'); } }, { - key: '_getHubSize', + key: '_controlNodeDragEnd', /** - * Get the hubsize from all remaining unlevelled nodes. - * - * @returns {number} + * connecting or restoring the control nodes. + * @param event * @private */ - value: function _getHubSize() { - var hubSize = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { - hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + 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'); } - return hubSize; + this.body.emitter.emit('_redraw'); } }, { - key: '_determineLevelsByHubsize', + key: '_handleConnect', + + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * 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. * - * @param hubsize * @private */ - value: function _determineLevelsByHubsize() { - var nodeId = undefined, - node = undefined; - var hubSize = 1; + 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 - while (hubSize > 0) { - // determine hubs - hubSize = this._getHubSize(); - if (hubSize === 0) break; + var pointer = this.lastTouch; + var node = this.selectionHandler.getNodeAt(pointer); - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length === hubSize) { - this._setLevelByHubsize(0, node); - } + 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: '_setLevelByHubsize', + 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', /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event * @private */ - value: function _setLevelByHubsize(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) return; + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId === node.id) { - childNode = node.edges[i].from; + // 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 { - childNode = node.edges[i].to; + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performAddEdge(connectFromId, node.id); + } } - this._setLevelByHubsize(level + 1, childNode); } + this.body.emitter.emit('_redraw'); } }, { - key: '_determineLevelsDirected', + key: '_performAddNode', + + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + + // ------------------------------ Performing all the actual data manipulation ------------------------// /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize - * @private + * Adds a node on the specified location */ - value: function _determineLevelsDirected() { - var nodeId = undefined, - node = undefined; - var minLevel = 10000; - - // set first node to source - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - this._setLevelDirected(minLevel, node); - } - } + value: function _performAddNode(clickData) { + var _this5 = this; - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; - } - } + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: 'new' + }; - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.hierarchicalLevels[nodeId] -= minLevel; + if (typeof this.options.addNode === 'function') { + if (this.options.addNode.length === 2) { + this.options.addNode(defaultData, function (finalizedData) { + if (finalizedData !== null && finalizedData !== undefined && _this5.inMode === 'addNode') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this5.body.data.nodes.getDataSet().add(finalizedData); + _this5.showManipulatorToolbar(); + } + }); + } else { + throw new Error('The function for add does not support two arguments (data,callback)'); + this.showManipulatorToolbar(); } + } else { + this.body.data.nodes.getDataSet().add(defaultData); + this.showManipulatorToolbar(); } } }, { - key: '_setLevelDirected', + key: '_performAddEdge', /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction + * connect two nodes with a new edge. * - * @param level - * @param edges - * @param parentId * @private */ - value: function _setLevelDirected(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) return; - - var childNode = undefined; - this.hierarchicalLevels[node.id] = level; + value: function _performAddEdge(sourceNodeId, targetNodeId) { + var _this6 = this; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId === node.id) { - childNode = node.edges[i].from; - this._setLevelDirected(level - 1, childNode); + 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 && _this6.inMode === 'addEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback + _this6.body.data.edges.getDataSet().add(finalizedData); + _this6.selectionHandler.unselectAll(); + _this6.showManipulatorToolbar(); + } + }); } else { - childNode = node.edges[i].to; - this._setLevelDirected(level + 1, childNode); + throw new Error('The function for connect does not support two arguments (data,callback)'); } + } else { + this.body.data.edges.getDataSet().add(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); } } }, { - key: '_placeBranchNodes', + key: '_performEditEdge', /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. + * connect two nodes with a new edge. * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel * @private */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = undefined; - var parentNode = undefined; - if (edges[i].toId === parentId) { - childNode = edges[i].from; - parentNode = edges[i].to; - } else { - childNode = edges[i].to; - parentNode = edges[i].from; - } - var childNodeLevel = this.hierarchicalLevels[childNode.id]; + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this7 = this; - if (this.positionedNodes[childNode.id] === undefined) { - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - if (childNodeLevel > parentLevel) { - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { - if (childNode.x === undefined) { - childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); - } - distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; - this.positionedNodes[childNode.id] = true; + 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 || _this7.inMode !== 'editEdge') { + // if for whatever reason the mode has changes (due to dataset change) disregard the callback) { + _this7.body.edges[defaultData.id].updateEdgeType(); + _this7.body.emitter.emit('_redraw'); } else { - if (childNode.y === undefined) { - childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); - } - distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; - } - this.positionedNodes[childNode.id] = true; - - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + _this7.body.data.edges.getDataSet().update(finalizedData); + _this7.selectionHandler.unselectAll(); + _this7.showManipulatorToolbar(); } - } + }); + } else { + throw new Error('The function for edit does not support two arguments (data, callback)'); } + } else { + this.body.data.edges.getDataSet().update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); } } }]); - return LayoutEngine; + return ManipulationSystem; })(); - exports['default'] = LayoutEngine; + exports['default'] = ManipulationSystem; module.exports = exports['default']; /***/ }, diff --git a/docs/network/index.html b/docs/network/index.html index d6222e56..16616f84 100644 --- a/docs/network/index.html +++ b/docs/network/index.html @@ -1308,6 +1308,16 @@ var options = { {node: nodeId} Fired interaction:{hover:true} and the mouse moved away from a node it was hovering over before. + + hoverEdge + {edge: edgeId} + Fired interaction:{hover:true} and the mouse hovers over an edge. + + + blurEdge + {edge: edgeId} + Fired interaction:{hover:true} and the mouse moved away from an edge it was hovering over before. + zoom {direction:'+'/'-', scale: Number} diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 132f258d..623c9a36 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -471,10 +471,6 @@ class SelectionHandler { } - - - - /** * This is called when someone clicks on a node. either select or deselect it. * If there is an existing selection and we don't want to append to it, clear the existing selection @@ -485,7 +481,12 @@ class SelectionHandler { blurObject(object) { if (object.hover === true) { object.hover = false; - this.body.emitter.emit("blurNode",{node:object.id}); + if (object instanceof Node) { + this.body.emitter.emit("blurNode", {node: object.id}); + } + else { + this.body.emitter.emit("blurEdge", {edge: object.id}); + } } } @@ -529,6 +530,9 @@ class SelectionHandler { if (object instanceof Node) { this.body.emitter.emit("hoverNode", {node: object.id}); } + else { + this.body.emitter.emit("hoverEdge", {edge: object.id}); + } } if (object instanceof Node && this.options.hoverConnectedEdges === true) { this._hoverConnectedEdges(object);