diff --git a/dist/vis.js b/dist/vis.js index e96a323e..4a3db05e 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.5.0-SNAPSHOT - * @date 2014-02-05 + * @date 2014-02-06 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -31,6 +31,7 @@ // If not available there, load via require. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); +var Emitter = require('emitter-component'); var Hammer; if (typeof window !== 'undefined') { @@ -55,8 +56,6 @@ else { } - - // Internet Explorer 8 and older does not support Array.indexOf, so we define // it here in that case. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ @@ -5352,6 +5351,15 @@ function ItemSet(parent, depends, options) { this.stack = new Stack(this, Object.create(this.options)); this.conversion = null; + + // event listeners for items + // TODO: implement event listeners + // TODO: event listeners must be removed when the ItemSet is deleted + //this.on('dragstart', this._onDragStart.bind(this)); + //this.on('drag', this._onDrag.bind(this)); + //this.on('dragend', this._onDragEnd.bind(this)); + + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis } @@ -5904,6 +5912,65 @@ ItemSet.prototype.toScreen = function toScreen(time) { return (time.valueOf() - conversion.offset) * conversion.scale; }; +// global (private) object to store drag params +var touchParams = {}; + +/** + * Start dragging the selected events + * @param {Event} event + * @private + */ +// TODO: move this function to ItemSet +ItemSet.prototype._onDragStart = function (event) { + var item = this._itemFromTarget(event); + + if (item && item.selected) { + touchParams.items = [item]; + //touchParams.items = this.getSelection(); // TODO: use the current selection + touchParams.itemsLeft = touchParams.items.map(function (item) { + return item.left; + }); + console.log('_onDragStart', touchParams) + event.stopPropagation(); + } +}; + +/** + * Drag selected items + * @param {Event} event + * @private + */ +// TODO: move this function to ItemSet +ItemSet.prototype._onDrag = function (event) { + if (touchParams.items) { + var deltaX = event.gesture.deltaX; + + touchParams.items.forEach(function (item, i) { + item.left = touchParams.itemsLeft[i] + deltaX; + item.reposition(); + }); + + event.stopPropagation(); + } +}; + +/** + * End of dragging selected items + * @param {Event} event + * @private + */ +// TODO: move this function to ItemSet +ItemSet.prototype._onDragEnd = function (event) { + if (touchParams.items) { + // actually apply the new locations + + touchParams.items = null; + + event.stopPropagation(); + } +}; + + /** * @constructor Item * @param {ItemSet} parent @@ -5980,11 +6047,11 @@ Item.prototype.reflow = function reflow() { /** * Return the items width - * @return {Integer} width + * @return {Number} width */ Item.prototype.getWidth = function getWidth() { return this.width; -} +}; /** * @constructor ItemBox @@ -7551,7 +7618,7 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) { /** * Create a timeline visualization * @param {HTMLElement} container - * @param {vis.DataSet | Array | DataTable} [items] + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] * @param {Object} [options] See Timeline.setOptions for the available options. * @constructor */ @@ -7595,6 +7662,13 @@ function Timeline (container, items, options) { this.rootPanel = new RootPanel(container, rootOptions); this.controller.add(this.rootPanel); + // single select (or unselect) when tapping an item + // TODO: implement ctrl+click + this.rootPanel.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); + // item panel var itemOptions = Object.create(this.options); itemOptions.left = function () { @@ -7634,26 +7708,20 @@ function Timeline (container, items, options) { // TODO: reckon with options moveable and zoomable // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + // TODO: enable moving again this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechange', properties); + me.emit('rangechange', properties); }); this.range.on('rangechanged', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechanged', properties); + me.emit('rangechanged', properties); }); - // single select (or unselect) when tapping an item - // TODO: implement ctrl+click - this.rootPanel.on('tap', this._onSelectItem.bind(this)); - - // multi select when holding mouse/touch, or on ctrl+click - this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); - // time axis var timeaxisOptions = Object.create(rootOptions); timeaxisOptions.range = this.range; @@ -7690,6 +7758,9 @@ function Timeline (container, items, options) { } } +// extend Timeline with the Emitter mixin +Emitter(Timeline.prototype); + /** * Set options * @param {Object} options TODO: describe the available options @@ -7723,7 +7794,7 @@ Timeline.prototype.getCustomTime = function() { /** * Set items - * @param {vis.DataSet | Array | DataTable | null} items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ Timeline.prototype.setItems = function(items) { var initialLoad = (this.itemsData == null); @@ -7784,7 +7855,7 @@ Timeline.prototype.setItems = function(items) { /** * Set groups - * @param {vis.DataSet | Array | DataTable} groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ Timeline.prototype.setGroups = function(groups) { var me = this; @@ -7917,56 +7988,19 @@ Timeline.prototype.getSelection = function getSelection() { return this.content ? this.content.getSelection() : []; }; -/** - * Add event listener - * @param {String} event Event name. Available events: - * 'rangechange', 'rangechanged', 'select' - * @param {function} callback Callback function, invoked as callback(properties) - * where properties is an optional object containing - * event specific properties. - */ -Timeline.prototype.on = function on (event, callback) { - var available = ['rangechange', 'rangechanged', 'select']; - - if (available.indexOf(event) == -1) { - throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); - } - - events.addListener(this, event, callback); -}; - -/** - * Remove an event listener - * @param {String} event Event name - * @param {function} callback Callback function - */ -Timeline.prototype.off = function off (event, callback) { - events.removeListener(this, event, callback); -}; - -/** - * Trigger an event - * @param {String} event Event name, available events: 'rangechange', - * 'rangechanged', 'select' - * @param {Object} [properties] Event specific properties - * @private - */ -Timeline.prototype._trigger = function _trigger(event, properties) { - events.trigger(this, event, properties || {}); -}; - /** * Handle selecting/deselecting an item when tapping it * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onSelectItem = function (event) { var item = this._itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -7978,6 +8012,7 @@ Timeline.prototype._onSelectItem = function (event) { * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onMultiSelectItem = function (event) { var selection, item = this._itemFromTarget(event); @@ -7999,7 +8034,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { } this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -8013,6 +8048,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { * @return {Item | null| item * @private */ +// TODO: move this function to ItemSet Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { var target = event.target; while (target) { @@ -16088,7 +16124,173 @@ if (typeof window !== 'undefined') { } -},{"hammerjs":2,"moment":3,"mousetrap":4}],2:[function(require,module,exports){ +},{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){ + +/** + * Expose `Emitter`. + */ + +module.exports = Emitter; + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + +function Emitter(obj) { + if (obj) return mixin(obj); +}; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + + function on() { + self.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; +}; + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +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; + } + + // 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; +}; + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + +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); + } + } + + return this; +}; + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; + +},{}],3:[function(require,module,exports){ /*! Hammer.JS - v1.0.5 - 2013-04-07 * http://eightmedia.github.com/hammer.js * @@ -17510,7 +17712,7 @@ else { } } })(this); -},{}],3:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ //! moment.js //! version : 2.5.1 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors @@ -19912,7 +20114,7 @@ else { } }).call(this); -},{}],4:[function(require,module,exports){ +},{}],5:[function(require,module,exports){ /** * Copyright 2012 Craig Campbell * diff --git a/package.json b/package.json index 73365fad..943db31a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "moment": "latest", "hammerjs": "1.0.5", "mousetrap": "latest", + "emitter-component": "latest", "node-watch": "latest" } } diff --git a/src/module/imports.js b/src/module/imports.js index ea8d8c92..bda6f792 100644 --- a/src/module/imports.js +++ b/src/module/imports.js @@ -6,6 +6,7 @@ // If not available there, load via require. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); +var Emitter = require('emitter-component'); var Hammer; if (typeof window !== 'undefined') { @@ -28,5 +29,3 @@ else { throw Error('mouseTrap is only available in a browser, not in node.js.'); } } - - diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 38c19fae..3d2ff97c 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -1,7 +1,7 @@ /** * Create a timeline visualization * @param {HTMLElement} container - * @param {vis.DataSet | Array | DataTable} [items] + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] * @param {Object} [options] See Timeline.setOptions for the available options. * @constructor */ @@ -45,6 +45,13 @@ function Timeline (container, items, options) { this.rootPanel = new RootPanel(container, rootOptions); this.controller.add(this.rootPanel); + // single select (or unselect) when tapping an item + // TODO: implement ctrl+click + this.rootPanel.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); + // item panel var itemOptions = Object.create(this.options); itemOptions.left = function () { @@ -84,26 +91,20 @@ function Timeline (container, items, options) { // TODO: reckon with options moveable and zoomable // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + // TODO: enable moving again this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechange', properties); + me.emit('rangechange', properties); }); this.range.on('rangechanged', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechanged', properties); + me.emit('rangechanged', properties); }); - // single select (or unselect) when tapping an item - // TODO: implement ctrl+click - this.rootPanel.on('tap', this._onSelectItem.bind(this)); - - // multi select when holding mouse/touch, or on ctrl+click - this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); - // time axis var timeaxisOptions = Object.create(rootOptions); timeaxisOptions.range = this.range; @@ -140,6 +141,9 @@ function Timeline (container, items, options) { } } +// extend Timeline with the Emitter mixin +Emitter(Timeline.prototype); + /** * Set options * @param {Object} options TODO: describe the available options @@ -173,7 +177,7 @@ Timeline.prototype.getCustomTime = function() { /** * Set items - * @param {vis.DataSet | Array | DataTable | null} items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ Timeline.prototype.setItems = function(items) { var initialLoad = (this.itemsData == null); @@ -234,7 +238,7 @@ Timeline.prototype.setItems = function(items) { /** * Set groups - * @param {vis.DataSet | Array | DataTable} groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ Timeline.prototype.setGroups = function(groups) { var me = this; @@ -367,56 +371,19 @@ Timeline.prototype.getSelection = function getSelection() { return this.content ? this.content.getSelection() : []; }; -/** - * Add event listener - * @param {String} event Event name. Available events: - * 'rangechange', 'rangechanged', 'select' - * @param {function} callback Callback function, invoked as callback(properties) - * where properties is an optional object containing - * event specific properties. - */ -Timeline.prototype.on = function on (event, callback) { - var available = ['rangechange', 'rangechanged', 'select']; - - if (available.indexOf(event) == -1) { - throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); - } - - events.addListener(this, event, callback); -}; - -/** - * Remove an event listener - * @param {String} event Event name - * @param {function} callback Callback function - */ -Timeline.prototype.off = function off (event, callback) { - events.removeListener(this, event, callback); -}; - -/** - * Trigger an event - * @param {String} event Event name, available events: 'rangechange', - * 'rangechanged', 'select' - * @param {Object} [properties] Event specific properties - * @private - */ -Timeline.prototype._trigger = function _trigger(event, properties) { - events.trigger(this, event, properties || {}); -}; - /** * Handle selecting/deselecting an item when tapping it * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onSelectItem = function (event) { var item = this._itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -428,6 +395,7 @@ Timeline.prototype._onSelectItem = function (event) { * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onMultiSelectItem = function (event) { var selection, item = this._itemFromTarget(event); @@ -449,7 +417,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { } this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -463,6 +431,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { * @return {Item | null| item * @private */ +// TODO: move this function to ItemSet Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { var target = event.target; while (target) {