// TODO: turn Stack into a Mixin?
|
|
|
|
/**
|
|
* @constructor Stack
|
|
* Stacks items on top of each other.
|
|
* @param {Object} [options]
|
|
*/
|
|
function Stack (options) {
|
|
this.options = options || {};
|
|
this.defaultOptions = {
|
|
order: function (a, b) {
|
|
// 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);
|
|
var bInt = (b.data.end - b.data.start);
|
|
return (aInt - bInt) || (a.data.start - b.data.start);
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
}
|
|
else {
|
|
if (b instanceof ItemRange) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return (a.data.start - b.data.start);
|
|
}
|
|
}
|
|
},
|
|
margin: {
|
|
item: 10,
|
|
axis: 20
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set options for the stack
|
|
* @param {Object} options Available options:
|
|
* {Number} [margin.item=10]
|
|
* {Number} [margin.axis=20]
|
|
* {function} [order] Stacking order
|
|
*/
|
|
Stack.prototype.setOptions = function setOptions (options) {
|
|
util.extend(this.options, options);
|
|
};
|
|
|
|
/**
|
|
* Order an array with items using a predefined order function for items
|
|
* @param {Item[]} items
|
|
*/
|
|
Stack.prototype.order = function order(items) {
|
|
//order the items
|
|
var order = this.options.order || this.defaultOptions.order;
|
|
if (!(typeof order === 'function')) {
|
|
throw new Error('Option order must be a function');
|
|
}
|
|
items.sort(order);
|
|
};
|
|
|
|
/**
|
|
* Order items by their start data
|
|
* @param {Item[]} items
|
|
*/
|
|
Stack.prototype.orderByStart = function orderByStart(items) {
|
|
items.sort(function (a, b) {
|
|
return a.data.start - b.data.start;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Order items by their end date. If they have no end date, their start date
|
|
* is used.
|
|
* @param {Item[]} items
|
|
*/
|
|
Stack.prototype.orderByEnd = function orderByEnd(items) {
|
|
items.sort(function (a, b) {
|
|
var aTime = ('end' in a.data) ? a.data.end : a.data.start,
|
|
bTime = ('end' in b.data) ? b.data.end : b.data.start;
|
|
|
|
return aTime - bTime;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adjust vertical positions of the events such that they don't overlap each
|
|
* other.
|
|
* @param {Item[]} items All visible items
|
|
* @param {boolean} [force=false] If true, all items will be re-stacked.
|
|
* If false (default), only items having a
|
|
* top===null will be re-stacked
|
|
* @private
|
|
*/
|
|
Stack.prototype.stack = function stack (items, force) {
|
|
var i,
|
|
iMax,
|
|
options = this.options,
|
|
marginItem,
|
|
marginAxis;
|
|
|
|
if (options.margin && options.margin.item !== undefined) {
|
|
marginItem = options.margin.item;
|
|
}
|
|
else {
|
|
marginItem = this.defaultOptions.margin.item
|
|
}
|
|
if (options.margin && options.margin.axis !== undefined) {
|
|
marginAxis = options.margin.axis;
|
|
}
|
|
else {
|
|
marginAxis = this.defaultOptions.margin.axis
|
|
}
|
|
|
|
if (force) {
|
|
// reset top position of all items
|
|
for (i = 0, iMax = items.length; i < iMax; i++) {
|
|
items[i].top = null;
|
|
}
|
|
}
|
|
|
|
// calculate new, non-overlapping positions
|
|
for (i = 0, iMax = items.length; i < iMax; i++) {
|
|
var item = items[i];
|
|
if (item.top === null) {
|
|
// initialize top position
|
|
item.top = marginAxis;
|
|
|
|
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 = 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, marginItem)) {
|
|
collidingItem = other;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (collidingItem != null) {
|
|
// There is a collision. Reposition the event above the colliding element
|
|
item.top = collidingItem.top + collidingItem.height + marginItem;
|
|
}
|
|
} while (collidingItem);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Test if the two provided items collide
|
|
* The items must have parameters left, width, top, and height.
|
|
* @param {Component} a The first item
|
|
* @param {Component} b The second item
|
|
* @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.
|
|
* @return {boolean} true if a and b collide, else false
|
|
*/
|
|
Stack.prototype.collision = function collision (a, b, margin) {
|
|
return ((a.left - margin) < (b.left + b.width) &&
|
|
(a.left + a.width + margin) > b.left &&
|
|
(a.top - margin) < (b.top + b.height) &&
|
|
(a.top + a.height + margin) > b.top);
|
|
};
|