diff --git a/HISTORY.md b/HISTORY.md
index cd335d69..5f65d033 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,11 +4,16 @@ http://visjs.org
## 2014-06-06, version 1.1.1
+### Timeline
+
+- Changed default value of option `showCurrentTime` to true.
+
### Graph
-- reduced the timestep a little for smoother animations.
-- fixed dataManipulation.initiallyVisible functionality (thanks theGrue).
-- forced typecast of fontSize to Number.
+- Reduced the timestep a little for smoother animations.
+- Fixed dataManipulation.initiallyVisible functionality (thanks theGrue).
+- Forced typecast of fontSize to Number.
+
## 2014-06-06, version 1.1.0
diff --git a/docs/timeline.html b/docs/timeline.html
index 179d11a1..43abb226 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -533,7 +533,7 @@ var options = {
| showCurrentTime |
boolean |
- false |
+ true |
Show a vertical bar at the current time. |
diff --git a/src/timeline/Range.js b/src/timeline/Range.js
index d50e8d13..1047848e 100644
--- a/src/timeline/Range.js
+++ b/src/timeline/Range.js
@@ -11,7 +11,18 @@ function Range(body, options) {
this.end = null; // Number
this.body = body;
- this.options = options || {};
+
+ // default options
+ this.defaultOptions = {
+ start: null,
+ end: null,
+ direction: 'horizontal', // 'horizontal' or 'vertical'
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
+ };
+ this.options = util.extend({}, this.defaultOptions);
// drag listeners for dragging
this.body.emitter.on('dragstart', this._onDragStart.bind(this));
@@ -32,9 +43,13 @@ function Range(body, options) {
this.setOptions(options);
}
+Range.prototype = new Component();
+
/**
* Set options for the range controller
* @param {Object} options Available options:
+ * {Number | Date | String} start Start date for the range
+ * {Number | Date | String} end End date for the range
* {Number} min Minimum value for start
* {Number} max Maximum value for end
* {Number} zoomMin Set a minimum value for
@@ -43,11 +58,14 @@ function Range(body, options) {
* (end - start).
*/
Range.prototype.setOptions = function (options) {
- util.extend(this.options, options);
+ if (options) {
+ // copy the options that we know
+ util.selectiveExtend(['direction', 'min', 'max', 'zoomMin', 'zoomMax'], this.options, options);
- // re-apply range with new limitations
- if (this.start !== null && this.end !== null) {
- this.setRange(this.start, this.end);
+ if ('start' in options || 'end' in options) {
+ // apply a new range. both start and end are optional
+ this.setRange(options.start, options.end);
+ }
}
};
diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js
index 321257f3..fc33abe1 100644
--- a/src/timeline/Timeline.js
+++ b/src/timeline/Timeline.js
@@ -12,65 +12,16 @@ function Timeline (container, items, options) {
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.defaultOptions = {
- orientation: 'bottom',
- direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
- stack: true,
-
- editable: {
- updateTime: false,
- updateGroup: false,
- add: false,
- remove: false
- },
-
- selectable: true,
-
- start: null,
- end: null,
- min: null,
- max: null,
- zoomMin: 10, // milliseconds
- zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
- // moveable: true, // TODO: option moveable
- // zoomable: true, // TODO: option zoomable
-
- showMinorLabels: true,
- showMajorLabels: true,
- showCurrentTime: false,
- showCustomTime: false,
-
- groupOrder: null,
width: null,
height: null,
maxHeight: null,
- minHeight: null,
-
- type: 'box',
- align: 'center',
- margin: {
- axis: 20,
- item: 10
- },
- padding: 5,
-
- onAdd: function (item, callback) {
- callback(item);
- },
- onUpdate: function (item, callback) {
- callback(item);
- },
- onMove: function (item, callback) {
- callback(item);
- },
- onRemove: function (item, callback) {
- callback(item);
- }
- };
+ minHeight: null
- this.options = {};
- util.deepExtend(this.options, this.defaultOptions);
+ // TODO: implement options moveable and zoomable
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
// Create the DOM, props, and emitter
this._create();
@@ -94,7 +45,9 @@ function Timeline (container, items, options) {
};
// range
- this.range = new Range(this.body, this.options);
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ // TODO: use default start and en of range?
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
@@ -102,21 +55,21 @@ function Timeline (container, items, options) {
this.body.range = this.range;
// time axis
- this.timeAxis = new TimeAxis(this.body, this.options);
+ this.timeAxis = new TimeAxis(this.body);
this.components.push(this.timeAxis);
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar
- this.currentTime = new CurrentTime(this.body, this.options);
+ this.currentTime = new CurrentTime(this.body);
this.components.push(this.currentTime);
// custom time bar
// Note: time bar will be attached in this.setOptions when selected
- this.customTime = new CustomTime(this.body, this.options);
+ this.customTime = new CustomTime(this.body);
this.components.push(this.customTime);
// item set
- this.itemSet = new ItemSet(this.body, this.options);
+ this.itemSet = new ItemSet(this.body);
this.components.push(this.itemSet);
this.on('change', this.redraw.bind(this));
@@ -132,6 +85,9 @@ function Timeline (container, items, options) {
if (items) {
this.setItems(items);
}
+ else {
+ this.redraw();
+ }
}
// turn Timeline into an event emitter
@@ -225,79 +181,41 @@ Timeline.prototype._create = function () {
};
/**
- * Set options
- * @param {Object} options TODO: describe the available options
+ * Set options. Options will be passed to all components loaded in the Timeline.
+ * @param {Object} [options]
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Timeline will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
*/
Timeline.prototype.setOptions = function (options) {
- util.deepExtend(this.options, options);
-
- if ('editable' in options) {
- var isBoolean = typeof options.editable === 'boolean';
-
- this.options.editable = {
- updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
- updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
- add: isBoolean ? options.editable : (options.editable.add || false),
- remove: isBoolean ? options.editable : (options.editable.remove || false)
- };
- }
-
- // 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
- this.setSelection(this.getSelection());
- }
- else {
- // remove selection
- this.setSelection([]);
- }
- }
-
- // 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) {
- throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
- }
- }).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)) {
- this.mainPanel.appendChild(this.currentTime);
- this.currentTime.start();
- }
- }
- else {
- if (this.mainPanel.hasChild(this.currentTime)) {
- this.currentTime.stop();
- this.mainPanel.removeChild(this.currentTime);
- }
- }
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end'];
+ util.selectiveExtend(fields, this.options, options);
- // add/remove the custom time bar
- if (this.options.showCustomTime) {
- if (!this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.appendChild(this.customTime);
- }
+ // enable/disable autoResize
+ this._initAutoResize();
}
- else {
- if (this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.removeChild(this.customTime);
- }
- }
-*/
- // enable/disable autoResize
- this._initAutoResize();
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
@@ -361,11 +279,11 @@ Timeline.prototype.setItems = function(items) {
this.itemsData = newDataSet;
this.itemSet && this.itemSet.setItems(newDataSet);
- if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
this.fit();
- var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
- var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
+ var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
+ var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
this.setWindow(start, end);
}
@@ -414,9 +332,13 @@ Timeline.prototype.clear = function(what) {
this.setGroups(null);
}
- // clear options
+ // clear options of timeline and of each of the components
if (!what || what.options) {
- this.setOptions(this.defaultOptions);
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ })
+
+ this.setOptions(this.defaultOptions); // this will also do a redraw
}
};
@@ -549,15 +471,15 @@ Timeline.prototype.redraw = function() {
var resized = false,
options = this.options,
props = this.props,
- dom = this.dom,
- editable = options.editable.updateTime || options.editable.updateGroup;
+ dom = this.dom;
// update class names
- dom.root.className = 'vis timeline root ' + options.orientation + (editable ? ' editable' : '');
+ dom.root.className = 'vis timeline root ' + options.orientation;
- // update root height options
+ // update root width and height options
dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
// calculate border widths
props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js
index 8e3a96e7..02e6368a 100644
--- a/src/timeline/component/Component.js
+++ b/src/timeline/component/Component.js
@@ -9,39 +9,14 @@ function Component (body, options) {
}
/**
- * Set parameters for the frame. Parameters will be merged in current parameter
- * set.
- * @param {Object} options Available parameters:
- * {String | function} [className]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
+ * Set options for the component. The new options will be merged into the
+ * current options.
+ * @param {Object} options
*/
Component.prototype.setOptions = function(options) {
if (options) {
util.extend(this.options, options);
-
- this.redraw();
- }
-};
-
-/**
- * Get an option value by name
- * The function will first check this.options object, and else will check
- * this.defaultOptions.
- * @param {String} name
- * @return {*} value
- */
-Component.prototype.getOption = function(name) {
- var value;
- if (this.options) {
- value = this.options[name];
- }
- if (value === undefined && this.defaultOptions) {
- value = this.defaultOptions[name];
}
- return value;
};
/**
diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js
index 975ad45c..946ccf04 100644
--- a/src/timeline/component/CurrentTime.js
+++ b/src/timeline/component/CurrentTime.js
@@ -10,9 +10,15 @@
function CurrentTime (body, options) {
this.body = body;
- this.options = options || {};
+ // default options
+ this.defaultOptions = {
+ showCurrentTime: true
+ };
+ this.options = util.extend({}, this.defaultOptions);
this._create();
+
+ this.setOptions(options);
}
CurrentTime.prototype = new Component();
@@ -31,6 +37,18 @@ CurrentTime.prototype._create = function() {
this.bar = bar;
};
+/**
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCurrentTime]
+ */
+CurrentTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCurrentTime'], this.options, options);
+ }
+};
+
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js
index e944ba5f..038d7be2 100644
--- a/src/timeline/component/CustomTime.js
+++ b/src/timeline/component/CustomTime.js
@@ -9,17 +9,36 @@
function CustomTime (body, options) {
this.body = body;
- this.options = options || {};
+
+ // default options
+ this.defaultOptions = {
+ showCustomTime: false
+ };
+ this.options = util.extend({}, this.defaultOptions);
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM
this._create();
+
+ this.setOptions(options);
}
CustomTime.prototype = new Component();
+/**
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCustomTime]
+ */
+CustomTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCustomTime'], this.options, options);
+ }
+};
+
/**
* Create the DOM for the custom time
* @private
diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js
index 678606bd..809aac9f 100644
--- a/src/timeline/component/ItemSet.js
+++ b/src/timeline/component/ItemSet.js
@@ -12,10 +12,45 @@ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
function ItemSet(body, options) {
this.body = body;
- // one options object is shared by this itemset and all its items
- this.options = options || {};
- this.itemOptions = Object.create(this.options);
- this.itemConversion = {
+ this.defaultOptions = {
+ type: 'box',
+ orientation: 'bottom', // 'top' or 'bottom'
+ align: 'center', // alignment of box items
+ stack: true,
+ groupOrder: null,
+
+ selectable: true,
+ editable: {
+ updateTime: false,
+ updateGroup: false,
+ add: false,
+ remove: false
+ },
+
+ onAdd: function (item, callback) {
+ callback(item);
+ },
+ onUpdate: function (item, callback) {
+ callback(item);
+ },
+ onMove: function (item, callback) {
+ callback(item);
+ },
+ onRemove: function (item, callback) {
+ callback(item);
+ },
+
+ margin: {
+ item: 10,
+ axis: 20
+ },
+ padding: 5
+ };
+
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+
+ this.conversion = {
toScreen: body.util.toScreen,
toTime: body.util.toTime
};
@@ -64,6 +99,8 @@ function ItemSet(body, options) {
// create the HTML DOM
this._create();
+
+ this.setOptions(options);
}
ItemSet.prototype = new Component();
@@ -138,7 +175,7 @@ ItemSet.prototype._create = function(){
/**
* Set options for the ItemSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
- * {String} [type]
+ * {String} type
* Default type for the items. Choose from 'box'
* (default), 'point', or 'range'. The default
* Style can be overwritten by individual items.
@@ -149,19 +186,92 @@ ItemSet.prototype._create = function(){
* {String} orientation
* Orientation of the item set. Choose 'top' or
* 'bottom' (default).
+ * {Function} groupOrder
+ * A sorting function for ordering groups
+ * {Boolean} stack
+ * If true (deafult), items will be stacked on
+ * top of each other.
* {Number} margin.axis
* Margin between the axis and the items in pixels.
* Default is 20.
* {Number} margin.item
* Margin between items in pixels. Default is 10.
+ * {Number} margin
+ * Set margin for both axis and items in pixels.
* {Number} padding
* Padding of the contents of an item in pixels.
* Must correspond with the items css. Default is 5.
- * {Function} snap
- * Function to let items snap to nice dates when
- * dragging items.
+ * {Boolean} selectable
+ * If true (default), items can be selected.
+ * {Boolean} editable
+ * Set all editable options to true or false
+ * {Boolean} editable.updateTime
+ * Allow dragging an item to an other moment in time
+ * {Boolean} editable.updateGroup
+ * Allow dragging an item to an other group
+ * {Boolean} editable.add
+ * Allow creating new items on double tap
+ * {Boolean} editable.remove
+ * Allow removing items by clicking the delete button
+ * top right of a selected item.
+ * {Function(item: Item, callback: Function)} onAdd
+ * Callback function triggered when an item is about to be added:
+ * when the user double taps an empty space in the Timeline.
+ * {Function(item: Item, callback: Function)} onUpdate
+ * Callback function fired when an item is about to be updated.
+ * This function typically has to show a dialog where the user
+ * change the item. If not implemented, nothing happens.
+ * {Function(item: Item, callback: Function)} onMove
+ * Fired when an item has been moved. If not implemented,
+ * the move action will be accepted.
+ * {Function(item: Item, callback: Function)} onRemove
+ * Fired when an item is about to be deleted.
+ * If not implemented, the item will be always removed.
*/
-ItemSet.prototype.setOptions = Component.prototype.setOptions;
+ItemSet.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('margin' in options) {
+ if (typeof options.margin === 'number') {
+ this.options.margin.axis = options.margin;
+ this.options.margin.item = options.margin;
+ }
+ else if (typeof options.margin === 'object'){
+ util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin);
+ }
+ }
+
+ if ('editable' in options) {
+ if (typeof options.editable === 'boolean') {
+ this.options.editable.updateTime = options.editable;
+ this.options.editable.updateGroup = options.editable;
+ this.options.editable.add = options.editable;
+ this.options.editable.remove = options.editable;
+ }
+ else if (typeof options.editable === 'object') {
+ util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
+ }
+ }
+
+ // callback functions
+ var addCallback = (function (name) {
+ if (name in options) {
+ var fn = options[name];
+ if (!(fn instanceof Function) || fn.length != 2) {
+ throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
+ }
+ this.options[name] = fn;
+ }
+ }).bind(this);
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
+
+ // force the itemSet to refresh: options like orientation and margins may be changed
+ this.markDirty();
+ }
+};
/**
* Mark the ItemSet dirty so it will refresh everything with next redraw
@@ -281,15 +391,11 @@ ItemSet.prototype.redraw = function() {
options = this.options,
orientation = options.orientation,
resized = false,
- frame = this.dom.frame;
+ frame = this.dom.frame,
+ editable = options.editable.updateTime || options.editable.updateGroup;
- // TODO: document this feature to specify one margin for both item and axis distance
- if (typeof margin === 'number') {
- margin = {
- item: margin,
- axis: margin
- };
- }
+ // update class name
+ frame.className = 'itemset' + (editable ? ' editable' : '');
// reorder the groups (if needed)
resized = this._orderGroups() || resized;
@@ -574,7 +680,7 @@ ItemSet.prototype._onUpdate = function(ids) {
if (!item) {
// create item
if (constructor) {
- item = new constructor(itemData, me.itemConversion, me.itemOptions);
+ item = new constructor(itemData, me.conversion, me.options);
item.id = id; // TODO: not so nice setting id afterwards
me._addItem(item);
}
diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js
index 3d1f268b..3bed910d 100644
--- a/src/timeline/component/TimeAxis.js
+++ b/src/timeline/component/TimeAxis.js
@@ -29,31 +29,38 @@ function TimeAxis (body, options) {
lineTop: 0
};
- this.options = Object.create(options) || {};
this.defaultOptions = {
orientation: 'bottom', // supported: 'top', 'bottom'
// TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true,
showMajorLabels: true
};
+ this.options = util.extend({}, this.defaultOptions);
this.body = body;
// create the HTML DOM
this._create();
+
+ this.setOptions(options);
}
TimeAxis.prototype = new Component();
/**
- * Set parameters for the timeaxis.
- * Parameters will be merged in current parameter set.
- * @param {Object} options Available parameters:
+ * Set options for the TimeAxis.
+ * Parameters will be merged in current options.
+ * @param {Object} options Available options:
* {string} [orientation]
* {boolean} [showMinorLabels]
* {boolean} [showMajorLabels]
*/
-TimeAxis.prototype.setOptions = Component.prototype.setOptions;
+TimeAxis.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
+ }
+};
/**
* Create the HTML DOM for the TimeAxis
@@ -84,9 +91,9 @@ TimeAxis.prototype.redraw = function () {
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');
+ var orientation = this.options.orientation,
+ showMinorLabels = this.options.showMinorLabels,
+ showMajorLabels = this.options.showMajorLabels;
// determine the width and height of the elemens for the axis
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
@@ -132,7 +139,7 @@ TimeAxis.prototype.redraw = function () {
* @private
*/
TimeAxis.prototype._repaintLabels = function () {
- var orientation = this.getOption('orientation');
+ var orientation = this.options.orientation;
// calculate range and step (step such that we have space for 7 characters per label)
var start = util.convert(this.body.range.start, 'Number'),
@@ -166,11 +173,11 @@ TimeAxis.prototype._repaintLabels = function () {
// TODO: lines must have a width, such that we can create css backgrounds
- if (this.getOption('showMinorLabels')) {
+ if (this.options.showMinorLabels) {
this._repaintMinorText(x, step.getLabelMinor(), orientation);
}
- if (isMajor && this.getOption('showMajorLabels')) {
+ if (isMajor && this.options.showMajorLabels) {
if (x > 0) {
if (xFirstMajorLabel == undefined) {
xFirstMajorLabel = x;
@@ -187,7 +194,7 @@ TimeAxis.prototype._repaintLabels = function () {
}
// create a major label on the left when needed
- if (this.getOption('showMajorLabels')) {
+ if (this.options.showMajorLabels) {
var leftTime = this.body.util.toTime(0),
leftText = step.getLabelMajor(leftTime),
widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css
index 2075b92b..413112cf 100644
--- a/src/timeline/component/css/item.css
+++ b/src/timeline/component/css/item.css
@@ -20,7 +20,7 @@
z-index: 999;
}
-.vis.timeline.editable .item.selected {
+.vis.timeline .editable .item.selected {
cursor: move;
}
diff --git a/src/util.js b/src/util.js
index 48ba74de..a68c3455 100644
--- a/src/util.js
+++ b/src/util.js
@@ -88,7 +88,34 @@ util.extend = function (a, b) {
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var prop in other) {
- if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+
+ return a;
+};
+
+/**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+util.selectiveExtend = function (props, a, b) {
+ if (!Array.isArray(props)) {
+ throw new Error('Array with property names expected as first argument');
+ }
+
+ for (var i = 1, len = arguments.length; i < len; i++) {
+ var other = arguments[i];
+
+ for (var p = 0, pp = props.length; p < pp; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
a[prop] = other[prop];
}
}