diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index 2f248878..6bc10bb7 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -3,12 +3,9 @@ /** * @constructor Stack * Stacks items on top of each other. - * @param {ItemSet} itemset * @param {Object} [options] */ -function Stack (itemset, options) { - this.itemset = itemset; - +function Stack (options) { this.options = options || {}; this.defaultOptions = { order: function (a, b) { @@ -34,24 +31,21 @@ function Stack (itemset, options) { } }, margin: { - item: 10 + item: 10, + axis: 20 } }; - - this.ordered = []; // ordered items } /** * Set options for the stack * @param {Object} options Available options: - * {ItemSet} itemset - * {Number} margin - * {function} order Stacking order + * {Number} [margin.item=10] + * {Number} [margin.axis=20] + * {function} [order] Stacking order */ Stack.prototype.setOptions = function setOptions (options) { util.extend(this.options, options); - - // TODO: register on data changes at the connected itemset, and update the changed part only and immediately }; /** @@ -101,16 +95,20 @@ Stack.prototype.stack = function stack (items) { var i, iMax, options = this.options, - orientation = options.orientation || this.defaultOptions.orientation, - axisOnTop = (orientation == 'top'), - margin, - parentHeight = this.itemset.height; + marginItem, + marginAxis; if (options.margin && options.margin.item !== undefined) { - margin = options.margin.item; + marginItem = options.margin.item; + } + else { + marginItem = this.defaultOptions.margin.item + } + if (options.margin && options.margin.axis !== undefined) { + marginAxis = options.margin.axis; } else { - margin = this.defaultOptions.margin.item + marginAxis = this.defaultOptions.margin.axis } // calculate new, non-overlapping positions @@ -118,13 +116,7 @@ Stack.prototype.stack = function stack (items) { var item = items[i]; if (item.top === null) { // initialize top position - if (axisOnTop) { - item.top = margin; - } - else { - // default or 'bottom' - item.top = parentHeight - item.height - 2 * margin; - } + item.top = marginAxis; do { // TODO: optimize checking for overlap. when there is a gap without items, @@ -132,7 +124,7 @@ Stack.prototype.stack = function stack (items) { var collidingItem = null; for (var j = 0, jj = items.length; j < jj; j++) { var other = items[j]; - if (other.top !== null && other !== item && this.collision(item, other, margin)) { + if (other.top !== null && other !== item && this.collision(item, other, marginItem)) { collidingItem = other; break; } @@ -140,12 +132,7 @@ Stack.prototype.stack = function stack (items) { if (collidingItem != null) { // There is a collision. Reposition the event above the colliding element - if (axisOnTop) { - item.top = collidingItem.top + collidingItem.height + margin; - } - else { - item.top = collidingItem.top - item.height - margin; - } + item.top = collidingItem.top + collidingItem.height + marginItem; } } while (collidingItem); } diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 497bd54e..3d97f2aa 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -27,6 +27,15 @@ function Timeline (container, items, options) { showCurrentTime: false, showCustomTime: false, + type: 'box', + align: 'center', + orientation: 'bottom', + margin: { + axis: 20, + item: 10 + }, + padding: 5, + onAdd: function (item, callback) { callback(item); }, diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 1a465bb8..e42ac4ad 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -25,17 +25,7 @@ function ItemSet(parent, depends, options) { // one options object is shared by this itemset and all its items this.options = options || {}; - this.defaultOptions = { - type: 'box', - align: 'center', - orientation: 'bottom', - margin: { - axis: 20, - item: 10 - }, - padding: 5 - }; - + this.itemOptions = Object.create(this.options); this.dom = {}; var me = this; @@ -65,7 +55,7 @@ function ItemSet(parent, depends, options) { this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup this.selection = []; // list with the ids of all selected nodes this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' - this.stack = new Stack(this, Object.create(this.options)); + this.stack = new Stack(Object.create(this.options)); this.conversion = null; this.touchParams = {}; // stores properties while dragging @@ -219,12 +209,10 @@ ItemSet.prototype._deselect = function _deselect(id) { /** * Repaint the component - * @return {Boolean} changed */ ItemSet.prototype.repaint = function repaint() { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, + var asSize = util.option.asSize, + asNumber = util.option.asNumber, options = this.options, orientation = this.getOption('orientation'), frame = this.frame; @@ -260,7 +248,6 @@ ItemSet.prototype.repaint = function repaint() { this.dom.axis = axis; this.frame = frame; - changed += 1; } if (!this.parent) { @@ -272,27 +259,9 @@ ItemSet.prototype.repaint = function repaint() { } if (!frame.parentNode) { parentContainer.appendChild(frame); - changed += 1; } if (!this.dom.axis.parentNode) { parentContainer.appendChild(this.dom.axis); - changed += 1; - } - - // reposition frame - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'top', asSize(options.top, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); - - // reposition axis - changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px')); - changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%')); - if (orientation == 'bottom') { - changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px'); - } - else { // orientation == 'top' - changed += update(this.dom.axis.style, 'top', this.top + 'px'); } // check whether zoomed (in that case we need to re-stack everything) @@ -300,7 +269,7 @@ ItemSet.prototype.repaint = function repaint() { var zoomed = this.visibleInterval != visibleInterval; this.visibleInterval = visibleInterval; - /* + /* TODO: implement+fix smarter way to update visible items // find the first visible item // TODO: use faster search, not linear var byEnd = this.orderedItems.byEnd; @@ -368,12 +337,65 @@ ItemSet.prototype.repaint = function repaint() { } // reposition visible items vertically - this.stack.order(this.visibleItems); + //this.stack.order(this.visibleItems); // TODO: solve ordering issue this.stack.stack(this.visibleItems); for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { this.visibleItems[i].repositionY(); } + // recalculate the height of the itemset + var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis, + marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item, + maxHeight = asNumber(options.maxHeight), + fixedHeight = (asSize(options.height) != null), + height; + + // recalculate the frames size and position + // TODO: request frame's actual top, left, width only when size is changed (mark as dirty) + if (fixedHeight) { + height = frame.offsetHeight; + } + else { + // height is not specified, determine the height from the height and positioned items + var visibleItems = this.visibleItems; + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + height = (max - min) + marginAxis + marginItem; + } + else { + height = marginAxis + marginItem; + } + } + if (maxHeight != null) { + height = Math.min(height, maxHeight); + } + this.top = frame.offsetTop; + this.left = frame.offsetLeft; + this.width = frame.offsetWidth; + this.height = height; + + // reposition frame + frame.style.left = asSize(options.left, '0px'); + frame.style.top = asSize(options.top, ''); + frame.style.bottom = asSize(options.bottom, ''); + frame.style.width = asSize(options.width, '100%'); + frame.style.height = asSize(options.height, this.height + 'px'); + + // reposition axis + this.dom.axis.style.left = asSize(options.left, '0px'); + this.dom.axis.style.width = asSize(options.width, '100%'); + if (orientation == 'bottom') { + this.dom.axis.style.top = (this.top + this.height) + 'px'; + } + else { // orientation == 'top' + this.dom.axis.style.top = this.top + 'px'; + } + return false; }; @@ -406,86 +428,21 @@ ItemSet.prototype.getAxis = function getAxis() { * @return {Boolean} resized */ ItemSet.prototype.reflow = function reflow () { - var changed = 0, - options = this.options, - marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.defaultOptions.margin.axis, - marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.defaultOptions.margin.item, - update = util.updateProperty, - asNumber = util.option.asNumber, - asSize = util.option.asSize, - frame = this.frame; - - if (frame) { - this._updateConversion(); - - /* TODO - util.forEach(this.items, function (item) { - changed += item.reflow(); - }); - */ - - // TODO: stack.update should be triggered via an event, in stack itself - // TODO: only update the stack when there are changed items - //this.stack.update(); - - var maxHeight = asNumber(options.maxHeight); - var fixedHeight = (asSize(options.height) != null); - var height; - if (fixedHeight) { - height = frame.offsetHeight; - } - else { - // height is not specified, determine the height from the height and positioned items - var visibleItems = this.visibleItems; // TODO: not so nice way to get the filtered items - if (visibleItems.length) { // TODO: calculate max height again - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); - }); - height = (max - min) + marginAxis + marginItem; - } - else { - height = marginAxis + marginItem; - } - } - if (maxHeight != null) { - height = Math.min(height, maxHeight); - } - height = 200; // TODO: cleanup - changed += update(this, 'height', height); - - // calculate height from items - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - changed += update(this, 'width', frame.offsetWidth); - } - else { - changed += 1; - } - - return false; + // TODO: remove this function + return true; }; /** * Hide this component from the DOM - * @return {Boolean} changed */ ItemSet.prototype.hide = function hide() { - var changed = false; - // remove the DOM if (this.frame && this.frame.parentNode) { this.frame.parentNode.removeChild(this.frame); - changed = true; } if (this.dom.axis && this.dom.axis.parentNode) { this.dom.axis.parentNode.removeChild(this.dom.axis); - changed = true; } - - return changed; }; /** @@ -566,16 +523,7 @@ ItemSet.prototype.removeItem = function removeItem (id) { ItemSet.prototype._onUpdate = function _onUpdate(ids) { var me = this, items = this.items, - defaultOptions = { - type: 'box', - align: 'center', - orientation: 'bottom', - margin: { - axis: 20, - item: 10 - }, - padding: 5 - }; + itemOptions = this.itemOptions; ids.forEach(function (id) { var itemData = me.itemsData.get(id), @@ -608,7 +556,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { if (!item) { // create item if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); + item = new constructor(me, itemData, options, itemOptions); item.id = id; } else { @@ -807,8 +755,7 @@ ItemSet.prototype._onDragEnd = function (event) { // prepare a change set for the changed items var changes = [], me = this, - dataset = this._myDataSet(), - type; + dataset = this._myDataSet(); this.touchParams.itemProps.forEach(function (props) { var id = props.item.id, diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 0db3d799..81cf911b 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -7,8 +7,8 @@ display: inline-block; padding: 5px; - -webkit-transition: top .4s ease-in-out, height .4s ease-in-out; - transition: top .4s ease-in-out, height .4s ease-in-out; + -webkit-transition: top .4s ease-in-out, bottom .4s ease-in-out, height .4s ease-in-out; + transition: top .4s ease-in-out, bottom .4s ease-in-out, height .4s ease-in-out; } .vis.timeline .item.selected { diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 5d4648a9..2f6e6e2a 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -211,21 +211,22 @@ ItemBox.prototype.repositionY = function repositionY () { line = this.dom.line, dot = this.dom.dot; - // reposition box - box.style.top = (this.top || 0) + 'px'; - - // reposition line if (orientation == 'top') { - line.style.top = 0 + 'px'; + box.style.top = (this.top || 0) + 'px'; + box.style.bottom = ''; + + line.style.top = '0px'; + line.style.bottom = ''; line.style.height = this.top + 'px'; } - else { - // orientation 'bottom' - line.style.top = (this.top + this.height) + 'px'; - line.style.height = Math.max(this.parent.height - this.top - this.height + - this.props.dot.height / 2, 0) + 'px'; + else { // orientation 'bottom' + box.style.top = ''; + box.style.bottom = (this.top || 0) + 'px'; + + line.style.top = ''; + line.style.bottom = '0px'; + line.style.height = this.top + 'px'; } - // reposition dot dot.style.top = (-this.props.dot.height / 2) + 'px'; } diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index e6865af5..742c1a91 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -176,5 +176,15 @@ ItemPoint.prototype.repositionX = function repositionX() { * @Override */ ItemPoint.prototype.repositionY = function repositionY () { - this.dom.point.style.top = this.top + 'px'; + var orientation = this.options.orientation || this.defaultOptions.orientation, + point = this.dom.point; + + if (orientation == 'top') { + point.style.top = this.top + 'px'; + point.style.bottom = ''; + } + else { + point.style.top = ''; + point.style.bottom = this.top + 'px'; + } } diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index a5143e71..9f47cd2f 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -190,7 +190,17 @@ ItemRange.prototype.repositionX = function repositionX() { * @Override */ ItemRange.prototype.repositionY = function repositionY() { - this.dom.box.style.top = this.top + 'px'; + var orientation = this.options.orientation || this.defaultOptions.orientation, + box = this.dom.box; + + if (orientation == 'top') { + box.style.top = this.top + 'px'; + box.style.bottom = ''; + } + else { + box.style.top = ''; + box.style.bottom = this.top + 'px'; + } }; /**