vendor/assets/vis/timeline/Timeline.js in vis-rails-0.0.6 vs vendor/assets/vis/timeline/Timeline.js in vis-rails-1.0.0

- old
+ new

@@ -4,16 +4,28 @@ * @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.options = { orientation: 'bottom', + direction: 'horizontal', // 'horizontal' or 'vertical' autoResize: true, - editable: false, + stack: true, + + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, + selectable: true, snap: null, // will be specified after timeaxis is created min: null, max: null, @@ -25,10 +37,18 @@ showMinorLabels: true, showMajorLabels: true, showCurrentTime: false, showCustomTime: false, + type: 'box', + align: 'center', + margin: { + axis: 20, + item: 10 + }, + padding: 5, + onAdd: function (item, callback) { callback(item); }, onUpdate: function (item, callback) { callback(item); @@ -36,157 +56,245 @@ onMove: function (item, callback) { callback(item); }, onRemove: function (item, callback) { callback(item); - } + }, + + toScreen: me._toScreen.bind(me), + toTime: me._toTime.bind(me) }; - // controller - this.controller = new Controller(); - // root panel - if (!container) { - throw new Error('No container element provided'); - } - var rootOptions = Object.create(this.options); - rootOptions.height = function () { - // TODO: change to height - if (me.options.height) { - // fixed height - return me.options.height; + 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'; + } } - else { - // auto height - return (me.timeaxis.height + me.content.height) + 'px'; - } - }; + }); this.rootPanel = new RootPanel(container, rootOptions); - this.controller.add(this.rootPanel); // single select (or unselect) when tapping an item - this.controller.on('tap', this._onSelectItem.bind(this)); + this.rootPanel.on('tap', this._onSelectItem.bind(this)); // multi select when holding mouse/touch, or on ctrl+click - this.controller.on('hold', this._onMultiSelectItem.bind(this)); + this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); // add item on doubletap - this.controller.on('doubletap', this._onAddItem.bind(this)); + this.rootPanel.on('doubletap', this._onAddItem.bind(this)); - // item panel - var itemOptions = Object.create(this.options); - itemOptions.left = function () { - return me.labelPanel.width; - }; - itemOptions.width = function () { - return me.rootPanel.width - me.labelPanel.width; - }; - itemOptions.top = null; - itemOptions.height = null; - this.itemPanel = new Panel(this.rootPanel, [], itemOptions); - this.controller.add(this.itemPanel); - - // label panel - var labelOptions = Object.create(this.options); - labelOptions.top = null; - labelOptions.left = null; - labelOptions.height = null; - labelOptions.width = function () { - if (me.content && typeof me.content.getLabelsWidth === 'function') { - return me.content.getLabelsWidth(); + // side panel + var sideOptions = util.extend(Object.create(this.options), { + top: function () { + return (sideOptions.orientation == 'top') ? '0' : ''; + }, + 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'); } - else { - return 0; - } - }; - this.labelPanel = new Panel(this.rootPanel, [], labelOptions); - this.controller.add(this.labelPanel); + }); + 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(rangeOptions); + 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.subscribe(this.controller, this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { - var force = true; - me.controller.emit('rangechange', properties); - me.controller.emit('request-reflow', force); + me.rootPanel.repaint(); + me.emit('rangechange', properties); }); this.range.on('rangechanged', function (properties) { - var force = true; - me.controller.emit('rangechanged', properties); - me.controller.emit('request-reflow', force); + me.rootPanel.repaint(); + me.emit('rangechanged', properties); }); - // time axis - var timeaxisOptions = Object.create(rootOptions); - timeaxisOptions.range = this.range; - timeaxisOptions.left = null; - timeaxisOptions.top = null; - timeaxisOptions.width = '100%'; - timeaxisOptions.height = null; - this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions); - this.timeaxis.setRange(this.range); - this.controller.add(this.timeaxis); - this.options.snap = this.timeaxis.snap.bind(this.timeaxis); + // 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); + // 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 - this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions); - this.controller.add(this.currenttime); + // Note: time bar will be attached in this.setOptions when selected + this.currentTime = new CurrentTime(this.range, rootOptions); // custom time bar - this.customtime = new CustomTime(this.timeaxis, [], rootOptions); - this.controller.add(this.customtime); + // 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); + }); - // create groupset - this.setGroups(null); + // 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); this.itemsData = null; // DataSet this.groupsData = null; // DataSet // apply options if (options) { this.setOptions(options); } - // create itemset and groupset + // create itemset if (items) { this.setItems(items); } } -/** - * Add an event listener to the timeline - * @param {String} event Available events: select, rangechange, rangechanged, - * timechange, timechanged - * @param {function} callback - */ -Timeline.prototype.on = function on (event, callback) { - this.controller.on(event, callback); -}; +// turn Timeline into an event emitter +Emitter(Timeline.prototype); /** - * Add an event listener from the timeline - * @param {String} event - * @param {function} callback - */ -Timeline.prototype.off = function off (event, callback) { - this.controller.off(event, callback); -}; - -/** * Set options * @param {Object} options TODO: describe the available options */ Timeline.prototype.setOptions = function (options) { util.extend(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) { @@ -198,44 +306,78 @@ // 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); - this.controller.reflow(); - this.controller.repaint(); + // 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); + } + } + + // add/remove the custom time bar + if (this.options.showCustomTime) { + if (!this.mainPanel.hasChild(this.customTime)) { + this.mainPanel.appendChild(this.customTime); + } + } + else { + if (this.mainPanel.hasChild(this.customTime)) { + this.mainPanel.removeChild(this.customTime); + } + } + + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error('Option order is deprecated. There is no replacement for this feature.'); + } + + // repaint everything + this.rootPanel.repaint(); }; /** * Set a custom time bar * @param {Date} time */ Timeline.prototype.setCustomTime = function (time) { - if (!this.customtime) { + if (!this.customTime) { throw new Error('Cannot get custom time: Custom time bar is not enabled'); } - this.customtime.setCustomTime(time); + this.customTime.setCustomTime(time); }; /** * Retrieve the current custom time. * @return {Date} customTime */ Timeline.prototype.getCustomTime = function() { - if (!this.customtime) { + if (!this.customTime) { throw new Error('Cannot get custom time: Custom time bar is not enabled'); } - return this.customtime.getCustomTime(); + return this.customTime.getCustomTime(); }; /** * Set items * @param {vis.DataSet | Array | google.visualization.DataTable | null} items @@ -246,134 +388,85 @@ // convert to type DataSet when needed var newDataSet; if (!items) { newDataSet = null; } - else if (items instanceof DataSet) { + else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; } - if (!(items instanceof DataSet)) { - newDataSet = new DataSet({ + else { + // turn an array into a dataset + newDataSet = new DataSet(items, { convert: { start: 'Date', end: 'Date' } }); - newDataSet.add(items); } // set items this.itemsData = newDataSet; - this.content.setItems(newDataSet); + this.itemSet.setItems(newDataSet); if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { - // apply the data range as range - var dataRange = this.getItemRange(); + this.fit(); - // add 5% space on both sides - var start = dataRange.min; - var end = dataRange.max; - if (start != null && end != null) { - var interval = (end.valueOf() - start.valueOf()); - if (interval <= 0) { - // prevent an empty interval - interval = 24 * 60 * 60 * 1000; // 1 day - } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } + 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; - // override specified start and/or end date - if (this.options.start != undefined) { - start = util.convert(this.options.start, 'Date'); - } - if (this.options.end != undefined) { - end = util.convert(this.options.end, 'Date'); - } - - // apply range if there is a min or max available - if (start != null || end != null) { - this.range.setRange(start, end); - } + this.setWindow(start, end); } }; /** * Set groups * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ -Timeline.prototype.setGroups = function(groups) { - var me = this; - this.groupsData = groups; +Timeline.prototype.setGroups = function setGroups(groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } + else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } + else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - // switch content type between ItemSet or GroupSet when needed - var Type = this.groupsData ? GroupSet : ItemSet; - if (!(this.content instanceof Type)) { - // remove old content set - if (this.content) { - this.content.hide(); - if (this.content.setItems) { - this.content.setItems(); // disconnect from items - } - if (this.content.setGroups) { - this.content.setGroups(); // disconnect from groups - } - this.controller.remove(this.content); - } + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); +}; - // create new content set - var options = Object.create(this.options); - util.extend(options, { - top: function () { - if (me.options.orientation == 'top') { - return me.timeaxis.height; - } - else { - return me.itemPanel.height - me.timeaxis.height - me.content.height; - } - }, - left: null, - width: '100%', - height: function () { - if (me.options.height) { - // fixed height - return me.itemPanel.height - me.timeaxis.height; - } - else { - // auto height - return null; - } - }, - maxHeight: function () { - // TODO: change maxHeight to be a css string like '100%' or '300px' - if (me.options.maxHeight) { - if (!util.isNumber(me.options.maxHeight)) { - throw new TypeError('Number expected for property maxHeight'); - } - return me.options.maxHeight - me.timeaxis.height; - } - else { - return null; - } - }, - labelContainer: function () { - return me.labelPanel.getContainer(); - } - }); +/** + * Set Timeline window such that it fits all items + */ +Timeline.prototype.fit = function fit() { + // apply the data range as range + var dataRange = this.getItemRange(); - this.content = new Type(this.itemPanel, [this.timeaxis], options); - if (this.content.setRange) { - this.content.setRange(this.range); + // add 5% space on both sides + var start = dataRange.min; + var end = dataRange.max; + if (start != null && end != null) { + var interval = (end.valueOf() - start.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day } - if (this.content.setItems) { - this.content.setItems(this.itemsData); - } - if (this.content.setGroups) { - this.content.setGroups(this.groupsData); - } - this.controller.add(this.content); + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); } + + // skip range set if there is no start and end date + if (start === null && end === null) { + return; + } + + this.range.setRange(start, end); }; /** * Get the data range of the item set. * @returns {{min: Date, max: Date}} range A range with a start and end Date. @@ -419,29 +512,42 @@ * @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) { - if (this.content) this.content.setSelection(ids); + 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.content ? this.content.getSelection() : []; + return this.itemSet.getSelection(); }; /** * Set the visible window. Both parameters are optional, you can change only - * start or only end. + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * * @param {Date | Number | String} [start] Start date of visible window * @param {Date | Number | String} [end] End date of visible window */ Timeline.prototype.setWindow = function setWindow(start, end) { - this.range.setRange(start, end); + if (arguments.length == 1) { + var range = arguments[0]; + this.range.setRange(range.start, range.end); + } + else { + this.range.setRange(start, end); + } }; /** * Get the visible window * @return {{start: Date, end: Date}} Visible range @@ -468,30 +574,36 @@ if (ctrlKey || shiftKey) { this._onMultiSelectItem(event); return; } - var item = ItemSet.itemFromTarget(event); + var oldSelection = this.getSelection(); + var item = ItemSet.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); - this.controller.emit('select', { - items: this.getSelection() - }); + var newSelection = this.getSelection(); + // if selection is changed, emit a select event + if (!util.equalArray(oldSelection, newSelection)) { + this.emit('select', { + items: this.getSelection() + }); + } + event.stopPropagation(); }; /** * Handle creation and updates of an item on double tap * @param event * @private */ Timeline.prototype._onAddItem = function (event) { if (!this.options.selectable) return; - if (!this.options.editable) return; + if (!this.options.editable.add) return; var me = this, item = ItemSet.itemFromTarget(event); if (item) { @@ -505,38 +617,30 @@ } }); } else { // add item - var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame); + var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame); var x = event.gesture.center.pageX - xAbs; var newItem = { - start: this.timeaxis.snap(this._toTime(x)), + start: this.timeAxis.snap(this._toTime(x)), content: 'new item' }; var id = util.randomUUID(); newItem[this.itemsData.fieldId] = id; - var group = GroupSet.groupFromTarget(event); + var group = ItemSet.groupFromTarget(event); if (group) { newItem.group = group.groupId; } // execute async handler to customize (or cancel) adding an item this.options.onAdd(newItem, function (item) { if (item) { me.itemsData.add(newItem); - - // select the created item after it is repainted - me.controller.once('repaint', function () { - me.setSelection([id]); - - me.controller.emit('select', { - items: me.getSelection() - }); - }.bind(me)); + // TODO: need to trigger a repaint? } }); } }; @@ -564,11 +668,11 @@ // item is already selected -> deselect it selection.splice(index, 1); } this.setSelection(selection); - this.controller.emit('select', { + this.emit('select', { items: this.getSelection() }); event.stopPropagation(); } @@ -579,11 +683,11 @@ * @param {int} x Position on the screen in pixels * @return {Date} time The datetime the corresponds with given position x * @private */ Timeline.prototype._toTime = function _toTime(x) { - var conversion = this.range.conversion(this.content.width); + var conversion = this.range.conversion(this.mainPanel.width); return new Date(x / conversion.scale + conversion.offset); }; /** * Convert a datetime (Date object) into a position on the screen @@ -591,8 +695,8 @@ * @return {int} x The position on the screen in pixels which corresponds * with the given date. * @private */ Timeline.prototype._toScreen = function _toScreen(time) { - var conversion = this.range.conversion(this.content.width); + var conversion = this.range.conversion(this.mainPanel.width); return (time.valueOf() - conversion.offset) * conversion.scale; };