diff --git a/docs/timeline/index.html b/docs/timeline/index.html index 14e71561..09acb325 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -946,6 +946,29 @@ function (option, path) { Callback function triggered when a group is about to be removed. The signature and semantics are the same as for onRemove. + + + onTimeout + Object + Object + Specify timeline bailing options when a specified timeout is reached. + + + onTimeout.timeoutMs + number + none + Number of milliseconds until the callback function should be called. + The callback will not be called if the timeline gets drawn completely before the timeoutMs limit. + + + + onTimeout.callback + function + none + + A callback function called when timeoutMs milliseconds pass and the timeline has yet to be fully drawn initially. + + onUpdate diff --git a/examples/timeline/other/onTimeout.html b/examples/timeline/other/onTimeout.html new file mode 100644 index 00000000..7f77822d --- /dev/null +++ b/examples/timeline/other/onTimeout.html @@ -0,0 +1,55 @@ + + + + + + Timeline | onTimeout example + + + + + + +
+ + + + + \ No newline at end of file diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index d3652641..dee44004 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -969,7 +969,7 @@ Core.prototype._redraw = function() { props.border.top - props.border.bottom, 0); } dom.center.style.top = offset + 'px'; - + // show shadows when vertical scrolling is available var visibilityTop = props.scrollTop == 0 ? 'hidden' : ''; var visibilityBottom = props.scrollTop == props.scrollTopMin ? 'hidden' : ''; diff --git a/lib/timeline/Stack.js b/lib/timeline/Stack.js index b25179b4..71e92277 100644 --- a/lib/timeline/Stack.js +++ b/lib/timeline/Stack.js @@ -35,8 +35,11 @@ exports.orderByEnd = function(items) { * @param {boolean} [force=false] * If true, all items will be repositioned. If false (default), only * items having a top===null will be re-stacked + * @param {function} shouldBailItemsRedrawFunction + * bailing function + * @return {boolean} shouldBail */ -exports.stack = function(items, margin, force) { +exports.stack = function(items, margin, force, shouldBailItemsRedrawFunction) { if (force) { // reset top position of all items for (var i = 0; i < items.length; i++) { @@ -50,6 +53,7 @@ exports.stack = function(items, margin, force) { if (item.stack && item.top === null) { // initialize top position item.top = margin.axis; + var shouldBail = false; do { // TODO: optimize checking for overlap. when there is a gap without items, @@ -57,6 +61,10 @@ exports.stack = function(items, margin, force) { var collidingItem = null; for (var j = 0, jj = items.length; j < jj; j++) { var other = items[j]; + shouldBail = shouldBailItemsRedrawFunction() || false; + + if (shouldBail) { return true; } + if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item, other.options.rtl)) { collidingItem = other; break; @@ -70,6 +78,7 @@ exports.stack = function(items, margin, force) { } while (collidingItem); } } + return shouldBail; }; /** diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index ba83f36f..2c2f5b22 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -28,6 +28,9 @@ var Validator = require('../shared/Validator').default; */ function Timeline (container, items, groups, options) { + this.initTime = new Date(); + this.itemsDone = false; + if (!(this instanceof Timeline)) { throw new SyntaxError('Constructor must be called with the new operator'); } @@ -78,6 +81,7 @@ function Timeline (container, items, groups, options) { this.options.rollingMode = options && options.rollingMode; this.options.onInitialDrawComplete = options && options.onInitialDrawComplete; + this.options.onTimeout = options && options.onTimeout; this.options.loadingScreenTemplate = options && options.loadingScreenTemplate; // Prepare loading screen @@ -202,6 +206,7 @@ function Timeline (container, items, groups, options) { if (!me.initialDrawDone && (me.initialRangeChangeDone || (!me.options.start && !me.options.end) || me.options.rollingMode)) { me.initialDrawDone = true; + me.itemSet.initialDrawDone = true; me.dom.root.style.visibility = 'visible'; me.dom.loadingScreen.parentNode.removeChild(me.dom.loadingScreen); if (me.options.onInitialDrawComplete) { @@ -212,6 +217,10 @@ function Timeline (container, items, groups, options) { } }); + this.on('destroyTimeline', () => { + me.destroy() + }); + // apply options if (options) { this.setOptions(options); @@ -285,6 +294,8 @@ Timeline.prototype.setOptions = function (options) { * @param {vis.DataSet | Array | null} items */ Timeline.prototype.setItems = function(items) { + this.itemsDone = false; + // convert to type DataSet when needed var newDataSet; if (!items) { diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 14e76d10..8b748735 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -13,6 +13,7 @@ function Group (groupId, data, itemSet) { this.subgroupStack = {}; this.subgroupStackAll = false; this.doInnerStack = false; + this.shouldBailStackItems = false; this.subgroupIndex = 0; this.subgroupOrderer = data && data.subgroupOrder; this.itemSet = itemSet; @@ -262,6 +263,35 @@ Group.prototype._calculateGroupSizeAndPosition = function() { this.width = offsetWidth; } +Group.prototype._shouldBailItemsRedraw = function() { + var me = this; + var timeoutOptions = this.itemSet.options.onTimeout; + var bailOptions = { + relativeBailingTime: this.itemSet.itemsSettingTime, + bailTimeMs: timeoutOptions && timeoutOptions.timeoutMs, + userBailFunction: timeoutOptions && timeoutOptions.callback, + shouldBailStackItems: this.shouldBailStackItems + }; + var bail = null; + if (!this.itemSet.initialDrawDone) { + if (bailOptions.shouldBailStackItems) { return true; } + if (Math.abs(new Date() - new Date(bailOptions.relativeBailingTime)) > bailOptions.bailTimeMs) { + if (bailOptions.userBailFunction && this.itemSet.userContinueNotBail == null) { + bailOptions.userBailFunction(function(didUserContinue) { + me.itemSet.userContinueNotBail = didUserContinue; + bail = !didUserContinue; + }) + } else if (me.itemSet.userContinueNotBail == false) { + bail = true; + } else { + bail = false; + } + } + } + + return bail; +} + Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, range) { var restack = forceRestack || this.stackDirty || this.isVisible && !lastIsVisible; @@ -270,6 +300,7 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran var visibleSubgroups = {}; var subgroup = null; + if (typeof this.itemSet.options.order === 'function') { // a custom order function // brute force restack of all items @@ -321,7 +352,7 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { return me.itemSet.options.order(a.data, b.data); }); - stack.stack(customOrderedItems, margin, true /* restack=true */); + this.shouldBailStackItems = stack.stack(customOrderedItems, margin, true, this._shouldBailItemsRedraw.bind(this)); } this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); @@ -339,7 +370,7 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran } else { // TODO: ugly way to access options... - stack.stack(this.visibleItems, margin, true /* restack=true */); + this.shouldBailStackItems = stack.stack(this.visibleItems, margin, true, this._shouldBailItemsRedraw.bind(this)); } } else { // no stacking @@ -347,6 +378,9 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran } } + if (this.shouldBailStackItems) { + this.itemSet.body.emitter.emit('destroyTimeline') + } this.stackDirty = false; } } diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 00586826..bb0e4014 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -115,12 +115,13 @@ function ItemSet(body, options) { // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); this.options.rtl = options.rtl; - + this.options.onTimeout = options.onTimeout; + // options for getting items from the DataSet with the correct type this.itemOptions = { type: {start: 'Date', end: 'Date'} }; - + this.conversion = { toScreen: body.util.toScreen, toTime: body.util.toTime @@ -128,11 +129,14 @@ function ItemSet(body, options) { this.dom = {}; this.props = {}; this.hammer = null; - + var me = this; this.itemsData = null; // DataSet this.groupsData = null; // DataSet - + this.itemsSettingTime = null; + this.initialItemSetDrawn = false; + this.userContinueNotBail = null; + // listeners for the DataSet of the items this.itemListeners = { 'add': function (event, params, senderId) { // eslint-disable-line no-unused-vars @@ -367,7 +371,7 @@ ItemSet.prototype.setOptions = function(options) { var fields = [ 'type', 'rtl', 'align', 'order', 'stack', 'stackSubgroups', 'selectable', 'multiselect', 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate', - 'hide', 'snap', 'groupOrderSwap', 'showTooltips', 'tooltip', 'tooltipOnItemUpdateTime' + 'hide', 'snap', 'groupOrderSwap', 'showTooltips', 'tooltip', 'tooltipOnItemUpdateTime', 'onTimeout' ]; util.selectiveExtend(fields, this.options, options); @@ -688,7 +692,6 @@ ItemSet.prototype.redraw = function() { this.lastStackSubgroups = options.stackSubgroups; this.props.lastWidth = this.props.width; - var firstGroup = this._firstGroup(); var firstMargin = { item: margin.item, @@ -834,6 +837,7 @@ ItemSet.prototype.getLabelSet = function() { * @param {vis.DataSet | null} items */ ItemSet.prototype.setItems = function(items) { + this.itemsSettingTime = new Date(); var me = this, ids, oldItemsData = this.itemsData; diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index e13e6ae6..ab7d147b 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -31,6 +31,11 @@ let allOptions = { offset: {number,'undefined': 'undefined'}, __type__: {object} }, + onTimeout: { + timeoutMs: {number}, + callback: {'function': 'function'}, + __type__: {object} + }, verticalScroll: { 'boolean': bool, 'undefined': 'undefined'}, horizontalScroll: { 'boolean': bool, 'undefined': 'undefined'}, autoResize: { 'boolean': bool},