From 0a2cbdff1418102e9e3f2996da6231f57bc24f0c Mon Sep 17 00:00:00 2001 From: josdejong Date: Wed, 30 Oct 2013 09:55:04 +0100 Subject: [PATCH 1/3] Fixed #18: Not loading hammer.js when running on node.js --- src/module/imports.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/module/imports.js b/src/module/imports.js index 58aff1f0..a466d2e9 100644 --- a/src/module/imports.js +++ b/src/module/imports.js @@ -5,4 +5,14 @@ // Try to load dependencies from the global window object. // If not available there, load via require. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); -var Hammer = (typeof window !== 'undefined') && window['Hammer'] || require('hammerjs'); + +var Hammer; +if (typeof window !== 'undefined') { + // load hammer.js only when running in a browser (where window is available) + Hammer = window['Hammer'] || require('hammerjs'); +} +else { + Hammer = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + } +} From 251d4e552f1199953f937c3b6629ef1a9f81cb74 Mon Sep 17 00:00:00 2001 From: Fedor Tirsel Date: Tue, 29 Oct 2013 16:51:16 +0100 Subject: [PATCH 2/3] Custom time bar as a component --- Jakefile.js | 4 +- src/timeline/Timeline.js | 21 ++ src/timeline/component/CustomTime.js | 255 ++++++++++++++++++++++ src/timeline/component/css/customtime.css | 6 + 4 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 src/timeline/component/CustomTime.js create mode 100644 src/timeline/component/css/customtime.css diff --git a/Jakefile.js b/Jakefile.js index 42857a70..c2961da0 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -35,7 +35,8 @@ task('build', {async: true}, function () { './src/timeline/component/css/itemset.css', './src/timeline/component/css/item.css', './src/timeline/component/css/timeaxis.css', - './src/timeline/component/css/currenttime.css' + './src/timeline/component/css/currenttime.css', + './src/timeline/component/css/customtime.css' ], header: '/* vis.js stylesheet */', separator: '\n' @@ -64,6 +65,7 @@ task('build', {async: true}, function () { './src/timeline/component/RootPanel.js', './src/timeline/component/TimeAxis.js', './src/timeline/component/CurrentTime.js', + './src/timeline/component/CustomTime.js', './src/timeline/component/ItemSet.js', './src/timeline/component/item/*.js', './src/timeline/component/Group.js', diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 27c62282..4e22693f 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -18,6 +18,7 @@ function Timeline (container, items, options) { showMinorLabels: true, showMajorLabels: true, showCurrentTime: false, + showCustomTime: false, autoResize: false }, options); @@ -114,6 +115,10 @@ function Timeline (container, items, options) { this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); this.controller.add(this.currenttime); + // custom time bar + this.customtime = new CustomTime(this.timeaxis, [], rootOptions); + this.controller.add(this.customtime); + // create itemset or groupset this.setGroups(null); @@ -141,6 +146,22 @@ Timeline.prototype.setOptions = function (options) { this.controller.repaint(); }; +/** + * Set a custom time bar + * @param {Date} time + */ +Timeline.prototype.setCustomTime = function (time) { + this.customtime._setCustomTime(time); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +Timeline.prototype.getCustomTime = function() { + return new Date(this.customtime.customTime.valueOf()); +}; + /** * Set items * @param {vis.DataSet | Array | DataTable | null} items diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js new file mode 100644 index 00000000..9efe6892 --- /dev/null +++ b/src/timeline/component/CustomTime.js @@ -0,0 +1,255 @@ +/** + * 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) { + this.id = util.randomUUID(); + this.parent = parent; + this.depends = depends; + + this.options = options || {}; + this.defaultOptions = { + showCustomTime: false + }; + + this.listeners = []; + this.customTime = new Date(); +} + +CustomTime.prototype = new Component(); + +CustomTime.prototype.setOptions = Component.prototype.setOptions; + +/** + * Get the container element of the bar, which can be used by a child to + * add its own widgets. + * @returns {HTMLElement} container + */ +CustomTime.prototype.getContainer = function () { + return this.frame; +}; + +/** + * Repaint the component + * @return {Boolean} changed + */ +CustomTime.prototype.repaint = function () { + var bar = this.frame, + parent = this.parent, + parentContainer = parent.parent.getContainer(); + + if (!parent) { + throw new Error('Cannot repaint bar: no parent attached'); + } + + if (!parentContainer) { + throw new Error('Cannot repaint bar: parent has no container element'); + } + + if (!this.getOption('showCustomTime')) { + if (bar) { + parentContainer.removeChild(bar); + delete this.frame; + } + + return; + } + + if (!bar) { + bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + + parentContainer.appendChild(bar); + + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); + + this.frame = bar; + + this.subscribe(this, 'movetime'); + } + + if (!parent.conversion) { + parent._updateConversion(); + } + + var x = parent.toScreen(this.customTime); + + bar.style.left = x + 'px'; + bar.title = 'Time: ' + this.customTime; + + return false; +}; + +/** + * Set custom time. + * @param {Date} time + */ +CustomTime.prototype._setCustomTime = function(time) { + this.customTime = new Date(time.valueOf()); + this.repaint(); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +CustomTime.prototype._getCustomTime = function() { + return new Date(this.customTime.valueOf()); +}; + +/** + * Add listeners for mouse and touch events to the component + * @param {Component} component + */ +CustomTime.prototype.subscribe = function (component, event) { + var me = this; + var listener = { + component: component, + event: event, + callback: function (event) { + me._onMouseDown(event, listener); + }, + params: {} + }; + + component.on('mousedown', listener.callback); + me.listeners.push(listener); + +}; + +/** + * 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. + */ +CustomTime.prototype.on = function (event, callback) { + var bar = this.frame; + if (!bar) { + throw new Error('Cannot add event listener: no parent attached'); + } + + events.addListener(this, event, callback); + util.addEventListener(bar, event, callback); +}; + +/** + * Start moving horizontally + * @param {Event} event + * @param {Object} listener Listener containing the component and params + * @private + */ +CustomTime.prototype._onMouseDown = function(event, listener) { + event = event || window.event; + var params = listener.params; + + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown) { + return; + } + + // get mouse position + params.mouseX = util.getPageX(event); + params.moved = false; + + params.customTime = this.customTime; + + // add event listeners to handle moving the custom time bar + var me = this; + if (!params.onMouseMove) { + params.onMouseMove = function (event) { + me._onMouseMove(event, listener); + }; + util.addEventListener(document, 'mousemove', params.onMouseMove); + } + if (!params.onMouseUp) { + params.onMouseUp = function (event) { + me._onMouseUp(event, listener); + }; + util.addEventListener(document, 'mouseup', params.onMouseUp); + } + + util.stopPropagation(event); + util.preventDefault(event); +}; + +/** + * Perform moving operating. + * This function activated from within the funcion CustomTime._onMouseDown(). + * @param {Event} event + * @param {Object} listener + * @private + */ +CustomTime.prototype._onMouseMove = function (event, listener) { + event = event || window.event; + var params = listener.params; + var parent = this.parent; + + // calculate change in mouse position + var mouseX = util.getPageX(event); + + if (params.mouseX === undefined) { + params.mouseX = mouseX; + } + + var diff = mouseX - params.mouseX; + + // if mouse movement is big enough, register it as a "moved" event + if (Math.abs(diff) >= 1) { + params.moved = true; + } + + var x = parent.toScreen(params.customTime); + var xnew = x + diff; + var time = parent.toTime(xnew); + this._setCustomTime(time); + + // fire a timechange event + events.trigger(this, 'timechange', {customTime: this.customTime}); + + util.preventDefault(event); +}; + +/** + * Stop moving operating. + * This function activated from within the function CustomTime._onMouseDown(). + * @param {event} event + * @param {Object} listener + * @private + */ +CustomTime.prototype._onMouseUp = function (event, listener) { + event = event || window.event; + var params = listener.params; + + // remove event listeners here, important for Safari + if (params.onMouseMove) { + util.removeEventListener(document, 'mousemove', params.onMouseMove); + params.onMouseMove = null; + } + if (params.onMouseUp) { + util.removeEventListener(document, 'mouseup', params.onMouseUp); + params.onMouseUp = null; + } + + if (params.moved) { + // fire a timechanged event + events.trigger(this, 'timechanged', {customTime: this.customTime}); + } +}; diff --git a/src/timeline/component/css/customtime.css b/src/timeline/component/css/customtime.css new file mode 100644 index 00000000..15a3792a --- /dev/null +++ b/src/timeline/component/css/customtime.css @@ -0,0 +1,6 @@ +.vis.timeline .customtime { + background-color: #6E94FF; + width: 2px; + cursor: move; + z-index: 9; +} \ No newline at end of file From 15a4ac52658c7fe2cb0b6abe5cbb66e6bc9f9023 Mon Sep 17 00:00:00 2001 From: josdejong Date: Wed, 30 Oct 2013 14:22:41 +0100 Subject: [PATCH 3/3] Fixed #6: options `min` and `max` are broken. --- HISTORY.md | 2 + src/timeline/Range.js | 97 ++++++++++++++++-------------- src/timeline/TimeStep.js | 3 +- src/timeline/Timeline.js | 37 ++++++------ src/timeline/component/TimeAxis.js | 13 ++-- test/timeline.html | 11 ++-- 6 files changed, 88 insertions(+), 75 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 0d6a30a2..e6175fff 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,8 @@ http://visjs.org - Implemented option `showCurrentTime`, displaying a red, vertical bar at current time. Thanks fi0dor. +- Fixed broken Timeline options `min` and `max`. +- Fixed not being able to load vis.js in node.js. ## 2013-09-20, version 0.2.0 diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 2215ea36..ef9c4ce3 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -8,16 +8,10 @@ */ function Range(options) { this.id = util.randomUUID(); - this.start = 0; // Number - this.end = 0; // Number - - // this.options = options || {}; // TODO: fix range options - this.options = { - min: null, - max: null, - zoomMin: null, - zoomMax: null - }; + this.start = null; // Number + this.end = null; // Number + + this.options = options || {}; this.listeners = []; @@ -27,8 +21,6 @@ function Range(options) { /** * Set options for the range controller * @param {Object} options Available options: - * {Number} start Set start value of the range - * {Number} end Set end value of the range * {Number} min Minimum value for start * {Number} max Maximum value for end * {Number} zoomMin Set a minimum value for @@ -39,8 +31,9 @@ function Range(options) { Range.prototype.setOptions = function (options) { util.extend(this.options, options); - if (options.start != null || options.end != null) { - this.setRange(options.start, options.end); + // re-apply range with new limitations + if (this.start !== null && this.end !== null) { + this.setRange(this.start, this.end); } }; @@ -119,8 +112,8 @@ Range.prototype._trigger = function (event) { /** * Set a new start and end range - * @param {Number} start - * @param {Number} end + * @param {Number} [start] + * @param {Number} [end] */ Range.prototype.setRange = function(start, end) { var changed = this._applyRange(start, end); @@ -134,21 +127,23 @@ Range.prototype.setRange = function(start, end) { * Set a new start and end range. This method is the same as setRange, but * does not trigger a range change and range changed event, and it returns * true when the range is changed - * @param {Number} start - * @param {Number} end + * @param {Number} [start] + * @param {Number} [end] * @return {Boolean} changed * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start; - var newEnd = (end != null) ? util.convert(end, 'Number') : this.end; - var diff; + var newStart = (start != null) ? util.convert(start, 'Number') : this.start, + newEnd = (end != null) ? util.convert(end, 'Number') : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; // check for valid number - if (isNaN(newStart)) { + if (isNaN(newStart) || newStart === null) { throw new Error('Invalid start "' + start + '"'); } - if (isNaN(newEnd)) { + if (isNaN(newEnd) || newEnd === null) { throw new Error('Invalid end "' + end + '"'); } @@ -158,64 +153,76 @@ Range.prototype._applyRange = function(start, end) { } // prevent start < min - if (this.options.min != null) { - var min = this.options.min.valueOf(); + if (min !== null) { if (newStart < min) { diff = (min - newStart); newStart += diff; newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } } } // prevent end > max - if (this.options.max != null) { - var max = this.options.max.valueOf(); + if (max !== null) { if (newEnd > max) { diff = (newEnd - max); newStart -= diff; newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } } } - // prevent (end-start) > zoomMin - if (this.options.zoomMin != null) { - var zoomMin = this.options.zoomMin.valueOf(); + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); if (zoomMin < 0) { zoomMin = 0; } if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) > zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { // zoom to the minimum diff = (zoomMin - (newEnd - newStart)); newStart -= diff / 2; newEnd += diff / 2; } - else { - // ingore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } } } - // prevent (end-start) > zoomMin - if (this.options.zoomMax != null) { - var zoomMax = this.options.zoomMax.valueOf(); + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); if (zoomMax < 0) { zoomMax = 0; } if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) < zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { // zoom to the maximum diff = ((newEnd - newStart) - zoomMax); newStart += diff / 2; newEnd -= diff / 2; } - else { - // ingore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } } } diff --git a/src/timeline/TimeStep.js b/src/timeline/TimeStep.js index 4823b9f7..ccbc6c9f 100644 --- a/src/timeline/TimeStep.js +++ b/src/timeline/TimeStep.js @@ -63,8 +63,7 @@ TimeStep.SCALE = { */ TimeStep.prototype.setRange = function(start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { - //throw "No legal start or end date in method setRange"; - return; + throw "No legal start or end date in method setRange"; } this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 27c62282..125499af 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -7,11 +7,12 @@ */ function Timeline (container, items, options) { var me = this; - this.options = util.extend({ + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.options = { orientation: 'bottom', min: null, max: null, - zoomMin: 10, // milliseconds + zoomMin: 10, // milliseconds zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds // moveable: true, // TODO: option moveable // zoomable: true, // TODO: option zoomable @@ -19,7 +20,7 @@ function Timeline (container, items, options) { showMajorLabels: true, showCurrentTime: false, autoResize: false - }, options); + }; // controller this.controller = new Controller(); @@ -72,19 +73,13 @@ function Timeline (container, items, options) { this.controller.add(this.labelPanel); // range - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.range = new Range({ - start: now.clone().add('days', -3).valueOf(), - end: now.clone().add('days', 4).valueOf() - }); - /* TODO: fix range options 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() ); - */ + // TODO: reckon with options moveable and zoomable this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); @@ -120,7 +115,12 @@ function Timeline (container, items, options) { this.itemsData = null; // DataSet this.groupsData = null; // DataSet - // set data + // apply options + if (options) { + this.setOptions(options); + } + + // set data (must be after options are applied) if (items) { this.setItems(items); } @@ -131,11 +131,12 @@ function Timeline (container, items, options) { * @param {Object} options TODO: describe the available options */ Timeline.prototype.setOptions = function (options) { - if (options) { - util.extend(this.options, options); - } + util.extend(this.options, options); - // TODO: apply range min,max + // force update of range + // options.start and options.end can be undefined + //this.range.setRange(options.start, options.end); + this.range.setRange(); this.controller.reflow(); this.controller.repaint(); @@ -174,7 +175,7 @@ Timeline.prototype.setItems = function(items) { // apply the data range as range var dataRange = this.getItemRange(); - // add 5% on both sides + // add 5% space on both sides var min = dataRange.min; var max = dataRange.max; if (min != null && max != null) { @@ -189,10 +190,10 @@ Timeline.prototype.setItems = function(items) { // override specified start and/or end date if (this.options.start != undefined) { - min = new Date(this.options.start.valueOf()); + min = util.convert(this.options.start, 'Date'); } if (this.options.end != undefined) { - max = new Date(this.options.end.valueOf()); + max = util.convert(this.options.end, 'Date'); } // apply range if there is a min or max available diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index 7b8a3535..ec976939 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -487,12 +487,13 @@ TimeAxis.prototype.reflow = function () { // calculate range and step this._updateConversion(); - var start = util.convert(range.start, 'Date'), - end = util.convert(range.end, 'Date'), - minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0); - this.step = new TimeStep(start, end, minimumStep); - changed += update(props.range, 'start', start.valueOf()); - changed += update(props.range, 'end', end.valueOf()); + var start = util.convert(range.start, 'Number'), + end = util.convert(range.end, 'Number'), + minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() + -this.toTime(0).valueOf(); + this.step = new TimeStep(new Date(start), new Date(end), minimumStep); + changed += update(props.range, 'start', start); + changed += update(props.range, 'end', end); changed += update(props.range, 'minimumStep', minimumStep.valueOf()); } diff --git a/test/timeline.html b/test/timeline.html index 33829560..8b725a7b 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -2,6 +2,7 @@ +