From 1a3f63f1870dd2ff7f9195069b8d803a8bfb5739 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 30 Jun 2015 16:38:58 +0200 Subject: [PATCH] fixed vertical dragging issue from hammer (which was found in network) in the timeline --- dist/vis.js | 29049 +++++++++++++------------ lib/timeline/Core.js | 1 + lib/timeline/component/CustomTime.js | 1 + lib/timeline/component/ItemSet.js | 1 + 4 files changed, 14529 insertions(+), 14523 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 0b30545c..5e146d82 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -84,55 +84,55 @@ return /******/ (function(modules) { // webpackBootstrap // utils 'use strict'; - exports.util = __webpack_require__(16); - exports.DOMutil = __webpack_require__(21); + exports.util = __webpack_require__(19); + exports.DOMutil = __webpack_require__(24); // data - exports.DataSet = __webpack_require__(22); - exports.DataView = __webpack_require__(24); - exports.Queue = __webpack_require__(23); + exports.DataSet = __webpack_require__(25); + exports.DataView = __webpack_require__(27); + exports.Queue = __webpack_require__(26); // Graph3d - exports.Graph3d = __webpack_require__(25); + exports.Graph3d = __webpack_require__(28); exports.graph3d = { - Camera: __webpack_require__(29), - Filter: __webpack_require__(30), - Point2d: __webpack_require__(26), - Point3d: __webpack_require__(28), - Slider: __webpack_require__(31), - StepNumber: __webpack_require__(32) + Camera: __webpack_require__(32), + Filter: __webpack_require__(33), + Point2d: __webpack_require__(29), + Point3d: __webpack_require__(31), + Slider: __webpack_require__(34), + StepNumber: __webpack_require__(35) }; // Timeline - exports.Timeline = __webpack_require__(33); + exports.Timeline = __webpack_require__(36); exports.Graph2d = __webpack_require__(57); exports.timeline = { - DateUtil: __webpack_require__(39), + DateUtil: __webpack_require__(42), DataStep: __webpack_require__(60), - Range: __webpack_require__(37), - stack: __webpack_require__(43), - TimeStep: __webpack_require__(45), + Range: __webpack_require__(40), + stack: __webpack_require__(44), + TimeStep: __webpack_require__(46), components: { items: { - Item: __webpack_require__(11), - BackgroundItem: __webpack_require__(48), - BoxItem: __webpack_require__(47), + Item: __webpack_require__(14), + BackgroundItem: __webpack_require__(49), + BoxItem: __webpack_require__(48), PointItem: __webpack_require__(9), - RangeItem: __webpack_require__(44) + RangeItem: __webpack_require__(45) }, - Component: __webpack_require__(35), - CurrentTime: __webpack_require__(34), - CustomTime: __webpack_require__(52), + Component: __webpack_require__(38), + CurrentTime: __webpack_require__(37), + CustomTime: __webpack_require__(13), DataAxis: __webpack_require__(59), GraphGroup: __webpack_require__(61), - Group: __webpack_require__(42), - BackgroundGroup: __webpack_require__(46), - ItemSet: __webpack_require__(41), + Group: __webpack_require__(43), + BackgroundGroup: __webpack_require__(47), + ItemSet: __webpack_require__(12), Legend: __webpack_require__(64), LineGraph: __webpack_require__(58), - TimeAxis: __webpack_require__(49) + TimeAxis: __webpack_require__(50) } }; @@ -157,10 +157,10 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(17); - exports.hammer = __webpack_require__(12); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(12); - exports.keycharm = __webpack_require__(51); + 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); /***/ }, /* 1 */ @@ -687,7 +687,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__(16); + var util = __webpack_require__(19); var EdgeBase = (function () { function EdgeBase(options, body, labelModule) { @@ -1282,10 +1282,10 @@ 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__(16); - var Hammer = __webpack_require__(12); - var hammerUtil = __webpack_require__(38); - var keycharm = __webpack_require__(51); + var util = __webpack_require__(19); + var Hammer = __webpack_require__(15); + var hammerUtil = __webpack_require__(41); + var keycharm = __webpack_require__(52); var NavigationHandler = (function () { function NavigationHandler(body, canvas) { @@ -1608,9 +1608,9 @@ 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__(16); - var Hammer = __webpack_require__(12); - var hammerUtil = __webpack_require__(38); + var util = __webpack_require__(19); + var Hammer = __webpack_require__(15); + var hammerUtil = __webpack_require__(41); /** * clears the toolbar div element of children @@ -2814,7 +2814,7 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Item = __webpack_require__(11); + var Item = __webpack_require__(14); /** * @constructor PointItem @@ -3019,7 +3019,7 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var DOMutil = __webpack_require__(21); + var DOMutil = __webpack_require__(24); function Points(groupId, options) { this.groupId = groupId; @@ -3098,16207 +3098,16183 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); + 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); /** - * @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 + * Create a timeline visualization + * @constructor */ - function Item(data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; + function Core() {} - this.selected = false; - this.displayed = false; - this.dirty = true; + // turn Core into an event emitter + Emitter(Core.prototype); - this.top = null; - this.left = null; - this.width = null; - this.height = null; + /** + * 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.editable = null; - if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { - this.editable = data.editable; - } - } + this.dom.container = container; - Item.prototype.stack = true; + 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'); - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + 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'; - /** - * Unselect current item - */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + 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); - /** - * Set data for the item. Existing data will be updated. The id should not - * be changed. When the item is displayed, it will be redrawn immediately. - * @param {Object} data - */ - Item.prototype.setData = function (data) { - var groupChanged = data.group != undefined && this.data.group != data.group; - if (groupChanged) { - this.parent.itemSet._moveToGroup(this, data.group); - } + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { - this.editable = data.editable; - } + 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.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + this.on('rangechange', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pan', this._onDrag.bind(this)); - /** - * 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(); + 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); } - } else { - this.parent = parent; } - }; + this.dom.root.addEventListener('mousewheel', onMouseWheel); + this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); - /** - * 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; - }; + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed - */ - Item.prototype.show = function () { - return false; - }; + this.customTimes = []; - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + // store state information needed for touch events + this.touch = {}; - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + this.redrawCount = 0; - /** - * Reposition the Item horizontally - */ - Item.prototype.repositionX = function () {}; + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); + }; /** - * Reposition the Item vertically + * 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 */ - Item.prototype.repositionY = function () {}; + 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); - /** - * 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 ('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.selected && editable && !this.dom.deleteButton) { - // create and show button - var me = this; + 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; + } + } - var deleteButton = document.createElement('div'); - deleteButton.className = 'vis-delete'; - deleteButton.title = 'Delete this item'; + // 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 + }; + } - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on('tap', function (event) { - event.stopPropagation(); - me.parent.removeFromDataSet(me); - }); + if ('hiddenDates' in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + } - 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); + 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; + } + } } - this.dom.deleteButton = 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; + 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(); } - 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); - } + // 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.content = content; + 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(); }; /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private + * Returns true when the Timeline is active. + * @returns {boolean} */ - Item.prototype._updateTitle = function (element) { - if (this.data.title != null) { - element.title = this.data.title || ''; - } else { - element.removeAttribute('vis-title'); - } + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; }; /** - * 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 + * Destroy the Core, clean up all DOM elements and event listeners. */ - Item.prototype._updateDataAttributes = function (element) { - if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { - var attributes = []; + Core.prototype.destroy = function () { + // unbind datasets + this.setItems(null); + this.setGroups(null); - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == 'all') { - attributes = Object.keys(this.data); - } else { - return; - } + // remove all event listeners + this.off(); - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; + // stop checking for changed size + this._stopAutoResize(); - if (value != null) { - element.setAttribute('data-' + name, value); - } else { - element.removeAttribute('data-' + name); - } + // 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; }; /** - * Update custom styles of the element - * @param element - * @private + * Set a custom time bar + * @param {Date} time + * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. */ - Item.prototype._updateStyle = function (element) { - // remove old styles - if (this.style) { - util.removeCssText(element, this.style); - this.style = null; + 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)); } - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; + if (customTimes.length > 0) { + customTimes[0].setCustomTime(time); } }; /** - * Stringify the items contents - * @param {string | Element | undefined} content - * @returns {string | undefined} - * @private + * Retrieve the current custom time. + * @param {number} [id=undefined] Id of the custom time bar. + * @return {Date | undefined} customTime */ - Item.prototype._contentToString = function (content) { - if (typeof content === 'string') return content; - if (content && 'outerHTML' in content) return content.outerHTML; - return content; - }; + Core.prototype.getCustomTime = function (id) { + var customTimes = this.customTimes.filter(function (component) { + return component.options.id === id; + }); - /** - * Return the width of the item left from its start date - * @return {number} - */ - Item.prototype.getWidthLeft = function () { - return 0; + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + } + return customTimes[0].getCustomTime(); }; /** - * Return the width of the item right from the max of its start and end date - * @return {number} + * 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 */ - Item.prototype.getWidthRight = function () { - return 0; - }; - - module.exports = Item; - - // should be implemented by the item + Core.prototype.addCustomTime = function (time, id) { + var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); - // should be implemented by the item + 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'); + } - // should be implemented by the item + var customTime = new CustomTime(this.body, { + time: timestamp, + id: id + }); -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { + this.customTimes.push(customTime); + this.components.push(customTime); + this.redraw(); - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - 'use strict'; + return id; + }; - if (typeof window !== 'undefined') { - var propagating = __webpack_require__(13); - var Hammer = window['Hammer'] || __webpack_require__(14); - module.exports = propagating(Hammer, { - preventDefault: 'mouse' + /** + * 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; }); - } else { - module.exports = function () { - throw Error('hammer.js is only available in a browser, not in node.js.'); - }; - } -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { - - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - - (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(); + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } - }(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 - /** - * 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 - }; + customTimes.forEach((function (customTime) { + this.customTimes.splice(this.customTimes.indexOf(customTime), 1); + this.components.splice(this.components.indexOf(customTime), 1); + customTime.destroy(); + }).bind(this)); + }; - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; + /** + * 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() || []; + }; - 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); + /** + * 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(); - PropagatingHammer.Manager = function (element, options) { - var o = Object.create(_options); - if (options) Hammer.extend(o, options); - return propagating(new Hammer.Manager(element, o), o); - }; + // skip range set if there is no min and max date + if (range.min === null && range.max === null) { + return; + } - return PropagatingHammer; - } + // 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); - // create a wrapper object which will override the functions - // `on`, `off`, `destroy`, and `emit` of the hammer instance - var wrapper = Object.create(hammer); + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(min, max, animation); + }; - // attach to DOM element - var element = hammer.element; - element.hammer = wrapper; + /** + * 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'); + }; - // 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; - } - }); + /** + * 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); + } + }; - /** @type {Object.>} */ - wrapper._handlers = {}; + /** + * 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(); - /** - * 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 - */ - wrapper.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = wrapper._handlers[event]; - if (!_handlers) { - wrapper._handlers[event] = _handlers = []; + var start = t - interval / 2; + var end = t + interval / 2; + var animation = options && options.animation !== undefined ? options.animation : true; - // register the static, propagated handler - hammer.on(event, propagatedHandler); - } - _handlers.push(handler); - }); + this.range.setRange(start, end, animation); + }; - return wrapper; - }; + /** + * 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) + }; + }; - /** - * 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 - */ - 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; - }) : []; + /** + * Force a redraw. Can be overridden by implementations of Core + */ + Core.prototype.redraw = function () { + this._redraw(); + }; - if (_handlers.length > 0) { - wrapper._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer.off(event, propagatedHandler); - delete wrapper._handlers[event]; - } - } - }); + /** + * 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; - return wrapper; - }; + if (!dom) return; // when destroyed - /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event - */ - wrapper.emit = function(eventType, event) { - _firstTarget = event.target; - hammer.emit(eventType, event); - }; + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - wrapper.destroy = function () { - // Detach from DOM element - delete hammer.element.hammer; + // 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'); + } - // clear all handlers - wrapper._handlers = {}; + // 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, ''); - // call original hammer destroy - hammer.destroy(); - }; + // 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; - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); - } + // 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; + } - /** - * A static event handler, applying event propagation. - * @param {Object} event - */ - function propagatedHandler(event) { - // let only a single hammer instance handle this event - if (event.type !== 'hammer.input') { - // it is possible that the same srcEvent is used with multiple hammer events, - // we keep track on which events are handled in an object _handled - if (!event.srcEvent._handled) { - event.srcEvent._handled = {}; - } + // 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; - if (event.srcEvent._handled[event.type]) { - return; - } - else { - event.srcEvent._handled[event.type] = true; - } - } + // TODO: compensate borders when any of the panels is empty. - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; + // 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'); - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + // 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; - // 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); - } - } + // 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; - elem = elem.parentNode; - } - } + // 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'; - return wrapper; - }; - })); + 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'; -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + // 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(); - 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'; + // 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'; - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + // 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; - var TYPE_FUNCTION = 'function'; + // 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; + } + }; - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + // 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 timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} + * 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. */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); + } + + this.currentTime.setCurrentTime(time); + }; /** - * 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} + * Get the current time. + * Only applicable when option `showCurrentTime` is true. + * @return {Date} Returns the current time. */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; - } - return false; - } + Core.prototype.getCurrentTime = function () { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); + } + + return this.currentTime.getCurrentTime(); + }; /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context + * 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 */ - function each(obj, iterator, context) { - var i; + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); + }; - if (!obj) { - return; - } + /** + * 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); + }; - 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); - } - } - } + /** + * 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); + }; /** - * 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 + * 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 */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; - } - i++; - } - return dest; - } + // 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; + }; /** - * 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 + * Initialize watching when option autoResize is true + * @private */ - function merge(dest, src) { - return extend(dest, src, true); - } + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); + } + }; /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; + Core.prototype._startAutoResize = function () { + var me = this; - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + this._stopAutoResize(); - if (properties) { - extend(childP, properties); + this._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; } - } - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + 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; - /** - * 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); + me.emit('change'); + } } - return val; - } + }; - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); + }; /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * Stop watching for a resize of the frame. + * @private */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } + 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; + }; /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * Start moving the timeline vertically + * @param {Event} event + * @private */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + Core.prototype._onTouch = function (event) { + this.touch.allowDragging = true; + this.touch.initialScrollTop = this.props.scrollTop; + }; /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found + * Start moving the timeline vertically + * @param {Event} event + * @private */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; - } + Core.prototype._onPinch = function (event) { + this.touch.allowDragging = false; + }; /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found + * Move the timeline vertically + * @param {Event} event + * @private */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } + 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); + + if (newScrollTop != oldScrollTop) { + this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + this.emit('verticalDrag'); + } + }; /** - * split string on whitespace - * @param {String} str - * @returns {Array} words + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; + }; /** - * 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 + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - 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; + 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; + } + + // 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; + }; /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} + * Get the current scrollTop + * @returns {number} scrollTop + * @private */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; /** - * 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}] + * Load a configurator + * @return {Object} + * @private */ - 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]; - }); - } - } + Core.prototype._createConfigurator = function () { + throw new Error('Cannot invoke abstract method _createConfigurator'); + }; - return results; - } + module.exports = Core; - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + 'use strict'; - if (prop in obj) { - return prop; - } - i++; - } - return undefined; - } + 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); - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var BACKGROUND = '__background__'; // reserved group id for background items without group /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} + * 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 getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } + function ItemSet(body, options) { + this.body = body; - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + 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, - 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); + selectable: true, + multiselect: false, - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, - var COMPUTE_INTERVAL = 25; + snap: TimeStep.snap, - 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']; + 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); + }, - /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor - */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 + } + }; - // 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); - } - }; + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); - this.init(); + // 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; - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - /** - * 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); + // listeners for the DataSet of the items + this.itemListeners = { + 'add': function add(event, params, senderId) { + me._onAdd(params.items); }, - - /** - * 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); + 'update': function update(event, params, senderId) { + me._onUpdate(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemove(params.items); } - }; - - /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} - */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; + }; - 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; + // 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); } - return new (Type)(manager, inputHandler); - } + }; - /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input - */ - function inputHandler(manager, eventType, input) { - var pointersLen = input.pointers.length; - var changedPointersLen = input.changedPointers.length; - var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); - var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw - if (isFirst) { - manager.session = {}; - } + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; + this._create(); - // compute scale, rotation etc - computeInputData(manager, input); + this.setOptions(options); + } - // emit secret event - manager.emit('hammer.input', input); + ItemSet.prototype = new Component(); - manager.recognize(input); - manager.session.prevInput = input; - } + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem + }; /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * Create the HTML DOM for the ItemSet */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; - - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } + ItemSet.prototype._create = function () { + var frame = document.createElement('div'); + frame.className = 'vis-itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; - } + // create background panel + var background = document.createElement('div'); + background.className = 'vis-background'; + frame.appendChild(background); + this.dom.background = background; - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'vis-foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; + // create axis panel + var axis = document.createElement('div'); + axis.className = 'vis-axis'; + this.dom.axis = axis; - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'vis-labelset'; + this.dom.labelSet = labelSet; - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); + // create ungrouped Group + this._updateUngrouped(); - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; - computeIntervalInputData(session, input); + // 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); - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; + // drag items when selected + this.hammer.on('hammer.input', (function (event) { + if (event.isFirst) { + this._onTouch(event); } - input.target = target; - } + }).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. - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on('press', this._onMultiSelectItem.bind(this)); - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + // add item on doubletap + this.hammer.on('doubletap', this._onAddItem.bind(this)); - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + // attach to the DOM + this.show(); + }; /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input + * 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. */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; + 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 (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + 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; + } + } - 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 ('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); + } + } + } + } - 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 ('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); + } } - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + // 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(); + } + }; /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData + * 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] */ - function simpleCloneInputData(input) { - // make a simple copy of the pointers because we will get a reference if we don't - // we only need clientXY for the calculations - var pointers = []; - var i = 0; - while (i < input.pointers.length) { - pointers[i] = { - clientX: round(input.pointers[i].clientX), - clientY: round(input.pointers[i].clientY) - }; - i++; - } + ItemSet.prototype.markDirty = function (options) { + this.groupIds = []; + this.stackDirty = true; - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); + } + }; /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties + * Destroy the ItemSet */ - 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) - }; - } + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } + this.hammer = null; - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } + this.body = null; + this.conversion = 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` + * Hide the component from the DOM */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction - */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } + }; /** - * 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 + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - 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]]; + ItemSet.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } - return Math.sqrt((x * x) + (y * y)); - } + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); + } - /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle - */ - function getAngle(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - return Math.atan2(y, x) * 180 / Math.PI; - } + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } + }; /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation + * 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 getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } + if (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; + // 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(); + } - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + // 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(); + } + } + }; /** - * Mouse events input - * @constructor - * @extends Input + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - 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; - } + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); + }; - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + /** + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items + */ + 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); - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; - } + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - if (eventType & INPUT_END) { - this.pressed = false; + // 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); } - - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); + } } - }); + } - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL + return ids; }; - // 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 + /** + * Deselect a selected item + * @param {String | Number} id + * @private + */ + ItemSet.prototype._deselect = function (id) { + var selection = this.selection; + for (var i = 0, ii = selection.length; i < ii; i++) { + if (selection[i] == id) { + // non-strict comparison! + selection.splice(i, 1); + break; + } + } }; - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } - /** - * Pointer events input - * @constructor - * @extends Input + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; + 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; - Input.apply(this, arguments); + // 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; - this.store = (this.manager.session.pointerEvents = []); - } + // update class name + frame.className = 'vis-itemset'; - 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) + resized = this._orderGroups() || resized; - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + // 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; - var isTouch = (pointerType == INPUT_TYPE_TOUCH); + 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; - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - // 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; - } + // 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; - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } + // update frame height + frame.style.height = asSize(height); - // update the event in the store - store[storeIndex] = ev; + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + // 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'; - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); - } - } - }); + // check if this component is resized + resized = this._isResized() || resized; - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + return resized; }; - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - /** - * Touch events input - * @constructor - * @extends Input + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup + * @private */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + 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]; - Input.apply(this, arguments); - } + return firstGroup || null; + }; - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + /** + * 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; - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; - if (!this.started) { - return; + 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 touches = normalizeSingleTouches.call(this, ev, type); - - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + ungrouped.add(item); } + } - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + ungrouped.show(); } - }); + } + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; + }; - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } + /** + * Set items + * @param {vis.DataSet | null} items + */ + ItemSet.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - return [all, changed]; - } + // 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'); + } - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } + + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); + + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + + // update the group holding all ungrouped items + this._updateUngrouped(); + } + }; /** - * Multi-user touch events input - * @constructor - * @extends Input + * Get the current items + * @returns {vis.DataSet | null} */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; + ItemSet.prototype.getItems = function () { + return this.itemsData; + }; - Input.apply(this, arguments); - } + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.off(event, callback); + }); - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - /** - * @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; + // 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'); + } - // 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]; - } + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); + // update the group holding all ungrouped items + this._updateUngrouped(); - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } + // update the order of all items in each group + this._order(); - // 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]); - } + this.body.emitter.emit('change', { queue: true }); + }; - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; - } - i++; - } + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function () { + return this.groupsData; + }; - if (!changedTargetTouches.length) { - return; - } + /** + * Remove an item by its id + * @param {String | Number} id + */ + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } + 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); + } + }); + } + }; /** - * 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 + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type + * @private */ - function TouchMouseInput() { - Input.apply(this, arguments); + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + }; - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } + /** + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId + * @private + */ + ItemSet.prototype._getGroupId = function (itemData) { + var type = this._getType(itemData); + if (type == 'background' && itemData.group == undefined) { + return BACKGROUND; + } else { + return this.groupsData ? itemData.group : UNGROUPED; + } + }; - inherit(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); + /** + * Handle updated items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - // 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; - } + ids.forEach((function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } + var constructor = ItemSet.types[type]; + var selected; - this.callback(manager, inputEvent, inputData); - }, + 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); + } + } - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); + 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)); - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); + }; - // 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'; + /** + * Handle added items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor + * Handle removed items + * @param {Number[]} ids + * @protected */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } + 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); + } + }); - 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 (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); + } + }; - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; - } - this.actions = value.toLowerCase().trim(); - }, + /** + * Update the order of item in all groups + * @private + */ + 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(); + }); + }; - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, + /** + * Handle updated groups + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - /** - * 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(' ')); - }, + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - /** - * 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; - } + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); - 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); + group = new Group(id, groupData, me); + me.groups[id] = group; - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); + // 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); + } } - }, + } - /** - * 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(); + group.order(); + group.show(); + } else { + // update group + group.setData(groupData); } + }); + + this.body.emitter.emit('change', { queue: true }); }; /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} + * Handle removed groups + * @param {Number[]} ids + * @private */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } - - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + if (group) { + group.hide(); + delete groups[id]; } + }); - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } + this.markDirty(); - return TOUCH_ACTION_AUTO; - } + this.body.emitter.emit('change', { queue: true }); + }; /** - * 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 + * Reorder the groups if needed + * @return {boolean} changed + * @private */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); - /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options - */ - function Recognizer(options) { - this.id = uniqueId(); + 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(); + }); - this.manager = null; - this.options = merge(options || {}, this.defaults); + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); + this.groupIds = groupIds; + } - this.state = STATE_POSSIBLE; + return changed; + } else { + return false; + } + }; - this.simultaneous = {}; - this.requireFail = []; - } + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + }; - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + /** + * 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; - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, + // update the items data (will redraw the item when displayed) + item.setData(itemData); - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; - } + // update group + if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); - } - return this; - }, + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + } + }; - /** - * 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; - } + /** + * 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(); - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, + // remove from items + delete this.items[item.id]; - /** - * 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; - } + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, + // remove from group + item.parent && item.parent.remove(item); + }; - /** - * 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; - } + /** + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private + */ + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); + } + } + return endArray; + }; - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, + /** + * 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; + }; - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, + /** + * Start dragging the selected events + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStart = function (event) { + var item = this.touchParams.item || null; + var me = this; + var props; - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; + if (item && item.selected) { - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } + if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { + return; + } - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } + // override options.editable + if (item.editable === false) { + return; + } - emit(); // simple 'eventName' events + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x, + dragLeft: true, + data: util.extend({}, item.data) // clone the items data + }; - /** - * 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; - }, + 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 + }; - /** - * can we emit? - * @returns {boolean} - */ - canEmit: function() { - var i = 0; - while (i < this.requireFail.length) { - if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { - return false; - } - i++; - } - return true; - }, + 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 + }; - /** - * 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); + return props; + }); + } - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } + 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); + } + }; - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } + /** + * Start creating a new range item by dragging. + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStartAddItem = function (event) { + var snap = this.options.snap || null; + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px + var time = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var start = snap ? snap(time, scale, step) : start; + var end = start; - this.state = this.process(inputDataClone); + var itemData = { + type: 'range', + start: start, + end: end, + content: 'new item' + }; - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); - } - }, + var id = util.randomUUID(); + itemData[this.itemsData._fieldId] = id; - /** - * 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 + var group = this.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, + 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); - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } + var props = { + item: newItem, + dragRight: true, + initialX: event.center.x, + data: util.extend({}, itemData) + }; + this.touchParams.itemProps = [props]; + + event.stopPropagation(); }; /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state + * Drag selected items + * @param {Event} event + * @private */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; - } - return ''; - } + ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - /** - * 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 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(); - /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} - */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; - } + // 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; - /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer - */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } + var itemData = util.extend({}, props.item.data); // clone the data - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + if (props.item.editable === false) { + return; + } - /** - * 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; - }, + var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; - /** - * 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 (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); - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var duration = initialEnd.valueOf() - initialStart.valueOf(); - // 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; + 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; } - return state | STATE_CHANGED; + } } - return STATE_FAILED; - } - }); + } - /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer - */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); - - this.pX = null; - this.pY = null; - } - - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, - - 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; - }, - - directionTest: function(input) { - var options = this.options; - var hasMoved = true; - var distance = input.distance; - var direction = input.direction; - var x = input.deltaX; - var y = input.deltaY; + var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; - // 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 (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; + } } - input.direction = direction; - - return hasMoved && distance > options.threshold && direction & options.direction; - }, - - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, - - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + } - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); + // confirm moving the item + me.options.onMoving(itemData, function (itemData) { + if (itemData) { + props.item.setData(itemData); } + }); + }); - this._super.emit.call(this, input); - } - }); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + } + }; /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId + * @private */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, - - 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); - }, + 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(); - 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); - } - } - }); + item.data.group = group.groupId; + } + }; /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer + * End of dragging selected items + * @param {Event} event + * @private */ - function PressRecognizer() { - Recognizer.apply(this, arguments); - - this._timer = null; - this._input = null; - } - - 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 - }, - - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - 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; + // prepare a change set for the changed items + var changes = []; + var me = this; + var dataset = this.itemsData.getDataSet(); - this._input = input; + 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; - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { - this.reset(); - } else if (input.eventType & INPUT_START) { - this.reset(); - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.time, this); - } else if (input.eventType & INPUT_END) { - return STATE_RECOGNIZED; - } - return STATE_FAILED; - }, + if (!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); + } - reset: function() { - clearTimeout(this._timer); - }, + // 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); - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit('change'); + } + }); + } + }); - 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); - } + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); } - }); + } + }; /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, + var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); + var shiftKey = event.srcEvent && event.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; + } - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + var oldSelection = this.getSelection(); - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } - }); + 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 + }); + } + }; /** - * Swipe - * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. - * @constructor - * @extends AttrRecognizer + * Handle creation and updates of an item on double tap + * @param event + * @private */ - function SwipeRecognizer() { - AttrRecognizer.apply(this, arguments); - } + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, + var me = this; + var snap = this.options.snap || null; + var item = this.itemFromTarget(event); - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + event.stopPropagation(); - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + if (item) { + // update item - 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; - } + // 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(); - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: 'new item' + }; - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + // 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; + } - this.manager.emit(this.options.event, input); + 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? + } + }); + } + }; /** - * 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 + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private */ - function TapRecognizer() { - Recognizer.apply(this, arguments); + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + var item = this.itemFromTarget(event); - this._timer = null; - this._input = null; - this.count = 0; - } + if (item) { + // multi select items (if allowed) - 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 - }, + var selection = this.options.multiselect ? this.getSelection() // take current selection + : []; // deselect current selection - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; - process: function(input) { - var options = this.options; + if (shiftKey && this.options.multiselect) { + // select all items between the old selection and the tapped item - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + // determine the selection range + selection.push(item.id); + var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); - this.reset(); + // select all items within the selection range + selection = []; + for (var id in this.items) { + if (this.items.hasOwnProperty(id)) { + var _item = this.items[id]; + var start = _item.data.start; + var end = _item.data.end !== undefined ? _item.data.end : start; - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); + 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); + } + } - // 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(); - } - - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + this.setSelection(selection); - this.pTime = input.timeStamp; - this.pCenter = input.center; + this.body.emitter.emit('select', { + items: this.getSelection(), + event: event + }); + } + }; - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + /** + * 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; - 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; - }, + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; + } + } + }); - reset: function() { - clearTimeout(this._timer); - }, + return { + min: min, + max: max + }; + }; - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); - } + /** + * 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 null; + }; /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor + * 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 */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } + 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 (i === 0 && clientY < top + foreground.offset) { + return group; + } + } + } + + return null; + }; /** - * @const {string} + * 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 */ - Hammer.VERSION = '2.0.4'; + ItemSet.itemSetFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; + } + target = target.parentNode; + } + + return null; + }; + + module.exports = ItemSet; + +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + 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); /** - * default settings - * @namespace + * 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 */ - 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, - /** - * 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 CustomTime(body, options) { + this.body = body; - /** - * @type {Boolean} - * @default true - */ - enable: true, + // default options + this.defaultOptions = { + locales: locales, + locale: 'en', + id: undefined + }; + this.options = util.extend({}, this.defaultOptions); - /** - * 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, + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); + } - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + this.eventParams = {}; // stores state parameters while dragging the bar - /** - * 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.setOptions(options); - /** - * 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', + // create the DOM + this._create(); + } - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + CustomTime.prototype = new Component(); - /** - * Disables the default callout shown when you touch and hold a touch target. - * On iOS, when you touch and hold a touch target such as a link, Safari displays - * a callout containing information about the link. This property allows you to disable that callout. - * @type {String} - * @default 'none' - */ - touchCallout: 'none', + /** + * Set 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); + } + }; - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + /** + * 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; - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + 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); - /** - * 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)' - } + // 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(); + //}); }; - var STOP = 1; - var FORCED_STOP = 2; - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor + * Destroy the CustomTime bar */ - function Manager(element, options) { - options = options || {}; + CustomTime.prototype.destroy = function () { + this.hide(); - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + this.hammer.destroy(); + this.hammer = null; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + this.body = null; + }; - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + /** + * 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); + } - toggleCssProps(this, true); + var x = this.body.util.toScreen(this.customTime); - 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 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); - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + this.bar.style.left = x + 'px'; + this.bar.title = title; - // 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; - }, + return 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; - }, + /** + * 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); + } + }; - /** - * 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; - } + /** + * Set custom time. + * @param {Date | number | string} time + */ + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, 'Date'); + this.redraw(); + }; - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); + /** + * Retrieve the current custom time. + * @return {Date} customTime + */ + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); + }; - var recognizer; - var recognizers = this.recognizers; + /** + * Start moving horizontally + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - // 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; + event.stopPropagation(); + }; - // 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; - } + /** + * Perform moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); - // 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(); - } + this.setCustomTime(time); - // 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++; - } - }, + // fire a timechange event + this.body.emitter.emit('timechange', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } + event.stopPropagation(); + }; - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; - }, + /** + * Stop moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - /** - * 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; - } - - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } + // fire a timechanged event + this.body.emitter.emit('timechanged', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - this.recognizers.push(recognizer); - recognizer.manager = this; + event.stopPropagation(); + }; - this.touchAction.update(); - return recognizer; - }, + /** + * 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; + } - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } + return null; + }; - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); + module.exports = CustomTime; - this.touchAction.update(); - return this; - }, +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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; - }, + 'use strict'; - /** - * 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; - }, + var Hammer = __webpack_require__(15); + var util = __webpack_require__(19); - /** - * 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); - } + /** + * @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 || {}; - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; - } + this.selected = false; + this.displayed = false; + this.dirty = true; - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + this.top = null; + this.left = null; + this.width = null; + this.height = null; - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; - } - }, + this.editable = null; + if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { + this.editable = data.editable; + } + } - /** - * 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); + Item.prototype.stack = true; - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; - } + /** + * Select current item + */ + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add + * Unselect current item */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; /** - * trigger dom event - * @param {String} event + * 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 */ - 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__(15)) { - !(__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'); - - -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - - /* WEBPACK VAR INJECTION */}.call(exports, {})) - -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { - - // 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'; + 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 moment = __webpack_require__(17); - var uuid = __webpack_require__(20); + if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { + this.editable = data.editable; + } - /** - * Test whether given object is a number - * @param {*} object - * @return {Boolean} isNumber - */ - exports.isNumber = function (object) { - return object instanceof Number || typeof object == 'number'; + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * Remove everything in the DOM object - * @param DOMobject + * Set a parent for the item + * @param {ItemSet | Group} parent */ - exports.recursiveDOMDelete = function (DOMobject) { - if (DOMobject) { - while (DOMobject.hasChildNodes() === true) { - exports.recursiveDOMDelete(DOMobject.firstChild); - DOMobject.removeChild(DOMobject.firstChild); + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); } + } else { + this.parent = parent; } }; /** - * 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} + * 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 */ - 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); - } + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; }; /** - * Test whether given object is a string - * @param {*} object - * @return {Boolean} isString + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed */ - exports.isString = function (object) { - return object instanceof String || typeof object == 'string'; + Item.prototype.show = function () { + return false; }; /** - * Test whether given object is a Date, or a String containing a Date - * @param {Date | String} object - * @return {Boolean} isDate + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed */ - 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; - } - } - + Item.prototype.hide = function () { return false; }; /** - * Create a semi UUID - * source: http://stackoverflow.com/a/105074/1262753 - * @return {String} uuid + * Repaint the item */ - exports.randomUUID = function () { - return uuid.v4(); - }; + Item.prototype.redraw = function () {}; /** - * assign all keys of an object that are not nested objects to a certain value (used for color objects). - * @param obj - * @param value + * Reposition the Item horizontally */ - exports.assignAllKeys = function (obj, value) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (typeof obj[prop] !== 'object') { - obj[prop] = value; - } + Item.prototype.repositionX = function () {}; + + /** + * 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); + }); + + 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; } }; /** - * 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 + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private */ - exports.fillIfDefined = function (a, b) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + 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; + } - for (var prop in a) { - if (b[prop] !== undefined) { - if (typeof b[prop] !== 'object') { - if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } - } else { - if (typeof a[prop] === 'object') { - exports.fillIfDefined(a[prop], b[prop], allowDeletion); - } + var 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); } } + + this.content = content; } }; /** - * 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 + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private */ - 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]; - } + Item.prototype._updateTitle = function (element) { + if (this.data.title != null) { + element.title = this.data.title || ''; + } else { + element.removeAttribute('vis-title'); } - return a; }; /** - * 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 + * 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 */ - 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]; + Item.prototype._updateDataAttributes = function (element) { + if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { + var attributes = []; + + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == 'all') { + attributes = Object.keys(this.data); + } else { + return; + } + + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; + + if (value != null) { + element.setAttribute('data-' + name, value); + } else { + element.removeAttribute('data-' + name); } } } - 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 + * Update custom styles of the element + * @param element + * @private */ - exports.selectiveExtend = function (props, a, b) { - if (!Array.isArray(props)) { - throw new Error('Array with property names expected as first argument'); + Item.prototype._updateStyle = function (element) { + // remove old styles + if (this.style) { + util.removeCssText(element, this.style); + this.style = null; } - 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)) { - a[prop] = other[prop]; - } - } + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; } - 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 + * Stringify the items contents + * @param {string | Element | undefined} content + * @returns {string | undefined} + * @private */ - exports.selectiveDeepExtend = 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 i = 2; i < arguments.length; i++) { - var other = arguments[i]; - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; - } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop], false, allowDeletion); - } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } - } - } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); - } else { - a[prop] = b[prop]; - } - } - } - } - return a; + Item.prototype._contentToString = function (content) { + if (typeof content === 'string') return content; + if (content && 'outerHTML' in content) return content.outerHTML; + return content; }; /** - * 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 + * Return the width of the item left from its start date + * @return {number} */ - 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; + Item.prototype.getWidthLeft = function () { + return 0; }; /** - * 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} + * Return the width of the item right from the max of its start and end date + * @return {number} */ - 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; + Item.prototype.getWidthRight = function () { + return 0; }; - /** - * 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; + module.exports = Item; - for (var i = 0, len = a.length; i < len; i++) { - if (a[i] != b[i]) return false; - } + // should be implemented by the item - return true; - }; + // should be implemented by the item - /** - * 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; + // should be implemented by the item - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + 'use strict'; + + 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.'); + }; + } + +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + + (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 - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); + /** + * 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 + }; - case 'number': - case 'Number': - return Number(object.valueOf()); + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; - case 'string': - case 'String': - return String(object); + 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); - case 'Date': - if (exports.isNumber(object)) { - return new Date(object); + PropagatingHammer.Manager = function (element, options) { + var o = Object.create(_options); + if (options) Hammer.extend(o, options); + return propagating(new Hammer.Manager(element, o), o); + }; + + return PropagatingHammer; + } + + // create a wrapper object which will override the functions + // `on`, `off`, `destroy`, and `emit` of the hammer instance + var wrapper = Object.create(hammer); + + // attach to DOM element + var element = hammer.element; + element.hammer = wrapper; + + // 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 (object instanceof Date) { - return new Date(object.valueOf()); - } else if (moment.isMoment(object)) { - return new Date(object.valueOf()); + if (event.isFirst) { + _firstTarget = event.target; } - 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 + }); + + /** @type {Object.>} */ + wrapper._handlers = {}; + + /** + * 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 + */ + 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); } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); - } + _handlers.push(handler); + }); - 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 + return wrapper; + }; + + /** + * 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 + */ + 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 (_handlers.length > 0) { + wrapper._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer.off(event, propagatedHandler); + delete wrapper._handlers[event]; + } } - } 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 + return wrapper; + }; + + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + wrapper.emit = function(eventType, event) { + _firstTarget = event.target; + hammer.emit(eventType, event); + }; + + wrapper.destroy = function () { + // Detach from DOM element + delete hammer.element.hammer; + + // 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); + } + + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + // it is possible that the same srcEvent is used with multiple hammer events, + // we keep track on which events are handled in an object _handled + if (!event.srcEvent._handled) { + event.srcEvent._handled = {}; } - } 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 + if (event.srcEvent._handled[event.type]) { + return; + } + else { + event.srcEvent._handled[event.type] = true; } - return '/Date(' + value + ')/'; - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); } - default: - throw new Error('Unknown type "' + type + '"'); - } - }; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - // parse ASP.Net Date pattern, - // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' - // code from http://momentjs.com/ - var ASPDateRegex = /^\/?Date\((\-?\d+)/i; + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - /** - * 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; + // 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); + } + } - if (type == 'object') { - if (object === null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'Boolean'; - } - if (object instanceof Number) { - return 'Number'; + elem = elem.parentNode; + } } - 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; - }; + return wrapper; + }; + })); - /** - * Used to extend an array and copy it. This is used to propagate paths recursively. + +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ * - * @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; - }; + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; + + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); + + var TYPE_FUNCTION = 'function'; + + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * Used to extend an array and copy it. This is used to propagate paths recursively. - * - * @param arr - * @param newValue - * @returns {Array} + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - exports.copyArray = function (arr) { - var newArr = []; - for (var i = 0; i < arr.length; i++) { - newArr.push(arr[i]); - } - return newArr; - }; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } /** - * 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. + * if the argument is an array, we want to execute the fn on each entry + * if it aint an array we don't want to do a thing. + * this is used by all the methods that accept a single and array argument. + * @param {*|Array} arg + * @param {String} fn + * @param {Object} [context] + * @returns {Boolean} */ - exports.getAbsoluteLeft = function (elem) { - return elem.getBoundingClientRect().left; - }; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; + } + return false; + } /** - * 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. + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - exports.getAbsoluteTop = function (elem) { - return elem.getBoundingClientRect().top; - }; + function each(obj, iterator, context) { + var i; + + if (!obj) { + return; + } + + 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); + } + } + } /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className + * 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 */ - 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(' '); - } - }; + function extend(dest, src, merge) { + var keys = Object.keys(src); + var i = 0; + while (i < keys.length) { + if (!merge || (merge && dest[keys[i]] === undefined)) { + dest[keys[i]] = src[keys[i]]; + } + i++; + } + return dest; + } /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className + * merge the values from src in the dest. + * means that properties that exist in dest will not be overwritten by src + * @param {Object} dest + * @param {Object} src + * @returns {Object} dest */ - exports.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(' '); - } - }; + function merge(dest, src) { + return extend(dest, src, true); + } /** - * 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) + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] */ - 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); - } + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; + + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; + + if (properties) { + extend(childP, properties); } - } - }; + } /** - * 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 + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} */ - exports.toArray = function (object) { - var array = []; + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - for (var prop in object) { - if (object.hasOwnProperty(prop)) array.push(object[prop]); - } + /** + * 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; + } - return array; - }; + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } /** - * Update a property in an object - * @param {Object} object - * @param {String} key - * @param {*} value - * @return {Boolean} changed + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - exports.updateProperty = function (object, key, value) { - if (object[key] !== value) { - object[key] = value; - return true; - } else { - return false; - } - }; + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } /** - * 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] + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - exports.addEventListener = function (element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) useCapture = false; + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; } + return false; + } - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent('on' + action, listener); // IE browsers - } - }; + /** + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found + */ + function inStr(str, find) { + return str.indexOf(find) > -1; + } /** - * 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] + * split string on whitespace + * @param {String} str + * @returns {Array} words */ - exports.removeEventListener = function (element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) useCapture = false; + function splitStr(str) { + return str.trim().split(/\s+/g); + } - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + /** + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index + */ + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); + } else { + var i = 0; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; + } + i++; + } + return -1; } - - 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. + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} */ - exports.preventDefault = function (event) { - if (!event) event = window.event; - - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } else { - event.returnValue = false; // IE browsers - } - }; + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } /** - * Get HTML element which is the target of the event - * @param {Event} event - * @return {Element} target element + * unique array with objects based on a key (like 'id') or just by the array's value + * @param {Array} src [{id:1},{id:2},{id:1}] + * @param {String} [key] + * @param {Boolean} [sort=False] + * @returns {Array} [{id:1},{id:2}] */ - exports.getTarget = function (event) { - // code from http://www.quirksmode.org/js/events_properties.html - if (!event) { - event = window.event; - } - - var target; + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - if (event.target) { - target = event.target; - } else if (event.srcElement) { - target = event.srcElement; - } + 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 (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } + } - return target; - }; + return results; + } /** - * Check if given element contains given parent somewhere in the DOM tree - * @param {Element} element - * @param {Element} parent + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - exports.hasParent = function (element, parent) { - var e = element; - - while (e) { - if (e === parent) { - return true; - } - e = e.parentNode; - } + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); - return false; - }; + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; - exports.option = {}; + if (prop in obj) { + return prop; + } + i++; + } + return undefined; + } /** - * Convert a value into a boolean - * @param {Boolean | function | undefined} value - * @param {Boolean} [defaultValue] - * @returns {Boolean} bool + * get a unique id + * @returns {number} uniqueId */ - exports.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } - - if (value != null) { - return value != false; - } - - return defaultValue || null; - }; + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } /** - * Convert a value into a number - * @param {Boolean | function | undefined} value - * @param {Number} [defaultValue] - * @returns {Number} number + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} */ - exports.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - if (value != null) { - return Number(value) || defaultValue || null; - } + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - return defaultValue || null; - }; + 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); - /** - * 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(); - } + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - if (value != null) { - return String(value); - } + var COMPUTE_INTERVAL = 25; - return defaultValue || null; - }; + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - /** - * Convert a size or location into a string with pixels or a percentage - * @param {String | Number | function | undefined} value - * @param {String} [defaultValue] - * @returns {String} size - */ - exports.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; - if (exports.isString(value)) { - return value; - } else if (exports.isNumber(value)) { - return value + 'px'; - } else { - return defaultValue || null; - } - }; + 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']; /** - * Convert a value into a DOM element - * @param {HTMLElement | function | undefined} value - * @param {HTMLElement} [defaultValue] - * @returns {HTMLElement | null} dom + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor */ - exports.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - return value || defaultValue || 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); + } + }; - /** - * 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; + 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); + } }; /** - * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. - * @param color - * @param opacity - * @returns {*} + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} */ - 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; + 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 { - return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; + Type = TouchMouseInput; } - } - }; + return new (Type)(manager, inputHandler); + } /** - * - * @param red 0 -- 255 - * @param green 0 -- 255 - * @param blue 0 -- 255 - * @returns {string} - * @constructor + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input */ - exports.RGBToHex = function (red, green, blue) { - return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); - }; + 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)); - /** - * 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 - } - }; - } 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; + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - if (exports.isString(color.highlight)) { - c.highlight = { - border: color.highlight, - background: color.highlight - }; - } else { - c.highlight = {}; - c.highlight.background = color.highlight && color.highlight.background || undefined; - c.highlight.border = color.highlight && color.highlight.border || undefined; + if (isFirst) { + manager.session = {}; } - 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; - } - } + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - return c; - }; + // compute scale, rotation etc + computeInputData(manager, input); + + // emit secret event + manager.emit('hammer.input', input); + + manager.recognize(input); + manager.session.prevInput = input; + } /** - * http://www.javascripter.net/faq/rgb2hsv.htm - * - * @param red - * @param green - * @param blue - * @returns {*} - * @constructor + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input */ - exports.RGBToHSV = function (red, green, blue) { - red = red / 255;green = green / 255;blue = blue / 255; - var minRGB = Math.min(red, Math.min(green, blue)); - var maxRGB = Math.max(red, Math.max(green, blue)); - - // Black-gray-white - if (minRGB == maxRGB) { - return { h: 0, s: 0, v: minRGB }; - } + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - // 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 }; - }; + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } - var cssUtil = { - // split a string with css styles into an object with key/values - split: function split(cssText) { - var styles = {}; + // 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; + } - 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; - } - }); + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - return styles; - }, + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - // 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('; '); - } - }; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - /** - * 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); + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - element.style.cssText = cssUtil.join(styles); - }; + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - /** - * 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); + computeIntervalInputData(session, input); - for (var key in removeStyles) { - if (removeStyles.hasOwnProperty(key)) { - delete styles[key]; + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; } - } + input.target = target; + } - element.style.cssText = cssUtil.join(styles); - }; + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - /** - * 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; + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; - 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); + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; + } - 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; - } + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } - return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; - }; + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - exports.HSVToHex = function (h, s, v) { - var rgb = exports.HSVToRGB(h, s, v); - return exports.RGBToHex(rgb.r, rgb.g, rgb.b); - }; + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - exports.hexToHSV = function (hex) { - var rgb = exports.hexToRGB(hex); - return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); - }; + 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); - exports.isValidHex = function (hex) { - var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); - return isOk; - }; + 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; + } - 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; - }; + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } /** - * This recursively redirects the prototype of JSON objects to the referenceObject - * This is used for default options. - * - * @param referenceObject - * @returns {*} + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData */ - exports.selectiveBridgeObject = function (fields, referenceObject) { - if (typeof referenceObject == 'object') { - var objectTo = Object.create(referenceObject); - for (var i = 0; i < fields.length; i++) { - if (referenceObject.hasOwnProperty(fields[i])) { - if (typeof referenceObject[fields[i]] == 'object') { - objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); - } - } + function 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++; } - 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]); - } - } - } - return objectTo; - } else { - return null; - } - }; + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } /** - * 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 + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties */ - exports.mergeOptions = function (mergeTarget, options, option) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + function getCenter(pointers) { + var pointersLength = pointers.length; - 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; - } - for (var prop in options[option]) { - if (options[option].hasOwnProperty(prop)) { - mergeTarget[option][prop] = options[option][prop]; - } - } - } + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; } - } - }; - /** - * 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; + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; + } - while (low <= high && iteration < maxIterations) { - var middle = Math.floor((low + high) / 2); + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } - var item = orderedItems[middle]; - var value = field2 === undefined ? item[field] : item[field][field2]; + /** + * calculate the velocity between two points. unit is in px per ms. + * @param {Number} deltaTime + * @param {Number} x + * @param {Number} y + * @return {Object} velocity `x` and `y` + */ + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } - 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; + /** + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction + */ + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; } - iteration++; - } - - return -1; - }; + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } /** - * 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 + * 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 */ - 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 getDistance(p1, p2, props) { + if (!props) { + props = PROPS_XY; + } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; - while (low <= high && iteration < maxIterations) { - // get a new guess - middle = Math.floor(0.5 * (high + low)); - prevValue = orderedItems[Math.max(0, middle - 1)][field]; - value = orderedItems[middle][field]; - nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; + return Math.sqrt((x * x) + (y * y)); + } - 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; - } + /** + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle + */ + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } - iteration++; - } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } - // didnt find anything. Return -1. - return -1; - }; + /** + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation + */ + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } - /* - * 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 + /** + * 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 */ - 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 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 }; -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. - 'use strict'; + /** + * Mouse events input + * @constructor + * @extends Input + */ + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(18); + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + Input.apply(this, arguments); + } - /* WEBPACK VAR INJECTION */(function(module) {//! moment.js - //! version : 2.10.3 - //! authors : Tim Wood, Iskren Chernev, Moment.js contributors - //! license : MIT - //! momentjs.com + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - (function (global, factory) { - true ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() - }(this, function () { 'use strict'; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; + } - var hookCallback; + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; + } - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); - } + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; - } + if (eventType & INPUT_END) { + this.pressed = false; + } - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } + }); - function isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; - } + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL + }; - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + // in IE10 the pointer types is defined as an enum + var IE10_POINTER_TYPE_ENUM = { + 2: INPUT_TYPE_TOUCH, + 3: INPUT_TYPE_PEN, + 4: INPUT_TYPE_MOUSE, + 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + }; - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); - } + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + /** + * Pointer events input + * @constructor + * @extends Input + */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } + Input.apply(this, arguments); - return a; - } + this.store = (this.manager.session.pointerEvents = []); + } - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - 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 - }; - } + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); - } - return m._pf; - } + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - 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; + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; + // 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 m._isValid; - } - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; } - return m; - } - - var momentProperties = utils_hooks__hooks.momentProperties = []; + // update the event in the store + store[storeIndex] = ev; - function copyConfig(to, from) { - var i, prop, val; + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); + } + }); + + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; + + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; + + /** + * Touch events input + * @constructor + * @extends Input + */ + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; + + Input.apply(this, arguments); + } + + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; + + if (!this.started) { + return; } - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } + var touches = normalizeSingleTouches.call(this, ev, type); + + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; } - return to; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } + }); - var updateInProgress = false; + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); - // 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; - } + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); - } + return [all, changed]; + } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - return value; - } + /** + * Multi-user touch events input + * @constructor + * @extends Input + */ + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - 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++; - } + Input.apply(this, arguments); + } + + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; } - return diffs + lengthDiff; - } - function Locale() { + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } + }); - var locales = {}; - var globalLocale; + /** + * @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; - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; + // 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]; } - // 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; + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - 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--; - } + // 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++; } - 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) { } + // 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 locales[name]; - } - - // 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); - } - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; } + i++; + } - return globalLocale._abbr; + if (!changedTargetTouches.length) { + return; } - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); + /** + * Combined touch and mouse input + * + * Touch has a higher priority then mouse, and while touching no mouse events are allowed. + * This because touch devices also emit mouse events while doing a touch. + * + * @constructor + * @extends Input + */ + function TouchMouseInput() { + Input.apply(this, arguments); - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - } + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - // returns locale data - function locale_locales__getLocale (key) { - var locale; + 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); - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; + // 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; } - if (!key) { - return globalLocale; + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; } - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } + this.callback(manager, inputEvent, inputData); + }, - return chooseLocale(key); + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); } + }); - var aliases = {}; + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; - } + // 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'; - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; - } + /** + * 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); + } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + 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(); + } - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; } + this.actions = value.toLowerCase().trim(); + }, - return normalizedInput; - } + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); + }, - 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); + /** + * 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()); } - }; - } - - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } + }); + return cleanTouchActions(actions.join(' ')); + }, - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } + /** + * 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; + } - // MOMENTS + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); - } + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; } - return this; - } - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + 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); - while (output.length < targetLength) { - output = '0' + output; + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } + }, - var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; + /** + * call preventDefault to prevent the browser's default behavior (scrolling in most cases) + * @param {Object} srcEvent + */ + preventSrc: function(srcEvent) { + this.manager.session.prevented = true; + srcEvent.preventDefault(); + } + }; - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + /** + * 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 formatFunctions = {}; + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - var formatTokenFunctions = {}; + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } - // token: 'M' - // padded: ['MM', 2] - // ordinal: 'Mo' - // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { - var func = callback; - if (typeof callback === 'string') { - func = function () { - return this[callback](); - }; - } - if (token) { - formatTokenFunctions[token] = func; - } - if (padded) { - formatTokenFunctions[padded[0]] = function () { - return zeroFill(func.apply(this, arguments), padded[1], padded[2]); - }; - } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; - } - } - - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); - } - return input.replace(/\\/g, ''); - } - - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; - - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } - - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } - - format = expandFormat(format, m.localeData()); - - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } - - return formatFunctions[format](m); + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; } - function expandFormat(format, locale) { - var i = 5; - - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + return TOUCH_ACTION_AUTO; + } - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + /** + * 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; - return format; - } + /** + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options + */ + function Recognizer(options) { + this.id = uniqueId(); - 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.manager = null; + this.options = merge(options || {}, this.defaults); - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + this.state = STATE_POSSIBLE; - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + this.simultaneous = {}; + this.requireFail = []; + } - // 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; + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - var regexes = {}; + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; - } + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; } - return regexes[token](config._strict, config._locale); - } - - // 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 tokens = {}; - - 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; + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); } - } - - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); - }); - } + return this; + }, - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); + /** + * 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 YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - // FORMATTING - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; - }); + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); - }); + /** + * 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; + } - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - // ALIASES + /** + * 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; + } - addUnitAlias('month', 'M'); + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - // PARSING + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; - }); + /** + * 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; - 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; + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); } - }); - - // LOCALES - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; - } + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; - } + emit(); // simple 'eventName' events - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; + /** + * 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; + }, - 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; + /** + * 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++; } - } - - // MOMENTS + return true; + }, - function setMonth (mom, value) { - var dayOfMonth; + /** + * 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); - // 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; - } + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; } - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; } - } - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); - } + this.state = this.process(inputDataClone); - function checkOverflow (m) { - var overflow; - var a = m._a; + // 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 (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; + /** + * 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 - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - getParsingFlags(m).overflow = overflow; - } + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; - return m; + /** + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; } + return ''; + } - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } - } - - 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); + /** + * 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 deprecations = {}; - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; - } + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} + */ + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); } + return otherRecognizer; + } - utils_hooks__hooks.suppressDeprecationWarnings = false; - - 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)?)?$/; + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - 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}/] - ]; + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 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/] - ]; + /** + * 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; + }, - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + /** + * 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; - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - 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'; + // 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; } - configFromStringAndFormat(config); - } else { - config._isValid = false; + return state | STATE_CHANGED; } + return STATE_FAILED; } + }); - // 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; - } - - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); - } - } + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - 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' : '')); - } - ); + this.pX = null; + this.pY = null; + } - 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); + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); } - return date; - } - - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); } - return date; - } - - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; - }); - - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - - // ALIASES + return actions; + }, - addUnitAlias('year', 'y'); + 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; - // PARSING + // 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; - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); + return hasMoved && distance > options.threshold && direction & options.direction; + }, - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - // HELPERS + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + this._super.emit.call(this, input); } + }); - // HOOKS + /** + * 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); + } - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - // MOMENTS + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - var getSetYear = makeGetSet('FullYear', false); + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - function getIsLeapYear () { - return isLeapYear(this.year()); + 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); + } } + }); - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - - // ALIASES + /** + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer + */ + function PressRecognizer() { + Recognizer.apply(this, arguments); - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); + this._timer = null; + this._input = null; + } - // PARSING + 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 + }, - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); + 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; - // HELPERS + this._input = input; - // 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; + // 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; + }, + reset: function() { + clearTimeout(this._timer); + }, - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + 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); } - - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; } + }); - // LOCALES - - function localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - } + /** + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer + */ + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - 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. - }; + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - function localeFirstDayOfWeek () { - return this._week.dow; - } + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - function localeFirstDayOfYear () { - return this._week.doy; + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); } + }); - // MOMENTS + /** + * 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); + } - function getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - } + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - } + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - // ALIASES + 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; + } - addUnitAlias('dayOfYear', 'DDD'); + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - // PARSING + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); - - // HELPERS + this.manager.emit(this.options.event, input); + } + }); - //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; + /** + * 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); - 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; + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + this._timer = null; + this._input = null; + this.count = 0; + } - // MOMENTS + 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 + }, - 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'); - } + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - // 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; - } + process: function(input) { + var options = this.options; - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; - } - return [now.getFullYear(), now.getMonth(), now.getDate()]; - } + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - // 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; + this.reset(); - if (config._d) { - return; + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - currentDate = currentDateArray(config); + // 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(); + } - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + this.pTime = input.timeStamp; + this.pCenter = input.center; - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; } - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } - - // 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]; - } + this._input = input; - // Zero out whatever was not defaulted, including time - for (; i < 7; i++) { - config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + // if 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; + }, - // 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; - } + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, - 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); - } + reset: function() { + clearTimeout(this._timer); + }, - if (config._nextDay) { - config._a[HOUR] = 24; + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); } } + }); - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // 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; - - 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); + /** + * 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); + } - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } + /** + * @const {string} + */ + Hammer.VERSION = '2.0.4'; - utils_hooks__hooks.ISO_8601 = function () {}; + /** + * 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, - // 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; - } + /** + * 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, - config._a = []; - getParsingFlags(config).empty = true; + /** + * @type {Boolean} + * @default true + */ + enable: true, - // 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; + /** + * 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, - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - 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); - } - } + /** + * 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] + ], - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } + /** + * 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', - // 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); + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - configFromArray(config); - checkOverflow(config); - } + /** + * 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', + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', - 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; - } + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' } + }; - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, + var STOP = 1; + var FORCED_STOP = 2; - scoreToBeat, - i, - currentScore; + /** + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Manager(element, options) { + options = options || {}; - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - 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); + this.handlers = {}; + this.session = {}; + this.recognizers = []; - if (!valid__isValid(tempConfig)) { - continue; - } + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; + toggleCssProps(this, true); - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + 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); + } - getParsingFlags(tempConfig).score = currentScore; + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); } - - extend(config, bestMoment || tempConfig); - } - - function configFromObject(config) { - if (config._d) { - return; + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); } + return this; + }, - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - - configFromArray(config); - } - - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; + /** + * 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; + }, - config._locale = config._locale || locale_locales__getLocale(config._l); + /** + * 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; + } - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); + + var recognizer; + var recognizers = this.recognizers; + + // 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; + + // 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; } - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; + + // 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 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++; } + }, - 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); + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; } - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } } + return null; + }, - return res; - } + /** + * 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; + } - 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); + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); } - } - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; + this.recognizers.push(recognizer); + recognizer.manager = this; - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; + this.touchAction.update(); + return recognizer; + }, + + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c._isAMomentObject = true; - c._useUTC = c._isUTC = isUTC; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - return createFromConfig(c); - } + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } + this.touchAction.update(); + return this; + }, - 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; - } - ); + /** + * 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; + }, - 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; - } - ); + /** + * 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; + }, - // 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(); + /** + * 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); } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } + + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; } - return res; - } - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - return pickBy('isBefore', args); - } + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; + } + }, - function max () { - var args = [].slice.call(arguments, 0); + /** + * 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); - return pickBy('isAfter', args); + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; } + }; - 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; + /** + * 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 : ''; + }); + } - this._data = {}; + /** + * trigger dom event + * @param {String} event + * @param {Object} data + */ + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } - this._locale = locale_locales__getLocale(); + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - this._bubble(); - } + 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, - function isDuration (obj) { - return obj instanceof Duration; - } + 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 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); - }); - } + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - offset('Z', ':'); - offset('ZZ', ''); + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - // PARSING + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); - }); + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); - // HELPERS + 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; + } - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; + })(window, document, 'Hammer'); - 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]); - return parts[0] === '+' ? minutes : -minutes; - } +/***/ }, +/* 18 */ +/***/ function(module, exports, __webpack_require__) { - // 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(); - } + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - 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; - } + /* WEBPACK VAR INJECTION */}.call(exports, {})) - // HOOKS +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { - // 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 () {}; + // utility functions - // MOMENTS + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. - // 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); - } - } + 'use strict'; - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } + var moment = __webpack_require__(20); + var uuid = __webpack_require__(23); - this.utcOffset(input, keepLocalTime); + /** + * Test whether given object is a number + * @param {*} object + * @return {Boolean} isNumber + */ + exports.isNumber = function (object) { + return object instanceof Number || typeof object == 'number'; + }; - return this; - } else { - return -this.utcOffset(); - } + /** + * 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); } + } + }; - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } + /** + * 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); + } + }; - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + /** + * Test whether given object is a string + * @param {*} object + * @return {Boolean} isString + */ + exports.isString = function (object) { + return object instanceof String || typeof object == 'string'; + }; - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; + /** + * 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; } + } - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); - } - return this; - } + return false; + }; - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); - } + /** + * Create a semi UUID + * source: http://stackoverflow.com/a/105074/1262753 + * @return {String} uuid + */ + exports.randomUUID = function () { + return uuid.v4(); + }; - return (this.utcOffset() - input) % 60 === 0; + /** + * 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; + } } + } + }; - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); - } + /** + * 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]; - 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; + 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]; } - - return false; + } else { + if (typeof a[prop] === 'object') { + exports.fillIfDefined(a[prop], b[prop], allowDeletion); + } + } } + } + }; - function isLocal () { - return !this._isUTC; + /** + * 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; + }; - function isUtcOffset () { - return this._isUTC; + /** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ + exports.extend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } } + } + return a; + }; - function isUtc () { - return this._isUTC && this._offset === 0; - } + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveExtend = function (props, a, b) { + if (!Array.isArray(props)) { + throw new Error('Array with property names expected as first argument'); + } - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } + } + } + return a; + }; - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; + /** + * 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]; - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; + // 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 { - duration.milliseconds = input; + a[prop] = b[prop]; } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = create__isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); - - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } - - ret = new Duration(duration); - - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; + } + } else if (Array.isArray(b[prop])) { + throw new TypeError('Arrays are not supported by deepExtend'); + } else { + a[prop] = b[prop]; } - - return ret; - } - - 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; + } } + } + return a; + }; - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; + /** + * 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]; - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; + // 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]; } - - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; + } } + } + return a; + }; - function momentsDifference(base, other) { - var res; - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); + /** + * 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 { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } } - - return res; + } 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; + }; - 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; - } + /** + * 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; - val = typeof val === 'string' ? +val : val; - dur = create__createDuration(val, period); - add_subtract__addSubtract(this, dur, direction); - return this; - }; - } + for (var i = 0, len = a.length; i < len; i++) { + if (a[i] != b[i]) return false; + } - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + return true; + }; - 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); - } - } + /** + * 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; - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } - 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))); - } + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } - function clone () { - return new Moment(this); - } + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); - 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); - } - } + case 'number': + case 'Number': + return Number(object.valueOf()); - 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 isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } + case 'string': + case 'String': + return String(object); - function isSame (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this === +input; + 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 { - inputMs = +local__createLocal(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + return moment(object).toDate(); // parse string } - } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); + } - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); + 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 Math.floor(number); + return moment(object); // parse string } - } - - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; - - units = normalizeUnits(units); + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); + } - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } + 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 { - 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 new Date(object).toISOString(); // parse string } - return asFloat ? output : absFloor(output); - } - - 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; + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); + } - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); + 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 { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); + value = new Date(object).valueOf(); // parse string } + return '/Date(' + value + ')/'; + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); + } - return -(wholeMonthDiff + adjust); - } + default: + throw new Error('Unknown type "' + type + '"'); + } + }; - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + // parse ASP.Net Date pattern, + // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' + // code from http://momentjs.com/ + var ASPDateRegex = /^\/?Date\((\-?\d+)/i; - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - } + /** + * 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; - 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]'); - } + if (type == 'object') { + if (object === null) { + return 'null'; } - - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); + if (object instanceof Boolean) { + return 'Boolean'; } - - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + if (object instanceof Number) { + return 'Number'; } - - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); + if (object instanceof String) { + return 'String'; } - - 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(object)) { + return 'Array'; } - - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); + 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'; + } - function locale (key) { - var newLocaleData; - - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - } + return type; + }; - 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); - } - } - ); + /** + * 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; + }; - function localeData () { - return this._locale; - } + /** + * 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; + }; - 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); - } + /** + * 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; + }; - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } + /** + * 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; + }; - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } + /** + * 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(' '); + } + }; - return this; - } + /** + * 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(' '); + } + }; - 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'); + /** + * 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); } - - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); + } else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } } + } + }; - function unix () { - return Math.floor(+this / 1000); - } + /** + * 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 = []; - function toDate () { - return this._offset ? new Date(+this) : this._d; - } + for (var prop in object) { + if (object.hasOwnProperty(prop)) array.push(object[prop]); + } - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; - } + return array; + }; - function moment_valid__isValid () { - return valid__isValid(this); - } + /** + * 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; + } + }; - function parsingFlags () { - return extend({}, getParsingFlags(this)); - } + /** + * 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; - function invalidAt () { - return getParsingFlags(this).overflow; + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent('on' + action, listener); // IE browsers + } + }; - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); + /** + * 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; - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent('on' + action, listener); + } + }; - // ALIASES + /** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ + exports.preventDefault = function (event) { + if (!event) event = window.event; - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } else { + event.returnValue = false; // IE browsers + } + }; - // PARSING + /** + * 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; + } - 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); - - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); - - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + var target; - // HELPERS + if (event.target) { + target = event.target; + } else if (event.srcElement) { + target = event.srcElement; + } - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; - } + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } - // MOMENTS + return target; + }; - 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'); - } + /** + * 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; - function getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); + while (e) { + if (e === parent) { + return true; } + e = e.parentNode; + } - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); - } + return false; + }; - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - } + exports.option = {}; - addFormatToken('Q', 0, 0, 'quarter'); + /** + * 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(); + } - // ALIASES + if (value != null) { + return value != false; + } - addUnitAlias('quarter', 'Q'); + return defaultValue || null; + }; - // PARSING + /** + * 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(); + } - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); + if (value != null) { + return Number(value) || defaultValue || null; + } - // MOMENTS + return defaultValue || null; + }; - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - } + /** + * 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(); + } - addFormatToken('D', ['DD', 2], 'Do', 'date'); + if (value != null) { + return String(value); + } - // ALIASES + return defaultValue || null; + }; - addUnitAlias('date', 'D'); + /** + * 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(); + } - // PARSING + if (exports.isString(value)) { + return value; + } else if (exports.isNumber(value)) { + return value + 'px'; + } else { + return defaultValue || null; + } + }; - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; - }); + /** + * 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(); + } - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); - }); + return value || defaultValue || null; + }; - // MOMENTS + /** + * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + * + * @param {String} hex + * @returns {{r: *, g: *, b: *}} | 255 range + */ + exports.hexToRGB = function (hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + }; - var getSetDayOfMonth = makeGetSet('Date', true); + /** + * 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 + ')'; + } + } + }; - addFormatToken('d', 0, 'do', 'day'); + /** + * + * @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); + }; - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); + /** + * 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 + } + }; + } 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; - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); + 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; + } - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); + 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; + } + } - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); + return c; + }; - // ALIASES + /** + * 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)); - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); + // Black-gray-white + if (minRGB == maxRGB) { + return { h: 0, s: 0, v: minRGB }; + } - // PARSING + // 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 }; + }; - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + var cssUtil = { + // split a string with css styles into an object with key/values + split: function split(cssText) { + var styles = {}; - 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; - } + 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; + } }); - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(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('; '); + } + }; - 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; - } + /** + * 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); - // LOCALES + element.style.cssText = cssUtil.join(styles); + }; - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } + /** + * 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); - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; + for (var key in removeStyles) { + if (removeStyles.hasOwnProperty(key)) { + delete styles[key]; } + } - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; - } + element.style.cssText = cssUtil.join(styles); + }; - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; + /** + * 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; - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + 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); - 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; - } - } - } + 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; + } - // MOMENTS + return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; + }; - 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; - } - } + exports.HSVToHex = function (h, s, v) { + var rgb = exports.HSVToRGB(h, s, v); + return exports.RGBToHex(rgb.r, rgb.g, rgb.b); + }; - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - } + exports.hexToHSV = function (hex) { + var rgb = exports.hexToRGB(hex); + return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); + }; - 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); - } + exports.isValidHex = function (hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; + }; - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); + 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 meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); - } - - meridiem('a', true); - meridiem('A', false); - - // ALIASES + /** + * 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]]); + } + } + } + return objectTo; + } else { + return null; + } + }; - addUnitAlias('hour', 'h'); + /** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ + exports.bridgeObject = function (referenceObject) { + if (typeof referenceObject == 'object') { + var objectTo = Object.create(referenceObject); + for (var i in referenceObject) { + if (referenceObject.hasOwnProperty(i)) { + if (typeof referenceObject[i] == 'object') { + objectTo[i] = exports.bridgeObject(referenceObject[i]); + } + } + } + return objectTo; + } else { + return null; + } + }; - // PARSING + /** + * 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]; - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; + 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; + } + for (var prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } + } + } } + } + }; - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); - - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - getParsingFlags(config).bigHour = true; - }); + /** + * 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; - // LOCALES + while (low <= high && iteration < maxIterations) { + var middle = Math.floor((low + high) / 2); - 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 item = orderedItems[middle]; + var value = field2 === undefined ? item[field] : item[field][field2]; - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } + 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; } + iteration++; + } - // MOMENTS - - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); + return -1; + }; - addFormatToken('m', ['mm', 2], 0, 'minute'); + /** + * 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; - // ALIASES + 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]; - addUnitAlias('minute', 'm'); + 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++; + } - // PARSING + // didnt find anything. Return -1. + return -1; + }; - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); + /* + * 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; + } + }; - // MOMENTS +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - var getSetMinute = makeGetSet('Minutes', false); + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + 'use strict'; - addFormatToken('s', ['ss', 2], 0, 'second'); + module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(21); - // ALIASES +/***/ }, +/* 21 */ +/***/ function(module, exports, __webpack_require__) { - addUnitAlias('second', 's'); + /* WEBPACK VAR INJECTION */(function(module) {//! moment.js + //! version : 2.10.3 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com - // PARSING + (function (global, factory) { + true ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() + }(this, function () { 'use strict'; - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); + var hookCallback; - // MOMENTS + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); + } - var getSetSecond = makeGetSet('Seconds', false); + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; + } - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + } - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; } - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + } - // ALIASES + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } - addUnitAlias('millisecond', 'ms'); + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - // PARSING + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } - 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); - }); + return a; + } - // MOMENTS + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); + } - var getSetMillisecond = makeGetSet('Milliseconds', false); + 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 + }; + } - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; + } - // MOMENTS + 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 getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + return m._isValid; } - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; + } - var momentPrototype__proto = Moment.prototype; + return m; + } - 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; + var momentProperties = utils_hooks__hooks.momentProperties = []; - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; + function copyConfig(to, from) { + var i, prop, val; - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; + } + if (typeof from._i !== 'undefined') { + to._i = from._i; + } + if (typeof from._f !== 'undefined') { + to._f = from._f; + } + if (typeof from._l !== 'undefined') { + to._l = from._l; + } + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; + } + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; + } + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; + } + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; + } + if (typeof from._pf !== 'undefined') { + to._pf = getParsingFlags(from); + } + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; + } - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; + return to; + } - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + var updateInProgress = false; - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; + // 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; + } + } - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + } - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + return value; + } - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; + function 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; + } - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; + function Locale() { + } - // 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 locales = {}; + var globalLocale; - var momentPrototype = momentPrototype__proto; + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } - function moment__createUnix (input) { - return local__createLocal(input * 1000); + // 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++; + } + return null; } - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); + function loadLocale(name) { + var oldLocale = null; + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && typeof module !== 'undefined' && + module && module.exports) { + try { + oldLocale = globalLocale._abbr; + !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); + // because defineLocale currently also sets the global locale, we + // want to undo that for lazy loaded locales + locale_locales__getSetGlobalLocale(oldLocale); + } catch (e) { } + } + return locales[name]; } - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function locale_locales__getSetGlobalLocale (key, values) { + var data; + if (key) { + if (typeof values === 'undefined') { + data = locale_locales__getLocale(key); + } + else { + data = defineLocale(key, values); + } - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } + } + + return globalLocale._abbr; } - 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' - }; + function defineLocale (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); + + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; } - return output; } - var defaultInvalidDate = 'Invalid date'; + // returns locale data + function locale_locales__getLocale (key) { + var locale; - function invalidDate () { - return this._invalidDate; + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); } - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; + var aliases = {}; - function ordinal (number) { - return this._ordinal.replace('%d', number); + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; } - function preParsePostFormat (string) { - return string; + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; } - 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 normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - 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); - } + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + return normalizedInput; } - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; + 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 { - this['_' + i] = prop; + return get_set__get(this, unit); + } + }; + } + + function get_set__get (mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } + + function get_set__set (mom, unit, value) { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + + // MOMENTS + + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } else { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + return this[units](value); } } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); + return this; } - var prototype__proto = Locale.prototype; + function zeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; - 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; + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + 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; - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - // 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; + var formatFunctions = {}; - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; + var formatTokenFunctions = {}; - 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); + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } } - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); } + return input.replace(/\\/g, ''); + } - format = format || ''; + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - if (index != null) { - return lists__get(format, index, field, setter); + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } } - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); - } - return out; + 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; + }; } - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); - } + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); - } + format = expandFormat(format, m.localeData()); - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); - } + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); + return formatFunctions[format](m); } - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); - } + function expandFormat(format, locale) { + var i = 5; - 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; + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; } - }); - // 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); + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - var mathAbs = Math.abs; + return format; + } - function duration_abs__abs () { - var data = this._data; + 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._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf - 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); + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - return this; - } + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); + // 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; - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; + var regexes = {}; - return duration._bubble(); + function addRegexToken (token, regex, strictRegex) { + regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; } - // 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); + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); } - // 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); + // 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, '\\$&'); } - 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 tokens = {}; - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + 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; + } + } - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } + } - hours = absFloor(minutes / 60); - data.hours = hours % 24; + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; - days += absFloor(hours / 24); + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); + // FORMATTING - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); - data.days = days; - data.months = months; - data.years = years; + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); - return this; + // ALIASES + + addUnitAlias('month', 'M'); + + // 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; + }); + + 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; + } + }); + + // LOCALES + + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m) { + return this._months[m.month()]; } - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m) { + return this._monthsShort[m.month()]; } - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + 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; + } + } } - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; + // MOMENTS - units = normalizeUnits(units); + function setMonth (mom, value) { + var dayOfMonth; - 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); + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; } } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; } - // 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 getSetMonth (value) { + if (value != null) { + setMonth(this, value); + utils_hooks__hooks.updateOffset(this, true); + return this; + } else { + return get_set__get(this, 'Month'); + } } - function makeAs (alias) { - return function () { - return this.as(alias); - }; + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); } - 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 checkOverflow (m) { + var overflow; + var a = m._a; - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; } - function makeGetter(name) { - return function () { - return this._data[name]; - }; + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); + } } - 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 deprecate(msg, fn) { + var firstTime = true, + msgWithStack = msg + '\n' + (new Error()).stack; - function weeks () { - return absFloor(this.days() / 7); + return extend(function () { + if (firstTime) { + warn(msgWithStack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); } - 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 - }; + var deprecations = {}; - // 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); + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } } - 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')); + utils_hooks__hooks.suppressDeprecationWarnings = false; - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; + var 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)?)?$/; - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } + 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}/] + ]; - // 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]; + // 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/] + ]; + + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = from_string__isoRegex.exec(string); + + 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; } - thresholds[threshold] = limit; - return true; } - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); + // date from iso format or fallback + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); - if (withSuffix) { - output = locale.pastFuture(+this, output); + if (matched !== null) { + config._d = new Date(+matched[1]); + return; } - return locale.postformat(output); + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); + } } - var iso_string__abs = Math.abs; + 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 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(); + 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); - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); } - - 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' : ''); + return date; } - var duration_prototype__proto = Duration.prototype; + function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } - 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; + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - // Side effect imports + // ALIASES - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); + addUnitAlias('year', 'y'); // PARSING - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); + addRegexToken('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); }); - // Side effect imports + // HELPERS + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } - utils_hooks__hooks.version = '2.10.3'; + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } - setHookCallback(local__createLocal); + // HOOKS - 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; + utils_hooks__hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - var _moment = utils_hooks__hooks; + // MOMENTS - return _moment; + var getSetYear = makeGetSet('FullYear', false); - })); - /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(19)(module))) + function getIsLeapYear () { + return isLeapYear(this.year()); + } -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + // ALIASES + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { + // PARSING - /* WEBPACK VAR INJECTION */(function(global) {'use strict'; + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); - var _rng; + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + }); - var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; + // HELPERS - 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; - }; - } + // 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; - 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 (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } + + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } + + adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; } - return _rnds; - }; - } + // LOCALES - // uuid.js - // - // Copyright (c) 2010-2012 Robert Kieffer - // MIT License - http://opensource.org/licenses/mit-license.php + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } - // 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 + 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. + }; - //var _rng = require('./rng'); + function localeFirstDayOfWeek () { + return this._week.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; - } + function localeFirstDayOfYear () { + return this._week.doy; + } - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = buf && offset || 0, - ii = 0; + // MOMENTS - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { - if (ii < 16) { - // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); } - }); - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + } - return buf; - } + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - // **`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++]]; - } + // ALIASES - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html + addUnitAlias('dayOfYear', 'DDD'); - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); + // PARSING - // 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]]; + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; + // HELPERS - // Previous uuid creation time - var _lastMSecs = 0, - _lastNSecs = 0; + //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; - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; + 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; - options = options || {}; + return { + year : dayOfYear > 0 ? year : year - 1, + dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + // MOMENTS - // 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 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'); + } - // 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; + // 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; + } - // Time since last uuid creation (in msecs) - var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; + } + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq === undefined) { - clockseq = clockseq + 1 & 16383; - } + // 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; - // 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 (config._d) { + return; + } - // 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'); - } + currentDate = currentDateArray(config); - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - // `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 (config._dayOfYear > daysInYear(yearToUse)) { + getParsingFlags(config)._overflowDayOfYear = true; + } - // `time_mid` - var tmh = msecs / 4294967296 * 10000 & 268435455; - b[i++] = tmh >>> 8 & 255; - b[i++] = tmh & 255; + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } - // `time_high_and_version` - b[i++] = tmh >>> 24 & 15 | 16; // include version - b[i++] = tmh >>> 16 & 255; + // 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]; + } - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 128; + // 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]; + } - // `clock_seq_low` - b[i++] = clockseq & 255; + // 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; + } - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } - return buf ? buf : unparse(b); - } + if (config._nextDay) { + config._a[HOUR] = 24; + } + } - // **`v4()` - Generate random UUID** + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - // 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; + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - if (typeof options == 'string') { - buf = options == 'binary' ? new Array(16) : null; - options = null; - } - options = options || {}; + // 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; - var rnds = options.random || (options.rng || _rng)(); + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = rnds[6] & 15 | 64; - rnds[8] = rnds[8] & 63 | 128; + if (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); - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; } - } - return buf || unparse(rnds); - } + utils_hooks__hooks.ISO_8601 = function () {}; - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; + // 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; + } - module.exports = uuid; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) + config._a = []; + getParsingFlags(config).empty = true; -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { + // 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; - // DOM utility methods + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - /** - * this prepares the JSON container for allocating SVG elements - * @param JSONcontainer - * @private - */ - 'use strict'; + 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); + } + } - 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 = []; + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } + + // 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); + + configFromArray(config); + checkOverflow(config); } - } - }; - /** - * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from - * which to remove the redundant elements. - * - * @param JSONcontainer - * @private - */ - exports.cleanupElements = function (JSONcontainer) { - // cleanup the redundant svgElements; - for (var elementType in JSONcontainer) { - if (JSONcontainer.hasOwnProperty(elementType)) { - if (JSONcontainer[elementType].redundant) { - for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { - JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); + + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; + + 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; } - 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; - }; - - /** - * 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); - } else { - DOMContainer.appendChild(element); - } - } - 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 {*} - */ - 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 - - if (labelObj) { - var label = exports.getSVGElement('text', JSONcontainer, svgContainer); - if (labelObj.xOffset) { - x = x + labelObj.xOffset; - } - - if (labelObj.yOffset) { - y = y + labelObj.yOffset; - } - if (labelObj.content) { - label.textContent = labelObj.content; - } - - if (labelObj.className) { - label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); - } - label.setAttributeNS(null, 'x', x); - label.setAttributeNS(null, 'y', y); - } - - return point; - }; - - /** - * 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); - } - } - }; - -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var util = __webpack_require__(16); - var Queue = __webpack_require__(23); - - /** - * DataSet - * - * Usage: - * var dataSet = new DataSet({ - * fieldId: '_id', - * type: { - * // ... - * } - * }); - * - * dataSet.add(item); - * dataSet.add(data); - * dataSet.update(item); - * dataSet.update(data); - * dataSet.remove(id); - * dataSet.remove(ids); - * var data = dataSet.get(); - * var data = dataSet.get(id); - * var data = dataSet.get(ids); - * var data = dataSet.get(ids, options, data); - * dataSet.clear(); - * - * A data set can: - * - add/remove/update data - * - gives triggers upon changes in the data - * - can import/export data in various data formats - * - * @param {Array} [data] Optional array with initial data - * @param {Object} [options] Available options: - * {String} fieldId Field name of the id in the - * items, 'id' by default. - * {Object.} [type] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by a field name or custom sort function. - * @throws Error - */ - DataSet.prototype.get = function (args) { - var me = this; - - // 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]; - } - - // 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'; - } - - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; - - // 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); - } - } - } - } - - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } - - // 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); - } - } - } - - // 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; - } - } - }; - - /** - * 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 (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - items.push(item); - } - } - } - - this._sort(items, order); - - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - ids.push(item[this._fieldId]); - } - } - } - } - } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); - } - } - - this._sort(items, order); - - 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]); - } - } - } - } - - return ids; - }; - - /** - * 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; - }; - - /** - * 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; - - 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); - } - } - } - } - }; - - /** - * Map every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Object[]} mappedItems - */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; - - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } - } - - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } - - return mappedItems; - }; - - /** - * 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; - } - - 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]; - } - } - } - - return filteredItem; - }; - - /** - * 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'); - } - }; - - /** - * 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; - - 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 (removedIds.length) { - this._trigger('remove', { items: removedIds }, senderId); - } - - 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; - }; - - /** - * 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); - - this._data = {}; - this.length = 0; - - this._trigger('remove', { items: ids }, senderId); - - return ids; - }; - - /** - * 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; - - 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; - }; - - /** - * 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; - - 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; - } - } - } - - return min; - }; - - /** - * 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++; - } - } - } - - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } - } - - 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; - } - - 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++; - - return id; - }; - - /** - * 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; - - // 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; - }; - - /** - * 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'); - } - - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } - - return id; - }; - - module.exports = DataSet; - -/***/ }, -/* 23 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * 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 configFromStringAndArray(config) { + var tempConfig, + bestMoment, - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; + scoreToBeat, + i, + currentScore; - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } - this.setOptions(options); - } + 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); - /** - * 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 (!valid__isValid(tempConfig)) { + continue; + } - this._flushIfNeeded(); - }; + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; - /** - * 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); + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - if (object.flush !== undefined) { - throw new Error('Target object already has a property flush'); - } - object.flush = function () { - queue.flush(); - }; + getParsingFlags(tempConfig).score = currentScore; - var methods = [{ - name: 'flush', - original: undefined - }]; + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - 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); + extend(config, bestMoment || tempConfig); } - } - - queue._extended = { - object: object, - methods: methods - }; - return queue; - }; - - /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. - */ - Queue.prototype.destroy = function () { - this.flush(); - - if (this._extended) { - var object = this._extended.object; - var methods = this._extended.methods; - for (var i = 0; i < methods.length; i++) { - var method = methods[i]; - if (method.original) { - object[method.name] = method.original; - } else { - delete object[method.name]; - } - } - this._extended = null; - } - }; + function configFromObject(config) { + if (config._d) { + return; + } - /** - * 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'); - } + var i = normalizeObjectUnits(config._i); + config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; + configFromArray(config); } - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this - }); - }; - }; - - /** - * 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); - } - - this._flushIfNeeded(); - }; - - /** - * Check whether the queue needs to be flushed - * @private - */ - Queue.prototype._flushIfNeeded = function () { - // flush when the maximum is exceeded. - if (this._queue.length > this.max) { - this.flush(); - } - - // flush after a period of inactivity when a delay is configured - clearTimeout(this._timeout); - if (this.queue.length > 0 && typeof this.delay === 'number') { - var me = this; - this._timeout = setTimeout(function () { - me.flush(); - }, this.delay); - } - }; - - /** - * 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 || []); - } - }; - - module.exports = Queue; - -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); + function createFromConfig (config) { + var input = config._i, + format = config._f, + res; - /** - * 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 + config._locale = config._locale || locale_locales__getLocale(config._l); - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + if (input === null || (format === undefined && input === '')) { + return valid__createInvalid({nullInput: true}); + } - this.setData(data); - } + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + 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); + } - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + res = new Moment(checkOverflow(config)); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } - if (this._data) { - // unsubscribe from current dataset - if (this._data.off) { - this._data.off('*', this.listener); + return res; } - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } + 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); + } } - this._ids = {}; - this.length = 0; - this._trigger('remove', { items: ids }); - } - - this._data = data; - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; - // 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 }); + 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; - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); + return createFromConfig(c); } - } - }; - - /** - * 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 = []; - // 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++; + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); } - } - // 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--; - } - } - } + 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; + } + ); - // trigger events - if (added.length) { - this._trigger('add', { items: added }); - } - if (removed.length) { - this._trigger('remove', { items: removed }); - } - }; + var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + return other > this ? this : other; + } + ); - /** - * Get 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; + // 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]; + } + } + return res; + } - // 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]; - } + // TODO: Use [].sort instead? + function min () { + var args = [].slice.call(arguments, 0); - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + return pickBy('isBefore', args); + } - // create a combined filter method when needed - if (this._options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me._options.filter(item) && options.filter(item); - }; - } + function max () { + var args = [].slice.call(arguments, 0); - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + return pickBy('isAfter', args); + } - return this._data && this._data.get.apply(this._data, getArguments); - }; + 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; - /** - * 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; + // 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; - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + this._data = {}; - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; + this._locale = locale_locales__getLocale(); + + this._bubble(); } - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; - } + function isDuration (obj) { + return obj instanceof Duration; + } - return ids; - }; + 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); + }); + } - /** - * 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; - }; + offset('Z', ':'); + offset('ZZ', ''); - /** - * 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 = []; + // PARSING - 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); - } - } + addRegexToken('Z', matchOffset); + addRegexToken('ZZ', matchOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(input); + }); - break; + // HELPERS - 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); + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; - 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 {} - } - } + 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]); - break; + return parts[0] === '+' ? minutes : -minutes; + } - 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); - } + // 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(); + } - break; + 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; } - this.length += added.length - removed.length; + // HOOKS - if (added.length) { - this._trigger('add', { items: added }, senderId); + // 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 () {}; + + // MOMENTS + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + utils_hooks__hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } } - if (updated.length) { - this._trigger('update', { items: updated, data: updatedData }, senderId); + + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } } - if (removed.length) { - this._trigger('remove', { items: removed }, senderId); + + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); } - } - }; - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - // 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; + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } - module.exports = DataView; + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(this._i)); + } + return this; + } - // nothing interesting for me :-( + function hasAlignedHourOffset (input) { + if (!input) { + input = 0; + } + else { + input = local__createLocal(input).utcOffset(); + } -/***/ }, -/* 25 */ -/***/ function(module, exports, __webpack_require__) { + return (this.utcOffset() - input) % 60 === 0; + } - 'use strict'; + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } - var Emitter = __webpack_require__(27); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var util = __webpack_require__(16); - var Point3d = __webpack_require__(28); - var Point2d = __webpack_require__(26); - var Camera = __webpack_require__(29); - var Filter = __webpack_require__(30); - var Slider = __webpack_require__(31); - var StepNumber = __webpack_require__(32); + 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; + } - /** - * @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'); - } + return false; + } - // 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%'; + function isLocal () { + return !this._isUTC; + } - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; + function isUtcOffset () { + return this._isUTC; + } - var passValueFn = function passValueFn(v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + function isUtc () { + return this._isUTC && this._offset === 0; + } - this.filterLabel = 'time'; - this.legendLabel = 'value'; + var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - 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' + // 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)$/; - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + function create__createDuration (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; - 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? + 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)); - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + ret = new Duration(duration); - 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 + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - // colors - this.axisColor = '#4D4D4D'; - this.gridColor = '#D3D3D3'; - this.dataColor = { - fill: '#7DC1FF', - stroke: '#3267D2', - strokeWidth: 1 // px - }; + return ret; + } - // create a frame and canvas - this.create(); + create__createDuration.fn = Duration.prototype; - // apply options (also when undefined) - this.setOptions(options); + 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; + } - // apply data - if (data) { - this.setData(data); - } - } + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - /** - * 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)); + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - // 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; + return res; } - } - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? + 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; + } - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + return res; + } - // 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); - }; + 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; + } - /** - * 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); - }; + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); + return this; + }; + } - /** - * 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, + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - // 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), + 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); + } + } - // 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)); + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); - return new Point3d(dx, dy, dz); - }; + 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))); + } - /** - * 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 clone () { + return new Moment(this); + } - // 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 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); + } + } - // 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); - }; + 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; + } + } - /** - * 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 isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + } - 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'; - } + 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)); + } + } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; - }; + function absFloor (number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } - /// 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 - }; + function diff (input, units, asFloat) { + var that = cloneWithOffset(input, this), + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, + delta, output; - /** - * 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; - } + units = normalizeUnits(units); - return -1; - }; + 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); + } - /** - * 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 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 (data.getNumberOfColumns() > 3) { - this.colFilter = 3; + 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 -(wholeMonthDiff + adjust); } - } 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; + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); } - } else { - throw 'Unknown style "' + this.style + '"'; - } - }; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; - }; + 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]'); + } + } - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); } - } - return counter; - }; - 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]); + function from (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); } - } - return distinctValues; - }; - Graph3d.prototype.getColumnRange = function (data, column) { - var minMax = { min: data[0][column], max: data[0][column] }; - for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { - minMax.min = data[i][column]; + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); } - if (minMax.max < data[i][column]) { - minMax.max = data[i][column]; + + function to (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); } - } - return minMax; - }; - /** - * 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; + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); + } - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); - } + function locale (key) { + var newLocaleData; - if (rawData === undefined) return; + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + 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); + } + } + ); - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error('Array, DataSet, or DataView expected'); - } + function localeData () { + return this._locale; + } - if (data.length == 0) return; + 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.dataSet = rawData; - this.dataTable = data; + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + return this; + } - // 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 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'); + } - // 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 to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); } - } - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + function unix () { + return Math.floor(+this / 1000); + } - // 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 toDate () { + return this._offset ? new Date(+this) : this._d; } - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; + function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; } - } - // 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 moment_valid__isValid () { + return valid__isValid(this); + } - 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; + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } - var zRange = this.getColumnRange(data, this.colZ); - this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; - this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; - if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; + function invalidAt () { + return getParsingFlags(this).overflow; + } - 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; - } + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); - // set the scale dependent on the ranges. - this._setScale(); - }; + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); - /** - * 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; + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } - var dataPoints = []; + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - 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 + // ALIASES - // 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; + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } - } + // PARSING - var sortNumber = function sortNumber(a, b) { - return a - b; - }; - dataX.sort(sortNumber); - dataY.sort(sortNumber); + 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); - // 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; + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + }); - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = utils_hooks__hooks.parseTwoDigitYear(input); + }); - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + // HELPERS - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + function weeksInYear(year, dow, doy) { + return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; + } - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + // MOMENTS - dataMatrix[xIndex][yIndex] = obj; + 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'); + } - dataPoints.push(obj); + function getSetISOWeekYear (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); } - // 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; - } - } + function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); } - } 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; - if (this.colValue !== undefined) { - point.value = data[i][this.colValue] || 0; - } + function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + } - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + addFormatToken('Q', 0, 0, 'quarter'); - dataPoints.push(obj); - } - } + // ALIASES - return dataPoints; - }; + addUnitAlias('quarter', 'Q'); - /** - * 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); - } + // PARSING - this.frame = document.createElement('div'); - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); - // 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); - } + // MOMENTS - 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 getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + } - // 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' + addFormatToken('D', ['DD', 2], 'Do', 'date'); - 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); + // ALIASES - // add the new graph to the container element - this.containerElement.appendChild(this.frame); - }; + addUnitAlias('date', 'D'); - /** - * 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; + // PARSING - this._resizeCanvas(); - }; + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); - /** - * 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%'; + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + // MOMENTS - // adjust with for margin - this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; - }; + var getSetDayOfMonth = makeGetSet('Date', true); - /** - * Start animation - */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; + addFormatToken('d', 0, 'do', 'day'); - this.frame.filter.slider.play(); - }; + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); - this.frame.filter.slider.stop(); - }; + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); - /** - * 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 - } + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); - // 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 - } - }; + // ALIASES - /** - * 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; - } + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + // PARSING - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); - this.redraw(); - }; + 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; + } + }); - /** - * 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; - }; + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); - /** - * Load data into the 3D Graph - */ - Graph3d.prototype._readData = function (data) { - // read the data - this._dataInitialize(data, this.style); + // HELPERS - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); - } + 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; + } - // draw the filter - this._redrawFilter(); - }; + // LOCALES - /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data - */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m) { + return this._weekdays[m.day()]; + } - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } + + function localeWeekdaysParse (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } + + 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; + } + } + } - this.animationStop(); + // MOMENTS - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + 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; + } + } - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + function getSetLocaleDayOfWeek (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } - 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; + 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); + } - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, function () { + return this.hours() % 12 || 12; + }); - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); } - 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; + meridiem('a', true); + meridiem('A', false); - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + // ALIASES - 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; + addUnitAlias('hour', 'h'); - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + // PARSING - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; } - // 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); - } + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); - this.setSize(this.width, this.height); + 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; + }); - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } + // LOCALES - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + 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'); + } - /** - * Redraw the Graph. - */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); - if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); - } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); - } else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); - } + // MOMENTS - this._redrawInfo(); - this._redrawLegend(); - }; + // 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); - /** - * Clear the canvas before redrawing - */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + addFormatToken('m', ['mm', 2], 0, 'minute'); - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; + // ALIASES - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function () { - var y; + addUnitAlias('minute', 'm'); - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + // PARSING - var dotSize = this.frame.clientWidth * 0.02; + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - 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 - } + // MOMENTS - var height = Math.max(this.frame.clientHeight * 0.25, 100); - var top = this.margin; - var right = this.frame.clientWidth - this.margin; - var left = right - widthMax; - var bottom = top + height; - } + var getSetMinute = makeGetSet('Minutes', false); - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options + addFormatToken('s', ['ss', 2], 0, 'second'); - 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); + // ALIASES - //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); + addUnitAlias('second', 's'); - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); - } + // PARSING - ctx.strokeStyle = this.axisColor; - ctx.strokeRect(left, top, widthMax, height); - } + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); - 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(); - } + // MOMENTS - 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; + var getSetSecond = makeGetSet('Seconds', false); - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); + }); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); - step.next(); + function millisecond__milliseconds (token) { + addFormatToken(0, [token, 3], 0, 'millisecond'); } - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } - }; + millisecond__milliseconds('SSS'); + millisecond__milliseconds('SSSS'); - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ''; + // ALIASES - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + addUnitAlias('millisecond', 'ms'); - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + // PARSING - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); + 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); + }); - // create an event handler - var me = this; - var onchange = function onchange() { - var index = slider.getIndex(); + // MOMENTS - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + var getSetMillisecond = makeGetSet('Milliseconds', false); - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; - } - }; + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } - }; + // MOMENTS - /** - * Redraw common information - */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; + } - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); - } - }; + var momentPrototype__proto = Moment.prototype; - /** - * 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; + 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; - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; - // 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; + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; - // draw x-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultXStep === undefined; - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } - while (!step.end()) { - var x = step.getCurrent(); + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - 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(); + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; - 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(); - } + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - 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); + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; - step.next(); - } + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - // 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(); + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - 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(); - } + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - 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); + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - step.next(); - } + // 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; - // 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(); + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + // 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); - 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(); + var momentPrototype = momentPrototype__proto; - // 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(); + function moment__createUnix (input) { + return local__createLocal(input * 1000); + } - // 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(); + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); + } - // 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'; + 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' + }; + + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.call(mom, now) : output; } - ctx.fillStyle = this.axisColor; - ctx.fillText(xLabel, text.x, text.y); - } - // draw y-label - var yLabel = this.yLabel; - if (yLabel.length > 0) { - xOffset = 0.1 / this.scale.x; - xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + var 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' + }; + + 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; } - 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); - } - }; + var defaultInvalidDate = 'Invalid date'; - /** - * 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; + function invalidDate () { + return this._invalidDate; + } - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; - 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; + function ordinal (number) { + return this._ordinal.replace('%d', number); + } - default: - R = 0;G = 0;B = 0;break; - } + function preParsePostFormat (string) { + return string; + } - return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; - }; + 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' + }; - /** - * 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; + 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); + } - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + 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); + } - // 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); + var prototype__proto = Locale.prototype; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + 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; - // 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; - } + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - 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; + // 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 (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; - 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) + 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); + } - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; } - 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 + format = format || ''; - 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 (index != null) { + return lists__get(format, index, field, setter); } - 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(); - } + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; } - } 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; - 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; + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } - 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(); - } + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } - 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; + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } - 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(); - } + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); } - } - }; - 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; - } - } + 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; + } + }); + + // 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); - return this.dataColor.strokeWidth; - }; + var mathAbs = Math.abs; - /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' - */ - Graph3d.prototype._redrawDataDot = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i; + function duration_abs__abs () { + var data = this._data; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); - // 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; + 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); - // 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; - } + return this; + } - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); - // 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]; + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; - 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(); + return duration._bubble(); } - // 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; + // 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); } - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 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); } - 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); - } + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years = 0; - // 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(); - } - }; + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' - */ - Graph3d.prototype._redrawDataBar = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i, j, surface, corners; + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - // 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; + hours = absFloor(minutes / 60); + data.hours = hours % 24; - // 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; - } + days += absFloor(hours / 24); - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // Accurately convert days to years, assume start from year 0. + years = absFloor(daysToYears(days)); + days -= absFloor(yearsToDays(years)); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absFloor(days / 30); + days %= 30; - // 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]; + // 12 months -> 1 year + years += absFloor(months / 12); + months %= 12; - // 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); + data.days = days; + data.months = months; + data.years = years; + + return this; } - // 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); + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; } - // 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) }]; + function yearsToDays (years) { + // years * 365 + absFloor(years / 4) - + // absFloor(years / 100) + absFloor(years / 400); + return years * 146097 / 400; + } - // 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); - }); + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; - // 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; + units = normalizeUnits(units); - // 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 (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); + } + } } - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } - // both are equal - return 0; - }); + 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'); - // 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(); + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); } - } - }; - /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' - */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - i; + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + 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'); - // 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); + function weeks () { + return absFloor(this.days() / 7); + } - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + 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 + }; - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + // 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); + } - 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); + 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')); - // 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); + 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); } - // finish the line - ctx.stroke(); - } - }; + // 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; + } - /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) - */ - Graph3d.prototype._onMouseDown = function (event) { - event = event || window.event; + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); - // 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 (withSuffix) { + output = locale.pastFuture(+this, output); + } - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; + return locale.postformat(output); + } - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); + var iso_string__abs = Math.abs; - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); + 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(); - this.frame.style.cursor = 'move'; + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - // 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); - }; + 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' : ''); + } - /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event - */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; + var duration_prototype__proto = Duration.prototype; + + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = duration_get__milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; + + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + // Side effect imports - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + // PARSING - // 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; - } + 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)); + }); - // 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; - } + // Side effect imports - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + utils_hooks__hooks.version = '2.10.3'; - util.preventDefault(event); - }; + setHookCallback(local__createLocal); - /** - * 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; + 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; - // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + var _moment = utils_hooks__hooks; - /** - * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point - * @param {Event} event A mouse move event - */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var boundingRect = this.frame.getBoundingClientRect(); - var mouseX = getMouseX(event) - boundingRect.left; - var mouseY = getMouseY(event) - boundingRect.top; + return _moment; - if (!this.showTooltip) { - return; - } + })); + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22)(module))) - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; - } + 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 (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); - } - }; +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Event handler for touchstart event on mobile devices - */ - Graph3d.prototype._onTouchStart = function (event) { - this.touchDown = true; + /* WEBPACK VAR INJECTION */(function(global) {'use strict'; - 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); + var _rng; - this._onMouseDown(event); - }; + var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; - /** - * Event handler for touchmove event on mobile devices - */ - Graph3d.prototype._onTouchMove = function (event) { - this._onMouseMove(event); - }; + 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; + }; + } - /** - * Event handler for touchend event on mobile devices - */ - Graph3d.prototype._onTouchEnd = function (event) { - this.touchDown = false; + if (!_rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + _rng = function () { + for (var i = 0, r; i < 16; i++) { + if ((i & 3) === 0) r = Math.random() * 4294967296; + _rnds[i] = r >>> ((i & 3) << 3) & 255; + } - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); + return _rnds; + }; + } - this._onMouseUp(event); - }; + // uuid.js + // + // Copyright (c) 2010-2012 Robert Kieffer + // MIT License - http://opensource.org/licenses/mit-license.php - /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event - */ - Graph3d.prototype._onWheel = function (event) { - if (!event) /* For IE. */ - event = window.event; + // 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 - // 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; - } + //var _rng = require('./rng'); - // 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); + // 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; + } - this.camera.setArmLength(newLength); - this.redraw(); + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = buf && offset || 0, + ii = 0; - this._hideTooltip(); - } + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { + if (ii < 16) { + // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } - // 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 buf; + } - /** - * 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 - */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; + // **`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++]]; + } - function sign(x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; - } + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html - 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)); + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); - // 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); - }; + // 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]]; - /** - * 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); + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; - 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); + // Previous uuid creation time + var _lastMSecs = 0, + _lastNSecs = 0; - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } - } + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; - return closestDataPoint; - }; + options = options || {}; - /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private - */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; - 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)'; + // 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(); - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; + // 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; - 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'; + // Time since last uuid creation (in msecs) + var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; - 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; + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 16383; } - this._hideTooltip(); + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } - this.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 + '
'; + // 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'); } - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + // `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; - 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'; - }; + // `time_mid` + var tmh = msecs / 4294967296 * 10000 & 268435455; + b[i++] = tmh >>> 8 & 255; + b[i++] = tmh & 255; - /** - * Hide the tooltip when displayed - * @private - */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; + // `time_high_and_version` + b[i++] = tmh >>> 24 & 15 | 16; // include version + b[i++] = tmh >>> 16 & 255; - 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); - } - } - } - } - }; + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 128; - /**--------------------------------------------------------------------------**/ + // `clock_seq_low` + b[i++] = clockseq & 255; - /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x - */ - function getMouseX(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - } + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } - /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y - */ - function getMouseY(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + return buf ? buf : unparse(b); } - module.exports = Graph3d; + // **`v4()` - Generate random UUID** - // use use defaults + // 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; -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { + if (typeof options == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; + } + options = options || {}; - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - "use strict"; + var rnds = options.random || (options.rng || _rng)(); - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; + // 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; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); } - module.exports = Point2d; + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; + + module.exports = uuid; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, -/* 27 */ +/* 24 */ /***/ function(module, exports, __webpack_require__) { - - /** - * Expose `Emitter`. - */ - - module.exports = Emitter; + // DOM utility methods /** - * Initialize a new `Emitter`. - * - * @api public + * this prepares the JSON container for allocating SVG elements + * @param JSONcontainer + * @private */ + 'use strict'; - function Emitter(obj) { - if (obj) return mixin(obj); + 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 = []; + } + } }; /** - * Mixin the emitter properties. + * 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 {Object} obj - * @return {Object} - * @api private + * @param JSONcontainer + * @private */ - - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; + 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 = []; + } + } } - return obj; - } + }; /** - * Listen on the given `event` with `fn`. + * 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 {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * @param elementType + * @param JSONcontainer + * @param svgContainer + * @returns {*} + * @private */ - - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; + 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; }; /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. + * 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 {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * @param elementType + * @param JSONcontainer + * @param DOMContainer + * @returns {*} + * @private */ - - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - - function on() { - self.off(event, on); - fn.apply(this, arguments); + 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); + } else { + DOMContainer.appendChild(element); + } } - - on.fn = fn; - this.on(event, on); - return this; + JSONcontainer[elementType].used.push(element); + return element; }; /** - * Remove the given callback for `event` or all - * registered callbacks. + * 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 {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * @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); + } - 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; + if (groupTemplate.style !== undefined) { + point.setAttributeNS(null, 'style', groupTemplate.style); } + point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point'); + //handle label - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; + if (labelObj) { + var label = exports.getSVGElement('text', JSONcontainer, svgContainer); + if (labelObj.xOffset) { + x = x + labelObj.xOffset; + } - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } - // 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; + if (labelObj.className) { + label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); } + label.setAttributeNS(null, 'x', x); + label.setAttributeNS(null, 'y', y); } - return this; + + return point; }; /** - * Emit `event` with the given args. + * draw a bar SVG element centered on the X coordinate * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} + * @param x + * @param y + * @param className */ - - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + 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); } } - - return this; }; - /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; - }; + 'use strict'; + + var util = __webpack_require__(19); + var Queue = __webpack_require__(26); /** - * Check if this emitter has `event` handlers. + * DataSet * - * @param {String} event - * @return {Boolean} - * @api public + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * type: { + * // ... + * } + * }); + * + * dataSet.add(item); + * dataSet.add(data); + * dataSet.update(item); + * dataSet.update(data); + * dataSet.remove(id); + * dataSet.remove(ids); + * var data = dataSet.get(); + * var data = dataSet.get(id); + * var data = dataSet.get(ids); + * var data = dataSet.get(ids, options, data); + * dataSet.clear(); + * + * A data set can: + * - add/remove/update data + * - gives triggers upon changes in the data + * - can import/export data in various data formats + * + * @param {Array} [data] Optional array with initial data + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object.} [type] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by a field name or custom sort function. + * @throws Error */ - function Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; + DataSet.prototype.get = function (args) { + var me = this; - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); + // 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]; + } - this.calculateCameraOrientation(); - } + // 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'; + } - /** - * 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; + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - this.calculateCameraOrientation(); - }; + // 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); + } + } + } + } - /** - * 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; + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); } - 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; + // 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); + } + } } - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + // 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; + } } }; /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical + * 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 */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; + 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 = []; - return rot; - }; + 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); + } + } + } - /** - * 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; + this._sort(items, order); - this.armLength = length; + 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]); + } + } - // 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; + this._sort(items, order); - this.calculateCameraOrientation(); - }; + 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]); + } + } + } + } - /** - * Retrieve the arm length - * @return {Number} length - */ - Camera.prototype.getArmLength = function () { - return this.armLength; + return ids; }; /** - * Retrieve the camera location - * @return {Point3d} cameraLocation + * Returns the DataSet itself. Is overwritten for example by the DataView, + * which returns the DataSet it is connected to instead. */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; + DataSet.prototype.getDataSet = function () { + return this; }; /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation + * 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. */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; - }; + DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - /** - * 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); + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - // calculate rotation of the camera - this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; + 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); + } + } + } + } }; - module.exports = Camera; - -/***/ }, -/* 30 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var DataView = __webpack_require__(24); - /** - * @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 + * 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 */ - function Filter(data, column, graph) { - this.data = data; - this.column = column; - this.graph = graph; // the parent graph + DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; - this.index = undefined; - this.value = undefined; + // 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)); + } + } + } - // read all distinct values and select the first one - this.values = graph.getDistinctValues(data.get(), this.column); + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } - // sort both numeric and string values correctly - this.values.sort(function (a, b) { - return a > b ? 1 : a < b ? -1 : 0; - }); + return mappedItems; + }; - if (this.values.length > 0) { - this.selectValue(0); + /** + * 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; } - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; - - this.loaded = false; - this.onLoadCallback = undefined; + var filteredItem = {}; - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); + if (Array.isArray(fields)) { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; + } + } } else { - this.loaded = true; + for (var field in item) { + if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { + filteredItem[fields[field]] = item[field]; + } + } } + + return filteredItem; }; /** - * Return the label - * @return {string} label + * Sort the provided array with items + * @param {Object[]} items + * @param {String | function} order A field name or custom sort function. + * @private */ - Filter.prototype.isLoaded = function () { - return this.loaded; + 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'); + } }; /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 + * 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 */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; + DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, + len, + removedId; - var i = 0; - while (this.dataPoints[i]) { - i++; + 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); + } } - return Math.round(i / len * 100); - }; + if (removedIds.length) { + this._trigger('remove', { items: removedIds }, senderId); + } - /** - * Return the label - * @return {string} label - */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; + return removedIds; }; /** - * Return the columnIndex of the filter - * @return {Number} columnIndex + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private */ - Filter.prototype.getColumn = function () { - return this.column; + 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; }; /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; + DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this._data); - return this.values[this.index]; - }; + this._data = {}; + this.length = 0; - /** - * Retrieve all values of the filter - * @return {Array} values - */ - Filter.prototype.getValues = function () { - return this.values; + this._trigger('remove', { items: ids }, senderId); + + return ids; }; /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value + * 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 */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; + DataSet.prototype.max = function (field) { + var data = this._data, + max = null, + maxField = null; - return this.values[index]; + 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; }; /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataPoints + * 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 */ - 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]; - - var dataView = new DataView(this.data, { filter: function filter(item) { - return item[f.column] == f.value; - } }).get(); - dataPoints = this.graph._getDataPoints(dataView); + DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - this.dataPoints[index] = dataPoints; + 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; + } + } } - return dataPoints; + return min; }; /** - * Set a callback function when the filter is fully loaded. + * 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. */ - Filter.prototype.setOnLoadCallback = function (callback) { - this.onLoadCallback = callback; - }; + 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; - /** - * 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'; + 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++; + } + } + } - this.index = index; - this.value = this.values[index]; + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); + } + } + + return values; }; /** - * Load all filtered rows in the background one by one - * Start this method without providing an index! + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id + * @private */ - Filter.prototype.loadInBackground = function (index) { - if (index === undefined) index = 0; - - var frame = this.graph.frame; - - if (index < this.values.length) { - var dataPointsTemp = this._getDataPoints(index); - //this.graph.redrawInfo(); // TODO: not neat + DataSet.prototype._addItem = function (item) { + var id = item[this._fieldId]; - // 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); + 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'); } - 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 me = this; - setTimeout(function () { - me.loadInBackground(index + 1); - }, 10); - this.loaded = false; } else { - this.loaded = true; + // generate an id + id = util.randomUUID(); + item[this._fieldId] = id; + } - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = undefined; + 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); } - - if (this.onLoadCallback) this.onLoadCallback(); } - }; - - module.exports = Filter; - -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; + this._data[id] = d; + this.length++; - var util = __webpack_require__(16); + return id; + }; /** - * @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. + * 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 */ - 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; - - 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); - - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); - - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); - - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); - - 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); - - 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); + DataSet.prototype._getItem = function (id, types) { + var field, value; - // 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); - }; + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; } - this.onChangeCallback = undefined; - - this.values = []; - this.index = undefined; - - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } - - /** - * Select the previous index - */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); + // 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; }; /** - * Select the next index + * 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 */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); + 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'); + } + + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } } + + return id; }; + module.exports = DataSet; + +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { + /** - * Select the next index + * 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 */ - Slider.prototype.playNext = function () { - var start = new Date(); - - 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); - } + 'use strict'; - var end = new Date(); - var diff = end - start; + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; - // 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 + // properties + this._queue = []; + this._timeout = null; + this._extended = null; - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); - }; + this.setOptions(options); + } /** - * Toggle start or stop playing - */ - Slider.prototype.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + * 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; } + + this._flushIfNeeded(); }; /** - * Start playing + * 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 */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; - - this.playNext(); + Queue.extend = function (object, options) { + var queue = new Queue(options); - if (this.frame) { - this.frame.play.value = 'Stop'; + if (object.flush !== undefined) { + throw new Error('Target object already has a property flush'); } - }; + object.flush = function () { + queue.flush(); + }; - /** - * Stop playing - */ - Slider.prototype.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; + var methods = [{ + name: 'flush', + original: undefined + }]; - if (this.frame) { - this.frame.play.value = 'Play'; + 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); + } } - }; - /** - * 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; - }; + queue._extended = { + object: object, + methods: methods + }; - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; + return queue; }; /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds + * Destroy the queue. The queue will first flush all queued actions, and in + * case it has extended an object, will restore the original object. */ - Slider.prototype.getPlayInterval = function (interval) { - return this.playInterval; - }; + Queue.prototype.destroy = function () { + this.flush(); - /** - * 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 (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; + } }; /** - * Execute the onchange callback function + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); + Queue.prototype.replace = function (object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error('Method ' + method + ' undefined'); } - }; - /** - * redraw the slider on the correct place - */ - 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'; + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = left + 'px'; - } + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; }; /** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry */ - Slider.prototype.setValues = function (values) { - this.values = values; + Queue.prototype.queue = function (entry) { + if (typeof entry === 'function') { + this._queue.push({ fn: entry }); + } else { + this._queue.push(entry); + } - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; + this._flushIfNeeded(); }; /** - * Select a value by its index - * @param {Number} index + * Check whether the queue needs to be flushed + * @private */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; + Queue.prototype._flushIfNeeded = function () { + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } - this.redraw(); - this.onChange(); - } else { - throw 'Error: index out of range'; + // 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); } }; /** - * retrieve the index of the currently selected vaue - * @return {Number} index + * Flush all queued calls */ - Slider.prototype.getIndex = function () { - return this.index; + Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); + } }; - /** - * retrieve the currently selected value - * @return {*} value - */ - Slider.prototype.get = function () { - return this.values[this.index]; - }; + module.exports = Queue; - 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; +/***/ }, +/* 27 */ +/***/ function(module, exports, __webpack_require__) { - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); + 'use strict'; - this.frame.style.cursor = 'move'; + var util = __webpack_require__(19); + var DataSet = __webpack_require__(25); + + /** + * 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 - // 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); + this.listener = function () { + me._onEvent.apply(me, arguments); }; - util.addEventListener(document, 'mousemove', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; - - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - var x = left - 3; - 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; + this.setData(data); + } - return index; - }; + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + /** + * Set a data source for the view + * @param {DataSet | DataView} data + */ + DataView.prototype.setData = function (data) { + var ids, i, len; - var x = index / (this.values.length - 1) * width; - var left = x + 3; + if (this._data) { + // unsubscribe from current dataset + if (this._data.off) { + this._data.off('*', this.listener); + } - return left; - }; + // 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 }); + } - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; + this._data = data; - var index = this.leftToIndex(x); + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; - this.setIndex(index); + // 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 }); - util.preventDefault(); + // subscribe to new dataset + if (this._data.on) { + this._data.on('*', this.listener); + } + } }; - 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); + /** + * 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 = []; - util.preventDefault(); - }; + // 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++; + } + } - module.exports = Slider; + // 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--; + } + } + } -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { + // trigger events + if (added.length) { + this._trigger('add', { items: added }); + } + if (removed.length) { + this._trigger('remove', { items: removed }); + } + }; /** - * @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. + * Get data from the data view * - * 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, .... + * Usage: * - * Example usage: - * var step = new StepNumber(0, 10, 2.5, true); - * step.start(); - * while (!step.end()) { - * alert(step.getCurrent()); - * step.next(); - * } + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) * - * Version: 1.0 + * get(id: Number) + * get(id: Number, options: Object) + * get(id: Number, options: Object, data: Array | DataTable) * - * @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"; - - function StepNumber(start, end, step, prettyStep) { - // set default values - this._start = 0; - this._end = 0; - this._step = 1; - this.prettyStep = true; - this.precision = 5; - - this._current = 0; - this.setRange(start, end, step, prettyStep); - }; - - /** - * Set a new range: start, end and step. + * get(ids: Number[]) + * get(ids: Number[], options: Object) + * get(ids: Number[], options: Object, data: Array | DataTable) * - * @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, ...) + * 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 */ - StepNumber.prototype.setRange = function (start, end, step, prettyStep) { - this._start = start ? start : 0; - this._end = end ? end : 0; + DataView.prototype.get = function (args) { + var me = this; - this.setStep(step, prettyStep); - }; + // 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]; + } - /** - * 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, ...) - */ - StepNumber.prototype.setStep = function (step, prettyStep) { - if (step === undefined || step <= 0) return; + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - if (prettyStep !== undefined) this.prettyStep = prettyStep; + // 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); + }; + } - if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); + + return this._data && this._data.get.apply(this._data, getArguments); }; /** - * 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 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 */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function log10(x) { - return Math.log(x) / Math.LN10; - }; + DataView.prototype.getIds = function (options) { + var ids; - // 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))); + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - // 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; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; + } - // for safety - if (prettyStep <= 0) { - prettyStep = 1; + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); + } else { + ids = []; } - return prettyStep; + return ids; }; /** - * returns the current value of the step - * @return {Number} current value + * 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 */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; + } + return dataSet || null; }; /** - * returns the current step size - * @return {Number} current step size + * 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 */ - StepNumber.prototype.getStep = function () { - return this._step; - }; + 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 = []; - /** - * 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; - }; + 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); + } + } - /** - * Do a step, add the step size to the current value - */ - StepNumber.prototype.next = function () { - this._current += this._step; - }; + break; - /** - * 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; + 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 {} + } + } + + break; + + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } + } + + break; + } + + this.length += added.length - removed.length; + + 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); + } + } }; - module.exports = StepNumber; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; + + // 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; + + module.exports = DataView; + + // nothing interesting for me :-( /***/ }, -/* 33 */ +/* 28 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Emitter = __webpack_require__(27); - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var Range = __webpack_require__(37); - var Core = __webpack_require__(40); - var TimeAxis = __webpack_require__(49); - var CurrentTime = __webpack_require__(34); - var CustomTime = __webpack_require__(52); - var ItemSet = __webpack_require__(41); - - 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; + 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); /** - * 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 + * @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 Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { 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, + // 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%'; - orientation: { - axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' - item: 'bottom' // not relevant - }, + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; - width: null, - height: null, - maxHeight: null, - minHeight: null + var passValueFn = function passValueFn(v) { + return v; }; - this.options = util.deepExtend({}, this.defaultOptions); + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - // Create the DOM, props, and emitter - this._create(container); + this.filterLabel = 'time'; + this.legendLabel = 'value'; - // all components listed here will be repainted automatically - this.components = []; + this.style = Graph3d.STYLE.DOT; + this.showPerspective = true; + this.showGrid = true; + this.keepAspectRatio = true; + this.showShadow = false; + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' - this.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; - }, + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - 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) - } - }; + 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? - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - // time axis - this.timeAxis = new TimeAxis(this.body); - this.timeAxis2 = null; // used in case of orientation option 'both' - this.components.push(this.timeAxis); + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + 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 - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + // colors + this.axisColor = '#4D4D4D'; + this.gridColor = '#D3D3D3'; + this.dataColor = { + fill: '#7DC1FF', + stroke: '#3267D2', + strokeWidth: 1 // px + }; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + // create a frame and canvas + this.create(); - 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 options (also when undefined) + this.setOptions(options); - // apply options - if (options) { - this.setOptions(options); + // apply data + if (data) { + this.setData(data); } + } - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); + /** + * 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)); + + // 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; + } } - } - // Extend the functionality from Core - Timeline.prototype = new Core(); + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); + }; /** - * Load a configurator - * @return {Object} - * @private + * 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 */ - Timeline.prototype._createConfigurator = function () { - return new Configurator(this, this.dom.container, configureOptions); + Graph3d.prototype._convert3Dto2D = function (point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * 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. + * 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 */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); + 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, + + // 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 translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + + return new Point3d(dx, dy, dz); }; - 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); + /** + * 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; + + // 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()); } - Core.prototype.setOptions.call(this, options); + // 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); + }; - if ('type' in options) { - if (options.type !== this.options.type) { - this.options.type = options.type; + /** + * 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; - // 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 - } - } + 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'; + } + + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + 'px'; + this.frame.style.borderStyle = 'solid'; + }; + + /// enumerate the available styles + Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT: 3, + DOTLINE: 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID: 7, + LINE: 8, + SURFACE: 9 + }; + + /** + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found + */ + Graph3d.prototype._getStyleNumber = function (styleName) { + switch (styleName) { + case 'dot': + return Graph3d.STYLE.DOT; + case 'dot-line': + return Graph3d.STYLE.DOTLINE; + case 'dot-color': + return Graph3d.STYLE.DOTCOLOR; + case 'dot-size': + return Graph3d.STYLE.DOTSIZE; + case 'line': + return Graph3d.STYLE.LINE; + case 'grid': + return Graph3d.STYLE.GRID; + case 'surface': + return Graph3d.STYLE.SURFACE; + case 'bar': + return Graph3d.STYLE.BAR; + case 'bar-color': + return Graph3d.STYLE.BARCOLOR; + case 'bar-size': + return Graph3d.STYLE.BARSIZE; } + + return -1; }; /** - * Set items - * @param {vis.DataSet | Array | null} items + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + 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; - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; + 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 { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' - } - }); + throw 'Unknown style "' + this.style + '"'; } + }; - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - if (this.options.start == undefined || this.options.end == undefined) { - var range = this.getItemRange(); - } + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } + } + return counter; + }; - var start = this.options.start != undefined ? this.options.start : range.min; - var end = this.options.end != undefined ? this.options.end : range.max; + 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; + }; - this.setWindow(start, end, { animation: false }); - } else { - this.fit({ animation: false }); + 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; }; /** - * Set groups - * @param {vis.DataSet | Array} groups + * 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 */ - 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); + Graph3d.prototype._dataInitialize = function (rawData, style) { + var me = this; + + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); } - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); - }; + if (rawData === undefined) return; - /** - * 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); + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); } - if (data && data.items) { - this.setItems(data.items); + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error('Array, DataSet, or DataView expected'); } - }; - /** - * 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); + if (data.length == 0) return; - if (options && options.focus) { - this.focus(ids, options); - } - }; + this.dataSet = rawData; + this.dataTable = data; - /** - * 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() || []; - }; + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on('*', this._onChange); - /** - * 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; + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - var ids = Array.isArray(id) ? id : [id]; + // 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'; - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: 'Date', - end: 'Date' + // 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(); + }); } - }); + } - // 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(); + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - if (start === null || s < start) { - start = s; + // 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; } - if (end === null || e > end) { - end = e; + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; } - }); + } - 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); + // 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; - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, animation); + 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; - /** - * 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); + var zRange = this.getColumnRange(data, this.colZ); + this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; + this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; + + if (this.colValue !== undefined) { + var valueRange = this.getColumnRange(data, this.colValue); + this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; + this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + + // set the scale dependent on the ranges. + this._setScale(); }; /** - * 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}} + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - 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; + 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; - if (min != null && max != null) { - var interval; - var factor; - var lhs; - var rhs; - var delta; + var dataPoints = []; - (function () { - var getStart = function (item) { - return util.convert(item.data.start, 'Date').valueOf(); - }; + 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 - var getEnd = function (item) { - var end = item.data.end != undefined ? item.data.end : item.data.start; - return util.convert(end, 'Date').valueOf(); - }; + // 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; - interval = max - min; - // ms - if (interval <= 0) { - interval = 10; + if (dataX.indexOf(x) === -1) { + dataX.push(x); } - factor = interval / _this.props.center.width; + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } + } - // calculate the date of the left side and right side of the items given - util.forEach(_this.itemSet.items, (function (item) { - item.show(); + var sortNumber = function sortNumber(a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - var start = getStart(item); - var end = getEnd(item); + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.length; i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; + z = data[i][this.colZ] || 0; - var left = new Date(start - (item.getWidthLeft() + 10) * factor); - var right = new Date(end + (item.getWidthRight() + 10) * factor); + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - if (left < min) { - min = left; - minItem = item; - } - if (right > max) { - max = right; - maxItem = item; - } - }).bind(_this)); + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - if (minItem && maxItem) { - lhs = minItem.getWidthLeft() + 10; - rhs = maxItem.getWidthRight() + 10; - delta = _this.props.center.width - lhs - rhs; - // px + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - if (delta > 0) { - min = getStart(minItem) - lhs * interval / delta; // ms - max = getEnd(maxItem) + rhs * interval / delta; // ms + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); + + dataMatrix[xIndex][yIndex] = obj; + + dataPoints.push(obj); + } + + // 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; + + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } + + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; + + dataPoints.push(obj); + } } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + return dataPoints; }; /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} + * 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. */ - Timeline.prototype.getDataRange = function () { - var min = null; - var max = null; + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - 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; - } - }); + this.frame = document.createElement('div'); + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement('canvas'); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + //if (!this.frame.canvas.getContext) { + { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null + 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' + + 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); + + // add the new graph to the container element + this.containerElement.appendChild(this.frame); }; /** - * 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. + * 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%') */ - 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); + Graph3d.prototype.setSize = function (width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - var item = this.itemSet.itemFromTarget(event); - var group = this.itemSet.groupFromTarget(event); - var customTime = CustomTime.customTimeFromTarget(event); + this._resizeCanvas(); + }; - 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; + /** + * 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 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.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - 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 - }; + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; }; - module.exports = Timeline; + /** + * Start animation + */ + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { + this.frame.filter.slider.play(); + }; - 'use strict'; + /** + * Stop animation + */ + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - var util = __webpack_require__(16); - var Component = __webpack_require__(35); - var moment = __webpack_require__(17); - var locales = __webpack_require__(36); + this.frame.filter.slider.stop(); + }; /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component + * 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 */ - function CurrentTime(body, options) { - this.body = body; + 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 + } - // default options - this.defaultOptions = { - showCurrentTime: true, + // 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 + } + }; - locales: locales, - locale: 'en' - }; - this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + /** + * 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; + } - this._create(); + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } - this.setOptions(options); - } + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } - CurrentTime.prototype = new Component(); + this.redraw(); + }; /** - * Create the HTML DOM for the current time bar - * @private + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance */ - 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.bar = bar; + Graph3d.prototype.getCameraPosition = function () { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; /** - * Destroy the CurrentTime bar + * Load data into the 3D Graph */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - this.body = null; + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } + + // draw the filter + this._redrawFilter(); }; /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); + + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Update the options. Options will be merged with current options + * @param {Object} options */ - 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); + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - this.start(); - } + this.animationStop(); - var now = new Date(new Date().valueOf() + this.offset); - var x = this.body.util.toScreen(now); + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - 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; + 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; } - 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); + 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; - 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(); - } + 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 false; - }; + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - /** - * Start auto refreshing the current time bar - */ - CurrentTime.prototype.start = function () { - var me = this; + 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; - function update() { - me.stop(); + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - // 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 (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } - me.redraw(); + // 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); + } - // start a renderTimer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); + this.setSize(this.width, this.height); + + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); } - update(); + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } }; /** - * Stop auto refreshing the current time bar + * Redraw the Graph. */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } + + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); + + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); } + + this._redrawInfo(); + this._redrawLegend(); }; /** - * 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. + * Clear the canvas before redrawing */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, 'Date').valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, canvas.width, canvas.height); }; /** - * Get the current time. - * @return {Date} Returns the current time. + * Redraw the legend showing the colors */ - CurrentTime.prototype.getCurrentTime = function () { - return new Date(new Date().valueOf() + this.offset); - }; + Graph3d.prototype._redrawLegend = function () { + var y; - module.exports = CurrentTime; + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { -/***/ }, -/* 35 */ -/***/ function(module, exports, __webpack_require__) { + var dotSize = this.frame.clientWidth * 0.02; + + var widthMin, widthMax; + if (this.style === Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } else { + widthMin = 20; // px + widthMax = 20; // px + } + + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; + } + + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options + + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); + + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); + + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } + + ctx.strokeStyle = this.axisColor; + ctx.strokeRect(left, top, widthMax, height); + } + + 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(); + } + + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + // print values along the color bar + var gridLineLen = 5; // px + var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); + step.start(); + if (step.getCurrent() < this.valueMin) { + step.next(); + } + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; + + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); + + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + + step.next(); + } + + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); + } + }; /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] + * Redraw the filter */ - "use strict"; + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ''; - function Component(body, options) { - this.options = null; - this.props = null; - } + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; + + // TODO: css here is not nice here... + this.frame.filter.style.padding = '10px'; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; + + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); + + // create an event handler + var me = this; + var onchange = function onchange() { + var index = slider.getIndex(); - /** - * 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); + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); + + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Redraw the slider */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } }; /** - * Destroy the component. Cleanup DOM and event listeners - */ - Component.prototype.destroy = function () {}; - - /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * Redraw common information */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + ctx.font = '14px arial'; // TODO: put in options + ctx.lineStyle = 'gray'; + ctx.fillStyle = 'gray'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; - return resized; + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); + } }; - module.exports = Component; - - // should be implemented by the component + /** + * 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; -/***/ }, -/* 36 */ -/***/ function(module, exports, __webpack_require__) { + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + 'px arial'; - // English - 'use strict'; + // 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; - exports['en'] = { - current: 'current', - time: 'time' - }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = this.defaultXStep === undefined; + step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); - // Dutch - exports['nl'] = { - current: 'huidige', - time: 'tijd' - }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; + 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(); -/***/ }, -/* 37 */ -/***/ function(module, exports, __webpack_require__) { + 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(); + } - 'use strict'; + 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); - var util = __webpack_require__(16); - var hammerUtil = __webpack_require__(38); - var moment = __webpack_require__(17); - var Component = __webpack_require__(35); - var DateUtil = __webpack_require__(39); + step.next(); + } - /** - * @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 + // 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(); - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; + 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(); + } - // 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); + 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); - this.props = { - touch: {} - }; - this.animationTimer = null; + step.next(); + } - // 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)); + // 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(); - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); + 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.setOptions(options); - } + // 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(); - Range.prototype = new Component(); + // 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(); - /** - * 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); + // 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 ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + // 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); + } + + // 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); } }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' + * 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 */ - function validateDirection(direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); + Graph3d.prototype._hsv2rgb = function (H, S, V) { + var R, G, B, C, Hi, X; + + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); + + 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; + + default: + R = 0;G = 0;B = 0;break; } - } + + return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; + }; /** - * 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] - * + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' */ - Range.prototype.setRange = function (start, end, animation, byUser) { - if (byUser !== true) { - byUser = false; + 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; + + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + + // 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); + + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + + // 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; } - 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(', ')); - } + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - var initTime = new Date().valueOf(); - var anyChanged = false; + 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) { - 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; + 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) - 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 }); + topSideVisible = crossproduct.z > 0; + } else { + topSideVisible = true; } - if (done) { - if (anyChanged) { - me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + 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 + + 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 { - // 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); + fillStyle = 'gray'; + strokeStyle = this.axisColor; } - } - }; - return next(); + 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 { - 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); + // grid style + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + + 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; + + 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(); + } + + 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; + + 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(); + } } } }; - /** - * Stop an animation - * @private - */ - Range.prototype._cancelAnimation = function () { - if (this.animationTimer) { - clearTimeout(this.animationTimer); - this.animationTimer = null; + 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; }; /** - * 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 + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - 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; - - // 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 + '"'); - } + Graph3d.prototype._redrawDataDot = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i; - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + // 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; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } + // 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; } - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } - } + // 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]; - // 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; - } + 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(); } - } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; + // 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 (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; - } + 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 changed = this.start != newStart || this.end != newEnd; + 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); + } - // 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'); + // 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(); } - - this.start = newStart; - this.end = newEnd; - return changed; }; /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end - }; - }; + Graph3d.prototype._redrawDataBar = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i, j, surface, corners; - /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); - }; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - /** - * 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) - }; - } else { - return { - offset: 0, - scale: 1 - }; + // 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; } - }; - /** - * 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; + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // 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; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.dragging = true; + // 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 (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; - } - }; + // 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); + } - /** - * Perform dragging operation - * @param {Event} event - * @private - */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + // 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); + } - // 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; + // 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 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; + // 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); + }); - // normalize dragging speed if cutout is in between. - var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - interval -= duration; + // 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; - 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; + // 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}) + } - // 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; - } + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; - this.previousDelta = delta; - this._applyRange(newStart, newEnd); + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; - // fire a rangechange event - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + // 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(); + } + } }; /** - * Stop dragging operation - * @param {event} event - * @private + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, + i; - // 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 (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.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; } - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; + + 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); + } + + // finish the line + ctx.stroke(); + } }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @private + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - Range.prototype._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._onMouseDown = function (event) { + event = event || window.event; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; + // 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 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); - } + // only react on left mouse button down + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!this.leftButtonDown && !this.touchDown) return; - // 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); + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); - this.zoom(scale, pointerDate, delta); - } + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); - }; + this.frame.style.cursor = 'move'; - /** - * Start of a touch gesture - * @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; + // 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); }; /** - * Handle pinch event - * @param {Event} event - * @private + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - 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); - } + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - // 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; + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - // 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 + // 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 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; + // snap vertically to nice angles + if (Math.abs(Math.sin(verticalNew)) < snapValue) { + verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; + } + if (Math.abs(Math.cos(verticalNew)) < snapValue) { + verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; } - this.setRange(newStart, newEnd, false, true); + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + util.preventDefault(event); }; /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - - validateDirection(direction); + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = false; - 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; - } + // remove event listeners here + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); }; /** - * 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 + * 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 */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) - }; - } + 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; - /** - * 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; + if (!this.showTooltip) { + return; } - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + } - // 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; + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; } - this.setRange(newStart, newEnd, false, true); + 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.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); + } }; /** - * 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 + * Event handler for touchstart event on mobile devices */ - Range.prototype.move = function (delta) { - // zoom start Date and end Date relative to the centerDate - var diff = this.end - this.start; - - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + Graph3d.prototype._onTouchStart = function (event) { + this.touchDown = true; - // TODO: reckon with min and max range + 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); - this.start = newStart; - this.end = newEnd; + this._onMouseDown(event); }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Event handler for touchmove event on mobile devices */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; - - var diff = center - moveTo; - - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; - - this.setRange(newStart, newEnd); + Graph3d.prototype._onTouchMove = function (event) { + this._onMouseMove(event); }; - module.exports = Range; - -/***/ }, -/* 38 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Hammer = __webpack_require__(12); - /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Event handler for touchend event on mobile devices */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst && !isTouching) { - callback(event); + Graph3d.prototype._onTouchEnd = function (event) { + this.touchDown = false; - isTouching = true; - setTimeout(function () { - isTouching = false; - }, 0); - } - }; + util.removeEventListener(document, 'touchmove', this.ontouchmove); + util.removeEventListener(document, 'touchend', this.ontouchend); - hammer.on('hammer.input', callback.inputHandler); + this._onMouseUp(event); }; - // isTouching is true while a touch action is being emitted - // this is a hack to prevent `touch` from being fired twice - var isTouching = false; - /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal && !isReleasing) { - callback(event); + Graph3d.prototype._onWheel = function (event) { + if (!event) /* For IE. */ + event = window.event; - isReleasing = true; - setTimeout(function () { - isReleasing = false; - }, 0); - } - }; + // 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 hammer.on('hammer.input', callback.inputHandler); - }; + // 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); - // isReleasing is true while a release action is being emitted - // this is a hack to prevent `release` from being fired twice - var isReleasing = false; + this.camera.setArmLength(newLength); + this.redraw(); - /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.offTouch = function (hammer, callback) { - hammer.off('hammer.input', callback.inputHandler); + this._hideTooltip(); + } + + // 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); }; /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * 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 */ - exports.offRelease = exports.offTouch; + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; -/***/ }, -/* 39 */ -/***/ function(module, exports, __webpack_require__) { + function sign(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; + } - "use strict"; + 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)); - var moment = __webpack_require__(17); + // 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); + }; /** - * used in Core to convert the options into a volatile variable - * - * @param Core + * 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 */ - 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); + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); + + if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // the data points are ordered from far away to closest + for (i = this.dataPoints.length - 1; i >= 0; i--) { + dataPoint = this.dataPoints[i]; + var surfaces = dataPoint.surfaces; + if (surfaces) { + for (var s = surfaces.length - 1; s >= 0; s--) { + // split each surface in two triangles, and see if the center point is inside one of these + var surface = surfaces[s]; + var corners = surface.corners; + var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; + var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; + if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { + // return immediately at the first hit + return dataPoint; + } + } + } + } + } else { + // find the closest data point, using distance to the center of the point on 2d screen + for (i = 0; i < this.dataPoints.length; i++) { + dataPoint = this.dataPoints[i]; + var point = dataPoint.screen; + if (point) { + var distX = Math.abs(x - point.x); + var distY = Math.abs(y - point.y); + var dist = Math.sqrt(distX * distX + distY * distY); + + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; } } - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time } } + + return closestDataPoint; }; /** - * create new entrees for the repeating hidden dates - * @param body - * @param hiddenDates + * Display a tooltip for given data point + * @param {Object} dataPoint + * @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; - - 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); + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - 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); - } + 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)'; - var duration = endDate - startDate; - if (duration >= 4 * pixelTime) { + line = document.createElement('div'); + line.style.position = 'absolute'; + line.style.height = '40px'; + line.style.width = '0'; + line.style.borderLeft = '1px solid #4d4d4d'; - 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"); + 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'; - endDate.dayOfYear(start.dayOfYear()); - endDate.year(start.year()); - endDate.subtract(7 - offset, "days"); + 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; + } - runUntil.add(1, "weeks"); - break; - case "weekly": - var dayOffset = endDate.diff(startDate, "days"); - var day = startDate.day(); + this._hideTooltip(); - // set the start date to the range.start - startDate.date(start.date()); - startDate.month(start.month()); - startDate.year(start.year()); - endDate = startDate.clone(); + 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 + '
'; + } - // force - startDate.day(day); - endDate.day(day); - endDate.add(dayOffset, "days"); + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); - startDate.subtract(1, "weeks"); - endDate.subtract(1, "weeks"); + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; - 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 left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - endDate.month(start.month()); - endDate.year(start.year()); - endDate.subtract(1, "months"); - endDate.add(offset, "months"); + 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'; + }; - 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"); + /** + * Hide the tooltip when displayed + * @private + */ + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - 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() }); + 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); } } } - // 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); - } } }; + /**--------------------------------------------------------------------------**/ + /** - * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. - * Scales with N^2 - * @param body + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - 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; - } - } - } - } + function getMouseX(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].remove !== true) { - safeDates.push(hiddenDates[i]); - } - } + /** + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y + */ + function getMouseY(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } - body.hiddenDates = safeDates; - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - }; + module.exports = Graph3d; - 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); - } - }; + // use use defaults + +/***/ }, +/* 29 */ +/***/ function(module, exports, __webpack_require__) { /** - * Used in TimeStep to avoid the hidden times. - * @param timeStep - * @param previousTime + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] */ - 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; - } - } + "use strict"; - 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; - } + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } - timeStep.current = newValue.toDate(); - } - }; + module.exports = Point2d; - ///** - // * 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(); - // } - //}; +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { + /** - * replaces the Core toScreen methods - * @param Core - * @param time - * @param width - * @returns {number} + * Expose `Emitter`. */ - 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; - } - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + module.exports = Emitter; - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; - } + /** + * Initialize a new `Emitter`. + * + * @api public + */ + + function Emitter(obj) { + if (obj) return mixin(obj); }; /** - * Replaces the core toTime methods - * @param body - * @param range - * @param x - * @param width - * @returns {Date} + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api 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); - } 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 newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; } - }; + return obj; + } /** - * Support function + * Listen on the given `event` with `fn`. * - * @param hiddenDates - * @param range - * @returns {number} + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - 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; + + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; }; /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; - }; - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - 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; - } - } + function on() { + self.off(event, on); + fn.apply(this, arguments); } - return timeOffset; + + on.fn = fn; + this.on(event, on); + return this; }; /** - * 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}} + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - 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; - } - } + + 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; + } + + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; } - return hiddenDuration; + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; }; /** - * 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 {*} + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} */ - 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; - } + + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); } - } else { - return time; } + + return this; }; /** - * Check if a time is hidden + * Return array of callbacks for `event`. * - * @param time - * @param hiddenDates - * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} + * @param {String} event + * @return {Array} + * @api public */ - 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; - } - } - return { hidden: false, startDate: startDate, endDate: endDate }; + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; }; -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; + }; - 'use strict'; - var Emitter = __webpack_require__(27); - var Hammer = __webpack_require__(12); - var hammerUtil = __webpack_require__(38); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var Range = __webpack_require__(37); - var ItemSet = __webpack_require__(41); - var TimeAxis = __webpack_require__(49); - var Activator = __webpack_require__(50); - var DateUtil = __webpack_require__(39); - var CustomTime = __webpack_require__(52); +/***/ }, +/* 31 */ +/***/ function(module, exports, __webpack_require__) { /** - * Create a timeline visualization - * @constructor + * @prototype Point3d + * @param {Number} [x] + * @param {Number} [y] + * @param {Number} [z] */ - function Core() {} + "use strict"; - // turn Core into an event emitter - Emitter(Core.prototype); + function Point3d(x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; + }; /** - * 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 + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b */ - 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'; + 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.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); + /** + * 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; + }; - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + /** + * Calculate the average of two 3d points + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} The average, (a+b)/2 + */ + Point3d.avg = function (a, b) { + return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); + }; - this.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); + /** + * 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(); - this.on('rangechange', this.redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pan', this._onDrag.bind(this)); + 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; - 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(); - } - }); + return crossproduct; + }; - // 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.listeners = {}; + /** + * 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); + }; - 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; - }); + module.exports = Point3d; - // 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)); +/***/ }, +/* 32 */ +/***/ function(module, exports, __webpack_require__) { - // emulate a release event (emitted after a pan, pinch, tap, or press) - hammerUtil.onRelease(this.hammer, (function (event) { - me.emit('release', event); - }).bind(this)); + 'use strict'; - function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); - } - } - this.dom.root.addEventListener('mousewheel', onMouseWheel); - this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); + var Point3d = __webpack_require__(31); - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + /** + * @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; - this.customTimes = []; + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - // store state information needed for touch events - this.touch = {}; + this.calculateCameraOrientation(); + } - this.redrawCount = 0; + /** + * 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; - // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); - container.appendChild(this.dom.root); + this.calculateCameraOrientation(); }; /** - * 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 + * 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. */ - 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; - } - } + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; + } - // 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 (vertical !== undefined) { + this.armRotation.vertical = vertical; + if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; + if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; + } - if ('hiddenDates' in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); - } + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } + }; - 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; - } - } - } + /** + * 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; - if ('showCustomTime' in options) { - throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); - } + return rot; + }; - // enable/disable autoResize - this._initAutoResize(); - } + /** + * 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; - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); + this.armLength = length; - // enable/disable configure - if ('configure' in options) { - if (!this.configurator) { - this.configurator = this._createConfigurator(); - } + // 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; - this.configurator.setOptions(options.configure); + this.calculateCameraOrientation(); + }; - // 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 }); - } + /** + * Retrieve the arm length + * @return {Number} length + */ + Camera.prototype.getArmLength = function () { + return this.armLength; + }; - // redraw everything - this._redraw(); + /** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; }; /** - * Returns true when the Timeline is active. - * @returns {boolean} + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; }; /** - * Destroy the Core, clean up all DOM elements and event listeners. + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm */ - 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(); + 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); - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); - } - this.dom = null; + // calculate rotation of the camera + this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; + }; - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } + module.exports = Camera; - // 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; +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); + 'use strict'; - this.body = null; - }; + var DataView = __webpack_require__(27); /** - * Set a custom time bar - * @param {Date} time - * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. + * @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 */ - Core.prototype.setCustomTime = function (time, id) { - var customTimes = this.customTimes.filter(function (component) { - return id === component.options.id; + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph + + 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 (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + if (this.values.length > 0) { + this.selectValue(0); } - if (customTimes.length > 0) { - customTimes[0].setCustomTime(time); + // 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 { + this.loaded = true; } }; /** - * Retrieve the current custom time. - * @param {number} [id=undefined] Id of the custom time bar. - * @return {Date | undefined} customTime + * Return the label + * @return {string} label */ - 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(); + Filter.prototype.isLoaded = function () { + return this.loaded; }; /** - * 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 + * Return the loaded progress + * @return {Number} percentage between 0 and 100 */ - Core.prototype.addCustomTime = function (time, id) { - var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); + Filter.prototype.getLoadedProgress = function () { + var len = this.values.length; - 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 i = 0; + while (this.dataPoints[i]) { + i++; } - var customTime = new CustomTime(this.body, { - time: timestamp, - id: id - }); - - this.customTimes.push(customTime); - this.components.push(customTime); - this.redraw(); - - return id; + return Math.round(i / len * 100); }; /** - * 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 + * Return the label + * @return {string} label */ - 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)); + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; }; /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Return the columnIndex of the filter + * @return {Number} columnIndex */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; + Filter.prototype.getColumn = function () { + return this.column; }; /** - * 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'. + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value */ - 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); + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(min, max, animation); + return this.values[this.index]; }; /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} - * @protected + * Retrieve all values of the filter + * @return {Array} values */ - Core.prototype.getDataRange = function () { - // must be implemented by Timeline and Graph2d - throw new Error('Cannot invoke abstract method getDataRange'); + Filter.prototype.getValues = function () { + return this.values; }; /** - * 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'. + * Retrieve one value of the filter + * @param {Number} index + * @return {*} value */ - 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); - } + Filter.prototype.getValue = function (index) { + if (index >= this.values.length) throw 'Error: index out of range'; + + return this.values[index]; }; /** - * 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'. + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - Core.prototype.moveTo = function (time, options) { - var interval = this.range.end - this.range.start; - var t = util.convert(time, 'Date').valueOf(); + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - var start = t - interval / 2; - var end = t + interval / 2; - var animation = options && options.animation !== undefined ? options.animation : true; + if (index === undefined) return []; - this.range.setRange(start, end, animation); + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; + + var dataView = new DataView(this.data, { filter: function filter(item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); + + this.dataPoints[index] = dataPoints; + } + + return dataPoints; }; /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * Set a callback function when the filter is fully loaded. */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; }; /** - * Force a redraw. Can be overridden by implementations of Core + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {Number} index */ - Core.prototype.redraw = function () { - this._redraw(); + Filter.prototype.selectValue = function (index) { + if (index >= this.values.length) throw 'Error: index out of range'; + + this.index = index; + this.value = this.values[index]; }; /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. - * @protected + * Load all filtered rows in the background one by one + * Start this method without providing an index! */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; - if (!dom) return; // when destroyed + var frame = this.graph.frame; - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat - // update class names - if (options.orientation == 'top') { - util.addClassName(dom.root, 'vis-top'); - util.removeClassName(dom.root, 'vis-bottom'); + // 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 me = this; + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); + this.loaded = false; } else { - util.removeClassName(dom.root, 'vis-top'); - util.addClassName(dom.root, 'vis-bottom'); + this.loaded = true; + + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } + + if (this.onLoadCallback) this.onLoadCallback(); } + }; - // 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, ''); + module.exports = Filter; - // 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; +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { - // 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; + 'use strict'; + + var util = __webpack_require__(19); + + /** + * @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; - // 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; + 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); - // TODO: compensate borders when any of the panels is empty. + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); - // 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'); + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); - // 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; + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); - // 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; + 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); - // 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'; + 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); - 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'; + // 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); + }; + } + + this.onChangeCallback = undefined; + + this.values = []; + this.index = undefined; + + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } + + /** + * Select the previous index + */ + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); + } + }; - // 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'; + /** + * Select the next index + */ + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } + }; - // 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(); + /** + * Select the next index + */ + Slider.prototype.playNext = function () { + var start = new Date(); - // 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); + 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); } - 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; + var end = new Date(); + var diff = end - start; - // 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; + // 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 + */ + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); } }; - // TODO: deprecated since version 1.1.0, remove some day - Core.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); + /** + * Start playing + */ + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; + + this.playNext(); + + if (this.frame) { + this.frame.play.value = 'Stop'; + } }; /** - * 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. + * Stop playing */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; + + if (this.frame) { + this.frame.play.value = 'Play'; } + }; - this.currentTime.setCurrentTime(time); + /** + * 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; }; /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); - } + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; + }; - return this.currentTime.getCurrentTime(); + /** + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds + */ + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; }; /** - * 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 + * 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. */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); + Slider.prototype.setPlayLoop = function (doLoop) { + this.playLoop = doLoop; }; /** - * 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 + * Execute the onchange callback function */ - // 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); + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } }; /** - * 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 + * redraw the slider on the correct place */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); + 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'; + + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + 'px'; + } }; /** - * 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 + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) */ - // 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; + Slider.prototype.setValues = function (values) { + this.values = values; + + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** - * Initialize watching when option autoResize is true - * @private + * Select a value by its index + * @param {Number} index */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); } else { - this._stopAutoResize(); + throw 'Error: index out of range'; } }; /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private + * retrieve the index of the currently selected vaue + * @return {Number} index */ - Core.prototype._startAutoResize = function () { - var me = this; + Slider.prototype.getIndex = function () { + return this.index; + }; - this._stopAutoResize(); + /** + * retrieve the currently selected value + * @return {*} value + */ + Slider.prototype.get = function () { + return this.values[this.index]; + }; - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; - } + 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; - 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; + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - me.emit('change'); - } - } + 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', this.onmousemove); + util.addEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); + }; - // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - this.watchTimer = setInterval(this._onResize, 1000); + 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; + + return index; + }; + + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length - 1) * width; + var left = x + 3; + + return left; + }; + + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + util.preventDefault(); + }; + + 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); + + util.preventDefault(); }; + module.exports = Slider; + +/***/ }, +/* 35 */ +/***/ function(module, exports, __webpack_require__) { + /** - * Stop watching for a resize of the frame. - * @private + * @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, ...) */ - Core.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } + "use strict"; - // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); - this._onResize = null; + function StepNumber(start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; + + this._current = 0; + this.setRange(start, end, step, prettyStep); }; /** - * Start moving the timeline vertically - * @param {Event} event - * @private + * 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, ...) */ - Core.prototype._onTouch = function (event) { - this.touch.allowDragging = true; - this.touch.initialScrollTop = this.props.scrollTop; + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; + + this.setStep(step, prettyStep); }; /** - * Start moving the timeline vertically - * @param {Event} event - * @private + * 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, ...) */ - Core.prototype._onPinch = function (event) { - this.touch.allowDragging = false; + StepNumber.prototype.setStep = function (step, prettyStep) { + if (step === undefined || step <= 0) return; + + if (prettyStep !== undefined) this.prettyStep = prettyStep; + + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** - * Move the timeline vertically - * @param {Event} event - * @private + * 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 */ - 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; + StepNumber.calculatePrettyStep = function (step) { + var log10 = function log10(x) { + return Math.log(x) / Math.LN10; + }; - var delta = event.deltaY; + // 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))); - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + // 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; - if (newScrollTop != oldScrollTop) { - this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already - this.emit('verticalDrag'); + // for safety + if (prettyStep <= 0) { + prettyStep = 1; } + + return prettyStep; }; /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * returns the current value of the step + * @return {Number} current value */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); }; /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * returns the current step size + * @return {Number} current step size */ - 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; - } - - // 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; + StepNumber.prototype.getStep = function () { + return this._step; + }; - return this.props.scrollTop; + /** + * 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; }; /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private + * Do a step, add the step size to the current value */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; + StepNumber.prototype.next = function () { + this._current += this._step; }; /** - * Load a configurator - * @return {Object} - * @private + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. */ - Core.prototype._createConfigurator = function () { - throw new Error('Cannot invoke abstract method _createConfigurator'); + StepNumber.prototype.end = function () { + return this._current > this._end; }; - module.exports = Core; + module.exports = StepNumber; /***/ }, -/* 41 */ +/* 36 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var TimeStep = __webpack_require__(45); - var Component = __webpack_require__(35); - var Group = __webpack_require__(42); - var BackgroundGroup = __webpack_require__(46); - var BoxItem = __webpack_require__(47); - var PointItem = __webpack_require__(9); - var RangeItem = __webpack_require__(44); - var BackgroundItem = __webpack_require__(48); + 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 UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items - var BACKGROUND = '__background__'; // reserved group id for background items without group + 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; /** - * 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 + * 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 ItemSet(body, options) { - this.body = body; + 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 = { - type: null, // 'box', 'point', 'range', 'background' + start: null, + end: null, + + autoResize: true, + orientation: { - item: 'bottom' // item orientation: 'top' or 'bottom' + axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' + item: 'bottom' // not relevant }, - align: 'auto', // alignment of box items - stack: true, - groupOrder: null, - selectable: true, - multiselect: false, + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, + // Create the DOM, props, and emitter + this._create(container); - snap: TimeStep.snap, + // all components listed here will be repainted automatically + this.components = []; - 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); + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) }, - - margin: { - item: { - horizontal: 10, - vertical: 10 + hiddenDates: [], + util: { + getScale: function getScale() { + return me.timeAxis.step.scale; }, - axis: 20 + getStep: function getStep() { + return me.timeAxis.step.step; + }, + + 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) } }; - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: { start: 'Date', end: 'Date' } - }; + // time axis + this.timeAxis = new TimeAxis(this.body); + this.timeAxis2 = null; // used in case of orientation option 'both' + this.components.push(this.timeAxis); - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); + + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - 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); - } + 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)); }; - // 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); - } - }; + // apply options + if (options) { + this.setOptions(options); + } - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM + // Extend the functionality from Core + Timeline.prototype = new Core(); - this._create(); + /** + * Load a configurator + * @return {Object} + * @private + */ + Timeline.prototype._createConfigurator = function () { + return new Configurator(this, this.dom.container, configureOptions); + }; - this.setOptions(options); - } + /** + * 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(); + }; - ItemSet.prototype = new Component(); + 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); + } - // available item types will be registered here - ItemSet.types = { - background: BackgroundItem, - box: BoxItem, - range: RangeItem, - point: PointItem + Core.prototype.setOptions.call(this, options); + + if ('type' in options) { + if (options.type !== this.options.type) { + this.options.type = options.type; + + // 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 + } + } + } }; /** - * Create the HTML DOM for the ItemSet + * Set items + * @param {vis.DataSet | Array | null} items */ - ItemSet.prototype._create = function () { - var frame = document.createElement('div'); - frame.className = 'vis-itemset'; - frame['timeline-itemset'] = this; - this.dom.frame = frame; + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - // create background panel - var background = document.createElement('div'); - background.className = 'vis-background'; - frame.appendChild(background); - this.dom.background = background; + // 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' + } + }); + } - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'vis-foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - // create axis panel - var axis = document.createElement('div'); - axis.className = 'vis-axis'; - this.dom.axis = axis; + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var range = this.getItemRange(); + } - // create labelset - var labelSet = document.createElement('div'); - labelSet.className = 'vis-labelset'; - this.dom.labelSet = labelSet; + var start = this.options.start != undefined ? this.options.start : range.min; + var end = this.options.end != undefined ? this.options.end : range.max; - // create ungrouped Group - this._updateUngrouped(); + this.setWindow(start, end, { animation: false }); + } else { + this.fit({ animation: false }); + } + } + }; - // create background Group - var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); - backgroundGroup.show(); - this.groups[BACKGROUND] = backgroundGroup; + /** + * Set groups + * @param {vis.DataSet | Array} groups + */ + 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); + } - // 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); + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); + }; - // drag items when selected - this.hammer.on('hammer.input', (function (event) { - if (event.isFirst) { - this._onTouch(event); - } - }).bind(this)); - this.hammer.on('panstart', this._onDragStart.bind(this)); - this.hammer.on('panmove', this._onDrag.bind(this)); - this.hammer.on('panend', this._onDragEnd.bind(this)); + /** + * 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); + } - // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); + if (data && data.items) { + this.setItems(data.items); + } + }; - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('press', this._onMultiSelectItem.bind(this)); + /** + * 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); - // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); + if (options && options.focus) { + this.focus(ids, options); + } + }; - // attach to the DOM - this.show(); + /** + * 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() || []; }; /** - * 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. + * 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'. */ - 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); + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - 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; - } - } + var ids = Array.isArray(id) ? id : [id]; - 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); - } - } - } + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: 'Date', + end: 'Date' } + }); - 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); - } + // 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 (start === null || s < start) { + start = s; } - // 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); + if (end === null || e > end) { + end = e; + } + }); - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); + 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); + + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animation); } }; /** - * 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] + * 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'. */ - ItemSet.prototype.markDirty = function (options) { - this.groupIds = []; - this.stackDirty = true; - - if (options && options.refreshItems) { - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - } + 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); }; /** - * Destroy the ItemSet + * 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}} */ - ItemSet.prototype.destroy = function () { - this.hide(); - this.setItems(null); - this.setGroups(null); + Timeline.prototype.getItemRange = function () { + var _this = this; - this.hammer = null; + // 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; - this.body = null; - this.conversion = null; - }; + if (min != null && max != null) { + var interval; + var factor; + var lhs; + var rhs; + var delta; - /** - * Hide the component from the DOM - */ - ItemSet.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + (function () { + var getStart = function (item) { + return util.convert(item.data.start, 'Date').valueOf(); + }; - // remove the axis with dots - if (this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - } + var getEnd = function (item) { + var end = item.data.end != undefined ? item.data.end : item.data.start; + return util.convert(end, 'Date').valueOf(); + }; - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + 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); + + 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 + }; }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Calculate the data range of the items start and end dates + * @returns {{min: Date | null, max: Date | null}} */ - ItemSet.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } + Timeline.prototype.getDataRange = function () { + var min = null; + var max = null; - // show axis with dots - if (!this.dom.axis.parentNode) { - this.body.dom.backgroundVertical.appendChild(this.dom.axis); + 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; + } + }); } - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; /** - * 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. + * 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. */ - ItemSet.prototype.setSelection = function (ids) { - var i, ii, id, item; + 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); - if (ids == undefined) ids = []; - if (!Array.isArray(ids)) ids = [ids]; + var item = this.itemSet.itemFromTarget(event); + var group = this.itemSet.groupFromTarget(event); + var customTime = CustomTime.customTimeFromTarget(event); - // 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(); - } + 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; - // 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(); - } + 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'; } + + 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 + }; }; + module.exports = Timeline; + +/***/ }, +/* 37 */ +/***/ 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); + /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ - ItemSet.prototype.getSelection = function () { - return this.selection.concat([]); - }; + function CurrentTime(body, options) { + this.body = body; + + // default options + this.defaultOptions = { + showCurrentTime: true, + + locales: locales, + locale: 'en' + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; + + this._create(); + + this.setOptions(options); + } + + CurrentTime.prototype = new Component(); /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Create the HTML DOM for the current time bar + * @private */ - 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); + 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%'; - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + this.bar = bar; + }; - // 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); - } - } - } - } + /** + * Destroy the CurrentTime bar + */ + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing - return ids; + this.body = null; }; /** - * Deselect a selected item - * @param {String | Number} id - * @private + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - 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; - } + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); } }; @@ -19306,1136 +19282,1402 @@ return /******/ (function(modules) { // webpackBootstrap * Repaint the component * @return {boolean} Returns true if the component is resized */ - 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; - - // 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; - - 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; - - // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - - // 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; + 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); - // update frame height - frame.style.height = asSize(height); + this.start(); + } - // calculate actual size - this.props.width = frame.offsetWidth; - this.props.height = height; + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); - // 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 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); - // check if this component is resized - resized = this._isResized() || resized; + 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(); + } - return resized; + return false; }; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup - * @private + * Start auto refreshing the current time bar */ - 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]; + CurrentTime.prototype.start = function () { + var me = this; - return firstGroup || null; - }; + function update() { + me.stop(); - /** - * 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; + // 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.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; + me.redraw(); - 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; + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - ungrouped.add(item); - } - } + update(); + }; - ungrouped.show(); - } + /** + * Stop auto refreshing the current time bar + */ + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; } }; /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * 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. */ - ItemSet.prototype.getLabelSet = function () { - return this.dom.labelSet; + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, 'Date').valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); }; /** - * Set items - * @param {vis.DataSet | null} items + * Get the current time. + * @return {Date} Returns the current time. */ - ItemSet.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; - - // 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'); - } + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); + }; - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + module.exports = CurrentTime; - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } +/***/ }, +/* 38 */ +/***/ function(module, exports, __webpack_require__) { - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + /** + * Prototype for visual components + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] + * @param {Object} [options] + */ + "use strict"; - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + function Component(body, options) { + this.options = null; + this.props = null; + } - // update the group holding all ungrouped items - this._updateUngrouped(); + /** + * 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); } }; /** - * Get the current items - * @returns {vis.DataSet | null} + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - ItemSet.prototype.getItems = function () { - return this.itemsData; + Component.prototype.redraw = function () { + // should be implemented by the component + return false; }; /** - * Set groups - * @param {vis.DataSet} groups + * Destroy the component. Cleanup DOM and event listeners */ - ItemSet.prototype.setGroups = function (groups) { - var me = this, - ids; + Component.prototype.destroy = function () {}; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.off(event, callback); - }); + /** + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected + */ + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - // 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'); - } + return resized; + }; - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + module.exports = Component; - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } + // should be implemented by the component - // update the group holding all ungrouped items - this._updateUngrouped(); +/***/ }, +/* 39 */ +/***/ function(module, exports, __webpack_require__) { - // update the order of all items in each group - this._order(); + // English + 'use strict'; - this.body.emitter.emit('change', { queue: true }); + exports['en'] = { + current: 'current', + time: 'time' + }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; + + // Dutch + exports['nl'] = { + current: 'huidige', + time: 'tijd' }; + 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); /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * @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 */ - ItemSet.prototype.getGroups = function () { - return this.groupsData; - }; + 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); + } + + Range.prototype = new Component(); /** - * Remove an item by its id - * @param {String | Number} id + * 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 */ - ItemSet.prototype.removeItem = function (id) { - var item = this.itemsData.get(id), - dataset = this.itemsData.getDataSet(); + 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); - 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); - } - }); + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } } }; /** - * Get the time of an item based on it's data and options.type - * @param {Object} itemData - * @returns {string} Returns the type - * @private + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - ItemSet.prototype._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + function validateDirection(direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); + } + } + + /** + * 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] + * + */ + 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; + + 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 }); + } + + 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); + } + } }; /** - * Get the group id for an item - * @param {Object} itemData - * @returns {string} Returns the groupId + * Stop an animation * @private */ - ItemSet.prototype._getGroupId = function (itemData) { - var type = this._getType(itemData); - if (type == 'background' && itemData.group == undefined) { - return BACKGROUND; - } else { - return this.groupsData ? itemData.group : UNGROUPED; + Range.prototype._cancelAnimation = function () { + if (this.animationTimer) { + clearTimeout(this.animationTimer); + this.animationTimer = null; } }; /** - * Handle updated items - * @param {Number[]} ids - * @protected + * 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 */ - ItemSet.prototype._onUpdate = function (ids) { - var me = this; + 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; - ids.forEach((function (id) { - var itemData = me.itemsData.get(id, me.itemOptions); - var item = me.items[id]; - var type = me._getType(itemData); + // 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 + '"'); + } - var constructor = ItemSet.types[type]; - var selected; + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - 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); + // 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; + } } } + } - 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(); + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; } - } 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;}'); + } + } + } + + // 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 { - throw new TypeError('Unknown item type "' + type + '"'); + // zoom to the minimum + diff = zoomMin - (newEnd - newStart); + newStart -= diff / 2; + newEnd += diff / 2; } } - }).bind(this)); + } - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', { queue: true }); + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + + 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; + } + } + } + + var changed = this.start != newStart || this.end != newEnd; + + // 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'); + } + + this.start = newStart; + this.end = newEnd; + return changed; }; /** - * Handle added items - * @param {Number[]} ids - * @protected + * Retrieve the current range. + * @return {Object} An object with start and end properties + */ + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end + }; + }; + + /** + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); + }; /** - * Handle removed items - * @param {Number[]} ids - * @protected + * 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 */ - 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 (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', { queue: true }); + 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 + }; } }; /** - * Update the order of item in all groups + * Start dragging horizontally or vertically + * @param {Event} event * @private */ - 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(); - }); - }; + Range.prototype._onDragStart = function (event) { + this.deltaDifference = 0; + this.previousDelta = 0; + // only allow dragging when configured as movable + if (!this.options.moveable) return; - /** - * Handle updated groups - * @param {Number[]} ids - * @private - */ - ItemSet.prototype._onUpdateGroups = function (ids) { - this._onAddGroups(ids); + // 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.start = this.start; + this.props.touch.end = this.end; + this.props.touch.dragging = true; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; + } }; /** - * Handle changed groups (added or updated) - * @param {Number[]} ids + * Perform dragging operation + * @param {Event} event * @private */ - ItemSet.prototype._onAddGroups = function (ids) { - var me = this; + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; + // 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 (!group) { - // check for reserved ids - if (id == UNGROUPED || id == BACKGROUND) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); - } + 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; - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); + // normalize dragging speed if cutout is in between. + var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + interval -= duration; - group = new Group(id, groupData, me); - me.groups[id] = group; + 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; - // 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); - } - } - } + // 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; + } - group.order(); - group.show(); - } else { - // update group - group.setData(groupData); - } - }); + this.previousDelta = delta; + this._applyRange(newStart, newEnd); - this.body.emitter.emit('change', { queue: true }); + // fire a rangechange event + this.body.emitter.emit('rangechange', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * Handle removed groups - * @param {Number[]} ids + * Stop dragging operation + * @param {event} event * @private */ - ItemSet.prototype._onRemoveGroups = function (ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - if (group) { - group.hide(); - delete groups[id]; - } - }); + // 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.markDirty(); + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; + } - this.body.emitter.emit('change', { queue: true }); + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * Reorder the groups if needed - * @return {boolean} changed + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event * @private */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - 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(); - }); + // 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; + } - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); + // 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.groupIds = groupIds; + // 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); } - return changed; - } else { - return false; + // 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.zoom(scale, pointerDate, delta); } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * Add a new item - * @param {Item} item + * Start of a touch gesture * @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); + 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; }; /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData + * Handle pinch event + * @param {Event} event * @private */ - ItemSet.prototype._updateItem = function (item, itemData) { - var oldGroupId = item.data.group; - var oldSubGroupId = item.data.subgroup; + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // update the items data (will redraw the item when displayed) - item.setData(itemData); + this.props.touch.allowDragging = false; - // update group - if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); + } - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); + 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; + + // 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); + + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; /** - * 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 + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date * @private */ - ItemSet.prototype._removeItem = function (item) { - // remove from DOM - item.hide(); - - // remove from items - delete this.items[item.id]; + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + validateDirection(direction); - // remove from group - item.parent && item.parent.remove(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; + } }; /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} + * 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 */ - ItemSet.prototype._constructByEndArray = function (array) { - var endArray = []; + function getPointer(touch, element) { + return { + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) + }; + } - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { - endArray.push(array[i]); - } + /** + * 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; } - return endArray; + + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + + // 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 }; /** - * 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 + * 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 */ - 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; + Range.prototype.move = function (delta) { + // zoom start Date and end Date relative to the centerDate + var diff = this.end - this.start; + + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; + + // TODO: reckon with min and max range + + this.start = newStart; + this.end = newEnd; }; /** - * Start dragging the selected events - * @param {Event} event - * @private + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - ItemSet.prototype._onDragStart = function (event) { - var item = this.touchParams.item || null; - var me = this; - var props; - - if (item && item.selected) { - - if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { - return; - } + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - // override options.editable - if (item.editable === false) { - return; - } + var diff = center - moveTo; - var dragLeftItem = this.touchParams.dragLeftItem; - var dragRightItem = this.touchParams.dragRightItem; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - if (dragLeftItem) { - props = { - item: dragLeftItem, - initialX: event.center.x, - dragLeft: true, - data: util.extend({}, item.data) // clone the items data - }; + this.setRange(newStart, newEnd); + }; - 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 - }; + module.exports = Range; - 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 - }; +/***/ }, +/* 41 */ +/***/ function(module, exports, __webpack_require__) { - return props; - }); - } + 'use strict'; - 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); - } - }; + var Hammer = __webpack_require__(15); /** - * Start creating a new range item by dragging. - * @param {Event} event - * @private + * Register a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - 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; + exports.onTouch = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFirst && !isTouching) { + callback(event); - var itemData = { - type: 'range', - start: start, - end: end, - content: 'new item' + isTouching = true; + setTimeout(function () { + isTouching = false; + }, 0); + } }; - var id = util.randomUUID(); - itemData[this.itemsData._fieldId] = id; + hammer.on('hammer.input', callback.inputHandler); + }; - var group = this.groupFromTarget(event); - if (group) { - itemData.group = group.groupId; - } + // isTouching is true while a touch action is being emitted + // this is a hack to prevent `touch` from being fired twice + var isTouching = false; - 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); + /** + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal && !isReleasing) { + callback(event); - var props = { - item: newItem, - dragRight: true, - initialX: event.center.x, - data: util.extend({}, itemData) + isReleasing = true; + setTimeout(function () { + isReleasing = false; + }, 0); + } }; - this.touchParams.itemProps = [props]; - event.stopPropagation(); + 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; + /** - * Drag selected items - * @param {Event} event - * @private + * Unregister a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - ItemSet.prototype._onDrag = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); + exports.offTouch = function (hammer, callback) { + hammer.off('hammer.input', callback.inputHandler); + }; - 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(); + /** + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offRelease = exports.offTouch; - // 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; +/***/ }, +/* 42 */ +/***/ function(module, exports, __webpack_require__) { - var itemData = util.extend({}, props.item.data); // clone the data + "use strict"; - if (props.item.editable === false) { - return; - } + var moment = __webpack_require__(20); - var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; + /** + * 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 + } + } + }; - 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); + /** + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates + */ + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - if (itemData.end != undefined) { - var initialEnd = util.convert(props.data.end, 'Date'); - var duration = initialEnd.valueOf() - initialStart.valueOf(); + var start = moment(body.range.start); + var end = moment(body.range.end); - 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; - } - } - } - } + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; + 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 (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; - } + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); } - } - - // confirm moving the item - me.options.onMoving(itemData, function (itemData) { - if (itemData) { - props.item.setData(itemData); + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); } - }); - }); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); - } - }; + var duration = endDate - startDate; + if (duration >= 4 * pixelTime) { - /** - * 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(); + 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"); - item.data.group = group.groupId; - } - }; + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); - /** - * End of dragging selected items - * @param {Event} event - * @private - */ - ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); - // prepare a change set for the changed items - var changes = []; - var me = this; - var dataset = this.itemsData.getDataSet(); + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); - 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; + // force + startDate.day(day); + endDate.day(day); + endDate.add(dayOffset, "days"); - 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); - } + startDate.subtract(1, "weeks"); + endDate.subtract(1, "weeks"); - // 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); + 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"); - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); + endDate.month(start.month()); + endDate.year(start.year()); + endDate.subtract(1, "months"); + endDate.add(offset, "months"); + + 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"); + + 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() }); + } } - }); - - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); + } + // 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); } } }; /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; - - var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); - var shiftKey = event.srcEvent && event.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; + 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; + } + } + } } - var oldSelection = this.getSelection(); - - var item = this.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); + } + } - var newSelection = this.getSelection(); + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + }; - // 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 - }); + 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); } }; /** - * Handle creation and updates of an item on double tap - * @param event - * @private + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime */ - ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) return; + 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; + } + } - var me = this; - var snap = this.options.snap || null; - var item = this.itemFromTarget(event); + 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; + } - event.stopPropagation(); + timeStep.current = newValue.toDate(); + } + }; - if (item) { - // update item + ///** + // * 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(); + // } + //}; - // 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); - } - }); + /** + * replaces the Core toScreen methods + * @param Core + * @param time + * @param width + * @returns {number} + */ + 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 { - // 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 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; + var hidden = exports.isHidden(time, Core.body.hiddenDates); + if (hidden.hidden == true) { + time = hidden.startDate; } - newItem[this.itemsData._fieldId] = util.randomUUID(); - - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; - } + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - // 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? - } - }); + var conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; } }; /** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} */ - 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)); + 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); - // 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; + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; + } + }; - 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); - } + /** + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} + */ + 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; } - - this.setSelection(selection); - - this.body.emitter.emit('select', { - items: this.getSelection(), - event: event - }); } + return duration; }; /** - * 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 + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; + }; - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = 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; } } - }); - - return { - min: min, - max: max - }; + } + return timeOffset; }; /** - * 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 + * 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}} */ - ItemSet.prototype.itemFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-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; + } } - target = target.parentNode; } - return null; + return hiddenDuration; }; /** - * 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 + * 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 {*} */ - 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; + 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 (i === 0 && clientY < top + foreground.offset) { - return group; + if (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; } } + } else { + return time; } - - return null; }; /** - * 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 + * Check if a time is hidden + * + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; + 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; } - target = target.parentNode; } - - return null; + return { hidden: false, startDate: startDate, endDate: endDate }; }; - module.exports = ItemSet; - /***/ }, -/* 42 */ +/* 43 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(16); - var stack = __webpack_require__(43); - var RangeItem = __webpack_require__(44); + var util = __webpack_require__(19); + var stack = __webpack_require__(44); + var RangeItem = __webpack_require__(45); /** * @constructor Group @@ -21014,7 +21256,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Group; /***/ }, -/* 43 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { // Utility functions for ordering and stacking of items @@ -21138,13 +21380,13 @@ return /******/ (function(modules) { // webpackBootstrap }; /***/ }, -/* 44 */ +/* 45 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(12); - var Item = __webpack_require__(11); + var Hammer = __webpack_require__(15); + var Item = __webpack_require__(14); /** * @constructor RangeItem @@ -21434,14 +21676,14 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = RangeItem; /***/ }, -/* 45 */ +/* 46 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var moment = __webpack_require__(17); - var DateUtil = __webpack_require__(39); - var util = __webpack_require__(16); + var moment = __webpack_require__(20); + var DateUtil = __webpack_require__(42); + var util = __webpack_require__(19); /** * @constructor TimeStep @@ -22124,13 +22366,13 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = TimeStep; /***/ }, -/* 46 */ +/* 47 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(16); - var Group = __webpack_require__(42); + var util = __webpack_require__(19); + var Group = __webpack_require__(43); /** * @constructor BackgroundGroup @@ -22188,13 +22430,13 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundGroup; /***/ }, -/* 47 */ +/* 48 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Item = __webpack_require__(11); - var util = __webpack_require__(16); + var Item = __webpack_require__(14); + var util = __webpack_require__(19); /** * @constructor BoxItem @@ -22428,15 +22670,15 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BoxItem; /***/ }, -/* 48 */ +/* 49 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Hammer = __webpack_require__(12); - var Item = __webpack_require__(11); - var BackgroundGroup = __webpack_require__(46); - var RangeItem = __webpack_require__(44); + var Hammer = __webpack_require__(15); + var Item = __webpack_require__(14); + var BackgroundGroup = __webpack_require__(47); + var RangeItem = __webpack_require__(45); /** * @constructor BackgroundItem @@ -22649,16 +22891,16 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundItem; /***/ }, -/* 49 */ +/* 50 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(16); - var Component = __webpack_require__(35); - var TimeStep = __webpack_require__(45); - var DateUtil = __webpack_require__(39); - var moment = __webpack_require__(17); + 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); /** * A horizontal time axis @@ -23088,15 +23330,15 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = TimeAxis; /***/ }, -/* 50 */ +/* 51 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var keycharm = __webpack_require__(51); - var Emitter = __webpack_require__(27); - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); + var keycharm = __webpack_require__(52); + var Emitter = __webpack_require__(30); + var Hammer = __webpack_require__(15); + var util = __webpack_require__(19); /** * Turn an element into an clickToUse element. @@ -23247,7 +23489,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Activator; /***/ }, -/* 51 */ +/* 52 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; @@ -23445,245 +23687,6 @@ return /******/ (function(modules) { // webpackBootstrap -/***/ }, -/* 52 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); - var Component = __webpack_require__(35); - var moment = __webpack_require__(17); - var locales = __webpack_require__(36); - - /** - * 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)); - // 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; - /***/ }, /* 53 */ /***/ function(module, exports, __webpack_require__) { @@ -23704,7 +23707,7 @@ return /******/ (function(modules) { // webpackBootstrap var _ColorPicker2 = _interopRequireDefault(_ColorPicker); - var util = __webpack_require__(16); + var util = __webpack_require__(19); /** * 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. @@ -24381,9 +24384,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__(12); - var hammerUtil = __webpack_require__(38); - var util = __webpack_require__(16); + var Hammer = __webpack_require__(15); + var hammerUtil = __webpack_require__(41); + var util = __webpack_require__(19); var ColorPicker = (function () { function ColorPicker() { @@ -24965,7 +24968,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__(16); + var util = __webpack_require__(19); var errorFound = false; var allOptions = undefined; @@ -25487,16 +25490,16 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Emitter = __webpack_require__(27); - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var Range = __webpack_require__(37); - var Core = __webpack_require__(40); - var TimeAxis = __webpack_require__(49); - var CurrentTime = __webpack_require__(34); - var CustomTime = __webpack_require__(52); + 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); @@ -25823,11 +25826,11 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var util = __webpack_require__(16); - var DOMutil = __webpack_require__(21); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); - var Component = __webpack_require__(35); + 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); @@ -26799,9 +26802,9 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var util = __webpack_require__(16); - var DOMutil = __webpack_require__(21); - var Component = __webpack_require__(35); + var util = __webpack_require__(19); + var DOMutil = __webpack_require__(24); + var Component = __webpack_require__(38); var DataStep = __webpack_require__(60); /** @@ -27630,8 +27633,8 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var util = __webpack_require__(16); - var DOMutil = __webpack_require__(21); + 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); @@ -27840,7 +27843,7 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var DOMutil = __webpack_require__(21); + var DOMutil = __webpack_require__(24); var Points = __webpack_require__(10); function Line(groupId, options) { @@ -28135,7 +28138,7 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var DOMutil = __webpack_require__(21); + var DOMutil = __webpack_require__(24); var Points = __webpack_require__(10); function Bargraph(groupId, options) { @@ -28383,9 +28386,9 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var util = __webpack_require__(16); - var DOMutil = __webpack_require__(21); - var Component = __webpack_require__(35); + var util = __webpack_require__(19); + var DOMutil = __webpack_require__(24); + var Component = __webpack_require__(38); /** * Legend for Graph2d @@ -28929,15 +28932,15 @@ return /******/ (function(modules) { // webpackBootstrap __webpack_require__(109); - var Emitter = __webpack_require__(27); - var Hammer = __webpack_require__(12); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); + 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 dotparser = __webpack_require__(110); var gephiParser = __webpack_require__(111); var Images = __webpack_require__(112); - var Activator = __webpack_require__(50); + var Activator = __webpack_require__(51); var locales = __webpack_require__(113); /** @@ -29484,7 +29487,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__(16); + var util = __webpack_require__(19); /** * @class Groups @@ -29636,9 +29639,9 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); + var util = __webpack_require__(19); + var DataSet = __webpack_require__(25); + var DataView = __webpack_require__(27); var NodesHandler = (function () { function NodesHandler(body, images, groups, layoutEngine) { @@ -30170,7 +30173,7 @@ return /******/ (function(modules) { // webpackBootstrap var _sharedValidator2 = _interopRequireDefault(_sharedValidator); - var util = __webpack_require__(16); + var util = __webpack_require__(19); /** * @class Node @@ -30626,7 +30629,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__(16); + var util = __webpack_require__(19); var Label = (function () { function Label(body, options) { @@ -32226,9 +32229,9 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - var util = __webpack_require__(16); - var DataSet = __webpack_require__(22); - var DataView = __webpack_require__(24); + var util = __webpack_require__(19); + var DataSet = __webpack_require__(25); + var DataView = __webpack_require__(27); var EdgesHandler = (function () { function EdgesHandler(body, images, groups) { @@ -32669,7 +32672,7 @@ return /******/ (function(modules) { // webpackBootstrap var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); - var util = __webpack_require__(16); + var util = __webpack_require__(19); /** * @class Edge @@ -33613,7 +33616,7 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsPhysicsFA2BasedCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedCentralGravitySolver); - var util = __webpack_require__(16); + var util = __webpack_require__(19); var PhysicsEngine = (function () { function PhysicsEngine(body) { @@ -35348,7 +35351,7 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); - var util = __webpack_require__(16); + var util = __webpack_require__(19); var ClusterEngine = (function () { function ClusterEngine(body) { @@ -36146,7 +36149,7 @@ return /******/ (function(modules) { // webpackBootstrap window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } - var util = __webpack_require__(16); + var util = __webpack_require__(19); var CanvasRenderer = (function () { function CanvasRenderer(body, canvas) { @@ -36530,10 +36533,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__(12); - var hammerUtil = __webpack_require__(38); + var Hammer = __webpack_require__(15); + var hammerUtil = __webpack_require__(41); - var util = __webpack_require__(16); + var util = __webpack_require__(19); /** * Create the main frame for the Network. @@ -36908,7 +36911,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__(16); + var util = __webpack_require__(19); var View = (function () { function View(body, canvas) { @@ -37323,7 +37326,7 @@ return /******/ (function(modules) { // webpackBootstrap var _componentsPopup2 = _interopRequireDefault(_componentsPopup); - var util = __webpack_require__(16); + var util = __webpack_require__(19); var InteractionHandler = (function () { function InteractionHandler(body, canvas, selectionHandler) { @@ -38205,7 +38208,7 @@ return /******/ (function(modules) { // webpackBootstrap var Node = __webpack_require__(69); var Edge = __webpack_require__(87); - var util = __webpack_require__(16); + var util = __webpack_require__(19); var SelectionHandler = (function () { function SelectionHandler(body, canvas) { @@ -38934,7 +38937,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__(16); + var util = __webpack_require__(19); var LayoutEngine = (function () { function LayoutEngine(body) { diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 5d81ffe6..545039c3 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -115,6 +115,7 @@ Core.prototype._create = function (container) { // 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 = [ diff --git a/lib/timeline/component/CustomTime.js b/lib/timeline/component/CustomTime.js index 86aec043..1bd3634d 100644 --- a/lib/timeline/component/CustomTime.js +++ b/lib/timeline/component/CustomTime.js @@ -82,6 +82,7 @@ CustomTime.prototype._create = function() { 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(); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 3bdb5895..8509c021 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -198,6 +198,7 @@ ItemSet.prototype._create = function(){ 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));