diff --git a/src/timeline/Range.js b/src/timeline/Range.js index d962cc72..2dfec719 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -3,41 +3,36 @@ * 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 {RootPanel} root Root panel, used to subscribe to events - * @param {Panel} parent Parent panel, used to attach to the DOM + * @param {{dom: Object, props: Object, emitter: Emitter, range: Range}} timeline * @param {Object} [options] See description at Range.setOptions */ -function Range(root, parent, options) { +function Range(timeline, options) { this.id = util.randomUUID(); this.start = null; // Number this.end = null; // Number - this.root = root; - this.parent = parent; + this.timeline = timeline; this.options = options || {}; // drag listeners for dragging - this.root.on('dragstart', this._onDragStart.bind(this)); - this.root.on('drag', this._onDrag.bind(this)); - this.root.on('dragend', this._onDragEnd.bind(this)); + this.timeline.emitter.on('dragstart', this._onDragStart.bind(this)); + this.timeline.emitter.on('drag', this._onDrag.bind(this)); + this.timeline.emitter.on('dragend', this._onDragEnd.bind(this)); // ignore dragging when holding - this.root.on('hold', this._onHold.bind(this)); + this.timeline.emitter.on('hold', this._onHold.bind(this)); // mouse wheel for zooming - this.root.on('mousewheel', this._onMouseWheel.bind(this)); - this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF + this.timeline.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + this.timeline.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF // pinch to zoom - this.root.on('touch', this._onTouch.bind(this)); - this.root.on('pinch', this._onPinch.bind(this)); + this.timeline.emitter.on('touch', this._onTouch.bind(this)); + this.timeline.emitter.on('pinch', this._onPinch.bind(this)); this.setOptions(options); } -// turn Range into an event emitter -Emitter(Range.prototype); - /** * Set options for the range controller * @param {Object} options Available options: @@ -80,8 +75,8 @@ Range.prototype.setRange = function(start, end) { start: new Date(this.start), end: new Date(this.end) }; - this.emit('rangechange', params); - this.emit('rangechanged', params); + this.timeline.emitter.emit('rangechange', params); + this.timeline.emitter.emit('rangechanged', params); } }; @@ -258,9 +253,8 @@ Range.prototype._onDragStart = function(event) { touchParams.start = this.start; touchParams.end = this.end; - var frame = this.parent.frame; - if (frame) { - frame.style.cursor = 'move'; + if (this.timeline.dom.root) { + this.timeline.dom.root.style.cursor = 'move'; } }; @@ -282,12 +276,12 @@ Range.prototype._onDrag = function (event) { var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, interval = (touchParams.end - touchParams.start), - width = (direction == 'horizontal') ? this.parent.width : this.parent.height, + width = (direction == 'horizontal') ? this.timeline.props.center.width : this.timeline.props.center.height, diffRange = -delta / width * interval; this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); - this.emit('rangechange', { + this.timeline.emitter.emit('rangechange', { start: new Date(this.start), end: new Date(this.end) }); @@ -305,12 +299,12 @@ Range.prototype._onDragEnd = function (event) { // TODO: reckon with option movable - if (this.parent.frame) { - this.parent.frame.style.cursor = 'auto'; + if (this.timeline.dom.root) { + this.timeline.dom.root.style.cursor = 'auto'; } // fire a rangechanged event - this.emit('rangechanged', { + this.timeline.emitter.emit('rangechanged', { start: new Date(this.start), end: new Date(this.end) }); @@ -353,7 +347,7 @@ Range.prototype._onMouseWheel = function(event) { // calculate center, the date to zoom around var gesture = util.fakeGesture(this, event), - pointer = getPointer(gesture.center, this.parent.frame), + pointer = getPointer(gesture.center, this.timeline.dom.root), pointerDate = this._pointerToDate(pointer); this.zoom(scale, pointerDate); @@ -396,21 +390,17 @@ Range.prototype._onHold = function () { * @private */ 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, this.parent.frame); + touchParams.center = getPointer(event.gesture.center, this.timeline.dom.root); } var scale = 1 / event.gesture.scale, - 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 + initDate = this._pointerToDate(touchParams.center); // calculate new start and end var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); @@ -434,12 +424,12 @@ Range.prototype._pointerToDate = function (pointer) { validateDirection(direction); if (direction == 'horizontal') { - var width = this.parent.width; + var width = this.timeline.props.center.width; conversion = this.conversion(width); return pointer.x / conversion.scale + conversion.offset; } else { - var height = this.parent.height; + var height = this.timeline.props.center.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 77ab6295..2ad131a5 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -72,13 +72,14 @@ function Timeline (container, items, options) { this.options = {}; util.deepExtend(this.options, this.defaultOptions); util.deepExtend(this.options, { + // FIXME: not nice passing these functions via the options snap: null, // will be specified after timeaxis is created toScreen: me._toScreen.bind(me), toTime: me._toTime.bind(me) }); - // Create the main DOM + // Create the DOM, props, and emitter this._create(); // attach the root panel to the provided container @@ -86,18 +87,37 @@ function Timeline (container, items, options) { container.appendChild(this.dom.root); // TODO: remove temporary contents - this.dom.background.innerHTML = 'background'; - this.dom.center.innerHTML = 'center'; - this.dom.center.innerHTML = 'center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
center
'; - this.dom.left.innerHTML = 'left'; - this.dom.right.innerHTML = 'right'; - this.dom.top.innerHTML = 'top'; - this.dom.bottom.innerHTML = 'bottom'; + //this.dom.background.innerHTML = 'background'; + this.dom.center.innerHTML = 'center'; + this.dom.center.innerHTML = 'center
center
center
center
center
center
center
center
center
center
center
center
center
center
center'; + this.dom.left.innerHTML = 'left'; + this.dom.right.innerHTML = 'right'; + this.dom.top.innerHTML = 'top'; + //this.dom.bottom.innerHTML = 'bottom'; + + this.components = []; + + // create a Range + this.range = new Range(this, this.options); + this.range.setRange( + now.clone().add('days', -3).valueOf(), + now.clone().add('days', 4).valueOf() + ); - this.repaint(); + // Create a TimeAxis + var timeAxis = new TimeAxis(this, this.options); + this.components.push(timeAxis); - /* TODO + // re-emit public events + this.emitter.on('rangechange', function (properties) { + me.emit('rangechange', properties); + }); + this.emitter.on('rangechanged', function (properties) { + me.emit('rangechanged', properties); + }); + + /* TODO // root panel var rootOptions = util.extend(Object.create(this.options), { height: function () { @@ -291,7 +311,7 @@ function Timeline (container, items, options) { this.itemSet.setRange(this.range); this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel)); this.contentPanel.appendChild(this.itemSet); - +*/ this.itemsData = null; // DataSet this.groupsData = null; // DataSet @@ -304,7 +324,6 @@ function Timeline (container, items, options) { if (items) { this.setItems(items); } - */ } // turn Timeline into an event emitter @@ -350,6 +369,36 @@ Timeline.prototype._create = function () { this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); + // TODO: move watch from RootPanel to here + + // create a central event bus + this.emitter = new Emitter(); + + this.emitter.on('rangechange', this.repaint.bind(this)); + + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = Hammer(this.dom.root, { + prevent_default: true + }); + this.listeners = {}; + + var me = this; + var events = [ + 'touch', 'pinch', 'tap', 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed 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; + }); + + // size properties of each of the panels this.props = { root: {}, background: {}, @@ -382,11 +431,11 @@ Timeline.prototype.setOptions = function (options) { remove: isBoolean ? options.editable : (options.editable.remove || false) }; } - +/* TODO // force update of range (apply new min/max etc.) // both start and end are optional this.range.setRange(options.start, options.end); - + */ if ('editable' in options || 'selectable' in options) { if (this.options.selectable) { // force update of selection @@ -397,10 +446,10 @@ Timeline.prototype.setOptions = function (options) { this.setSelection([]); } } - +/* TODO // force the itemSet to refresh: options like orientation and margins may be changed this.itemSet.markDirty(); - +*/ // validate the callback functions var validateCallback = (function (fn) { if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) { @@ -409,6 +458,7 @@ Timeline.prototype.setOptions = function (options) { }).bind(this); ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback); + /* TODO // add/remove the current time bar if (this.options.showCurrentTime) { if (!this.mainPanel.hasChild(this.currentTime)) { @@ -434,7 +484,7 @@ Timeline.prototype.setOptions = function (options) { this.mainPanel.removeChild(this.customTime); } } - +*/ // TODO: remove deprecation error one day (deprecated since version 0.8.0) if (options && options.order) { throw new Error('Option order is deprecated. There is no replacement for this feature.'); @@ -495,7 +545,7 @@ Timeline.prototype.setItems = function(items) { // set items this.itemsData = newDataSet; - this.itemSet.setItems(newDataSet); + this.itemSet && this.itemSet.setItems(newDataSet); if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { this.fit(); @@ -631,7 +681,7 @@ Timeline.prototype.getItemRange = function getItemRange() { * unselected. */ Timeline.prototype.setSelection = function setSelection (ids) { - this.itemSet.setSelection(ids); + this.itemSet && this.itemSet.setSelection(ids); }; /** @@ -639,7 +689,7 @@ Timeline.prototype.setSelection = function setSelection (ids) { * @return {Array} ids The ids of the selected items */ Timeline.prototype.getSelection = function getSelection() { - return this.itemSet.getSelection(); + return this.itemSet && this.itemSet.getSelection() || []; }; /** @@ -746,8 +796,6 @@ Timeline.prototype.repaint = function repaint() { dom.background.style.width = props.background.width + 'px'; dom.centerContainer.style.width = props.center.width + 'px'; - dom.leftContainer.style.width = (props.left.width + props.border.left) + 'px'; - dom.rightContainer.style.width = (props.right.width + props.border.right) + 'px'; dom.top.style.width = props.top.width + 'px'; dom.bottom.style.width = props.bottom.width + 'px'; @@ -765,9 +813,15 @@ Timeline.prototype.repaint = function repaint() { dom.bottom.style.left = props.left.width + 'px'; dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px'; - /* TODO: repaint contents - this.rootPanel.repaint(); - */ + // repaint all components + var resized = false; + this.components.forEach(function (component) { + resized = component.repaint() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + this.repaint(); + } }; /** @@ -900,7 +954,7 @@ Timeline.prototype._onMultiSelectItem = function (event) { * @private */ Timeline.prototype._toTime = function _toTime(x) { - var conversion = this.range.conversion(this.mainPanel.width); + var conversion = this.range.conversion(this.props.center.width); return new Date(x / conversion.scale + conversion.offset); }; @@ -912,6 +966,6 @@ Timeline.prototype._toTime = function _toTime(x) { * @private */ Timeline.prototype._toScreen = function _toScreen(time) { - var conversion = this.range.conversion(this.mainPanel.width); + var conversion = this.range.conversion(this.props.center.width); return (time.valueOf() - conversion.offset) * conversion.scale; }; diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 76ac811c..3d91b28a 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -2,20 +2,10 @@ * Prototype for visual components */ function Component () { - this.id = null; - this.parent = null; - this.childs = null; this.options = null; - - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; + this.props = null; } -// Turn the Component into an event emitter -Emitter(Component.prototype); - /** * Set parameters for the frame. Parameters will be merged in current parameter * set. @@ -52,15 +42,6 @@ Component.prototype.getOption = function getOption(name) { return value; }; -/** - * Get the frame element of the component, the outer HTML DOM element. - * @returns {HTMLElement | null} frame - */ -Component.prototype.getFrame = function getFrame() { - // should be implemented by the component - return null; -}; - /** * Repaint the component * @return {boolean} Returns true if the component is resized @@ -77,10 +58,11 @@ Component.prototype.repaint = function repaint() { * @protected */ Component.prototype._isResized = function _isResized() { - var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height); + var resized = (this.props._previousWidth !== this.props.width || + this.props._previousHeight !== this.props.height); - this._previousWidth = this.width; - this._previousHeight = this.height; + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; return resized; }; diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index b437d8b8..b27fe8d3 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -1,14 +1,14 @@ /** * A horizontal time axis + * @param {{dom: Object, props: Object, emitter: Emitter, range: Range}} timeline * @param {Object} [options] See TimeAxis.setOptions for the available * options. * @constructor TimeAxis * @extends Component */ -function TimeAxis (options) { - this.id = util.randomUUID(); - +function TimeAxis (timeline, options) { this.dom = { + frame: null, majorLines: [], majorTexts: [], minorLines: [], @@ -37,7 +37,7 @@ function TimeAxis (options) { showMajorLabels: true }; - this.range = null; + this.timeline = timeline; // create the HTML DOM this._create(); @@ -45,34 +45,21 @@ function TimeAxis (options) { TimeAxis.prototype = new Component(); -// TODO: comment options +/** + * Set parameters for the timeaxis. + * Parameters will be merged in current parameter set. + * @param {Object} options Available parameters: + * {string} [orientation] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] + */ TimeAxis.prototype.setOptions = Component.prototype.setOptions; /** * Create the HTML DOM for the TimeAxis */ TimeAxis.prototype._create = function _create() { - this.frame = document.createElement('div'); -}; - -/** - * Set a range (start and end) - * @param {Range | Object} range A Range or an object containing start and end. - */ -TimeAxis.prototype.setRange = function (range) { - if (!(range instanceof Range) && (!range || !range.start || !range.end)) { - throw new TypeError('Range must be an instance of Range, ' + - 'or an object containing start and end.'); - } - this.range = range; -}; - -/** - * Get the outer frame of the time axis - * @return {HTMLElement} frame - */ -TimeAxis.prototype.getFrame = function getFrame() { - return this.frame; + this.dom.frame = document.createElement('div'); }; /** @@ -80,67 +67,55 @@ TimeAxis.prototype.getFrame = function getFrame() { * @return {boolean} Returns true if the component is resized */ TimeAxis.prototype.repaint = function () { - var asSize = util.option.asSize, - options = this.options, + var options = this.options, props = this.props, - frame = this.frame; + frame = this.dom.frame; + + // determine the correct parent DOM element (depending on option orientation) + var parent = (options.orientation == 'top') ? this.timeline.dom.top : this.timeline.dom.bottom; // update classname frame.className = 'timeaxis'; // TODO: add className from options if defined - var parent = frame.parentNode; - if (parent) { - // calculate character width and height - this._calculateCharSize(); - - // TODO: recalculate sizes only needed when parent is resized or options is changed - var orientation = this.getOption('orientation'), - showMinorLabels = this.getOption('showMinorLabels'), - showMajorLabels = this.getOption('showMajorLabels'); - - // determine the width and height of the elemens for the axis - var parentHeight = this.parent.height; - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - this.height = props.minorLabelHeight + props.majorLabelHeight; - this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized? - - props.minorLineHeight = parentHeight + props.minorLabelHeight; - props.minorLineWidth = 1; // TODO: really calculate width - props.majorLineHeight = parentHeight + this.height; - props.majorLineWidth = 1; // TODO: really calculate width - - // take frame offline while updating (is almost twice as fast) - var beforeChild = frame.nextSibling; - parent.removeChild(frame); - - // TODO: top/bottom positioning should be determined by options set in the Timeline, not here - if (orientation == 'top') { - frame.style.top = '0'; - frame.style.left = '0'; - frame.style.bottom = ''; - frame.style.width = asSize(options.width, '100%'); - frame.style.height = this.height + 'px'; - } - else { // bottom - frame.style.top = ''; - frame.style.bottom = '0'; - frame.style.left = '0'; - frame.style.width = asSize(options.width, '100%'); - frame.style.height = this.height + 'px'; - } + // calculate character width and height + this._calculateCharSize(); - this._repaintLabels(); + // TODO: recalculate sizes only needed when parent is resized or options is changed + var orientation = this.getOption('orientation'), + showMinorLabels = this.getOption('showMinorLabels'), + showMajorLabels = this.getOption('showMajorLabels'); - this._repaintLine(); + // determine the width and height of the elemens for the axis + var backgroundHeight = this.timeline.props.background.height; + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + props.height = props.minorLabelHeight + props.majorLabelHeight; + props.width = frame.offsetWidth; // TODO: only update the width when the frame is resized? - // put frame online again - if (beforeChild) { - parent.insertBefore(frame, beforeChild); - } - else { - parent.appendChild(frame) - } + props.minorLineHeight = backgroundHeight + props.minorLabelHeight; + props.minorLineWidth = 1; // TODO: really calculate width + props.majorLineHeight = backgroundHeight + this.props.height; + props.majorLineWidth = 1; // TODO: really calculate width + + // take frame offline while updating (is almost twice as fast) + var beforeChild = frame.nextSibling; + frame.parentNode && frame.parentNode.removeChild(frame); + + frame.style.top = '0'; + frame.style.left = '0'; + frame.style.width = '100%'; + frame.style.height = this.props.height + 'px'; + + this._repaintLabels(); + + this._repaintLine(); + + // put frame online again at the same place + if (beforeChild) { + parent.insertBefore(frame, beforeChild); + } + else { + parent.appendChild(frame) } return this._isResized(); @@ -154,8 +129,8 @@ TimeAxis.prototype._repaintLabels = function () { var orientation = this.getOption('orientation'); // calculate range and step (step such that we have space for 7 characters per label) - var start = util.convert(this.range.start, 'Number'), - end = util.convert(this.range.end, 'Number'), + var start = util.convert(this.timeline.range.start, 'Number'), + end = util.convert(this.timeline.range.end, 'Number'), minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf() -this.options.toTime(0).valueOf(); var step = new TimeStep(new Date(start), new Date(end), minimumStep); @@ -244,7 +219,7 @@ TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { label = document.createElement('div'); label.appendChild(content); label.className = 'text minor'; - this.frame.appendChild(label); + this.dom.frame.appendChild(label); } this.dom.minorTexts.push(label); @@ -279,7 +254,7 @@ TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { label = document.createElement('div'); label.className = 'text major'; label.appendChild(content); - this.frame.appendChild(label); + this.dom.frame.appendChild(label); } this.dom.majorTexts.push(label); @@ -311,7 +286,7 @@ TimeAxis.prototype._repaintMinorLine = function (x, orientation) { // create vertical line line = document.createElement('div'); line.className = 'grid vertical minor'; - this.frame.appendChild(line); + this.dom.frame.appendChild(line); } this.dom.minorLines.push(line); @@ -342,7 +317,7 @@ TimeAxis.prototype._repaintMajorLine = function (x, orientation) { // create vertical line line = document.createElement('DIV'); line.className = 'grid vertical major'; - this.frame.appendChild(line); + this.dom.frame.appendChild(line); } this.dom.majorLines.push(line); @@ -366,7 +341,7 @@ TimeAxis.prototype._repaintMajorLine = function (x, orientation) { */ TimeAxis.prototype._repaintLine = function() { var line = this.dom.line, - frame = this.frame, + frame = this.dom.frame, orientation = this.getOption('orientation'); // line before all axis elements @@ -417,7 +392,7 @@ TimeAxis.prototype._calculateCharSize = function () { this.dom.measureCharMinor.style.position = 'absolute'; this.dom.measureCharMinor.appendChild(document.createTextNode('0')); - this.frame.appendChild(this.dom.measureCharMinor); + this.dom.frame.appendChild(this.dom.measureCharMinor); } this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; @@ -429,7 +404,7 @@ TimeAxis.prototype._calculateCharSize = function () { this.dom.measureCharMajor.style.position = 'absolute'; this.dom.measureCharMajor.appendChild(document.createTextNode('0')); - this.frame.appendChild(this.dom.measureCharMajor); + this.dom.frame.appendChild(this.dom.measureCharMajor); } this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; diff --git a/src/timeline/component/css/panel.css b/src/timeline/component/css/panel.css index 113c6922..66010dfa 100644 --- a/src/timeline/component/css/panel.css +++ b/src/timeline/component/css/panel.css @@ -12,7 +12,7 @@ .vis.timeline .vispanel { position: absolute; - overflow: hidden; + padding: 0; margin: 0; diff --git a/src/timeline/component/css/timeaxis.css b/src/timeline/component/css/timeaxis.css index 38f8ccb9..d67245eb 100644 --- a/src/timeline/component/css/timeaxis.css +++ b/src/timeline/component/css/timeaxis.css @@ -1,5 +1,5 @@ .vis.timeline .timeaxis { - position: absolute; + position: relative; } .vis.timeline .timeaxis .text { @@ -29,7 +29,6 @@ left: 0; width: 100%; height: 0; - border-bottom: 1px solid; } .vis.timeline .timeaxis .grid.minor {