From 27f9b20a282e2b1e335df8f4ca8219449e764c2f Mon Sep 17 00:00:00 2001 From: josdejong Date: Wed, 2 Apr 2014 11:20:07 +0200 Subject: [PATCH] Started reworking component/panel architecture --- Jakefile.js | 1 - src/module/exports.js | 1 - src/timeline/Controller.js | 184 -------------------------- src/timeline/Range.js | 161 ++++++++++------------ src/timeline/Timeline.js | 167 ++++++++++++----------- src/timeline/component/Component.js | 60 +-------- src/timeline/component/CurrentTime.js | 15 +-- src/timeline/component/CustomTime.js | 13 +- src/timeline/component/GroupSet.js | 5 - src/timeline/component/ItemSet.js | 4 - src/timeline/component/Panel.js | 62 +++++++-- src/timeline/component/RootPanel.js | 148 +++++++-------------- src/timeline/component/TimeAxis.js | 7 +- test/timeline.html | 2 + 14 files changed, 271 insertions(+), 559 deletions(-) delete mode 100644 src/timeline/Controller.js diff --git a/Jakefile.js b/Jakefile.js index 671fb6e1..c85d1b40 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -67,7 +67,6 @@ task('build', {async: true}, function () { './src/timeline/TimeStep.js', './src/timeline/Stack.js', './src/timeline/Range.js', - './src/timeline/Controller.js', './src/timeline/component/Component.js', './src/timeline/component/Panel.js', './src/timeline/component/RootPanel.js', diff --git a/src/module/exports.js b/src/module/exports.js index b53f3e5e..a2f25d6e 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -4,7 +4,6 @@ var vis = { util: util, - Controller: Controller, DataSet: DataSet, DataView: DataView, Range: Range, diff --git a/src/timeline/Controller.js b/src/timeline/Controller.js deleted file mode 100644 index 3204851d..00000000 --- a/src/timeline/Controller.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @constructor Controller - * - * A Controller controls the reflows and repaints of all components, - * and is used as an event bus for all components. - */ -function Controller () { - var me = this; - - this.id = util.randomUUID(); - this.components = {}; - - /** - * Listen for a 'request-reflow' event. The controller will schedule a reflow - * @param {Boolean} [force] If true, an immediate reflow is forced. Default - * is false. - */ - var reflowTimer = null; - this.on('request-reflow', function requestReflow(force) { - if (force) { - me.reflow(); - } - else { - if (!reflowTimer) { - reflowTimer = requestAnimationFrame(function () { - reflowTimer = null; - me.reflow(); - }); - } - } - }); - - /** - * Request a repaint. The controller will schedule a repaint - * @param {Boolean} [force] If true, an immediate repaint is forced. Default - * is false. - */ - var repaintTimer = null; - this.on('request-repaint', function requestRepaint(force) { - if (force) { - me.repaint(); - } - else { - if (!repaintTimer) { - repaintTimer = requestAnimationFrame(function () { - repaintTimer = null; - me.repaint(); - }); - } - } - }); -} - -// Extend controller with Emitter mixin -Emitter(Controller.prototype); - -/** - * Add a component to the controller - * @param {Component} component - */ -Controller.prototype.add = function add(component) { - // validate the component - if (component.id == undefined) { - throw new Error('Component has no field id'); - } - if (!(component instanceof Component) && !(component instanceof Controller)) { - throw new TypeError('Component must be an instance of ' + - 'prototype Component or Controller'); - } - - // add the component - component.setController(this); - this.components[component.id] = component; -}; - -/** - * Remove a component from the controller - * @param {Component | String} component - */ -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) { - 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]; - } -}; - -/** - * Repaint all components - */ -Controller.prototype.repaint = function repaint() { - var changed = false; - - // cancel any running repaint request - if (this.repaintTimer) { - clearTimeout(this.repaintTimer); - this.repaintTimer = undefined; - } - - var done = {}; - - function repaint(component, id) { - if (!(id in done)) { - // first repaint the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - repaint(dep, dep.id); - }); - } - if (component.parent) { - repaint(component.parent, component.parent.id); - } - - // repaint the component itself and mark as done - changed = component.repaint() || changed; - done[id] = true; - } - } - - util.forEach(this.components, repaint); - - this.emit('repaint'); - - // immediately reflow when needed - if (changed) { - this.reflow(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop -}; - -/** - * Reflow all components - */ -Controller.prototype.reflow = function reflow() { - var resized = false; - - // cancel any running repaint request - if (this.reflowTimer) { - clearTimeout(this.reflowTimer); - this.reflowTimer = undefined; - } - - var done = {}; - - function reflow(component, id) { - if (!(id in done)) { - // first reflow the components on which this component is dependent - if (component.depends) { - component.depends.forEach(function (dep) { - reflow(dep, dep.id); - }); - } - if (component.parent) { - reflow(component.parent, component.parent.id); - } - - // reflow the component itself and mark as done - resized = component.reflow() || resized; - done[id] = true; - } - } - - util.forEach(this.components, reflow); - - this.emit('reflow'); - - // immediately repaint when needed - //if (resized) { - if (true) { // TODO: fix this loop - this.repaint(); - } - // TODO: limit the number of nested reflows/repaints, prevent loop -}; diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 328eb2c4..c87de883 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -3,22 +3,58 @@ * A Range controls a numeric range with a start and end value. * The Range adjusts the range based on mouse events or programmatic changes, * and triggers events when the range is changing or has been changed. + * @param {Component} parent + * @param {Emitter} emitter * @param {Object} [options] See description at Range.setOptions - * @extends Controller */ -function Range(options) { +function Range(parent, emitter, options) { this.id = util.randomUUID(); this.start = null; // Number this.end = null; // Number + this.parent = parent; + this.emitter = emitter; this.options = options || {}; + // drag start listener + var me = this; + emitter.on('dragstart', function (event) { + me._onDragStart(event, parent); + }); + + // drag listener + emitter.on('drag', function (event) { + me._onDrag(event, parent); + }); + + // drag end listener + emitter.on('dragend', function (event) { + me._onDragEnd(event, parent); + }); + + // ignore dragging when holding + emitter.on('hold', function (event) { + me._onHold(); + }); + + // mouse wheel + function mousewheel (event) { + me._onMouseWheel(event, parent); + } + emitter.on('mousewheel', mousewheel); + emitter.on('DOMMouseScroll', mousewheel); // For FF + + // pinch + emitter.on('touch', function (event) { + me._onTouch(event); + }); + emitter.on('pinch', function (event) { + me._onPinch(event, parent); + }); + this.setOptions(options); } -// extend the Range prototype with an event emitter mixin -Emitter(Range.prototype); - /** * Set options for the range controller * @param {Object} options Available options: @@ -49,59 +85,6 @@ function validateDirection (direction) { } } -/** - * Add listeners for mouse and touch events to the 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 (controller, component, event, direction) { - var me = this; - - if (event == 'move') { - // drag start listener - controller.on('dragstart', function (event) { - me._onDragStart(event, component); - }); - - // drag listener - controller.on('drag', function (event) { - me._onDrag(event, component, direction); - }); - - // drag end listener - 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); - } - controller.on('mousewheel', mousewheel); - controller.on('DOMMouseScroll', mousewheel); // For FF - - // pinch - controller.on('touch', function (event) { - me._onTouch(event); - }); - controller.on('pinch', function (event) { - me._onPinch(event, component, direction); - }); - } - else { - throw new TypeError('Unknown event "' + event + '". ' + - 'Choose "move" or "zoom".'); - } -}; - /** * Set a new start and end range * @param {Number} [start] @@ -114,8 +97,8 @@ Range.prototype.setRange = function(start, end) { start: this.start, end: this.end }; - this.emit('rangechange', params); - this.emit('rangechanged', params); + this.emitter.emit('rangechange', params); + this.emitter.emit('rangechanged', params); } }; @@ -280,10 +263,9 @@ var touchParams = {}; /** * Start dragging horizontally or vertically * @param {Event} event - * @param {Object} component * @private */ -Range.prototype._onDragStart = function(event, component) { +Range.prototype._onDragStart = function(event) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (touchParams.ignore) return; @@ -293,7 +275,7 @@ Range.prototype._onDragStart = function(event, component) { touchParams.start = this.start; touchParams.end = this.end; - var frame = component.frame; + var frame = this.parent.frame; if (frame) { frame.style.cursor = 'move'; } @@ -302,11 +284,10 @@ Range.prototype._onDragStart = function(event, component) { /** * Perform dragging operating. * @param {Event} event - * @param {Component} component - * @param {String} direction 'horizontal' or 'vertical' * @private */ -Range.prototype._onDrag = function (event, component, direction) { +Range.prototype._onDrag = function (event) { + var direction = this.options.direction; validateDirection(direction); // TODO: reckon with option movable @@ -318,12 +299,12 @@ Range.prototype._onDrag = function (event, component, direction) { var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, interval = (touchParams.end - touchParams.start), - width = (direction == 'horizontal') ? component.width : component.height, + width = (direction == 'horizontal') ? this.parent.width : this.parent.height, diffRange = -delta / width * interval; this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); - this.emit('rangechange', { + this.emitter.emit('rangechange', { start: this.start, end: this.end }); @@ -332,22 +313,21 @@ Range.prototype._onDrag = function (event, component, direction) { /** * Stop dragging operating. * @param {event} event - * @param {Component} component * @private */ -Range.prototype._onDragEnd = function (event, component) { +Range.prototype._onDragEnd = function (event) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen if (touchParams.ignore) return; // TODO: reckon with option movable - if (component.frame) { - component.frame.style.cursor = 'auto'; + if (this.parent.frame) { + this.parent.frame.style.cursor = 'auto'; } // fire a rangechanged event - this.emit('rangechanged', { + this.emitter.emit('rangechanged', { start: this.start, end: this.end }); @@ -357,13 +337,9 @@ Range.prototype._onDragEnd = function (event, component) { * Event handler for mouse wheel event, used to zoom * Code from http://adomas.org/javascript-mouse-wheel/ * @param {Event} event - * @param {Component} component - * @param {String} direction 'horizontal' or 'vertical' * @private */ -Range.prototype._onMouseWheel = function(event, component, direction) { - validateDirection(direction); - +Range.prototype._onMouseWheel = function(event) { // TODO: reckon with option zoomable // retrieve delta @@ -394,8 +370,8 @@ Range.prototype._onMouseWheel = function(event, component, direction) { // calculate center, the date to zoom around var gesture = util.fakeGesture(this, event), - pointer = getPointer(gesture.center, component.frame), - pointerDate = this._pointerToDate(component, direction, pointer); + pointer = getPointer(gesture.center, this.parent.frame), + pointerDate = this._pointerToDate(pointer); this.zoom(scale, pointerDate); } @@ -434,24 +410,23 @@ Range.prototype._onHold = function () { /** * Handle pinch event * @param {Event} event - * @param {Component} component - * @param {String} direction 'horizontal' or 'vertical' * @private */ -Range.prototype._onPinch = function (event, component, direction) { +Range.prototype._onPinch = function (event) { + var direction = this.options.direction; touchParams.ignore = true; // TODO: reckon with option zoomable if (event.gesture.touches.length > 1) { if (!touchParams.center) { - touchParams.center = getPointer(event.gesture.center, component.frame); + touchParams.center = getPointer(event.gesture.center, this.parent.frame); } var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(component, direction, touchParams.center), - center = getPointer(event.gesture.center, component.frame), - date = this._pointerToDate(component, direction, center), + initDate = this._pointerToDate(touchParams.center), + center = getPointer(event.gesture.center, this.parent.frame), + date = this._pointerToDate(this.parent, center), delta = date - initDate; // TODO: utilize delta // calculate new start and end @@ -465,21 +440,23 @@ Range.prototype._onPinch = function (event, component, direction) { /** * Helper function to calculate the center date for zooming - * @param {Component} component * @param {{x: Number, y: Number}} pointer - * @param {String} direction 'horizontal' or 'vertical' * @return {number} date * @private */ -Range.prototype._pointerToDate = function (component, direction, pointer) { +Range.prototype._pointerToDate = function (pointer) { var conversion; + var direction = this.options.direction; + + validateDirection(direction); + if (direction == 'horizontal') { - var width = component.width; + var width = this.parent.width; conversion = this.conversion(width); return pointer.x / conversion.scale + conversion.offset; } else { - var height = component.height; + var height = this.parent.height; conversion = this.conversion(height); return pointer.y / conversion.scale + conversion.offset; } diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index fde92a9b..5844bcae 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -6,10 +6,14 @@ * @constructor */ function Timeline (container, items, options) { + // validate arguments + if (!container) throw new Error('No container element provided'); + var me = this; var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); this.options = { orientation: 'bottom', + direction: 'horizontal', // 'horizontal' or 'vertical' autoResize: true, editable: false, selectable: true, @@ -29,7 +33,6 @@ function Timeline (container, items, options) { type: 'box', align: 'center', - orientation: 'bottom', margin: { axis: 20, item: 10 @@ -50,106 +53,110 @@ function Timeline (container, items, options) { } }; - // controller - this.controller = new Controller(); + // event bus + this.emitter = new Emitter(); // root panel - if (!container) { - throw new Error('No container element provided'); - } var rootOptions = Object.create(this.options); rootOptions.height = function () { - // TODO: change to height if (me.options.height) { // fixed height return me.options.height; } else { // auto height - return (me.timeaxis.height + me.content.height) + 'px'; + // return (me.timeaxis.height + me.content.height) + 'px'; + // TODO: return the sum of the height of the childs } }; - this.rootPanel = new RootPanel(container, rootOptions); - this.controller.add(this.rootPanel); + this.rootPanel = new RootPanel(container, this.emitter, rootOptions); // single select (or unselect) when tapping an item - this.controller.on('tap', this._onSelectItem.bind(this)); + this.emitter.on('tap', this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click - this.controller.on('hold', this._onMultiSelectItem.bind(this)); + this.emitter.on('hold', this._onMultiSelectItem.bind(this)); // add item on doubletap - this.controller.on('doubletap', this._onAddItem.bind(this)); + this.emitter.on('doubletap', this._onAddItem.bind(this)); - // item panel - var itemOptions = Object.create(this.options); - itemOptions.left = function () { - return me.labelPanel.width; - }; - itemOptions.width = function () { - return me.rootPanel.width - me.labelPanel.width; - }; - itemOptions.top = null; - itemOptions.height = null; - this.itemPanel = new Panel(this.rootPanel, [], itemOptions); - this.controller.add(this.itemPanel); + // range + // TODO: move range inside rootPanel? + var rangeOptions = Object.create(this.options); + this.range = new Range(this.rootPanel, this.emitter, rangeOptions); + this.range.setRange( + now.clone().add('days', -3).valueOf(), + now.clone().add('days', 4).valueOf() + ); // label panel var labelOptions = Object.create(this.options); - labelOptions.top = null; - labelOptions.left = null; - labelOptions.height = null; + labelOptions.top = '0'; + labelOptions.bottom = null; + labelOptions.left = '0'; + labelOptions.right = null; + labelOptions.height = '100%'; labelOptions.width = function () { + /* TODO: dynamically determine the width of the label panel if (me.content && typeof me.content.getLabelsWidth === 'function') { return me.content.getLabelsWidth(); } else { return 0; } + */ + return 200; }; - this.labelPanel = new Panel(this.rootPanel, [], labelOptions); - this.controller.add(this.labelPanel); - - // range - var rangeOptions = Object.create(this.options); - this.range = new Range(rangeOptions); - this.range.setRange( - now.clone().add('days', -3).valueOf(), - now.clone().add('days', 4).valueOf() - ); - - 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.emit('rangechange', properties); - me.controller.emit('request-reflow', force); - }); - this.range.on('rangechanged', function (properties) { - var force = true; - me.controller.emit('rangechanged', properties); - me.controller.emit('request-reflow', force); - }); - - // time axis - var timeaxisOptions = Object.create(rootOptions); - timeaxisOptions.range = this.range; - timeaxisOptions.left = null; - timeaxisOptions.top = null; - timeaxisOptions.width = '100%'; - timeaxisOptions.height = null; - this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); - this.timeaxis.setRange(this.range); - this.controller.add(this.timeaxis); - this.options.snap = this.timeaxis.snap.bind(this.timeaxis); - + this.labelPanel = new Panel(labelOptions); + this.rootPanel.appendChild(this.labelPanel); + + // main panel (contains time axis and itemsets) + var mainOptions = Object.create(this.options); + mainOptions.top = '0'; + mainOptions.bottom = null; + mainOptions.left = null; + mainOptions.right = '0'; + mainOptions.height = '100%'; + mainOptions.width = function () { + return me.rootPanel.width - me.labelPanel.width; + }; + this.mainPanel = new Panel(mainOptions); + this.rootPanel.appendChild(this.mainPanel); + + // content panel (contains itemset(s)) + var contentOptions = Object.create(this.options); + contentOptions.top = '0'; + contentOptions.bottom = null; + contentOptions.left = '0'; + contentOptions.right = null; + contentOptions.height = function () { + return me.mainPanel.height - me.timeAxis.height; + }; + contentOptions.width = null; + this.contentPanel = new Panel(contentOptions); + this.mainPanel.appendChild(this.contentPanel); + + // panel with time axis + var timeAxisOptions = Object.create(rootOptions); + timeAxisOptions.range = this.range; + timeAxisOptions.left = null; + timeAxisOptions.top = null; + timeAxisOptions.width = null; + timeAxisOptions.height = null; // height is determined by the + this.timeAxis = new TimeAxis(timeAxisOptions); + this.timeAxis.setRange(this.range); + this.options.snap = this.timeAxis.snap.bind(this.timeAxis); + this.mainPanel.appendChild(this.timeAxis); + + /* TODO // current time bar - this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); - this.controller.add(this.currenttime); + this.currenttime = new CurrentTime(rootOptions); + this.mainPanel.appendChild(this.currenttime); // custom time bar - this.customtime = new CustomTime(this.timeaxis, [], rootOptions); - this.controller.add(this.customtime); + this.customtime = new CustomTime(rootOptions); + this.mainPanel.appendChild(this.customtime); + */ // create groupset this.setGroups(null); @@ -175,7 +182,7 @@ function Timeline (container, items, options) { * @param {function} callback */ Timeline.prototype.on = function on (event, callback) { - this.controller.on(event, callback); + this.emitter.on(event, callback); }; /** @@ -184,7 +191,7 @@ Timeline.prototype.on = function on (event, callback) { * @param {function} callback */ Timeline.prototype.off = function off (event, callback) { - this.controller.off(event, callback); + this.emitter.off(event, callback); }; /** @@ -217,8 +224,8 @@ Timeline.prototype.setOptions = function (options) { }).bind(this); ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback); - this.controller.reflow(); - this.controller.repaint(); + //this.controller.reflow(); // TODO: remove + this.rootPanel.repaint(); }; /** @@ -272,7 +279,9 @@ Timeline.prototype.setItems = function(items) { // set items this.itemsData = newDataSet; + /* TODO this.content.setItems(newDataSet); + */ if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { // apply the data range as range @@ -326,7 +335,7 @@ Timeline.prototype.setGroups = function(groups) { if (this.content.setGroups) { this.content.setGroups(); // disconnect from groups } - this.controller.remove(this.content); + //this.controller.remove(this.content); // TODO: cleanup } // create new content set @@ -367,6 +376,7 @@ Timeline.prototype.setGroups = function(groups) { } }); + /* TODO this.content = new Type(this.itemPanel, [this.timeaxis], options); if (this.content.setRange) { this.content.setRange(this.range); @@ -377,7 +387,7 @@ Timeline.prototype.setGroups = function(groups) { if (this.content.setGroups) { this.content.setGroups(this.groupsData); } - this.controller.add(this.content); + */ } }; @@ -482,7 +492,7 @@ Timeline.prototype._onSelectItem = function (event) { var selection = item ? [item.id] : []; this.setSelection(selection); - this.controller.emit('select', { + this.emitter.emit('select', { items: this.getSelection() }); @@ -535,10 +545,11 @@ Timeline.prototype._onAddItem = function (event) { me.itemsData.add(newItem); // select the created item after it is repainted - me.controller.once('repaint', function () { + // FIXME: just repaint the whole thing, not via an emitted event + me.emitter.once('repaint', function () { me.setSelection([id]); - me.controller.emit('select', { + me.emitter.emit('select', { items: me.getSelection() }); }.bind(me)); @@ -573,7 +584,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { } this.setSelection(selection); - this.controller.emit('select', { + this.emitter.emit('select', { items: this.getSelection() }); diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index b51de873..412b76cd 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -4,8 +4,7 @@ function Component () { this.id = null; this.parent = null; - this.depends = null; - this.controller = null; + this.childs = null; this.options = null; this.frame = null; // main DOM element @@ -29,10 +28,7 @@ Component.prototype.setOptions = function setOptions(options) { if (options) { util.extend(this.options, options); - if (this.controller) { - this.requestRepaint(); - this.requestReflow(); - } + this.repaint(); } }; @@ -54,23 +50,6 @@ 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 @@ -98,15 +77,6 @@ Component.prototype.repaint = function repaint() { // should be implemented by the component }; -/** - * Reflow the component - * @return {Boolean} resized - */ -Component.prototype.reflow = function reflow() { - // should be implemented by the component - return false; -}; - /** * Hide the component from the DOM * @return {Boolean} changed @@ -134,29 +104,3 @@ Component.prototype.show = function show() { return false; } }; - -/** - * Request a repaint. The controller will schedule a repaint - */ -Component.prototype.requestRepaint = function requestRepaint() { - if (this.controller) { - this.controller.emit('request-repaint'); - } - else { - throw new Error('Cannot request a repaint: no controller configured'); - // TODO: just do a repaint when no parent is configured? - } -}; - -/** - * Request a reflow. The controller will schedule a reflow - */ -Component.prototype.requestReflow = function requestReflow() { - if (this.controller) { - this.controller.emit('request-reflow'); - } - else { - throw new Error('Cannot request a reflow: no controller configured'); - // TODO: just do a reflow when no parent is configured? - } -}; diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index 4a1ff151..0e4b7ce3 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -1,18 +1,13 @@ /** * A current time bar - * @param {Component} parent - * @param {Component[]} [depends] Components on which this components depends - * (except for the parent) * @param {Object} [options] Available parameters: * {Boolean} [showCurrentTime] * @constructor CurrentTime * @extends Component */ -function CurrentTime (parent, depends, options) { +function CurrentTime (options) { this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; this.options = options || {}; this.defaultOptions = { @@ -39,13 +34,13 @@ CurrentTime.prototype.getContainer = function () { */ CurrentTime.prototype.repaint = function () { var bar = this.frame, - parent = this.parent, - parentContainer = parent.parent.getContainer(); + parent = this.parent; if (!parent) { throw new Error('Cannot repaint bar: no parent attached'); } + var parentContainer = parent.parent.getContainer(); // FIXME: this is weird if (!parentContainer) { throw new Error('Cannot repaint bar: parent has no container element'); } @@ -70,10 +65,6 @@ CurrentTime.prototype.repaint = function () { this.frame = bar; } - if (!parent.conversion) { - parent._updateConversion(); - } - var now = new Date(); var x = parent.toScreen(now); diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js index 15c3bc28..313f12ec 100644 --- a/src/timeline/component/CustomTime.js +++ b/src/timeline/component/CustomTime.js @@ -1,18 +1,13 @@ /** * A custom time bar - * @param {Component} parent - * @param {Component[]} [depends] Components on which this components depends - * (except for the parent) * @param {Object} [options] Available parameters: * {Boolean} [showCustomTime] * @constructor CustomTime * @extends Component */ -function CustomTime (parent, depends, options) { +function CustomTime (options) { this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; this.options = options || {}; this.defaultOptions = { @@ -50,7 +45,7 @@ CustomTime.prototype.repaint = function () { throw new Error('Cannot repaint bar: no parent attached'); } - var parentContainer = parent.parent.getContainer(); + var parentContainer = parent.parent.getContainer(); // FIXME: this is weird if (!parentContainer) { throw new Error('Cannot repaint bar: parent has no container element'); } @@ -92,10 +87,6 @@ CustomTime.prototype.repaint = function () { this.hammer.on('dragend', this._onDragEnd.bind(this)); } - if (!parent.conversion) { - parent._updateConversion(); - } - var x = parent.toScreen(this.customTime); bar.style.left = x + 'px'; diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index ac114884..01549523 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -500,11 +500,6 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) { ids.forEach(function (id) { queue[id] = action; }); - - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } }; /** diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 0016cdc3..48b259a9 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -177,10 +177,6 @@ ItemSet.prototype.setSelection = function setSelection(ids) { item.select(); } } - - if (this.controller) { - this.requestRepaint(); - } } }; diff --git a/src/timeline/component/Panel.js b/src/timeline/component/Panel.js index 4c60169d..b58c36f4 100644 --- a/src/timeline/component/Panel.js +++ b/src/timeline/component/Panel.js @@ -1,8 +1,5 @@ /** * A panel can contain components - * @param {Component} [parent] - * @param {Component[]} [depends] Components on which this components depends - * (except for the parent) * @param {Object} [options] Available parameters: * {String | Number | function} [left] * {String | Number | function} [top] @@ -12,10 +9,10 @@ * @constructor Panel * @extends Component */ -function Panel(parent, depends, options) { +function Panel(options) { this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; + this.parent = null; + this.childs = []; this.options = options || {}; } @@ -42,6 +39,40 @@ Panel.prototype.getContainer = function () { return this.frame; }; +/** + * Append a child to the panel + * @param {Component} child + */ +Panel.prototype.appendChild = function (child) { + this.childs.push(child); + child.parent = this; +}; + +/** + * Insert a child to the panel + * @param {Component} child + * @param {Component} beforeChild + */ +Panel.prototype.insertBefore = function (child, beforeChild) { + var index = this.childs.indexOf(beforeChild); + if (index != -1) { + this.childs.splice(index, 0, child); + child.parent = this; + } +}; + +/** + * Remove a child from the panel + * @param {Component} child + */ +Panel.prototype.removeChild = function (child) { + var index = this.childs.indexOf(child); + if (index != -1) { + this.childs.splice(index, 1); + child.parent = null; + } +}; + /** * Repaint the component */ @@ -65,18 +96,33 @@ Panel.prototype.repaint = function () { // update className frame.className = 'vpanel' + (options.className ? (' ' + asSize(options.className)) : ''); + // repaint the child components + this._repaintChilds(); + // update frame size this._updateSize(); }; +/** + * Repaint all childs of the panel + * @private + */ +Panel.prototype._repaintChilds = function () { + for (var i = 0, ii = this.childs.length; i < ii; i++) { + this.childs[i].repaint(); + } +}; + /** * Apply the size from options to the panel, and recalculate it's actual size. * @private */ Panel.prototype._updateSize = function () { // apply size - this.frame.style.top = util.option.asSize(this.options.top, '0px'); - this.frame.style.left = util.option.asSize(this.options.left, '0px'); + this.frame.style.top = util.option.asSize(this.options.top, null); + this.frame.style.bottom = util.option.asSize(this.options.bottom, null); + this.frame.style.left = util.option.asSize(this.options.left, null); + this.frame.style.right = util.option.asSize(this.options.right, null); this.frame.style.width = util.option.asSize(this.options.width, '100%'); this.frame.style.height = util.option.asSize(this.options.height, '100%'); diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index bfc2c476..0d2469b3 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -2,32 +2,15 @@ * A root panel can hold components. The root panel must be initialized with * a DOM element as container. * @param {HTMLElement} container + * @param {Emitter} emitter * @param {Object} [options] Available parameters: see RootPanel.setOptions. * @constructor RootPanel * @extends Panel */ -function RootPanel(container, options) { +function RootPanel(container, emitter, 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', 'doubletap', '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.emitter = emitter; this.options = options || {}; this.defaultOptions = { @@ -47,50 +30,66 @@ RootPanel.prototype = new Panel(); * {String | Number | function} [height] * {Boolean | function} [autoResize] */ -RootPanel.prototype.setOptions = Component.prototype.setOptions; +RootPanel.prototype.setOptions = function (options) { + if (options) { + util.extend(this.options, options); + + this.repaint(); + + var autoResize = this.getOption('autoResize'); + if (autoResize) { + this._watch(); + } + else { + this._unwatch(); + } + } +}; /** * Repaint the component */ RootPanel.prototype.repaint = function () { - var asSize = util.option.asSize, - options = this.options, - frame = this.frame; - // create frame - if (!frame) { - frame = document.createElement('div'); - this.frame = frame; - + if (!this.frame) { if (!this.container) throw new Error('Cannot repaint root panel: no container attached'); - this.container.appendChild(frame); + this.frame = document.createElement('div'); + this.container.appendChild(this.frame); - this._registerListeners(); + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = Hammer(this.frame, { + prevent_default: true + }); + this.listeners = {}; + + var me = this; + var events = [ + 'touch', 'pinch', 'tap', 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox + ]; + events.forEach(function (event) { + var listener = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.emitter.emit.apply(me.emitter, args); + }; + me.hammer.on(event, listener); + me.listeners[event] = listener; + }); } // update class name + var options = this.options; var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : ''); if (options.className) className += ' ' + util.option.asString(className); - frame.className = className; + this.frame.className = className; + + // repaint the child components + this._repaintChilds(); // update frame size this._updateSize(); - - this._updateWatch(); -}; - -/** - * Update watching for resize, depending on the current option - * @private - */ -RootPanel.prototype._updateWatch = function () { - var autoResize = this.getOption('autoResize'); - if (autoResize) { - this._watch(); - } - else { - this._unwatch(); - } }; /** @@ -117,7 +116,8 @@ RootPanel.prototype._watch = function () { (me.frame.clientHeight != me.lastHeight)) { me.lastWidth = me.frame.clientWidth; me.lastHeight = me.frame.clientHeight; - me.requestRepaint(); + me.repaint(); + // TODO: emit a resize event } } }; @@ -140,53 +140,3 @@ RootPanel.prototype._unwatch = function () { // TODO: remove event listener on window.resize }; - -/** - * Set controller for this component, or remove current controller by passing - * null as parameter value. - * @param {Controller | null} controller - */ -RootPanel.prototype.setController = function setController (controller) { - this.controller = controller || null; - - if (this.controller) { - this._registerListeners(); - } - else { - this._unregisterListeners(); - } -}; - -/** - * Register event emitters emitted by the rootpanel - * @private - */ -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]); - } - } - } -}; - -/** - * 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; - } -}; diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index db14b489..ec6fc1cd 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -1,17 +1,12 @@ /** * A horizontal time axis - * @param {Component} parent - * @param {Component[]} [depends] Components on which this components depends - * (except for the parent) * @param {Object} [options] See TimeAxis.setOptions for the available * options. * @constructor TimeAxis * @extends Component */ -function TimeAxis (parent, depends, options) { +function TimeAxis (options) { this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; this.dom = { majorLines: [], diff --git a/test/timeline.html b/test/timeline.html index 0e22470a..c659501a 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -68,6 +68,8 @@ end: now.clone().add('days', 7), //maxHeight: 200, height: 200, + showCurrentTime: true, + showCustomTime: true, //start: moment('2013-01-01'), //end: moment('2013-12-31'), //min: moment('2013-01-01'),