vendor/assets/vis/timeline/Timeline.js in vis-rails-1.0.2 vs vendor/assets/vis/timeline/Timeline.js in vis-rails-2.0.0
- old
+ new
@@ -4,273 +4,68 @@
* @param {vis.DataSet | Array | google.visualization.DataTable} [items]
* @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor
*/
function Timeline (container, items, options) {
- // validate arguments
- if (!container) throw new Error('No container element provided');
-
var me = this;
- var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.defaultOptions = {
- orientation: 'bottom',
- direction: 'horizontal', // 'horizontal' or 'vertical'
+ start: null,
+ end: null,
+
autoResize: true,
- stack: true,
- editable: {
- updateTime: false,
- updateGroup: false,
- add: false,
- remove: false
- },
-
- selectable: true,
-
- 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,
-
+ orientation: 'bottom',
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.defaultOptions);
- this.options = {};
- util.deepExtend(this.options, this.defaultOptions);
- util.deepExtend(this.options, {
- snap: null, // will be specified after timeaxis is created
+ // Create the DOM, props, and emitter
+ this._create(container);
- toScreen: me._toScreen.bind(me),
- toTime: me._toTime.bind(me)
- });
+ // all components listed here will be repainted automatically
+ this.components = [];
- // root panel
- var rootOptions = util.extend(Object.create(this.options), {
- height: function () {
- if (me.options.height) {
- // fixed height
- return me.options.height;
- }
- else {
- // auto height
- // TODO: implement a css based solution to automatically have the right hight
- return (me.timeAxis.height + me.contentPanel.height) + 'px';
- }
- }
- });
- this.rootPanel = new RootPanel(container, rootOptions);
-
- // single select (or unselect) when tapping an item
- this.rootPanel.on('tap', this._onSelectItem.bind(this));
-
- // multi select when holding mouse/touch, or on ctrl+click
- this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
-
- // add item on doubletap
- this.rootPanel.on('doubletap', this._onAddItem.bind(this));
-
- // side panel
- var sideOptions = util.extend(Object.create(this.options), {
- top: function () {
- return (sideOptions.orientation == 'top') ? '0' : '';
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
},
- bottom: function () {
- return (sideOptions.orientation == 'top') ? '' : '0';
- },
- left: '0',
- right: null,
- height: '100%',
- width: function () {
- if (me.itemSet) {
- return me.itemSet.getLabelsWidth();
- }
- else {
- return 0;
- }
- },
- className: function () {
- return 'side' + (me.groupsData ? '' : ' hidden');
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toTime: me._toTime.bind(me)
}
- });
- this.sidePanel = new Panel(sideOptions);
- this.rootPanel.appendChild(this.sidePanel);
+ };
- // main panel (contains time axis and itemsets)
- var mainOptions = util.extend(Object.create(this.options), {
- left: function () {
- // we align left to enable a smooth resizing of the window
- return me.sidePanel.width;
- },
- right: null,
- height: '100%',
- width: function () {
- return me.rootPanel.width - me.sidePanel.width;
- },
- className: 'main'
- });
- this.mainPanel = new Panel(mainOptions);
- this.rootPanel.appendChild(this.mainPanel);
-
// range
- // TODO: move range inside rootPanel?
- var rangeOptions = Object.create(this.options);
- this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
- this.range.setRange(
- now.clone().add('days', -3).valueOf(),
- now.clone().add('days', 4).valueOf()
- );
- this.range.on('rangechange', function (properties) {
- me.rootPanel.repaint();
- me.emit('rangechange', properties);
- });
- this.range.on('rangechanged', function (properties) {
- me.rootPanel.repaint();
- me.emit('rangechanged', properties);
- });
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
- // panel with time axis
- var timeAxisOptions = util.extend(Object.create(rootOptions), {
- range: this.range,
- left: null,
- top: null,
- width: null,
- height: null
- });
- this.timeAxis = new TimeAxis(timeAxisOptions);
- this.timeAxis.setRange(this.range);
- this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
- this.mainPanel.appendChild(this.timeAxis);
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
- // content panel (contains itemset(s))
- var contentOptions = util.extend(Object.create(this.options), {
- top: function () {
- return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
- },
- bottom: function () {
- return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
- },
- left: null,
- right: null,
- height: null,
- width: null,
- className: 'content'
- });
- this.contentPanel = new Panel(contentOptions);
- this.mainPanel.appendChild(this.contentPanel);
-
- // content panel (contains the vertical lines of box items)
- var backgroundOptions = util.extend(Object.create(this.options), {
- top: function () {
- return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
- },
- bottom: function () {
- return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
- },
- left: null,
- right: null,
- height: function () {
- return me.contentPanel.height;
- },
- width: null,
- className: 'background'
- });
- this.backgroundPanel = new Panel(backgroundOptions);
- this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
-
- // panel with axis holding the dots of item boxes
- var axisPanelOptions = util.extend(Object.create(rootOptions), {
- left: 0,
- top: function () {
- return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
- },
- bottom: function () {
- return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
- },
- width: '100%',
- height: 0,
- className: 'axis'
- });
- this.axisPanel = new Panel(axisPanelOptions);
- this.mainPanel.appendChild(this.axisPanel);
-
- // content panel (contains itemset(s))
- var sideContentOptions = util.extend(Object.create(this.options), {
- top: function () {
- return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
- },
- bottom: function () {
- return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
- },
- left: null,
- right: null,
- height: null,
- width: null,
- className: 'side-content'
- });
- this.sideContentPanel = new Panel(sideContentOptions);
- this.sidePanel.appendChild(this.sideContentPanel);
-
// current time bar
- // Note: time bar will be attached in this.setOptions when selected
- this.currentTime = new CurrentTime(this.range, rootOptions);
+ 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(rootOptions);
- this.customTime.on('timechange', function (time) {
- me.emit('timechange', time);
- });
- this.customTime.on('timechanged', function (time) {
- me.emit('timechanged', time);
- });
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- // itemset containing items and groups
- var itemOptions = util.extend(Object.create(this.options), {
- left: null,
- right: null,
- top: null,
- bottom: null,
- width: null,
- height: null
- });
- this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
- this.itemSet.setRange(this.range);
- this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
- this.contentPanel.appendChild(this.itemSet);
+ // item set
+ this.itemSet = new ItemSet(this.body);
+ this.components.push(this.itemSet);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
// apply options
@@ -280,92 +75,221 @@
// create itemset
if (items) {
this.setItems(items);
}
+ else {
+ this.redraw();
+ }
}
// turn Timeline into an event emitter
Emitter(Timeline.prototype);
/**
- * Set options
- * @param {Object} options TODO: describe the available options
+ * Create the main DOM for the Timeline: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Timeline will
+ * be attached.
+ * @private
*/
-Timeline.prototype.setOptions = function (options) {
- util.deepExtend(this.options, options);
+Timeline.prototype._create = function (container) {
+ this.dom = {};
- if ('editable' in options) {
- var isBoolean = typeof options.editable === 'boolean';
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
- 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)
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
+
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontal);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
+
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
+
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('change', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.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.emit.apply(me, args);
};
- }
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
- // force update of range (apply new min/max etc.)
- // both start and end are optional
- this.range.setRange(options.start, options.end);
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
- if ('editable' in options || 'selectable' in options) {
- if (this.options.selectable) {
- // force update of selection
- this.setSelection(this.getSelection());
- }
- else {
- // remove selection
- this.setSelection([]);
- }
- }
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
+};
- // force the itemSet to refresh: options like orientation and margins may be changed
- this.itemSet.markDirty();
+/**
+ * Destroy the Timeline, clean up all DOM elements and event listeners.
+ */
+Timeline.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
- // 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);
+ // remove all event listeners
+ this.off();
- // add/remove the current time bar
- if (this.options.showCurrentTime) {
- if (!this.mainPanel.hasChild(this.currentTime)) {
- this.mainPanel.appendChild(this.currentTime);
- this.currentTime.start();
- }
+ // stop checking for changed size
+ this._stopAutoResize();
+
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
}
- else {
- if (this.mainPanel.hasChild(this.currentTime)) {
- this.currentTime.stop();
- this.mainPanel.removeChild(this.currentTime);
+ this.dom = null;
+
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
}
}
+ this.listeners = null;
+ this.hammer = null;
- // add/remove the custom time bar
- if (this.options.showCustomTime) {
- if (!this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.appendChild(this.customTime);
- }
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
+
+ this.body = null;
+};
+
+/**
+ * Set options. Options will be passed to all components loaded in the Timeline.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Timeline,
+ * can be 'bottom' (default) or 'top'.
+ * {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) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
+ util.selectiveExtend(fields, this.options, options);
+
+ // enable/disable autoResize
+ this._initAutoResize();
}
- else {
- if (this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.removeChild(this.customTime);
- }
- }
+ // 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) {
throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
- // repaint everything
- this.rootPanel.repaint();
+ // redraw everything
+ this.redraw();
};
/**
* Set a custom time bar
* @param {Date} time
@@ -406,36 +330,36 @@
newDataSet = items;
}
else {
// turn an array into a dataset
newDataSet = new DataSet(items, {
- convert: {
+ type: {
start: 'Date',
end: 'Date'
}
});
}
// 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)) {
+ 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);
}
};
/**
* Set groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
-Timeline.prototype.setGroups = function setGroups(groups) {
+Timeline.prototype.setGroups = function(groups) {
// convert to type DataSet when needed
var newDataSet;
if (!groups) {
newDataSet = null;
}
@@ -459,31 +383,35 @@
* timeline.clear({options: true}); // clear options only
*
* @param {Object} [what] Optionally specify what to clear. By default:
* {items: true, groups: true, options: true}
*/
-Timeline.prototype.clear = function clear(what) {
+Timeline.prototype.clear = function(what) {
// clear items
if (!what || what.items) {
this.setItems(null);
}
// clear groups
if (!what || what.groups) {
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
}
};
/**
* Set Timeline window such that it fits all items
*/
-Timeline.prototype.fit = function fit() {
+Timeline.prototype.fit = function() {
// apply the data range as range
var dataRange = this.getItemRange();
// add 5% space on both sides
var start = dataRange.min;
@@ -510,33 +438,35 @@
* Get the data range of the item set.
* @returns {{min: Date, max: Date}} range A range with a start and end Date.
* When no minimum is found, min==null
* When no maximum is found, max==null
*/
-Timeline.prototype.getItemRange = function getItemRange() {
+Timeline.prototype.getItemRange = function() {
// calculate min from start filed
var itemsData = this.itemsData,
min = null,
max = null;
if (itemsData) {
// calculate the minimum value of the field 'start'
var minItem = itemsData.min('start');
- min = minItem ? minItem.start.valueOf() : null;
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
// calculate maximum value of fields 'start' and 'end'
var maxStartItem = itemsData.max('start');
if (maxStartItem) {
- max = maxStartItem.start.valueOf();
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
}
var maxEndItem = itemsData.max('end');
if (maxEndItem) {
if (max == null) {
- max = maxEndItem.end.valueOf();
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
}
else {
- max = Math.max(max, maxEndItem.end.valueOf());
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
}
}
}
return {
@@ -550,20 +480,20 @@
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
-Timeline.prototype.setSelection = function setSelection (ids) {
- this.itemSet.setSelection(ids);
+Timeline.prototype.setSelection = function(ids) {
+ this.itemSet && this.itemSet.setSelection(ids);
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
-Timeline.prototype.getSelection = function getSelection() {
- return this.itemSet.getSelection();
+Timeline.prototype.getSelection = function() {
+ return this.itemSet && this.itemSet.getSelection() || [];
};
/**
* Set the visible window. Both parameters are optional, you can change only
* start or only end. Syntax:
@@ -575,11 +505,11 @@
* object with properties start and end.
*
* @param {Date | Number | String | Object} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
-Timeline.prototype.setWindow = function setWindow(start, end) {
+Timeline.prototype.setWindow = function(start, end) {
if (arguments.length == 1) {
var range = arguments[0];
this.range.setRange(range.start, range.end);
}
else {
@@ -589,166 +519,333 @@
/**
* Get the visible window
* @return {{start: Date, end: Date}} Visible range
*/
-Timeline.prototype.getWindow = function setWindow() {
+Timeline.prototype.getWindow = function() {
var range = this.range.getRange();
return {
start: new Date(range.start),
end: new Date(range.end)
};
};
/**
- * Force a repaint of the Timeline. Can be useful to manually repaint when
+ * Force a redraw of the Timeline. Can be useful to manually redraw when
* option autoResize=false
*/
-Timeline.prototype.repaint = function repaint() {
- this.rootPanel.repaint();
-};
+Timeline.prototype.redraw = function() {
+ var resized = false,
+ options = this.options,
+ props = this.props,
+ dom = this.dom;
-/**
- * Handle selecting/deselecting an item when tapping it
- * @param {Event} event
- * @private
- */
-// TODO: move this function to ItemSet
-Timeline.prototype._onSelectItem = function (event) {
- if (!this.options.selectable) return;
+ if (!dom) return; // when destroyed
- var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
- var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
- if (ctrlKey || shiftKey) {
- this._onMultiSelectItem(event);
- return;
- }
+ // update class names
+ dom.root.className = 'vis timeline root ' + options.orientation;
- var oldSelection = this.getSelection();
+ // 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, '');
- var item = ItemSet.itemFromTarget(event);
- var selection = item ? [item.id] : [];
- this.setSelection(selection);
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
- var newSelection = this.getSelection();
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
- // if selection is changed, emit a select event
- if (!util.equalArray(oldSelection, newSelection)) {
- this.emit('select', {
- items: this.getSelection()
- });
+ // TODO: compensate borders when any of the panels is empty.
+
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
+
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
+
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
+
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
+
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = props.left.width + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
+
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Timeline or of the contents of the center changed
+ this._updateScrollTop();
+
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0);
}
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
- event.stopPropagation();
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
+
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep repainting until all sizes are settled
+ this.redraw();
+ }
};
+// TODO: deprecated since version 1.1.0, remove some day
+Timeline.prototype.repaint = function () {
+ throw new Error('Function repaint is deprecated. Use redraw instead.');
+};
+
/**
- * Handle creation and updates of an item on double tap
- * @param event
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
* @private
*/
-Timeline.prototype._onAddItem = function (event) {
- if (!this.options.selectable) return;
- if (!this.options.editable.add) return;
+// TODO: move this function to Range
+Timeline.prototype._toTime = function(x) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return new Date(x / conversion.scale + conversion.offset);
+};
- var me = this,
- item = ItemSet.itemFromTarget(event);
+/**
+ * Convert a datetime (Date object) into a position on the screen
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+// TODO: move this function to Range
+Timeline.prototype._toScreen = function(time) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+};
- if (item) {
- // update item
-
- // execute async handler to update the item (or cancel it)
- var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
- this.options.onUpdate(itemData, function (itemData) {
- if (itemData) {
- me.itemsData.update(itemData);
- }
- });
+/**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+Timeline.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
}
else {
- // add item
- var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
- var x = event.gesture.center.pageX - xAbs;
- var newItem = {
- start: this.timeAxis.snap(this._toTime(x)),
- content: 'new item'
- };
+ this._stopAutoResize();
+ }
+};
- // when default type is a range, add a default end date to the new item
- if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
- newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
- }
+/**
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+Timeline.prototype._startAutoResize = function () {
+ var me = this;
- var id = util.randomUUID();
- newItem[this.itemsData.fieldId] = id;
+ this._stopAutoResize();
- var group = ItemSet.groupFromTarget(event);
- if (group) {
- newItem.group = group.groupId;
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
}
- // execute async handler to customize (or cancel) adding an item
- this.options.onAdd(newItem, function (item) {
- if (item) {
- me.itemsData.add(newItem);
- // TODO: need to trigger a repaint?
+ if (me.dom.root) {
+ // check whether the frame is resized
+ if ((me.dom.root.clientWidth != me.props.lastWidth) ||
+ (me.dom.root.clientHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.clientWidth;
+ me.props.lastHeight = me.dom.root.clientHeight;
+
+ me.emit('change');
}
- });
+ }
+ };
+
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
+
+ this.watchTimer = setInterval(this._onResize, 1000);
+};
+
+/**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+Timeline.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
}
+
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
};
/**
- * Handle selecting/deselecting multiple items when holding an item
+ * Start moving the timeline vertically
* @param {Event} event
* @private
*/
-// TODO: move this function to ItemSet
-Timeline.prototype._onMultiSelectItem = function (event) {
- if (!this.options.selectable) return;
+Timeline.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
+};
- var selection,
- item = ItemSet.itemFromTarget(event);
+/**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Timeline.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
+};
- if (item) {
- // multi select items
- selection = this.getSelection(); // current selection
- var index = selection.indexOf(item.id);
- if (index == -1) {
- // item is not yet selected -> select it
- selection.push(item.id);
- }
- else {
- // item is already selected -> deselect it
- selection.splice(index, 1);
- }
- this.setSelection(selection);
+/**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Timeline.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+};
- this.emit('select', {
- items: this.getSelection()
- });
+/**
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Timeline.prototype._onDrag = function (event) {
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (!this.touch.allowDragging) return;
- event.stopPropagation();
+ var delta = event.gesture.deltaY;
+
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
}
};
/**
- * Convert a position on screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
-Timeline.prototype._toTime = function _toTime(x) {
- var conversion = this.range.conversion(this.mainPanel.width);
- return new Date(x / conversion.scale + conversion.offset);
+Timeline.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
};
/**
- * Convert a datetime (Date object) into a position on the screen
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
-Timeline.prototype._toScreen = function _toScreen(time) {
- var conversion = this.range.conversion(this.mainPanel.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
+Timeline.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ }
+ this.props.scrollTopMin = scrollTopMin;
+ }
+
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+
+ return this.props.scrollTop;
+};
+
+/**
+ * Get the current scrollTop
+ * @returns {number} scrollTop
+ * @private
+ */
+Timeline.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
};