Browse Source

Halfway rebuilding ItemBox without dynamic reflow

css_transitions
josdejong 10 years ago
parent
commit
9f653909df
4 changed files with 419 additions and 391 deletions
  1. +131
    -25
      src/timeline/Stack.js
  2. +123
    -137
      src/timeline/component/ItemSet.js
  3. +4
    -2
      src/timeline/component/RootPanel.js
  4. +161
    -227
      src/timeline/component/item/ItemBox.js

+ 131
- 25
src/timeline/Stack.js View File

@ -1,3 +1,5 @@
// TODO: turn Stack into a Mixin?
/** /**
* @constructor Stack * @constructor Stack
* Stacks items on top of each other. * Stacks items on top of each other.
@ -10,9 +12,8 @@ function Stack (itemset, options) {
this.options = options || {}; this.options = options || {};
this.defaultOptions = { this.defaultOptions = {
order: function (a, b) { 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 (a instanceof ItemRange) {
if (b instanceof ItemRange) { if (b instanceof ItemRange) {
var aInt = (a.data.end - a.data.start); var aInt = (a.data.end - a.data.start);
@ -28,6 +29,9 @@ function Stack (itemset, options) {
return 1; return 1;
} }
else { else {
if (!a.data) {
throw new Error('hu')
}
return (a.data.start - b.data.start); return (a.data.start - b.data.start);
} }
} }
@ -59,7 +63,7 @@ Stack.prototype.setOptions = function setOptions (options) {
*/ */
Stack.prototype.update = function update() { Stack.prototype.update = function update() {
this._order(); 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'); 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<String, Item>} items
* @return {Item[]} sorted items
*/
Stack.prototype.order = function order(items) {
var ordered = []; 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; var order = this.options.order || this.defaultOptions.order;
if (!(typeof order === 'function')) { if (!(typeof order === 'function')) {
throw new Error('Option order must be a function'); throw new Error('Option order must be a function');
} }
ordered.sort(order); ordered.sort(order);
this.ordered = ordered;
return ordered;
}; };
/** /**
* Adjust vertical positions of the events such that they don't overlap each * Adjust vertical positions of the events such that they don't overlap each
* other. * other.
* @param {Item[]} items
* @private * @private
*/ */
Stack.prototype._stack = function _stack () {
Stack.prototype._stack = function _stack (items) {
var i, var i,
iMax, iMax,
ordered = this.ordered,
options = this.options, options = this.options,
orientation = options.orientation || this.defaultOptions.orientation, orientation = options.orientation || this.defaultOptions.orientation,
axisOnTop = (orientation == 'top'), 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) { if (options.margin && options.margin.item !== undefined) {
margin = options.margin.item; margin = options.margin.item;
@ -116,14 +127,40 @@ Stack.prototype._stack = function _stack () {
margin = this.defaultOptions.margin.item 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; 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 { do {
// TODO: optimize checking for overlap. when there is a gap without items, // 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 // 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) { if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element // There is a collision. Reposition the event above the colliding element
if (axisOnTop) { 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 * Check if the destiny position of given item overlaps with any
* of the other items from index itemStart to itemEnd. * 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 * when the margin between the two is smaller than
* the requested margin. * 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; var collision = this.collision;
// we loop from end to start, as we suppose that the chance of a // we loop from end to start, as we suppose that the chance of a

+ 123
- 137
src/timeline/component/ItemSet.js View File

@ -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.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
this.stack = new Stack(this, Object.create(this.options)); 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). * Set range (start and end).
* @param {Range | Object} range A Range or an object containing 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, asSize = util.option.asSize,
options = this.options, options = this.options,
orientation = this.getOption('orientation'), orientation = this.getOption('orientation'),
defaultOptions = this.defaultOptions,
frame = this.frame; frame = this.frame;
if (!frame) { if (!frame) {
@ -314,105 +298,56 @@ ItemSet.prototype.repaint = function repaint() {
this._updateConversion(); 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) { if (frame) {
this._updateConversion(); this._updateConversion();
/* TODO
util.forEach(this.items, function (item) { util.forEach(this.items, function (item) {
changed += item.reflow(); changed += item.reflow();
}); });
*/
// TODO: stack.update should be triggered via an event, in stack itself // TODO: stack.update should be triggered via an event, in stack itself
// TODO: only update the stack when there are changed items // TODO: only update the stack when there are changed items
this.stack.update();
//this.stack.update();
var maxHeight = asNumber(options.maxHeight); var maxHeight = asNumber(options.maxHeight);
var fixedHeight = (asSize(options.height) != null); var fixedHeight = (asSize(options.height) != null);
@ -473,7 +410,8 @@ ItemSet.prototype.reflow = function reflow () {
else { else {
// height is not specified, determine the height from the height and positioned items // 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 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 min = visibleItems[0].top;
var max = visibleItems[0].top + visibleItems[0].height; var max = visibleItems[0].top + visibleItems[0].height;
util.forEach(visibleItems, function (item) { util.forEach(visibleItems, function (item) {
@ -489,6 +427,7 @@ ItemSet.prototype.reflow = function reflow () {
if (maxHeight != null) { if (maxHeight != null) {
height = Math.min(height, maxHeight); height = Math.min(height, maxHeight);
} }
height = 200; // TODO: cleanup
changed += update(this, 'height', height); changed += update(this, 'height', height);
// calculate height from items // calculate height from items
@ -500,7 +439,7 @@ ItemSet.prototype.reflow = function reflow () {
changed += 1; changed += 1;
} }
return (changed > 0);
return false;
}; };
/** /**
@ -599,17 +538,65 @@ ItemSet.prototype.removeItem = function removeItem (id) {
* @private * @private
*/ */
ItemSet.prototype._onUpdate = function _onUpdate(ids) { 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 * @param {Number[]} ids
* @private * @private
*/ */
ItemSet.prototype._onAdd = function _onAdd(ids) {
this._toQueue('add', ids);
};
ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
/** /**
* Handle removed items * Handle removed items
@ -617,29 +604,28 @@ ItemSet.prototype._onAdd = function _onAdd(ids) {
* @private * @private
*/ */
ItemSet.prototype._onRemove = function _onRemove(ids) { 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) { 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 * Calculate the scale and offset to convert a position on screen to the
* corresponding date and vice versa. * corresponding date and vice versa.

+ 4
- 2
src/timeline/component/RootPanel.js View File

@ -150,8 +150,10 @@ RootPanel.prototype._watch = function () {
if (me.frame) { if (me.frame) {
// check whether the frame is resized // 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(); me.requestReflow();
} }
} }

+ 161
- 227
src/timeline/component/item/ItemBox.js View File

@ -11,294 +11,228 @@
function ItemBox (parent, data, options, defaultOptions) { function ItemBox (parent, data, options, defaultOptions) {
this.props = { this.props = {
dot: { dot: {
left: 0,
top: 0,
width: 0, width: 0,
height: 0 height: 0
}, },
line: { line: {
top: 0,
left: 0,
width: 0, width: 0,
height: 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); Item.call(this, parent, data, options, defaultOptions);
} }
ItemBox.prototype = new Item (null, null); 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 * Repaint the item
* @return {Boolean} changed * @return {Boolean} changed
*/ */
ItemBox.prototype.repaint = function repaint() { ItemBox.prototype.repaint = function repaint() {
// TODO: make an efficient 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) { if (!dom) {
this._create();
this.dom = {};
dom = 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 * 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 * @override
*/ */
ItemBox.prototype.reposition = function reposition() { ItemBox.prototype.reposition = function reposition() {
var dom = this.dom, var dom = this.dom,
props = this.props, 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';
}; };

Loading…
Cancel
Save