From fe49e7e1d1f8264f72dd65df183cf1984c0d5200 Mon Sep 17 00:00:00 2001 From: Yotam Berkowitz Date: Fri, 29 Sep 2017 21:42:24 +0300 Subject: [PATCH] Improve Item redraw and initial draw performance (#3475) * Make items redraws return queues * Parallel initial items redraw * Seperate read and write actions in items * Parallel all items redraws * Remove comments * Fix linting comments * Fix redraws on actions * Add stress example * Fix example files * Explain and fix example * Fix comment issues --- .../timeline/other/stressPerformance.html | 66 +++++++ lib/timeline/Timeline.js | 26 ++- lib/timeline/component/Group.js | 87 +++++++-- lib/timeline/component/ItemSet.js | 3 +- lib/timeline/component/item/BackgroundItem.js | 106 ++++++++--- lib/timeline/component/item/BoxItem.js | 167 ++++++++++++------ lib/timeline/component/item/PointItem.js | 160 ++++++++++++----- lib/timeline/component/item/RangeItem.js | 122 +++++++++---- 8 files changed, 555 insertions(+), 182 deletions(-) create mode 100644 examples/timeline/other/stressPerformance.html diff --git a/examples/timeline/other/stressPerformance.html b/examples/timeline/other/stressPerformance.html new file mode 100644 index 00000000..94906e06 --- /dev/null +++ b/examples/timeline/other/stressPerformance.html @@ -0,0 +1,66 @@ + + + + + + Timeline | Stress Performance example + + + + + + +
+ + + + + \ No newline at end of file diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index c1089613..d98d3529 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -469,13 +469,30 @@ Timeline.prototype.getItemRange = function () { } var factor = interval / this.props.center.width; - // calculate the date of the left side and right side of the items given - util.forEach(this.itemSet.items, function (item) { + var redrawQueue = {}; + var redrawQueueLength = 0; + + // collect redraw functions + util.forEach(this.itemSet.items, function (item, key) { if (item.groupShowing) { - item.show(); - item.repositionX(); + var returnQueue = true; + redrawQueue[key] = item.redraw(returnQueue); + redrawQueueLength = redrawQueue[key].length; + } + }) + + var needRedraw = redrawQueueLength > 0; + if (needRedraw) { + // redraw all regular items + for (var i = 0; i < redrawQueueLength; i++) { + util.forEach(redrawQueue, function (fns) { + fns[i](); + }); } + } + // calculate the date of the left side and right side of the items given + util.forEach(this.itemSet.items, function (item) { var start = getStart(item); var end = getEnd(item); var startSide; @@ -489,7 +506,6 @@ Timeline.prototype.getItemRange = function () { endSide = end + (item.getWidthRight() + 10) * factor; } - if (startSide < min) { min = startSide; minItem = item; diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 9d2301e6..734e0d15 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -210,19 +210,38 @@ Group.prototype._didMarkerHeightChange = function() { var markerHeight = this.dom.marker.clientHeight; if (markerHeight != this.lastMarkerHeight) { this.lastMarkerHeight = markerHeight; - util.forEach(this.items, function (item) { + var redrawQueue = {}; + var redrawQueueLength = 0; + + util.forEach(this.items, function (item, key) { item.dirty = true; - if (item.displayed) item.redraw(); - }); + if (item.displayed) { + var returnQueue = true; + redrawQueue[key] = item.redraw(returnQueue); + redrawQueueLength = redrawQueue[key].length; + } + }) + + var needRedraw = redrawQueueLength > 0; + if (needRedraw) { + // redraw all regular items + for (var i = 0; i < redrawQueueLength; i++) { + util.forEach(redrawQueue, function (fns) { + fns[i](); + }); + } + } return true; } } Group.prototype._calculateGroupSizeAndPosition = function() { - var foreground = this.dom.foreground; - this.top = foreground.offsetTop; - this.right = foreground.offsetLeft; - this.width = foreground.offsetWidth; + var offsetTop = this.dom.foreground.offsetTop + var offsetLeft = this.dom.foreground.offsetLeft + var offsetWidth = this.dom.foreground.offsetWidth + this.top = offsetTop; + this.right = offsetLeft; + this.width = offsetWidth; } Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, range) { @@ -237,13 +256,32 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran // show all items var me = this; var limitSize = false; - util.forEach(this.items, function (item) { + + var redrawQueue = {}; + var redrawQueueLength = 0; + + util.forEach(this.items, function (item, key) { if (!item.displayed) { - item.redraw(); + var returnQueue = true; + redrawQueue[key] = item.redraw(returnQueue); + redrawQueueLength = redrawQueue[key].length; me.visibleItems.push(item); } - item.repositionX(limitSize); - }); + }) + + var needRedraw = redrawQueueLength > 0; + if (needRedraw) { + // redraw all regular items + for (var i = 0; i < redrawQueueLength; i++) { + util.forEach(redrawQueue, function (fns) { + fns[i](); + }); + } + } + + for (i = 0; i < this.items.length; i++) { + this.items[i].repositionX(limitSize); + } // order all items and force a restacking var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { @@ -727,14 +765,31 @@ Group.prototype._updateItemsInRange = function(orderedItems, oldVisibleItems, ra }); } - // finally, we reposition all the visible items. + var redrawQueue = {}; + var redrawQueueLength = 0; + for (i = 0; i < visibleItems.length; i++) { var item = visibleItems[i]; - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); + if (!item.displayed) { + var returnQueue = true; + redrawQueue[i] = item.redraw(returnQueue); + redrawQueueLength = redrawQueue[i].length; + } + } + + var needRedraw = redrawQueueLength > 0; + if (needRedraw) { + // redraw all regular items + for (var j = 0; j < redrawQueueLength; j++) { + util.forEach(redrawQueue, function (fns) { + fns[j](); + }); + } + } + + for (i = 0; i < visibleItems.length; i++) { + visibleItems[i].repositionX(); } - return visibleItems; }; diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 5bd9bb41..967d1132 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -693,7 +693,8 @@ ItemSet.prototype.redraw = function() { redrawQueueLength = redrawQueue[key].length; }); - if (redrawQueueLength) { + var needRedraw = redrawQueueLength > 0; + if (needRedraw) { var redrawResults = {}; for (var i = 0; i < redrawQueueLength; i++) { diff --git a/lib/timeline/component/item/BackgroundItem.js b/lib/timeline/component/item/BackgroundItem.js index 9e1e3b19..4545c586 100644 --- a/lib/timeline/component/item/BackgroundItem.js +++ b/lib/timeline/component/item/BackgroundItem.js @@ -37,6 +37,7 @@ function BackgroundItem (data, conversion, options) { BackgroundItem.prototype = new Item (null, null, null); BackgroundItem.prototype.baseClassName = 'vis-item vis-background'; + BackgroundItem.prototype.stack = false; /** @@ -49,51 +50,49 @@ BackgroundItem.prototype.isVisible = function(range) { return (this.data.start < range.end) && (this.data.end > range.start); }; -/** - * Repaint the item - */ -BackgroundItem.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { +BackgroundItem.prototype._createDomElement = function() { + if (!this.dom) { // create DOM this.dom = {}; - dom = this.dom; // background box - dom.box = document.createElement('div'); + this.dom.box = document.createElement('div'); // className is updated in redraw() // frame box (to prevent the item contents from overflowing - dom.frame = document.createElement('div'); - dom.frame.className = 'vis-item-overflow'; - dom.box.appendChild(dom.frame); + this.dom.frame = document.createElement('div'); + this.dom.frame.className = 'vis-item-overflow'; + this.dom.box.appendChild(this.dom.frame); // contents box - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.frame.appendChild(dom.content); + this.dom.content = document.createElement('div'); + this.dom.content.className = 'vis-item-content'; + this.dom.frame.appendChild(this.dom.content); // Note: we do NOT attach this item as attribute to the DOM, // such that background items cannot be selected - //dom.box['timeline-item'] = this; + //this.dom.box['timeline-item'] = this; this.dirty = true; } +} - // append DOM to parent DOM +BackgroundItem.prototype._appendDomElement = function() { if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.box.parentNode) { + if (!this.dom.box.parentNode) { var background = this.parent.dom.background; if (!background) { throw new Error('Cannot redraw item: parent has no background container element'); } - background.appendChild(dom.box); + background.appendChild(this.dom.box); } this.displayed = true; +} - // Update DOM when item is marked dirty. An item is marked dirty when: +BackgroundItem.prototype._updateDirtyDomComponents = function() { + // update dirty DOM. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected @@ -105,16 +104,71 @@ BackgroundItem.prototype.redraw = function() { // update class var className = (this.data.className ? (' ' + this.data.className) : '') + (this.selected ? ' vis-selected' : ''); - dom.box.className = this.baseClassName + className; + this.dom.box.className = this.baseClassName + className; + } +} - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; +BackgroundItem.prototype._getDomComponentsSizes = function() { + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(this.dom.content).overflow !== 'hidden'; + return { + content: { + width: this.dom.content.offsetWidth + } + } +} - // recalculate size - this.props.content.width = this.dom.content.offsetWidth; - this.height = 0; // set height zero, so this item will be ignored when stacking items +BackgroundItem.prototype._updateDomComponentsSizes = function(sizes) { + // recalculate size + this.props.content.width = sizes.content.width; + this.height = 0; // set height zero, so this item will be ignored when stacking items - this.dirty = false; + this.dirty = false; +} + +BackgroundItem.prototype._repaintDomAdditionals = function() { +} + +/** + * Repaint the item + * @param {boolean} [returnQueue=false] return the queue + * @return {boolean} the redraw result or the redraw queue if returnQueue=true + */ +BackgroundItem.prototype.redraw = function(returnQueue) { + var sizes + var queue = [ + // create item DOM + this._createDomElement.bind(this), + + // append DOM to parent DOM + this._appendDomElement.bind(this), + + this._updateDirtyDomComponents.bind(this), + + (function() { + if (this.dirty) { + sizes = this._getDomComponentsSizes.bind(this)(); + } + }).bind(this), + + (function() { + if (this.dirty) { + this._updateDomComponentsSizes.bind(this)(sizes); + } + }).bind(this), + + // repaint DOM additionals + this._repaintDomAdditionals.bind(this) + ]; + + if (returnQueue) { + return queue; + } else { + var result; + queue.forEach(function (fn) { + result = fn(); + }); + return result; } }; diff --git a/lib/timeline/component/item/BoxItem.js b/lib/timeline/component/item/BoxItem.js index f9b6fc8b..e93b0990 100644 --- a/lib/timeline/component/item/BoxItem.js +++ b/lib/timeline/component/item/BoxItem.js @@ -58,60 +58,58 @@ BoxItem.prototype.isVisible = function(range) { return isVisible; }; -/** - * Repaint the item - */ -BoxItem.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { +BoxItem.prototype._createDomElement = function() { + if (!this.dom) { // create DOM this.dom = {}; - dom = this.dom; // create main box - dom.box = document.createElement('DIV'); + this.dom.box = document.createElement('DIV'); // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'vis-item-content'; - dom.box.appendChild(dom.content); + this.dom.content = document.createElement('DIV'); + this.dom.content.className = 'vis-item-content'; + this.dom.box.appendChild(this.dom.content); // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'vis-line'; + this.dom.line = document.createElement('DIV'); + this.dom.line.className = 'vis-line'; // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'vis-dot'; + this.dom.dot = document.createElement('DIV'); + this.dom.dot.className = 'vis-dot'; // attach this item as attribute - dom.box['timeline-item'] = this; + this.dom.box['timeline-item'] = this; this.dirty = true; } +} - // append DOM to parent DOM +BoxItem.prototype._appendDomElement = function() { if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.box.parentNode) { + if (!this.dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); - foreground.appendChild(dom.box); + foreground.appendChild(this.dom.box); } - if (!dom.line.parentNode) { + if (!this.dom.line.parentNode) { var background = this.parent.dom.background; if (!background) throw new Error('Cannot redraw item: parent has no background container element'); - background.appendChild(dom.line); + background.appendChild(this.dom.line); } - if (!dom.dot.parentNode) { + if (!this.dom.dot.parentNode) { var axis = this.parent.dom.axis; if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); - axis.appendChild(dom.dot); + axis.appendChild(this.dom.dot); } this.displayed = true; +} - // Update DOM when item is marked dirty. An item is marked dirty when: +BoxItem.prototype._updateDirtyDomComponents = function() { + // An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected @@ -126,41 +124,104 @@ BoxItem.prototype.redraw = function() { var className = (this.data.className? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.box.className = 'vis-item vis-box' + className; - dom.line.className = 'vis-item vis-line' + className; - dom.dot.className = 'vis-item vis-dot' + className; - - // set initial position in the visible range of the grid so that the - // rendered box size can be determinated correctly, even the content - // has a dynamic width (fixes #2032). - var previousRight = dom.box.style.right; - var previousLeft = dom.box.style.left; - if (this.options.rtl) { - dom.box.style.right = "0px"; - } else { - dom.box.style.left = "0px"; - } - - // recalculate size - this.props.dot.height = dom.dot.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.line.width = dom.line.offsetWidth; - this.width = dom.box.offsetWidth; - this.height = dom.box.offsetHeight; + this.dom.box.className = 'vis-item vis-box' + className; + this.dom.line.className = 'vis-item vis-line' + className; + this.dom.dot.className = 'vis-item vis-dot' + className; + } +} - // restore previous position - if (this.options.rtl) { - dom.box.style.right = previousRight; - } else { - dom.box.style.left = previousLeft; +BoxItem.prototype._getDomComponentsSizes = function() { + return { + previous: { + right: this.dom.box.style.right, + left: this.dom.box.style.left + }, + dot: { + height: this.dom.dot.offsetHeight, + width: this.dom.dot.offsetWidth + }, + line: { + width: this.dom.line.offsetWidth + }, + box: { + width: this.dom.box.offsetWidth, + height: this.dom.box.offsetHeight } + } +} + +BoxItem.prototype._updateDomComponentsSizes = function(sizes) { + if (this.options.rtl) { + this.dom.box.style.right = "0px"; + } else { + this.dom.box.style.left = "0px"; + } - this.dirty = false; + // recalculate size + this.props.dot.height = sizes.dot.height; + this.props.dot.width = sizes.dot.width; + this.props.line.width = sizes.line.width; + this.width = sizes.box.width; + this.height = sizes.box.height; + + // restore previous position + if (this.options.rtl) { + this.dom.box.style.right = sizes.previous.right; + } else { + this.dom.box.style.left = sizes.previous.left; } - this._repaintOnItemUpdateTimeTooltip(dom.box); + this.dirty = false; +} + +BoxItem.prototype._repaintDomAdditionals = function() { + this._repaintOnItemUpdateTimeTooltip(this.dom.box); this._repaintDragCenter(); - this._repaintDeleteButton(dom.box); + this._repaintDeleteButton(this.dom.box); +} + +/** + * Repaint the item + * @param {boolean} [returnQueue=false] return the queue + * @return {boolean} the redraw queue if returnQueue=true + */ +BoxItem.prototype.redraw = function(returnQueue) { + var sizes + var queue = [ + // create item DOM + this._createDomElement.bind(this), + + // append DOM to parent DOM + this._appendDomElement.bind(this), + + // update dirty DOM + this._updateDirtyDomComponents.bind(this), + + (function() { + if (this.dirty) { + sizes = this._getDomComponentsSizes(); + } + }).bind(this), + + (function() { + if (this.dirty) { + this._updateDomComponentsSizes.bind(this)(sizes); + } + }).bind(this), + + // repaint DOM additionals + this._repaintDomAdditionals.bind(this) + ]; + + if (returnQueue) { + return queue; + } else { + var result; + queue.forEach(function (fn) { + result = fn(); + }); + return result; + } }; /** diff --git a/lib/timeline/component/item/PointItem.js b/lib/timeline/component/item/PointItem.js index 4d98d670..721a8a82 100644 --- a/lib/timeline/component/item/PointItem.js +++ b/lib/timeline/component/item/PointItem.js @@ -48,49 +48,48 @@ PointItem.prototype.isVisible = function(range) { return (this.data.start.getTime() + widthInMs > range.start ) && (this.data.start < range.end); }; -/** - * Repaint the item - */ -PointItem.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { + +PointItem.prototype._createDomElement = function() { + if (!this.dom) { // create DOM this.dom = {}; - dom = this.dom; // background box - dom.point = document.createElement('div'); + this.dom.point = document.createElement('div'); // className is updated in redraw() // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.point.appendChild(dom.content); + this.dom.content = document.createElement('div'); + this.dom.content.className = 'vis-item-content'; + this.dom.point.appendChild(this.dom.content); // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + this.dom.dot = document.createElement('div'); + this.dom.point.appendChild(this.dom.dot); // attach this item as attribute - dom.point['timeline-item'] = this; + this.dom.point['timeline-item'] = this; this.dirty = true; } +} - // append DOM to parent DOM +PointItem.prototype._appendDomElement = function() { if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.point.parentNode) { + if (!this.dom.point.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { throw new Error('Cannot redraw item: parent has no foreground container element'); } - foreground.appendChild(dom.point); + foreground.appendChild(this.dom.point); } this.displayed = true; +} - // Update DOM when item is marked dirty. An item is marked dirty when: +PointItem.prototype._updateDirtyDomComponents = function() { + // An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected @@ -104,40 +103,105 @@ PointItem.prototype.redraw = function() { var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.point.className = 'vis-item vis-point' + className; - dom.dot.className = 'vis-item vis-dot' + className; - - // recalculate size of dot and contents - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; - - // resize contents - if (this.options.rtl) { - dom.content.style.marginRight = 2 * this.props.dot.width + 'px'; - } else { - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - } - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - - // reposition the dot - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - if (this.options.rtl) { - dom.dot.style.right = (this.props.dot.width / 2) + 'px'; - } else { - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + this.dom.point.className = 'vis-item vis-point' + className; + this.dom.dot.className = 'vis-item vis-dot' + className; + } +} + +PointItem.prototype._getDomComponentsSizes = function() { + return { + dot: { + width: this.dom.dot.offsetWidth, + height: this.dom.dot.offsetHeight + }, + content: { + width: this.dom.content.offsetWidth, + height: this.dom.content.offsetHeight + }, + point: { + width: this.dom.point.offsetWidth, + height: this.dom.point.offsetHeight } + } +} - this.dirty = false; +PointItem.prototype._updateDomComponentsSizes = function(sizes) { + // recalculate size of dot and contents + this.props.dot.width = sizes.dot.width; + this.props.dot.height = sizes.dot.height; + this.props.content.height = sizes.content.height; + + // resize contents + if (this.options.rtl) { + this.dom.content.style.marginRight = 2 * this.props.dot.width + 'px'; + } else { + this.dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; } - - this._repaintOnItemUpdateTimeTooltip(dom.point); + //this.dom.content.style.marginRight = ... + 'px'; // TODO: margin right + + // recalculate size + this.width = sizes.point.width; + this.height = sizes.point.height; + + // reposition the dot + this.dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; + if (this.options.rtl) { + this.dom.dot.style.right = (this.props.dot.width / 2) + 'px'; + } else { + this.dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + } + + this.dirty = false; +} + +PointItem.prototype._repaintDomAdditionals = function() { + this._repaintOnItemUpdateTimeTooltip(this.dom.point); this._repaintDragCenter(); - this._repaintDeleteButton(dom.point); + this._repaintDeleteButton(this.dom.point); +} + +/** + * Repaint the item + * @param {boolean} [returnQueue=false] return the queue + * @return {boolean} the redraw queue if returnQueue=true + */ +PointItem.prototype.redraw = function(returnQueue) { + var sizes + var queue = [ + // create item DOM + this._createDomElement.bind(this), + + // append DOM to parent DOM + this._appendDomElement.bind(this), + + // update dirty DOM + this._updateDirtyDomComponents.bind(this), + + (function() { + if (this.dirty) { + sizes = this._getDomComponentsSizes(); + } + }).bind(this), + + (function() { + if (this.dirty) { + this._updateDomComponentsSizes.bind(this)(sizes); + } + }).bind(this), + + // repaint DOM additionals + this._repaintDomAdditionals.bind(this) + ]; + + if (returnQueue) { + return queue; + } else { + var result; + queue.forEach(function (fn) { + result = fn(); + }); + return result; + } }; /** diff --git a/lib/timeline/component/item/RangeItem.js b/lib/timeline/component/item/RangeItem.js index e1dbef4f..7457b266 100644 --- a/lib/timeline/component/item/RangeItem.js +++ b/lib/timeline/component/item/RangeItem.js @@ -46,55 +46,54 @@ RangeItem.prototype.isVisible = function(range) { return (this.data.start < range.end) && (this.data.end > range.start); }; -/** - * Repaint the item - */ -RangeItem.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { +RangeItem.prototype._createDomElement = function() { + if (!this.dom) { // create DOM this.dom = {}; - dom = this.dom; // background box - dom.box = document.createElement('div'); + this.dom.box = document.createElement('div'); // className is updated in redraw() // frame box (to prevent the item contents from overflowing) - dom.frame = document.createElement('div'); - dom.frame.className = 'vis-item-overflow'; - dom.box.appendChild(dom.frame); + this.dom.frame = document.createElement('div'); + this.dom.frame.className = 'vis-item-overflow'; + this.dom.box.appendChild(this.dom.frame); // visible frame box (showing the frame that is always visible) - dom.visibleFrame = document.createElement('div'); - dom.visibleFrame.className = 'vis-item-visible-frame'; - dom.box.appendChild(dom.visibleFrame); + this.dom.visibleFrame = document.createElement('div'); + this.dom.visibleFrame.className = 'vis-item-visible-frame'; + this.dom.box.appendChild(this.dom.visibleFrame); // contents box - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.frame.appendChild(dom.content); + this.dom.content = document.createElement('div'); + this.dom.content.className = 'vis-item-content'; + this.dom.frame.appendChild(this.dom.content); // attach this item as attribute - dom.box['timeline-item'] = this; + this.dom.box['timeline-item'] = this; this.dirty = true; } - // append DOM to parent DOM +} + +RangeItem.prototype._appendDomElement = function() { if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.box.parentNode) { + if (!this.dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { throw new Error('Cannot redraw item: parent has no foreground container element'); } - foreground.appendChild(dom.box); + foreground.appendChild(this.dom.box); } this.displayed = true; +} - // Update DOM when item is marked dirty. An item is marked dirty when: +RangeItem.prototype._updateDirtyDomComponents = function() { + // update dirty DOM. An item is marked dirty when: // - the item is not yet rendered // - the item's data is changed // - the item is selected/deselected @@ -109,27 +108,84 @@ RangeItem.prototype.redraw = function() { var className = (this.data.className ? (' ' + this.data.className) : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.box.className = this.baseClassName + className; + this.dom.box.className = this.baseClassName + className; - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.frame).overflow !== 'hidden'; - - // recalculate size // turn off max-width to be able to calculate the real width // this causes an extra browser repaint/reflow, but so be it this.dom.content.style.maxWidth = 'none'; - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; - this.dom.content.style.maxWidth = ''; + } +} - this.dirty = false; +RangeItem.prototype._getDomComponentsSizes = function() { + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(this.dom.frame).overflow !== 'hidden'; + return { + content: { + width: this.dom.content.offsetWidth, + }, + box: { + height: this.dom.box.offsetHeight + } } +} - this._repaintOnItemUpdateTimeTooltip(dom.box); - this._repaintDeleteButton(dom.box); +RangeItem.prototype._updateDomComponentsSizes = function(sizes) { + this.props.content.width = sizes.content.width; + this.height = sizes.box.height; + this.dom.content.style.maxWidth = ''; + this.dirty = false; +} + +RangeItem.prototype._repaintDomAdditionals = function() { + this._repaintOnItemUpdateTimeTooltip(this.dom.box); + this._repaintDeleteButton(this.dom.box); this._repaintDragCenter(); this._repaintDragLeft(); this._repaintDragRight(); +} + +/** + * Repaint the item + * @param {boolean} [returnQueue=false] return the queue + * @return {boolean} the redraw queue if returnQueue=true + */ +RangeItem.prototype.redraw = function(returnQueue) { + var sizes; + var queue = [ + // create item DOM + this._createDomElement.bind(this), + + // append DOM to parent DOM + this._appendDomElement.bind(this), + + // update dirty DOM + this._updateDirtyDomComponents.bind(this), + + (function() { + if (this.dirty) { + sizes = this._getDomComponentsSizes.bind(this)(); + } + }).bind(this), + + (function() { + if (this.dirty) { + this._updateDomComponentsSizes.bind(this)(sizes); + } + }).bind(this), + + // repaint DOM additionals + this._repaintDomAdditionals.bind(this) + ]; + + if (returnQueue) { + return queue; + } else { + var result; + queue.forEach(function (fn) { + result = fn(); + }); + return result; + } }; /**