From ac3ea3c11f9c76d7ad079d8c4c7d2ed6fedb0c47 Mon Sep 17 00:00:00 2001 From: josdejong Date: Thu, 6 Feb 2014 15:41:46 +0100 Subject: [PATCH 1/5] Integrated an emitter-component as Emitter mixin --- dist/vis.js | 322 +++++++++++++++++++++++++++++++-------- package.json | 1 + src/module/imports.js | 3 +- src/timeline/Timeline.js | 73 +++------ 4 files changed, 285 insertions(+), 114 deletions(-) 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) { From 9e68b81d319c356af7052da6745569983f152411 Mon Sep 17 00:00:00 2001 From: josdejong Date: Thu, 6 Feb 2014 17:28:43 +0100 Subject: [PATCH 2/5] Start with implementing an event bus to propagate events between the components of the Timeline --- dist/vis.js | 242 ++++++++++++++++++++-------- src/timeline/Controller.js | 11 +- src/timeline/Range.js | 19 +-- src/timeline/Timeline.js | 8 +- src/timeline/component/Component.js | 17 ++ src/timeline/component/ItemSet.js | 133 +++++++++++++++ src/timeline/component/RootPanel.js | 103 ++++++------ src/timeline/component/item/Item.js | 4 +- 8 files changed, 403 insertions(+), 134 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 4a3db05e..462af842 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -3254,26 +3254,27 @@ function validateDirection (direction) { /** * Add listeners for mouse and touch events to the component - * @param {Component} component + * @param {Controller} controller + * @param {Component} component Should be a rootpanel * @param {String} event Available events: 'move', 'zoom' * @param {String} direction Available directions: 'horizontal', 'vertical' */ -Range.prototype.subscribe = function (component, event, direction) { +Range.prototype.subscribe = function (controller, component, event, direction) { var me = this; if (event == 'move') { // drag start listener - component.on('dragstart', function (event) { + controller.on('dragstart', function (event) { me._onDragStart(event, component); }); // drag listener - component.on('drag', function (event) { + controller.on('drag', function (event) { me._onDrag(event, component, direction); }); // drag end listener - component.on('dragend', function (event) { + controller.on('dragend', function (event) { me._onDragEnd(event, component); }); } @@ -3282,14 +3283,14 @@ Range.prototype.subscribe = function (component, event, direction) { function mousewheel (event) { me._onMouseWheel(event, component, direction); } - component.on('mousewheel', mousewheel); - component.on('DOMMouseScroll', mousewheel); // For FF + controller.on('mousewheel', mousewheel); + controller.on('DOMMouseScroll', mousewheel); // For FF // pinch - component.on('touch', function (event) { + controller.on('touch', function (event) { me._onTouch(); }); - component.on('pinch', function (event) { + controller.on('pinch', function (event) { me._onPinch(event, component, direction); }); } @@ -3771,6 +3772,9 @@ function Controller () { this.reflowTimer = undefined; } +// Extend controller with Emitter mixin +Emitter(Controller.prototype); + /** * Add a component to the controller * @param {Component} component @@ -3786,7 +3790,7 @@ Controller.prototype.add = function add(component) { } // add the component - component.controller = this; + component.setController(this); this.components[component.id] = component; }; @@ -3798,13 +3802,17 @@ Controller.prototype.remove = function remove(component) { var id; for (id in this.components) { if (this.components.hasOwnProperty(id)) { - if (id == component || this.components[id] == component) { + if (id == component || this.components[id] === component) { break; } } } if (id) { + // unregister the controller (gives the component the ability to unregister + // event listeners and clean up other stuff) + this.components[id].setController(null); + delete this.components[id]; } }; @@ -3988,6 +3996,23 @@ Component.prototype.getOption = function getOption(name) { return value; }; +/** + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller + */ +Component.prototype.setController = function setController (controller) { + this.controller = controller || null; +}; + +/** + * Get controller of this component + * @return {Controller} controller + */ +Component.prototype.getController = function getController () { + return this.controller; +}; + /** * Get the container element of the component, which can be used by a child to * add its own widgets. Not all components do have a container for childs, in @@ -4205,12 +4230,29 @@ function RootPanel(container, options) { this.id = util.randomUUID(); this.container = container; + // create functions to be used as DOM event listeners + var me = this; + this.hammer = null; + + // create listeners for all interesting events, these events will be emitted + // via the controller + var events = [ + 'touch', 'pinch', 'tap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox + ]; + this.listeners = {}; + events.forEach(function (event) { + me.listeners[event] = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.controller.emit.apply(me.controller, args); + }; + }); + this.options = options || {}; this.defaultOptions = { autoResize: true }; - - this.listeners = {}; // event listeners } RootPanel.prototype = new Panel(); @@ -4243,6 +4285,8 @@ RootPanel.prototype.repaint = function () { this.frame = frame; + this._registerListeners(); + changed += 1; } if (!frame.parentNode) { @@ -4264,7 +4308,6 @@ RootPanel.prototype.repaint = function () { changed += update(frame.style, 'width', asSize(options.width, '100%')); changed += update(frame.style, 'height', asSize(options.height, '100%')); - this._updateEventEmitters(); this._updateWatch(); return (changed > 0); @@ -4353,59 +4396,52 @@ RootPanel.prototype._unwatch = function () { }; /** - * Event handler - * @param {String} event name of the event, for example 'click', 'mousemove' - * @param {function} callback callback handler, invoked with the raw HTML Event - * as parameter. + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller */ -RootPanel.prototype.on = function (event, callback) { - // register the listener at this component - var arr = this.listeners[event]; - if (!arr) { - arr = []; - this.listeners[event] = arr; - } - arr.push(callback); +RootPanel.prototype.setController = function setController (controller) { + this.controller = controller || null; - this._updateEventEmitters(); + if (this.controller) { + this._registerListeners(); + } + else { + this._unregisterListeners(); + } }; /** - * Update the event listeners for all event emitters + * Register event emitters emitted by the rootpanel * @private */ -RootPanel.prototype._updateEventEmitters = function () { - if (this.listeners) { - var me = this; - util.forEach(this.listeners, function (listeners, event) { - if (!me.emitters) { - me.emitters = {}; - } - if (!(event in me.emitters)) { - // create event - var frame = me.frame; - if (frame) { - //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging - var callback = function(event) { - listeners.forEach(function (listener) { - // TODO: filter on event target! - listener(event); - }); - }; - me.emitters[event] = callback; +RootPanel.prototype._registerListeners = function () { + if (this.frame && this.controller && !this.hammer) { + this.hammer = Hammer(this.frame, { + prevent_default: true + }); - if (!me.hammer) { - me.hammer = Hammer(frame, { - prevent_default: true - }); - } - me.hammer.on(event, callback); - } + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.on(event, this.listeners[event]); } - }); + } + } +}; - // TODO: be able to delete event listeners - // TODO: be able to move event listeners to a parent when available +/** + * Unregister event emitters from the rootpanel + * @private + */ +RootPanel.prototype._unregisterListeners = function () { + if (this.hammer) { + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.off(event, this.listeners[event]); + } + } + + this.hammer = null; } }; @@ -5308,6 +5344,13 @@ function ItemSet(parent, depends, options) { this.parent = parent; this.depends = depends; + // event listeners + this.listeners = { + dragstart: this._onDragStart.bind(this), + drag: this._onDrag.bind(this), + dragend: this._onDragEnd.bind(this) + }; + // one options object is shared by this itemset and all its items this.options = options || {}; this.defaultOptions = { @@ -5351,15 +5394,6 @@ 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 } @@ -5400,6 +5434,55 @@ ItemSet.types = { */ ItemSet.prototype.setOptions = Component.prototype.setOptions; + + +/** + * Set controller for this component + * @param {Controller | null} controller + */ +ItemSet.prototype.setController = function setController (controller) { + var event; + + // unregister old event listeners + if (this.controller) { + for (event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.controller.off(event, this.listeners[event]); + } + } + } + + this.controller = controller || null; + + // register new event listeners + if (this.controller) { + for (event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.controller.on(event, this.listeners[event]); + } + } + } +}; + +// attach event listeners for dragging items to the controller +(function (me) { + var _controller = null; + var _onDragStart = null; + var _onDrag = null; + var _onDragEnd = null; + + Object.defineProperty(me, 'controller', { + get: function () { + return _controller; + }, + + set: function (controller) { + + } + }); +}) (this); + + /** * Set range (start and end). * @param {Range | Object} range A Range or an object containing start and end. @@ -5923,7 +6006,7 @@ var touchParams = {}; // TODO: move this function to ItemSet ItemSet.prototype._onDragStart = function (event) { var item = this._itemFromTarget(event); - +console.log('_onDragStart', event) if (item && item.selected) { touchParams.items = [item]; //touchParams.items = this.getSelection(); // TODO: use the current selection @@ -5970,7 +6053,24 @@ ItemSet.prototype._onDragEnd = function (event) { } }; +/** + * 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 + * @private + */ +ItemSet.prototype._itemFromTarget = function _itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + return null; +}; /** * @constructor Item * @param {ItemSet} parent @@ -7664,10 +7764,10 @@ function Timeline (container, items, options) { // single select (or unselect) when tapping an item // TODO: implement ctrl+click - this.rootPanel.on('tap', this._onSelectItem.bind(this)); + this.controller.on('tap', this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click - this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); + this.controller.on('hold', this._onMultiSelectItem.bind(this)); // item panel var itemOptions = Object.create(this.options); @@ -7709,8 +7809,8 @@ 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.subscribe(this.controller, this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); diff --git a/src/timeline/Controller.js b/src/timeline/Controller.js index 185d341b..8ba7e563 100644 --- a/src/timeline/Controller.js +++ b/src/timeline/Controller.js @@ -11,6 +11,9 @@ function Controller () { this.reflowTimer = undefined; } +// Extend controller with Emitter mixin +Emitter(Controller.prototype); + /** * Add a component to the controller * @param {Component} component @@ -26,7 +29,7 @@ Controller.prototype.add = function add(component) { } // add the component - component.controller = this; + component.setController(this); this.components[component.id] = component; }; @@ -38,13 +41,17 @@ Controller.prototype.remove = function remove(component) { var id; for (id in this.components) { if (this.components.hasOwnProperty(id)) { - if (id == component || this.components[id] == component) { + if (id == component || this.components[id] === component) { break; } } } if (id) { + // unregister the controller (gives the component the ability to unregister + // event listeners and clean up other stuff) + this.components[id].setController(null); + delete this.components[id]; } }; diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 70c9083d..f3e04390 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -48,26 +48,27 @@ function validateDirection (direction) { /** * Add listeners for mouse and touch events to the component - * @param {Component} component + * @param {Controller} controller + * @param {Component} component Should be a rootpanel * @param {String} event Available events: 'move', 'zoom' * @param {String} direction Available directions: 'horizontal', 'vertical' */ -Range.prototype.subscribe = function (component, event, direction) { +Range.prototype.subscribe = function (controller, component, event, direction) { var me = this; if (event == 'move') { // drag start listener - component.on('dragstart', function (event) { + controller.on('dragstart', function (event) { me._onDragStart(event, component); }); // drag listener - component.on('drag', function (event) { + controller.on('drag', function (event) { me._onDrag(event, component, direction); }); // drag end listener - component.on('dragend', function (event) { + controller.on('dragend', function (event) { me._onDragEnd(event, component); }); } @@ -76,14 +77,14 @@ Range.prototype.subscribe = function (component, event, direction) { function mousewheel (event) { me._onMouseWheel(event, component, direction); } - component.on('mousewheel', mousewheel); - component.on('DOMMouseScroll', mousewheel); // For FF + controller.on('mousewheel', mousewheel); + controller.on('DOMMouseScroll', mousewheel); // For FF // pinch - component.on('touch', function (event) { + controller.on('touch', function (event) { me._onTouch(); }); - component.on('pinch', function (event) { + controller.on('pinch', function (event) { me._onPinch(event, component, direction); }); } diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 3d2ff97c..3b9efc4a 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -47,10 +47,10 @@ function Timeline (container, items, options) { // single select (or unselect) when tapping an item // TODO: implement ctrl+click - this.rootPanel.on('tap', this._onSelectItem.bind(this)); + this.controller.on('tap', this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click - this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); + this.controller.on('hold', this._onMultiSelectItem.bind(this)); // item panel var itemOptions = Object.create(this.options); @@ -92,8 +92,8 @@ 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.subscribe(this.controller, this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 8d15e0c6..c7c8cd3c 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -55,6 +55,23 @@ Component.prototype.getOption = function getOption(name) { return value; }; +/** + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller + */ +Component.prototype.setController = function setController (controller) { + this.controller = controller || null; +}; + +/** + * Get controller of this component + * @return {Controller} controller + */ +Component.prototype.getController = function getController () { + return this.controller; +}; + /** * Get the container element of the component, which can be used by a child to * add its own widgets. Not all components do have a container for childs, in diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 20d7259b..35aab3ab 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -16,6 +16,13 @@ function ItemSet(parent, depends, options) { this.parent = parent; this.depends = depends; + // event listeners + this.listeners = { + dragstart: this._onDragStart.bind(this), + drag: this._onDrag.bind(this), + dragend: this._onDragEnd.bind(this) + }; + // one options object is shared by this itemset and all its items this.options = options || {}; this.defaultOptions = { @@ -99,6 +106,55 @@ ItemSet.types = { */ ItemSet.prototype.setOptions = Component.prototype.setOptions; + + +/** + * Set controller for this component + * @param {Controller | null} controller + */ +ItemSet.prototype.setController = function setController (controller) { + var event; + + // unregister old event listeners + if (this.controller) { + for (event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.controller.off(event, this.listeners[event]); + } + } + } + + this.controller = controller || null; + + // register new event listeners + if (this.controller) { + for (event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.controller.on(event, this.listeners[event]); + } + } + } +}; + +// attach event listeners for dragging items to the controller +(function (me) { + var _controller = null; + var _onDragStart = null; + var _onDrag = null; + var _onDragEnd = null; + + Object.defineProperty(me, 'controller', { + get: function () { + return _controller; + }, + + set: function (controller) { + + } + }); +}) (this); + + /** * Set range (start and end). * @param {Range | Object} range A Range or an object containing start and end. @@ -610,3 +666,80 @@ ItemSet.prototype.toScreen = function toScreen(time) { var conversion = this.conversion; 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); +console.log('_onDragStart', 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(); + } +}; + +/** + * 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 + * @private + */ +ItemSet.prototype._itemFromTarget = function _itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + + return null; +}; \ No newline at end of file diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index 6bfd2db2..e4c6166d 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -10,12 +10,29 @@ function RootPanel(container, options) { this.id = util.randomUUID(); this.container = container; + // create functions to be used as DOM event listeners + var me = this; + this.hammer = null; + + // create listeners for all interesting events, these events will be emitted + // via the controller + var events = [ + 'touch', 'pinch', 'tap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox + ]; + this.listeners = {}; + events.forEach(function (event) { + me.listeners[event] = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.controller.emit.apply(me.controller, args); + }; + }); + this.options = options || {}; this.defaultOptions = { autoResize: true }; - - this.listeners = {}; // event listeners } RootPanel.prototype = new Panel(); @@ -48,6 +65,8 @@ RootPanel.prototype.repaint = function () { this.frame = frame; + this._registerListeners(); + changed += 1; } if (!frame.parentNode) { @@ -69,7 +88,6 @@ RootPanel.prototype.repaint = function () { changed += update(frame.style, 'width', asSize(options.width, '100%')); changed += update(frame.style, 'height', asSize(options.height, '100%')); - this._updateEventEmitters(); this._updateWatch(); return (changed > 0); @@ -158,58 +176,51 @@ RootPanel.prototype._unwatch = function () { }; /** - * Event handler - * @param {String} event name of the event, for example 'click', 'mousemove' - * @param {function} callback callback handler, invoked with the raw HTML Event - * as parameter. + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller */ -RootPanel.prototype.on = function (event, callback) { - // register the listener at this component - var arr = this.listeners[event]; - if (!arr) { - arr = []; - this.listeners[event] = arr; - } - arr.push(callback); +RootPanel.prototype.setController = function setController (controller) { + this.controller = controller || null; - this._updateEventEmitters(); + if (this.controller) { + this._registerListeners(); + } + else { + this._unregisterListeners(); + } }; /** - * Update the event listeners for all event emitters + * Register event emitters emitted by the rootpanel * @private */ -RootPanel.prototype._updateEventEmitters = function () { - if (this.listeners) { - var me = this; - util.forEach(this.listeners, function (listeners, event) { - if (!me.emitters) { - me.emitters = {}; +RootPanel.prototype._registerListeners = function () { + if (this.frame && this.controller && !this.hammer) { + this.hammer = Hammer(this.frame, { + prevent_default: true + }); + + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.on(event, this.listeners[event]); } - if (!(event in me.emitters)) { - // create event - var frame = me.frame; - if (frame) { - //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging - var callback = function(event) { - listeners.forEach(function (listener) { - // TODO: filter on event target! - listener(event); - }); - }; - me.emitters[event] = callback; - - if (!me.hammer) { - me.hammer = Hammer(frame, { - prevent_default: true - }); - } - me.hammer.on(event, callback); - } + } + } +}; + +/** + * Unregister event emitters from the rootpanel + * @private + */ +RootPanel.prototype._unregisterListeners = function () { + if (this.hammer) { + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.off(event, this.listeners[event]); } - }); + } - // TODO: be able to delete event listeners - // TODO: be able to move event listeners to a parent when available + this.hammer = null; } }; diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 590ee551..a0d68354 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -74,8 +74,8 @@ Item.prototype.reflow = function reflow() { /** * Return the items width - * @return {Integer} width + * @return {Number} width */ Item.prototype.getWidth = function getWidth() { return this.width; -} +}; From 5801a0e7706c53ba4f16fa5c53cb4defff93e7df Mon Sep 17 00:00:00 2001 From: josdejong Date: Fri, 7 Feb 2014 11:12:49 +0100 Subject: [PATCH 3/5] Implemented dragging items --- src/timeline/Controller.js | 2 + src/timeline/Range.js | 36 ++++++-- src/timeline/Stack.js | 14 +-- src/timeline/Timeline.js | 24 +---- src/timeline/component/ItemSet.js | 110 ++++++++++++++++------- src/timeline/component/item/Item.js | 9 ++ src/timeline/component/item/ItemBox.js | 2 +- src/timeline/component/item/ItemPoint.js | 2 +- src/timeline/component/item/ItemRange.js | 4 +- 9 files changed, 131 insertions(+), 72 deletions(-) diff --git a/src/timeline/Controller.js b/src/timeline/Controller.js index 8ba7e563..ebb3e494 100644 --- a/src/timeline/Controller.js +++ b/src/timeline/Controller.js @@ -61,6 +61,7 @@ Controller.prototype.remove = function remove(component) { * @param {Boolean} [force] If true, an immediate reflow is forced. Default * is false. */ +// TODO: change requestReflow into an event Controller.prototype.requestReflow = function requestReflow(force) { if (force) { this.reflow(); @@ -81,6 +82,7 @@ Controller.prototype.requestReflow = function requestReflow(force) { * @param {Boolean} [force] If true, an immediate repaint is forced. Default * is false. */ +// TODO: change requestReflow into an event Controller.prototype.requestRepaint = function requestRepaint(force) { if (force) { this.repaint(); diff --git a/src/timeline/Range.js b/src/timeline/Range.js index f3e04390..0bea4d63 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -71,6 +71,11 @@ Range.prototype.subscribe = function (controller, component, event, direction) { controller.on('dragend', function (event) { me._onDragEnd(event, component); }); + + // ignore dragging when holding + controller.on('hold', function (event) { + me._onHold(); + }); } else if (event == 'zoom') { // mouse wheel @@ -82,7 +87,7 @@ Range.prototype.subscribe = function (controller, component, event, direction) { // pinch controller.on('touch', function (event) { - me._onTouch(); + me._onTouch(event); }); controller.on('pinch', function (event) { me._onPinch(event, component, direction); @@ -312,7 +317,7 @@ var touchParams = {}; Range.prototype._onDragStart = function(event, component) { // 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 (touchParams.pinching) return; + if (touchParams.ignore) return; touchParams.start = this.start; touchParams.end = this.end; @@ -335,7 +340,7 @@ Range.prototype._onDrag = function (event, component, direction) { // 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 (touchParams.pinching) return; + if (touchParams.ignore) return; var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, interval = (touchParams.end - touchParams.start), @@ -357,7 +362,7 @@ Range.prototype._onDrag = function (event, component, direction) { Range.prototype._onDragEnd = function (event, component) { // 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 (touchParams.pinching) return; + if (touchParams.ignore) return; if (component.frame) { component.frame.style.cursor = 'auto'; @@ -418,14 +423,29 @@ Range.prototype._onMouseWheel = function(event, component, direction) { }; /** - * On start of a touch gesture, initialize scale to 1 + * Start of a touch gesture * @private */ -Range.prototype._onTouch = function () { +Range.prototype._onTouch = function (event) { touchParams.start = this.start; touchParams.end = this.end; - touchParams.pinching = false; + touchParams.ignore = false; touchParams.center = null; + + // don't move the range when dragging a selected event + // TODO: it's not so neat to have to know about the state of the ItemSet + var item = ItemSet.itemFromTarget(event); + if (item && item.selected) { + touchParams.ignore = true; + } +}; + +/** + * On start of a hold gesture + * @private + */ +Range.prototype._onHold = function () { + touchParams.ignore = true; }; /** @@ -436,7 +456,7 @@ Range.prototype._onTouch = function () { * @private */ Range.prototype._onPinch = function (event, component, direction) { - touchParams.pinching = true; + touchParams.ignore = true; if (event.gesture.touches.length > 1) { if (!touchParams.center) { diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index 017c98ce..16ac34b7 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -1,11 +1,11 @@ /** * @constructor Stack * Stacks items on top of each other. - * @param {ItemSet} parent + * @param {ItemSet} itemset * @param {Object} [options] */ -function Stack (parent, options) { - this.parent = parent; +function Stack (itemset, options) { + this.itemset = itemset; this.options = options || {}; this.defaultOptions = { @@ -43,14 +43,14 @@ function Stack (parent, options) { /** * Set options for the stack * @param {Object} options Available options: - * {ItemSet} parent + * {ItemSet} itemset * {Number} margin * {function} order Stacking order */ Stack.prototype.setOptions = function setOptions (options) { util.extend(this.options, options); - // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately + // TODO: register on data changes at the connected itemset, and update the changed part only and immediately }; /** @@ -70,9 +70,9 @@ Stack.prototype.update = function update() { * @private */ Stack.prototype._order = function _order () { - var items = this.parent.items; + var items = this.itemset.items; if (!items) { - throw new Error('Cannot stack items: parent does not contain items'); + throw new Error('Cannot stack items: ItemSet does not contain items'); } // TODO: store the sorted items, to have less work later on diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 3b9efc4a..664bc20c 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -378,7 +378,7 @@ Timeline.prototype.getSelection = function getSelection() { */ // TODO: move this function to ItemSet Timeline.prototype._onSelectItem = function (event) { - var item = this._itemFromTarget(event); + var item = ItemSet.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); @@ -398,7 +398,7 @@ Timeline.prototype._onSelectItem = function (event) { // TODO: move this function to ItemSet Timeline.prototype._onMultiSelectItem = function (event) { var selection, - item = this._itemFromTarget(event); + item = ItemSet.itemFromTarget(event); if (!item) { // do nothing... @@ -423,23 +423,3 @@ Timeline.prototype._onMultiSelectItem = function (event) { event.stopPropagation(); }; - -/** - * 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 - * @private - */ -// TODO: move this function to ItemSet -Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; - } - - return null; -}; \ No newline at end of file diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 35aab3ab..e44d4c87 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -17,7 +17,7 @@ function ItemSet(parent, depends, options) { this.depends = depends; // event listeners - this.listeners = { + this.eventListeners = { dragstart: this._onDragStart.bind(this), drag: this._onDrag.bind(this), dragend: this._onDragEnd.bind(this) @@ -42,6 +42,7 @@ function ItemSet(parent, depends, options) { this.itemsData = null; // DataSet this.range = null; // Range or Object {start: number, end: number} + // data change listeners this.listeners = { 'add': function (event, params, senderId) { if (senderId != me.id) { @@ -66,6 +67,8 @@ function ItemSet(parent, depends, options) { this.stack = new Stack(this, Object.create(this.options)); this.conversion = null; + this.touchParams = {}; // stores properties while dragging + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis } @@ -117,9 +120,9 @@ ItemSet.prototype.setController = function setController (controller) { // unregister old event listeners if (this.controller) { - for (event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.controller.off(event, this.listeners[event]); + for (event in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.off(event, this.eventListeners[event]); } } } @@ -128,9 +131,9 @@ ItemSet.prototype.setController = function setController (controller) { // register new event listeners if (this.controller) { - for (event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - this.controller.on(event, this.listeners[event]); + for (event in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.on(event, this.eventListeners[event]); } } } @@ -251,6 +254,7 @@ ItemSet.prototype.repaint = function repaint() { if (!frame) { frame = document.createElement('div'); frame.className = 'itemset'; + frame['timeline-itemset'] = this; var className = options.className; if (className) { @@ -667,25 +671,21 @@ 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); -console.log('_onDragStart', event) + var itemSet = ItemSet.itemSetFromTarget(event), + item = ItemSet.itemFromTarget(event), + me = this; + 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; + this.touchParams.items = this.getSelection().map(function (id) { + return me.items[id]; }); - console.log('_onDragStart', touchParams) + event.stopPropagation(); } }; @@ -695,16 +695,21 @@ console.log('_onDragStart', event) * @param {Event} event * @private */ -// TODO: move this function to ItemSet ItemSet.prototype._onDrag = function (event) { - if (touchParams.items) { + if (this.touchParams.items) { var deltaX = event.gesture.deltaX; - touchParams.items.forEach(function (item, i) { - item.left = touchParams.itemsLeft[i] + deltaX; - item.reposition(); + // adjust the offset of the items being dragged + this.touchParams.items.forEach(function (item) { + item.setOffset(deltaX); }); + // TODO: implement snapping to nice dates + + // TODO: implement dragging from one group to another + + this.requestReflow(); + event.stopPropagation(); } }; @@ -714,12 +719,38 @@ ItemSet.prototype._onDrag = function (event) { * @param {Event} event * @private */ -// TODO: move this function to ItemSet ItemSet.prototype._onDragEnd = function (event) { - if (touchParams.items) { - // actually apply the new locations + if (this.touchParams.items) { + var deltaX = event.gesture.deltaX, + scale = this.conversion.scale; - touchParams.items = null; + // prepare a changeset for the changed items + var changes = this.touchParams.items.map(function (item) { + item.setOffset(0); + + var change = { + id: item.id + }; + + if ('start' in item.data) { + change.start = new Date(item.data.start.valueOf() + deltaX / scale); + } + if ('end' in item.data) { + change.end = new Date(item.data.end.valueOf() + deltaX / scale); + } + + return change; + }); + this.touchParams.items = null; + + // find the root DataSet from our DataSet/DataView + var data = this.itemsData; + while (data instanceof DataView) { + data = data.data; + } + + // apply the changes to the data + data.update(changes); event.stopPropagation(); } @@ -729,10 +760,9 @@ ItemSet.prototype._onDragEnd = function (event) { * 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 - * @private + * @return {Item | null} item */ -ItemSet.prototype._itemFromTarget = function _itemFromTarget (event) { +ItemSet.itemFromTarget = function itemFromTarget (event) { var target = event.target; while (target) { if (target.hasOwnProperty('timeline-item')) { @@ -742,4 +772,22 @@ ItemSet.prototype._itemFromTarget = function _itemFromTarget (event) { } return null; -}; \ No newline at end of file +}; + +/** + * 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 + */ +ItemSet.itemSetFromTarget = function itemSetFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; + } + target = target.parentNode; + } + + return null; +}; diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index a0d68354..cdb69c76 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -20,6 +20,7 @@ function Item (parent, data, options, defaultOptions) { this.left = 0; this.width = 0; this.height = 0; + this.offset = 0; } /** @@ -72,6 +73,14 @@ Item.prototype.reflow = function reflow() { return false; }; +/** + * Give the item a display offset in pixels + * @param {Number} offset Offset on screen in pixels + */ +Item.prototype.setOffset = function setOffset(offset) { + this.offset = offset; +}; + /** * Return the items width * @return {Number} width diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index cde86f5a..8ea236c9 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -187,7 +187,7 @@ ItemBox.prototype.reflow = function reflow() { update = util.updateProperty; props = this.props; options = this.options; - start = this.parent.toScreen(this.data.start); + start = this.parent.toScreen(this.data.start) + this.offset; align = options.align || this.defaultOptions.align; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; orientation = options.orientation || this.defaultOptions.orientation; diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index 1a78a92b..2d5124e1 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -157,7 +157,7 @@ ItemPoint.prototype.reflow = function reflow() { options = this.options; orientation = options.orientation || this.defaultOptions.orientation; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - start = this.parent.toScreen(this.data.start); + start = this.parent.toScreen(this.data.start) + this.offset; changed += update(this, 'width', dom.point.offsetWidth); changed += update(this, 'height', dom.point.offsetHeight); diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index a2feec99..bb7b8386 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -157,8 +157,8 @@ ItemRange.prototype.reflow = function reflow() { props = this.props; options = this.options; parent = this.parent; - start = parent.toScreen(this.data.start); - end = parent.toScreen(this.data.end); + start = parent.toScreen(this.data.start) + this.offset; + end = parent.toScreen(this.data.end) + this.offset; update = util.updateProperty; box = dom.box; parentWidth = parent.width; From ad39a54d7d82d443fc98ae1d9b24c4717d8999b8 Mon Sep 17 00:00:00 2001 From: josdejong Date: Tue, 18 Feb 2014 16:58:25 +0100 Subject: [PATCH 4/5] Fixed filename case errors --- Jakefile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jakefile.js b/Jakefile.js index a36a934c..1022f161 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -83,8 +83,8 @@ task('build', {async: true}, function () { './src/graph/Groups.js', './src/graph/Images.js', './src/graph/graphMixins/physics/PhysicsMixin.js', - './src/graph/graphMixins/physics/BarnesHut.js', - './src/graph/graphMixins/physics/Repulsion.js', + './src/graph/graphMixins/physics/barnesHut.js', + './src/graph/graphMixins/physics/repulsion.js', './src/graph/graphMixins/ManipulationMixin.js', './src/graph/graphMixins/SectorsMixin.js', './src/graph/graphMixins/ClusterMixin.js', From d48fd1ed692af47f612e3d8ad936a6a6e1bcff2f Mon Sep 17 00:00:00 2001 From: josdejong Date: Tue, 18 Feb 2014 17:09:42 +0100 Subject: [PATCH 5/5] Fixed filename case errors --- Jakefile.js | 4 ++-- examples/graph/20_navigation.html | 8 ++++---- .../physics/{barnesHut.js => BarnesHut.js} | 0 .../physics/{repulsion.js => Repulsion.js} | 0 .../downarrow.png => src/graph/img/downArrow.png | Bin src/graph/img/downarrow.png | Bin 4460 -> 0 bytes .../leftarrow.png => src/graph/img/leftArrow.png | Bin src/graph/img/leftarrow.png | Bin 4531 -> 0 bytes .../graph/img/rightArrow.png | Bin src/graph/img/rightarrow.png | Bin 4514 -> 0 bytes .../img/uparrow.png => src/graph/img/upArrow.png | Bin src/graph/img/uparrow.png | Bin 4461 -> 0 bytes 12 files changed, 6 insertions(+), 6 deletions(-) rename src/graph/graphMixins/physics/{barnesHut.js => BarnesHut.js} (100%) rename src/graph/graphMixins/physics/{repulsion.js => Repulsion.js} (100%) rename dist/img/downarrow.png => src/graph/img/downArrow.png (100%) delete mode 100644 src/graph/img/downarrow.png rename dist/img/leftarrow.png => src/graph/img/leftArrow.png (100%) delete mode 100644 src/graph/img/leftarrow.png rename dist/img/rightarrow.png => src/graph/img/rightArrow.png (100%) delete mode 100644 src/graph/img/rightarrow.png rename dist/img/uparrow.png => src/graph/img/upArrow.png (100%) delete mode 100644 src/graph/img/uparrow.png diff --git a/Jakefile.js b/Jakefile.js index 1022f161..a36a934c 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -83,8 +83,8 @@ task('build', {async: true}, function () { './src/graph/Groups.js', './src/graph/Images.js', './src/graph/graphMixins/physics/PhysicsMixin.js', - './src/graph/graphMixins/physics/barnesHut.js', - './src/graph/graphMixins/physics/repulsion.js', + './src/graph/graphMixins/physics/BarnesHut.js', + './src/graph/graphMixins/physics/Repulsion.js', './src/graph/graphMixins/ManipulationMixin.js', './src/graph/graphMixins/SectorsMixin.js', './src/graph/graphMixins/ClusterMixin.js', diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html index 8bc2006a..742d85eb 100644 --- a/examples/graph/20_navigation.html +++ b/examples/graph/20_navigation.html @@ -130,10 +130,10 @@ - - - - + + + + diff --git a/src/graph/graphMixins/physics/barnesHut.js b/src/graph/graphMixins/physics/BarnesHut.js similarity index 100% rename from src/graph/graphMixins/physics/barnesHut.js rename to src/graph/graphMixins/physics/BarnesHut.js diff --git a/src/graph/graphMixins/physics/repulsion.js b/src/graph/graphMixins/physics/Repulsion.js similarity index 100% rename from src/graph/graphMixins/physics/repulsion.js rename to src/graph/graphMixins/physics/Repulsion.js diff --git a/dist/img/downarrow.png b/src/graph/img/downArrow.png similarity index 100% rename from dist/img/downarrow.png rename to src/graph/img/downArrow.png diff --git a/src/graph/img/downarrow.png b/src/graph/img/downarrow.png deleted file mode 100644 index e77d5e6d4157b12a5b2fa08a9fc138f0ab9eb4e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4460 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�EM>-Bgb&Rym2{we-b7Oawc(c4k4MzLw~<#m(JZ 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 Re)#|Z002ovPDHLkV1iMdkGuc? diff --git a/dist/img/rightarrow.png b/src/graph/img/rightArrow.png similarity index 100% rename from dist/img/rightarrow.png rename to src/graph/img/rightArrow.png diff --git a/src/graph/img/rightarrow.png b/src/graph/img/rightarrow.png deleted file mode 100644 index c3a209d8b0a58355305aae03b5520a1e070d8eab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4514 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
Icons: