diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index a551ece0..66df4435 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -1,3 +1,5 @@ +// TODO: turn Stack into a Mixin? + /** * @constructor Stack * Stacks items on top of each other. @@ -10,9 +12,8 @@ function Stack (itemset, options) { this.options = options || {}; this.defaultOptions = { order: function (a, b) { - //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup - // Order: ranges over non-ranges, ranged ordered by width, and - // lastly ordered by start. + // Order: ranges over non-ranges, ranged ordered by width, + // and non-ranges ordered by start. if (a instanceof ItemRange) { if (b instanceof ItemRange) { var aInt = (a.data.end - a.data.start); @@ -28,6 +29,9 @@ function Stack (itemset, options) { return 1; } else { + if (!a.data) { + throw new Error('hu') + } return (a.data.start - b.data.start); } } @@ -59,7 +63,7 @@ Stack.prototype.setOptions = function setOptions (options) { */ Stack.prototype.update = function update() { this._order(); - this._stack(); + this._stack(this.ordered); }; /** @@ -73,41 +77,48 @@ Stack.prototype._order = function _order () { throw new Error('Cannot stack items: ItemSet does not contain items'); } - // TODO: store the sorted items, to have less work later on + // TODO: use sorted items instead of ordering every time + + this.ordered = this.order(items); +}; + +/** + * Order a map with items + * @param {Object} items + * @return {Item[]} sorted items + */ +Stack.prototype.order = function order(items) { var ordered = []; - var index = 0; - // items is a map (no array) - util.forEach(items, function (item) { - if (item.visible) { - ordered[index] = item; - index++; - } - }); - //if a customer stack order function exists, use it. + // convert map to array + for (var id in items) { + if (items.hasOwnProperty(id)) ordered.push(items[id]); + } + + //order the items var order = this.options.order || this.defaultOptions.order; if (!(typeof order === 'function')) { throw new Error('Option order must be a function'); } - ordered.sort(order); - this.ordered = ordered; + return ordered; }; /** * Adjust vertical positions of the events such that they don't overlap each * other. + * @param {Item[]} items * @private */ -Stack.prototype._stack = function _stack () { +Stack.prototype._stack = function _stack (items) { var i, iMax, - ordered = this.ordered, options = this.options, orientation = options.orientation || this.defaultOptions.orientation, axisOnTop = (orientation == 'top'), - margin; + margin, + parentHeight = this.itemset.height; // TODO: should use the height of the itemsets parent if (options.margin && options.margin.item !== undefined) { margin = options.margin.item; @@ -116,14 +127,40 @@ Stack.prototype._stack = function _stack () { margin = this.defaultOptions.margin.item } - // calculate new, non-overlapping positions - for (i = 0, iMax = ordered.length; i < iMax; i++) { - var item = ordered[i]; + // initialize top position + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + + //* + if (orientation == 'top') { + item.top = margin; + } + else { + // default or 'bottom' + item.top = parentHeight - item.height - 2 * margin; + } + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; var collidingItem = null; + + /* TODO: cleanup + // initialize top position + if (orientation == 'top') { + item.top = margin; + } + else { + // default or 'bottom' + item.top = parentHeight - item.height - 2 * margin; + } + //*/ + do { // TODO: optimize checking for overlap. when there is a gap without items, // you only need to check for items from the next item on, not from zero - collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); + collidingItem = this._checkOverlap (items, i, 0, i - 1, margin); if (collidingItem != null) { // There is a collision. Reposition the event above the colliding element if (axisOnTop) { @@ -137,6 +174,75 @@ Stack.prototype._stack = function _stack () { } }; +/** + * Stack an item on top of given set of items + * @param {Item} item + * @param {Item[]} items + * @private + */ +Stack.prototype.stack = function stack (item, items) { + var options = this.options, + orientation = options.orientation || this.defaultOptions.orientation, + axisOnTop = (orientation == 'top'), + margin, + parentHeight = this.itemset.height; // TODO: should use the height of the itemsets parent + + if (options.margin && options.margin.item !== undefined) { + margin = options.margin.item; + } + else { + margin = this.defaultOptions.margin.item + } + + // initialize top position + if (orientation == 'top') { + item.top = margin; + } + else { + // default or 'bottom' + item.top = parentHeight - item.height - 2 * margin; + } + + // calculate new, non-overlapping position + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = this.checkOverlap(item, items, margin); + 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; + } + } + } while (collidingItem); +}; + +/** + * Check if the destiny position of given item overlaps with any + * of the other items from index itemStart to itemEnd. + * @param {Item} item item to be checked + * @param {Item[]} items Array with items + * @return {Object | null} colliding item, or undefined when no collisions + * @param {Number} margin A minimum required margin. + * If margin is provided, the two items will be + * marked colliding when they overlap or + * when the margin between the two is smaller than + * the requested margin. + */ +Stack.prototype.checkOverlap = function checkOverlap (item, items, margin) { + for (var i = 0, ii = items.length; i < ii; i++) { + var b = items[i]; + if (b !== item && b.top !== null && this.collision(item, b, margin)) { + return b; + } + } + + return null; +}; + /** * Check if the destiny position of given item overlaps with any * of the other items from index itemStart to itemEnd. @@ -151,8 +257,8 @@ Stack.prototype._stack = function _stack () { * when the margin between the two is smaller than * the requested margin. */ -Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, - itemStart, itemEnd, margin) { +Stack.prototype._checkOverlap = function _checkOverlap (items, itemIndex, + itemStart, itemEnd, margin) { var collision = this.collision; // we loop from end to start, as we suppose that the chance of a diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 0e3a1720..c010e74e 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -61,7 +61,11 @@ function ItemSet(parent, depends, options) { } }; - this.items = {}; // object with an Item for every data item + this.items = {}; // object with an Item for every data item + this.orderedItems = []; // ordered items + this.visibleItems = []; // visible, ordered items + this.visibleItemsStart = 0; // start index of visible items in this.orderedItems + this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems 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)); @@ -142,25 +146,6 @@ ItemSet.prototype.setController = function setController (controller) { } }; -// attach event listeners for dragging items to the controller -(function (me) { - var _controller = null; - var _onDragStart = null; - var _onDrag = null; - var _onDragEnd = null; - - Object.defineProperty(me, 'controller', { - get: function () { - return _controller; - }, - - set: function (controller) { - - } - }); -}) (this); - - /** * Set range (start and end). * @param {Range | Object} range A Range or an object containing start and end. @@ -245,7 +230,6 @@ ItemSet.prototype.repaint = function repaint() { asSize = util.option.asSize, options = this.options, orientation = this.getOption('orientation'), - defaultOptions = this.defaultOptions, frame = this.frame; if (!frame) { @@ -314,105 +298,56 @@ ItemSet.prototype.repaint = function repaint() { this._updateConversion(); - var me = this, - queue = this.queue, - itemsData = this.itemsData, - items = this.items, - dataOptions = { - // TODO: cleanup - // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className'] - }; + // find start of visible items + var start = this.visibleItemsStart; + var item = this.orderedItems[start]; + while (item && item.isVisible() && start > 0) { + start--; + item = this.orderedItems[start]; + } + while (item && !item.isVisible()) { + if (item.displayed) item.hide(); - // show/hide added/changed/removed items - for (var id in queue) { - if (queue.hasOwnProperty(id)) { - var entry = queue[id], - item = items[id], - action = entry.action; - - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - var itemData = itemsData && itemsData.get(id, dataOptions); - - if (itemData) { - var type = itemData.type || - (itemData.start && itemData.end && 'range') || - options.type || - 'box'; - var constructor = ItemSet.types[type]; - - // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, hide and delete the item - changed += item.hide(); - item = null; - } - else { - item.data = itemData; // TODO: create a method item.setData ? - changed++; - } - } - - if (!item) { - // create item - if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); - item.id = entry.id; // we take entry.id, as id itself is stringified - changed++; - } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } - - // force a repaint (not only a reposition) - item.repaint(); - - items[id] = item; - } + start++; + item = this.orderedItems[start]; + } + this.visibleItemsStart = start; - // update queue - delete queue[id]; - break; + // find end of visible items + var end = Math.max(this.visibleItemsStart, this.visibleItemsEnd); + item = this.orderedItems[end]; + while (item && item.isVisible()) { + end++; + item = this.orderedItems[end]; + } + item = this.orderedItems[end - 1]; + while (item && !item.isVisible() && end > 0) { + if (item.displayed) item.hide(); - case 'remove': - if (item) { - // remove the item from the set selected items - if (item.selected) { - me._deselect(id); - } + end--; + item = this.orderedItems[end - 1]; + } + this.visibleItemsEnd = end; - // remove DOM of the item - changed += item.hide(); - } + console.log('visible items', start, end); // TODO: cleanup - // update lists - delete items[id]; - delete queue[id]; - break; + this.visibleItems = this.orderedItems.slice(start, end); - default: - console.log('Error: unknown action "' + action + '"'); - } - } + // show visible items + for (var i = start; i < end; i++) { + var item = this.orderedItems[i]; + if (!item.displayed) item.show(); + item.top = null; // TODO: do not re-stack every time, only on scroll } - // reposition all items. Show items only when in the visible area - util.forEach(this.items, function (item) { - if (item.visible) { - changed += item.show(); - item.reposition(); - } - else { - changed += item.hide(); - } - }); + // reposition visible items + for (var i = start; i < end; i++) { + var item = this.orderedItems[i]; + this.stack.stack(item, this.visibleItems); + item.reposition(); + } - return (changed > 0); + return false; }; /** @@ -456,13 +391,15 @@ ItemSet.prototype.reflow = function reflow () { 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(); + //this.stack.update(); var maxHeight = asNumber(options.maxHeight); var fixedHeight = (asSize(options.height) != null); @@ -473,7 +410,8 @@ ItemSet.prototype.reflow = function reflow () { else { // height is not specified, determine the height from the height and positioned items var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items - if (visibleItems.length) { + //if (visibleItems.length) { // TODO: calculate max height again + if (false) { var min = visibleItems[0].top; var max = visibleItems[0].top + visibleItems[0].height; util.forEach(visibleItems, function (item) { @@ -489,6 +427,7 @@ ItemSet.prototype.reflow = function reflow () { if (maxHeight != null) { height = Math.min(height, maxHeight); } + height = 200; // TODO: cleanup changed += update(this, 'height', height); // calculate height from items @@ -500,7 +439,7 @@ ItemSet.prototype.reflow = function reflow () { changed += 1; } - return (changed > 0); + return false; }; /** @@ -599,17 +538,65 @@ ItemSet.prototype.removeItem = function removeItem (id) { * @private */ ItemSet.prototype._onUpdate = function _onUpdate(ids) { - this._toQueue('update', ids); + var me = this, + defaultOptions = { + type: 'box', + align: 'center', + orientation: 'bottom', + margin: { + axis: 20, + item: 10 + }, + padding: 5 + }; + + ids.forEach(function (id) { + var itemData = me.itemsData.get(id), + item = items[id], + type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + } + } + + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + item.id = id; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } + } + + me.items[id] = item; + }); + + this._order(); + this.repaint(); }; /** - * Handle changed items + * Handle added items * @param {Number[]} ids * @private */ -ItemSet.prototype._onAdd = function _onAdd(ids) { - this._toQueue('add', ids); -}; +ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; /** * Handle removed items @@ -617,29 +604,28 @@ ItemSet.prototype._onAdd = function _onAdd(ids) { * @private */ ItemSet.prototype._onRemove = function _onRemove(ids) { - this._toQueue('remove', ids); -}; - -/** - * Put items in the queue to be added/updated/remove - * @param {String} action can be 'add', 'update', 'remove' - * @param {Number[]} ids - */ -ItemSet.prototype._toQueue = function _toQueue(action, ids) { - var queue = this.queue; + var me = this; ids.forEach(function (id) { - queue[id] = { - id: id, - action: action - }; + var item = me.items[id]; + if (item) { + item.hide(); // TODO: only hide when displayed + delete me.items[id]; + delete me.visibleItems[id]; + } }); - if (this.controller) { - //this.requestReflow(); - this.requestRepaint(); - } + this._order(); }; +/** + * Order the items + * @private + */ +ItemSet.prototype._order = function _order() { + // reorder the items + this.orderedItems = this.stack.order(this.items); +} + /** * Calculate the scale and offset to convert a position on screen to the * corresponding date and vice versa. diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index 35f6710c..058e5af0 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -150,8 +150,10 @@ RootPanel.prototype._watch = function () { if (me.frame) { // check whether the frame is resized - if ((me.frame.clientWidth != me.width) || - (me.frame.clientHeight != me.height)) { + if ((me.frame.clientWidth != me.lastWidth) || + (me.frame.clientHeight != me.lastHeight)) { + me.lastWidth = me.frame.clientWidth; + me.lastHeight = me.frame.clientHeight; me.requestReflow(); } } diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 5415cb04..6f464e38 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -11,294 +11,228 @@ function ItemBox (parent, data, options, defaultOptions) { this.props = { dot: { - left: 0, - top: 0, width: 0, height: 0 }, line: { - top: 0, - left: 0, width: 0, height: 0 } }; + this.displayed = false; + this.dirty = true; + + // validate data + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } + Item.call(this, parent, data, options, defaultOptions); } ItemBox.prototype = new Item (null, null); +/** + * Check whether this item is visible in the current time window + * @returns {boolean} True if visible + */ +ItemBox.prototype.isVisible = function isVisible () { + // determine visibility + var data = this.data; + var range = this.parent && this.parent.range; + + if (data && range) { + // TODO: account for the width of the item. Right now we add 1/4 to the window + var interval = (range.end - range.start) / 4; + interval = 0; // TODO: remove + return (data.start > range.start - interval) && (data.start < range.end + interval); + } + else { + return false; + } +} + /** * Repaint the item * @return {Boolean} changed */ ItemBox.prototype.repaint = function repaint() { // TODO: make an efficient repaint - var changed = false; - var dom = this.dom; + var dom, + update = util.updateProperty, + props= this.props; + // create DOM + dom = this.dom; if (!dom) { - this._create(); + this.dom = {}; dom = this.dom; - changed = true; - } - - if (dom) { - if (!this.parent) { - throw new Error('Cannot repaint item: no parent attached'); - } - if (!dom.box.parentNode) { - var foreground = this.parent.getForeground(); - if (!foreground) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no foreground container element'); - } - foreground.appendChild(dom.box); - changed = true; - } + // create main box + dom.box = document.createElement('DIV'); - if (!dom.line.parentNode) { - var background = this.parent.getBackground(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no background container element'); - } - background.appendChild(dom.line); - changed = true; - } + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); - if (!dom.dot.parentNode) { - var axis = this.parent.getAxis(); - if (!background) { - throw new Error('Cannot repaint time axis: ' + - 'parent has no axis container element'); - } - axis.appendChild(dom.dot); - changed = true; - } + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; - this._repaintDeleteButton(dom.box); - - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } - changed = true; - } + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; - dom.dot.className = 'item dot' + className; - changed = true; - } + // attach this item as attribute + dom.box['timeline-item'] = this; } - return changed; -}; - -/** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - * @return {Boolean} changed - */ -ItemBox.prototype.show = function show() { - if (!this.dom || !this.dom.box.parentNode) { - return this.repaint(); + // append DOM to parent DOM + if (!this.parent) { + throw new Error('Cannot repaint item: no parent attached'); } - else { - return false; + if (!dom.box.parentNode) { + var foreground = this.parent.getForeground(); + if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element'); + foreground.appendChild(dom.box); } -}; - -/** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed - */ -ItemBox.prototype.hide = function hide() { - var changed = false, - dom = this.dom; - if (dom) { - if (dom.box.parentNode) { - dom.box.parentNode.removeChild(dom.box); - changed = true; + if (!dom.line.parentNode) { + var background = this.parent.getBackground(); + if (!background) throw new Error('Cannot repaint time axis: parent has no background container element'); + background.appendChild(dom.line); + } + if (!dom.dot.parentNode) { + var axis = this.parent.getAxis(); + if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element'); + axis.appendChild(dom.dot); + } + this.displayed = true; + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); } - if (dom.line.parentNode) { - dom.line.parentNode.removeChild(dom.line); + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; } - if (dom.dot.parentNode) { - dom.dot.parentNode.removeChild(dom.dot); + else { + throw new Error('Property "content" missing in item ' + this.data.id); } - } - return changed; -}; -/** - * Reflow the item: calculate its actual size and position from the DOM - * @return {boolean} resized returns true if the axis is resized - * @override - */ -ItemBox.prototype.reflow = function reflow() { - var changed = 0, - update, - dom, - props, - options, - margin, - start, - align, - orientation, - top, - left, - data, - range; - - if (this.data.start == undefined) { - throw new Error('Property "start" missing in item ' + this.data.id); + this.dirty = true; } - data = this.data; - range = this.parent && this.parent.range; - if (data && range) { - // TODO: account for the width of the item - var interval = (range.end - range.start); - this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); - } - else { - this.visible = false; + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; + + this.dirty = true; } - if (this.visible) { - dom = this.dom; - if (dom) { - update = util.updateProperty; - props = this.props; - options = this.options; - start = this.parent.toScreen(this.data.start) + this.offset; - align = options.align || this.defaultOptions.align; - margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - orientation = options.orientation || this.defaultOptions.orientation; - - changed += update(props.dot, 'height', dom.dot.offsetHeight); - changed += update(props.dot, 'width', dom.dot.offsetWidth); - changed += update(props.line, 'width', dom.line.offsetWidth); - changed += update(props.line, 'height', dom.line.offsetHeight); - changed += update(props.line, 'top', dom.line.offsetTop); - changed += update(this, 'width', dom.box.offsetWidth); - changed += update(this, 'height', dom.box.offsetHeight); - if (align == 'right') { - left = start - this.width; - } - else if (align == 'left') { - left = start; - } - else { - // default or 'center' - left = start - this.width / 2; - } - changed += update(this, 'left', left); - - changed += update(props.line, 'left', start - props.line.width / 2); - changed += update(props.dot, 'left', start - props.dot.width / 2); - changed += update(props.dot, 'top', -props.dot.height / 2); - if (orientation == 'top') { - top = margin; - - changed += update(this, 'top', top); - } - else { - // default or 'bottom' - var parentHeight = this.parent.height; - top = parentHeight - this.height - margin; - - changed += update(this, 'top', top); - } - } - else { - changed += 1; - } + // recalculate size + if (this.dirty) { + update(props.dot, 'height', dom.dot.offsetHeight); + update(props.dot, 'width', dom.dot.offsetWidth); + update(props.line, 'width', dom.line.offsetWidth); + update(this, 'width', dom.box.offsetWidth); + update(this, 'height', dom.box.offsetHeight); + + this.dirty = false; } - return (changed > 0); + // TODO: repaint delete button + this._repaintDeleteButton(dom.box); + + return false; }; /** - * Create an items DOM - * @private + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. */ -ItemBox.prototype._create = function _create() { - var dom = this.dom; - if (!dom) { - this.dom = dom = {}; - - // create the box - dom.box = document.createElement('DIV'); - // className is updated in repaint() - - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); +ItemBox.prototype.show = function show() { + if (!this.displayed) { + this.repaint(); + } +}; - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; +/** + * Hide the item from the DOM (when visible) + */ +ItemBox.prototype.hide = function hide() { + var dom = this.dom; - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + if (this.displayed) { + if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); + if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); + if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); - // attach this item as attribute - dom.box['timeline-item'] = this; + this.displayed = false; } + + this.top = null; }; /** * Reposition the item, recalculate its left, top, and width, using the current - * range and size of the items itemset + * range and size of the items ItemSet * @override */ ItemBox.prototype.reposition = function reposition() { var dom = this.dom, props = this.props, - orientation = this.options.orientation || this.defaultOptions.orientation; + options = this.options, + start = this.parent.toScreen(this.data.start) + this.offset, + align = options.align || this.defaultOptions.align, + orientation = this.options.orientation || this.defaultOptions.orientation, + left; + + var box = dom.box, + line = dom.line, + dot = dom.dot; + + // calculate left and top position of the box + if (align == 'right') { + this.left = start - this.width; + } + else if (align == 'left') { + this.left = start; + } + else { + // default or 'center' + this.left = start - this.width / 2; + } - if (dom) { - var box = dom.box, - line = dom.line, - dot = dom.dot; + // NOTE: this.top is determined when stacking items - box.style.left = this.left + 'px'; - box.style.top = this.top + 'px'; + // reposition box + box.style.left = this.left + 'px'; + box.style.top = (this.top || 0) + 'px'; - line.style.left = props.line.left + 'px'; - if (orientation == 'top') { - line.style.top = 0 + 'px'; - 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'; - } - - dot.style.left = props.dot.left + 'px'; - dot.style.top = props.dot.top + 'px'; + // reposition line + line.style.left = (start - props.line.width / 2) + 'px'; + if (orientation == 'top') { + line.style.top = 0 + 'px'; + 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'; + } + + // reposition dot + dot.style.left = (start - props.dot.width / 2) + 'px'; + dot.style.top = (-props.dot.height / 2) + 'px'; };