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/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/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 e77d5e6d..00000000 Binary files a/src/graph/img/downarrow.png and /dev/null differ diff --git a/dist/img/leftarrow.png b/src/graph/img/leftArrow.png similarity index 100% rename from dist/img/leftarrow.png rename to src/graph/img/leftArrow.png diff --git a/src/graph/img/leftarrow.png b/src/graph/img/leftarrow.png deleted file mode 100644 index 3823536e..00000000 Binary files a/src/graph/img/leftarrow.png and /dev/null differ 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 c3a209d8..00000000 Binary files a/src/graph/img/rightarrow.png and /dev/null differ diff --git a/dist/img/uparrow.png b/src/graph/img/upArrow.png similarity index 100% rename from dist/img/uparrow.png rename to src/graph/img/upArrow.png diff --git a/src/graph/img/uparrow.png b/src/graph/img/uparrow.png deleted file mode 100644 index 8aedced7..00000000 Binary files a/src/graph/img/uparrow.png and /dev/null differ 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/Controller.js b/src/timeline/Controller.js index 185d341b..ebb3e494 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]; } }; @@ -54,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(); @@ -74,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 70c9083d..0bea4d63 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -48,42 +48,48 @@ 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); }); + + // ignore dragging when holding + controller.on('hold', function (event) { + me._onHold(); + }); } else if (event == 'zoom') { // mouse wheel 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) { - me._onTouch(); + controller.on('touch', function (event) { + me._onTouch(event); }); - component.on('pinch', function (event) { + controller.on('pinch', function (event) { me._onPinch(event, component, direction); }); } @@ -311,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; @@ -334,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), @@ -356,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'; @@ -417,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; }; /** @@ -435,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 38c19fae..664bc20c 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.controller.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.controller.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 - this.range.subscribe(this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); + // TODO: enable moving again + 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); - 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 item = ItemSet.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -428,9 +395,10 @@ 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); + item = ItemSet.itemFromTarget(event); if (!item) { // do nothing... @@ -449,28 +417,9 @@ Timeline.prototype._onMultiSelectItem = function (event) { } this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); 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 - */ -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/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..e44d4c87 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.eventListeners = { + 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 = { @@ -35,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) { @@ -59,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 } @@ -99,6 +109,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.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.off(event, this.eventListeners[event]); + } + } + } + + this.controller = controller || null; + + // register new event listeners + if (this.controller) { + for (event in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.on(event, this.eventListeners[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. @@ -195,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) { @@ -610,3 +670,124 @@ ItemSet.prototype.toScreen = function toScreen(time) { var conversion = this.conversion; return (time.valueOf() - conversion.offset) * conversion.scale; }; + +/** + * Start dragging the selected events + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragStart = function (event) { + var itemSet = ItemSet.itemSetFromTarget(event), + item = ItemSet.itemFromTarget(event), + me = this; + + if (item && item.selected) { + this.touchParams.items = this.getSelection().map(function (id) { + return me.items[id]; + }); + + event.stopPropagation(); + } +}; + +/** + * Drag selected items + * @param {Event} event + * @private + */ +ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.items) { + var deltaX = event.gesture.deltaX; + + // 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(); + } +}; + +/** + * End of dragging selected items + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.items) { + var deltaX = event.gesture.deltaX, + scale = this.conversion.scale; + + // 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(); + } +}; + +/** + * 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.itemFromTarget = function itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + + 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 + */ +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/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..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,10 +73,18 @@ 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 {Integer} width + * @return {Number} width */ Item.prototype.getWidth = function getWidth() { return this.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;
Icons: