Browse Source

Reworked options and setOptions

css_transitions
jos 10 years ago
parent
commit
f672f7b583
11 changed files with 303 additions and 206 deletions
  1. +8
    -3
      HISTORY.md
  2. +1
    -1
      docs/timeline.html
  3. +23
    -5
      src/timeline/Range.js
  4. +57
    -135
      src/timeline/Timeline.js
  5. +3
    -28
      src/timeline/component/Component.js
  6. +19
    -1
      src/timeline/component/CurrentTime.js
  7. +20
    -1
      src/timeline/component/CustomTime.js
  8. +124
    -18
      src/timeline/component/ItemSet.js
  9. +19
    -12
      src/timeline/component/TimeAxis.js
  10. +1
    -1
      src/timeline/component/css/item.css
  11. +28
    -1
      src/util.js

+ 8
- 3
HISTORY.md View File

@ -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

+ 1
- 1
docs/timeline.html View File

@ -533,7 +533,7 @@ var options = {
<tr>
<td>showCurrentTime</td>
<td>boolean</td>
<td>false</td>
<td>true</td>
<td>Show a vertical bar at the current time.</td>
</tr>

+ 23
- 5
src/timeline/Range.js View File

@ -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);
}
}
};

+ 57
- 135
src/timeline/Timeline.js View File

@ -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;

+ 3
- 28
src/timeline/component/Component.js View File

@ -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;
};
/**

+ 19
- 1
src/timeline/component/CurrentTime.js View File

@ -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

+ 20
- 1
src/timeline/component/CustomTime.js View File

@ -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

+ 124
- 18
src/timeline/component/ItemSet.js View File

@ -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);
}

+ 19
- 12
src/timeline/component/TimeAxis.js View File

@ -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

+ 1
- 1
src/timeline/component/css/item.css View File

@ -20,7 +20,7 @@
z-index: 999;
}
.vis.timeline.editable .item.selected {
.vis.timeline .editable .item.selected {
cursor: move;
}

+ 28
- 1
src/util.js View File

@ -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.<String>} 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];
}
}

Loading…
Cancel
Save