diff --git a/dist/vis.js b/dist/vis.js index c867002a..5702d4b4 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 3.6.5-SNAPSHOT - * @date 2014-11-11 + * @date 2014-11-12 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -101,47 +101,47 @@ return /******/ (function(modules) { // webpackBootstrap // Timeline exports.Timeline = __webpack_require__(18); - exports.Graph2d = __webpack_require__(40); + exports.Graph2d = __webpack_require__(41); exports.timeline = { DateUtil: __webpack_require__(24), - DataStep: __webpack_require__(43), + DataStep: __webpack_require__(44), Range: __webpack_require__(21), - stack: __webpack_require__(33), + stack: __webpack_require__(50), TimeStep: __webpack_require__(27), components: { items: { Item: __webpack_require__(35), - BackgroundItem: __webpack_require__(39), - BoxItem: __webpack_require__(37), - PointItem: __webpack_require__(38), - RangeItem: __webpack_require__(34) + BackgroundItem: __webpack_require__(38), + BoxItem: __webpack_require__(34), + PointItem: __webpack_require__(36), + RangeItem: __webpack_require__(37) }, Component: __webpack_require__(23), CurrentTime: __webpack_require__(28), CustomTime: __webpack_require__(30), - DataAxis: __webpack_require__(42), - GraphGroup: __webpack_require__(44), + DataAxis: __webpack_require__(43), + GraphGroup: __webpack_require__(45), Group: __webpack_require__(32), - BackgroundGroup: __webpack_require__(36), + BackgroundGroup: __webpack_require__(33), ItemSet: __webpack_require__(31), - Legend: __webpack_require__(48), - LineGraph: __webpack_require__(41), + Legend: __webpack_require__(49), + LineGraph: __webpack_require__(42), TimeAxis: __webpack_require__(26) } }; // Network - exports.Network = __webpack_require__(49); + exports.Network = __webpack_require__(51); exports.network = { - Edge: __webpack_require__(56), - Groups: __webpack_require__(53), - Images: __webpack_require__(54), - Node: __webpack_require__(55), - Popup: __webpack_require__(57), - dotparser: __webpack_require__(51), - gephiParser: __webpack_require__(52) + Edge: __webpack_require__(57), + Groups: __webpack_require__(54), + Images: __webpack_require__(55), + Node: __webpack_require__(56), + Popup: __webpack_require__(58), + dotparser: __webpack_require__(52), + gephiParser: __webpack_require__(53) }; // Deprecated since v3.0.0 @@ -1301,14 +1301,14 @@ return /******/ (function(modules) { // webpackBootstrap * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses * this function will then iterate in both directions over this sorted list to find all visible items. * - * @param {Item[]} orderedItems Items ordered by start - * @param {{start: number, end: number}} range + * @param {Item[]} orderedItems | Items ordered by start + * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher * @param {String} field * @param {String} field2 * @returns {number} * @private */ - exports.binarySearch = function(orderedItems, range, field, field2) { + exports.binarySearchCustom = function(orderedItems, searchFunction, field, field2) { var maxIterations = 10000; var iteration = 0; var low = 0; @@ -1318,12 +1318,13 @@ return /******/ (function(modules) { // webpackBootstrap var middle = Math.floor((low + high) / 2); var item = orderedItems[middle]; - if (item.isVisible(range)) { // jihaa, found a visible item! + var value = (field2 === undefined) ? item[field] : item[field][field2]; + + var searchResult = searchFunction(value); + if (searchResult == 0) { // jihaa, found a visible item! return middle; } - - var value = (field2 === undefined) ? item[field] : item[field][field2]; - if (value < range.start) { // it is too small --> increase low + else if (searchResult == -1) { // it is too small --> increase low low = middle + 1; } else { // it is too big --> decrease high @@ -1348,7 +1349,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {number} * @private */ - exports.binarySearchGeneric = function(orderedItems, target, field, sidePreference) { + exports.binarySearchValue = function(orderedItems, target, field, sidePreference) { var maxIterations = 10000; var iteration = 0; var low = 0; @@ -13232,7 +13233,7 @@ return /******/ (function(modules) { // webpackBootstrap var CurrentTime = __webpack_require__(28); var CustomTime = __webpack_require__(30); var ItemSet = __webpack_require__(31); - var Activator = __webpack_require__(69); + var Activator = __webpack_require__(39); var DateUtil = __webpack_require__(24); /** @@ -15426,11 +15427,11 @@ return /******/ (function(modules) { // webpackBootstrap var DataView = __webpack_require__(9); var Component = __webpack_require__(23); var Group = __webpack_require__(32); - var BackgroundGroup = __webpack_require__(36); - var BoxItem = __webpack_require__(37); - var PointItem = __webpack_require__(38); - var RangeItem = __webpack_require__(34); - var BackgroundItem = __webpack_require__(39); + var BackgroundGroup = __webpack_require__(33); + var BoxItem = __webpack_require__(34); + var PointItem = __webpack_require__(36); + var RangeItem = __webpack_require__(37); + var BackgroundItem = __webpack_require__(38); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -16895,8 +16896,8 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var stack = __webpack_require__(33); - var RangeItem = __webpack_require__(34); + var stack = __webpack_require__(50); + var RangeItem = __webpack_require__(37); /** * @constructor Group @@ -16922,7 +16923,7 @@ return /******/ (function(modules) { // webpackBootstrap this.items = {}; // items filtered by groupId of this group this.visibleItems = []; // items currently visible in window - this.orderedItems = { // items sorted by start and by end + this.orderedItems = { byStart: [], byEnd: [] }; @@ -17257,6 +17258,7 @@ return /******/ (function(modules) { // webpackBootstrap // TODO: also remove from ordered items? }; + /** * Remove an item from the corresponding DataSet * @param {Item} item @@ -17265,35 +17267,31 @@ return /******/ (function(modules) { // webpackBootstrap this.itemSet.removeItem(item.id); }; + /** * Reorder the items */ Group.prototype.order = function() { var array = util.toArray(this.items); - this.orderedItems.byStart = array; - this.orderedItems.byEnd = this._constructByEndArray(array); - - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); - }; - - /** - * Create an array containing all items being a range (having an end date) - * @param {Item[]} array - * @returns {RangeItem[]} - * @private - */ - Group.prototype._constructByEndArray = function(array) { + var startArray = []; var endArray = []; for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { + if (array[i].data.end !== undefined) { endArray.push(array[i]); } + startArray.push(array[i]); } - return endArray; + this.orderedItems = { + byStart: startArray, + byEnd: endArray + }; + + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; + /** * Update the visible items * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date @@ -17302,79 +17300,111 @@ return /******/ (function(modules) { // webpackBootstrap * @return {Item[]} visibleItems The new visible items. * @private */ - Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { - var initialPosByStart, - newVisibleItems = [], - i; + Group.prototype._updateVisibleItems = function(orderedItems, oldVisibleItems, range) { + var visibleItems = []; + var visibleItemsLookup = {}; + var interval = (range.end - range.start) / 4; + var lowerBound = range.start - interval; + var upperBound = range.end + interval; + var item, i; + + // this function is used to do the binary search. + var searchFunction = function (value) { + if (value < lowerBound) {return -1;} + else if (value >= lowerBound && value <= upperBound) {return 0;} + else {return 1;} + } // first check if the items that were in view previously are still in view. - // this handles the case for the RangeItem that is both before and after the current one. - if (visibleItems.length > 0) { - for (i = 0; i < visibleItems.length; i++) { - this._checkIfVisible(visibleItems[i], newVisibleItems, range); + // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! + // also cleans up invisible items. + if (oldVisibleItems.length > 0) { + for (i = 0; i < oldVisibleItems.length; i++) { + item = oldVisibleItems[i]; + if (item.isVisible(range)) { + visibleItems.push(item); + visibleItemsLookup[item.id] = true; + } + else { + if (item.displayed) item.hide(); + } } } - // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime) - if (newVisibleItems.length == 0) { - initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); - } - else { - initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); - } + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data','start'); - // use visible search to find a visible RangeItem (only based on endTime) - var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); + // we do a binary search for the items that have defined end times. + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data','end'); - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByStart != -1) { - for (i = initialPosByStart; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - } + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. + this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { + return (item.data.start < lowerBound || item.data.start > upperBound); + }); - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByEnd != -1) { - for (i = initialPosByEnd; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. + this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { + return (item.data.end < lowerBound || item.data.end > upperBound); + }); + + + // finally, we reposition all the visible items. + for (i = 0; i < visibleItems.length; i++) { + item = visibleItems[i]; + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); } - return newVisibleItems; - }; + // debug + //console.log("new line") + //if (this.groupId == null) { + // for (i = 0; i < orderedItems.byStart.length; i++) { + // item = orderedItems.byStart[i].data; + // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") + // } + // for (i = 0; i < orderedItems.byEnd.length; i++) { + // item = orderedItems.byEnd[i].data; + // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") + // } + //} + return visibleItems; + }; + Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { + var item; + var i; - /** - * this function checks if an item is invisible. If it is NOT we make it visible - * and add it to the global visible items. If it is, return true. - * - * @param {Item} item - * @param {Item[]} visibleItems - * @param {{start:number, end:number}} range - * @returns {boolean} - * @private - */ - Group.prototype._checkIfInvisible = function(item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - item.repositionX(); - if (visibleItems.indexOf(item) == -1) { - visibleItems.push(item); + if (initialPos != -1) { + for (i = initialPos; i >= 0; i--) { + item = items[i]; + if (breakCondition(item)) { + break; + } + else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } } - return false; } - else { - if (item.displayed) item.hide(); - return true; + + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; + } + else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } } - }; + } + } + /** * this function is very similar to the _checkIfInvisible() but it does not @@ -17399,6 +17429,8 @@ return /******/ (function(modules) { // webpackBootstrap } }; + + module.exports = Group; @@ -17406,201 +17438,144 @@ return /******/ (function(modules) { // webpackBootstrap /* 33 */ /***/ function(module, exports, __webpack_require__) { - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + var util = __webpack_require__(1); + var Group = __webpack_require__(32); /** - * Order items by their start data - * @param {Item[]} items + * @constructor BackgroundGroup + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - exports.orderByStart = function(items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); - }; + function BackgroundGroup (groupId, data, itemSet) { + Group.call(this, groupId, data, itemSet); - /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items - */ - exports.orderByEnd = function(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; + this.width = 0; + this.height = 0; + this.top = 0; + this.left = 0; + } - return aTime - bTime; - }); - }; + BackgroundGroup.prototype = Object.create(Group.prototype); /** - * Adjust vertical positions of the items such that they don't overlap each - * other. - * @param {Item[]} items - * All visible items + * Repaint this group + * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * Margins between items and between items and the axis. - * @param {boolean} [force=false] - * If true, all items will be repositioned. If false (default), only - * items having a top===null will be re-stacked + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized */ - exports.stack = function(items, margin, force) { - var i, iMax; + BackgroundGroup.prototype.redraw = function(range, margin, restack) { + var resized = false; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.stack && item.top === null) { - // initialize top position - item.top = margin.axis; + // calculate actual size + this.width = this.dom.background.offsetWidth; - 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 && other.stack && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = '0'; - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item.vertical; - } - } while (collidingItem); - } + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); } - }; + return resized; + }; /** - * Adjust vertical positions of the items without stacking them - * @param {Item[]} items - * All visible items - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * Margins between items and between items and the axis. + * Show this group: attach to the DOM */ - exports.nostack = function(items, margin, subgroups) { - var i, iMax, newTop; - - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - if (items[i].data.subgroup !== undefined) { - newTop = margin.axis; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } - items[i].top = newTop; - } - else { - items[i].top = margin.axis; - } + BackgroundGroup.prototype.show = function() { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); } }; - /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {{horizontal: number, vertical: number}} margin - * An object containing a horizontal and vertical - * minimum required margin. - * @return {boolean} true if a and b collide, else false - */ - exports.collision = function(a, b, margin) { - return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && - (a.left + a.width + margin.horizontal - EPSILON) > b.left && - (a.top - margin.vertical + EPSILON) < (b.top + b.height) && - (a.top + a.height + margin.vertical - EPSILON) > b.top); - }; + module.exports = BackgroundGroup; /***/ }, /* 34 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(19); var Item = __webpack_require__(35); + var util = __webpack_require__(1); /** - * @constructor RangeItem + * @constructor BoxItem * @extends Item - * @param {Object} data Object containing parameters start, end + * @param {Object} data Object containing parameters start * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options - * // TODO: describe options + * // TODO: describe available options */ - function RangeItem (data, conversion, options) { + function BoxItem (data, conversion, options) { this.props = { - content: { - width: 0 + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 } }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data.id); - } - if (data.end == undefined) { - throw new Error('Property "end" missing in item ' + data.id); + throw new Error('Property "start" missing in item ' + data); } } Item.call(this, data, conversion, options); } - RangeItem.prototype = new Item (null, null, null); - - RangeItem.prototype.baseClassName = 'item range'; + BoxItem.prototype = new Item (null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - RangeItem.prototype.isVisible = function(range) { + BoxItem.prototype.isVisible = function(range) { // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); }; /** * Repaint the item */ - RangeItem.prototype.redraw = function() { + BoxItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // background box - dom.box = document.createElement('div'); - // className is updated in redraw() + // create main box + dom.box = document.createElement('DIV'); - // contents box - dom.content = document.createElement('div'); + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); dom.content.className = 'content'; dom.box.appendChild(dom.content); + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + // attach this item as attribute dom.box['timeline-item'] = this; @@ -17613,11 +17588,19 @@ return /******/ (function(modules) { // webpackBootstrap } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; - if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); - } + if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); foreground.appendChild(dom.box); } + if (!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); + } + if (!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); + } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: @@ -17631,30 +17614,30 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.box); // update class - var className = (this.data.className ? (' ' + this.data.className) : '') + + var className = (this.data.className? ' ' + this.data.className : '') + (this.selected ? ' selected' : ''); - dom.box.className = this.baseClassName + className; - - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; // recalculate size - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; + 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.dirty = false; } this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); }; /** - * Show the item in the DOM (when not already visible). The items DOM will + * Show the item in the DOM (when not already displayed). The items DOM will * be created when needed. */ - RangeItem.prototype.show = function() { + BoxItem.prototype.show = function() { if (!this.displayed) { this.redraw(); } @@ -17662,15 +17645,14 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) - * @return {Boolean} changed */ - RangeItem.prototype.hide = function() { + BoxItem.prototype.hide = function() { if (this.displayed) { - var box = this.dom.box; + var dom = this.dom; - if (box.parentNode) { - box.parentNode.removeChild(box); - } + 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); this.top = null; this.left = null; @@ -17683,150 +17665,66 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - RangeItem.prototype.repositionX = function() { - var parentWidth = this.parent.width; + BoxItem.prototype.repositionX = function() { var start = this.conversion.toScreen(this.data.start); - var end = this.conversion.toScreen(this.data.end); - var contentLeft; - var contentWidth; + var align = this.options.align; + var left; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; + // calculate left position of the box + if (align == 'right') { + this.left = start - this.width; } - var boxWidth = Math.max(end - start, 1); - - if (this.overflow) { + else if (align == 'left') { this.left = start; - this.width = boxWidth + this.props.content.width; - contentWidth = this.props.content.width; - - // Note: The calculation of width is an optimistic calculation, giving - // a width which will not change when moving the Timeline - // So no re-stacking needed, which is nicer for the eye; } else { - this.left = start; - this.width = boxWidth; - contentWidth = Math.min(end - start, this.props.content.width); + // default or 'center' + this.left = start - this.width / 2; } - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - - switch (this.options.align) { - case 'left': - this.dom.content.style.left = '0'; - break; - - case 'right': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px'; - break; + // reposition box + box.style.left = this.left + 'px'; - case 'center': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px'; - break; + // reposition line + line.style.left = (start - this.props.line.width / 2) + 'px'; - default: // 'auto' - if (this.overflow) { - // when range exceeds left of the window, position the contents at the left of the visible area - contentLeft = Math.max(-start, 0); - } - else { - // when range exceeds left of the window, position the contents at the left of the visible area - if (start < 0) { - contentLeft = Math.min(-start, - (end - start - this.props.content.width - 2 * this.options.padding)); - // TODO: remove the need for options.padding. it's terrible. - } - else { - contentLeft = 0; - } - } - this.dom.content.style.left = contentLeft + 'px'; - } + // reposition dot + dot.style.left = (start - this.props.dot.width / 2) + 'px'; }; /** * Reposition the item vertically * @Override */ - RangeItem.prototype.repositionY = function() { - var orientation = this.options.orientation, - box = this.dom.box; + BoxItem.prototype.repositionY = function() { + var orientation = this.options.orientation; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; if (orientation == 'top') { - box.style.top = this.top + 'px'; - } - else { - box.style.top = (this.parent.height - this.top - this.height) + 'px'; - } - }; - - /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragLeft = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { - // create and show drag area - var dragLeft = document.createElement('div'); - dragLeft.className = 'drag-left'; - dragLeft.dragLeftItem = this; - - // TODO: this should be redundant? - Hammer(dragLeft, { - preventDefault: true - }).on('drag', function () { - //console.log('drag left') - }); + box.style.top = (this.top || 0) + 'px'; - this.dom.box.appendChild(dragLeft); - this.dom.dragLeft = dragLeft; - } - else if (!this.selected && this.dom.dragLeft) { - // delete drag area - if (this.dom.dragLeft.parentNode) { - this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); - } - this.dom.dragLeft = null; + line.style.top = '0'; + line.style.height = (this.parent.top + this.top + 1) + 'px'; + line.style.bottom = ''; } - }; - - /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragRight = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { - // create and show drag area - var dragRight = document.createElement('div'); - dragRight.className = 'drag-right'; - dragRight.dragRightItem = this; - - // TODO: this should be redundant? - Hammer(dragRight, { - preventDefault: true - }).on('drag', function () { - //console.log('drag right') - }); + else { // orientation 'bottom' + var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty + var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - this.dom.box.appendChild(dragRight); - this.dom.dragRight = dragRight; - } - else if (!this.selected && this.dom.dragRight) { - // delete drag area - if (this.dom.dragRight.parentNode) { - this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); - } - this.dom.dragRight = null; + box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; + line.style.top = (itemSetHeight - lineHeight) + 'px'; + line.style.bottom = '0'; } + + dot.style.top = (-this.props.dot.height / 2) + 'px'; }; - module.exports = RangeItem; + module.exports = BoxItem; /***/ }, @@ -18096,76 +17994,12 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 36 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Group = __webpack_require__(32); - - /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function BackgroundGroup (groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); - - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; - } - - BackgroundGroup.prototype = Object.create(Group.prototype); - - /** - * Repaint this group - * @param {{start: number, end: number}} range - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items - * @return {boolean} Returns true if the group is resized - */ - BackgroundGroup.prototype.redraw = function(range, margin, restack) { - var resized = false; - - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - - // calculate actual size - this.width = this.dom.background.offsetWidth; - - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = '0'; - - // update vertical position of items after they are re-stacked and the height of the group is calculated - for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { - var item = this.visibleItems[i]; - item.repositionY(margin); - } - - return resized; - }; - - /** - * Show this group: attach to the DOM - */ - BackgroundGroup.prototype.show = function() { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; - - module.exports = BackgroundGroup; - - -/***/ }, -/* 37 */ /***/ function(module, exports, __webpack_require__) { var Item = __webpack_require__(35); - var util = __webpack_require__(1); /** - * @constructor BoxItem + * @constructor PointItem * @extends Item * @param {Object} data Object containing parameters start * content, className. @@ -18174,15 +18008,16 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] Configuration options * // TODO: describe available options */ - function BoxItem (data, conversion, options) { + function PointItem (data, conversion, options) { this.props = { dot: { + top: 0, width: 0, height: 0 }, - line: { - width: 0, - height: 0 + content: { + height: 0, + marginLeft: 0 } }; @@ -18196,14 +18031,14 @@ return /******/ (function(modules) { // webpackBootstrap Item.call(this, data, conversion, options); } - BoxItem.prototype = new Item (null, null, null); + PointItem.prototype = new Item (null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - BoxItem.prototype.isVisible = function(range) { + PointItem.prototype.isVisible = function(range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; @@ -18213,31 +18048,28 @@ return /******/ (function(modules) { // webpackBootstrap /** * Repaint the item */ - BoxItem.prototype.redraw = function() { + PointItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // create main box - dom.box = document.createElement('DIV'); + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); + // contents box, right from the dot + dom.content = document.createElement('div'); dom.content.className = 'content'; - dom.box.appendChild(dom.content); - - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; + dom.point.appendChild(dom.content); - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); // attach this item as attribute - dom.box['timeline-item'] = this; + dom.point['timeline-item'] = this; this.dirty = true; } @@ -18246,20 +18078,12 @@ return /******/ (function(modules) { // webpackBootstrap if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.box.parentNode) { + if (!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.box); - } - if (!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); - } - if (!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); + if (!foreground) { + throw new Error('Cannot redraw item: parent has no foreground container element'); + } + foreground.appendChild(dom.point); } this.displayed = true; @@ -18269,35 +18093,41 @@ return /******/ (function(modules) { // webpackBootstrap // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + this._updateTitle(this.dom.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); // update class var className = (this.data.className? ' ' + this.data.className : '') + (this.selected ? ' selected' : ''); - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; + dom.point.className = 'item point' + className; dom.dot.className = 'item dot' + className; // recalculate size - this.props.dot.height = dom.dot.offsetHeight; + this.width = dom.point.offsetWidth; + this.height = dom.point.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.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; + + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + + dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; + dom.dot.style.left = (this.props.dot.width / 2) + 'px'; this.dirty = false; } - this._repaintDeleteButton(dom.box); + this._repaintDeleteButton(dom.point); }; /** - * Show the item in the DOM (when not already displayed). The items DOM will + * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ - BoxItem.prototype.show = function() { + PointItem.prototype.show = function() { if (!this.displayed) { this.redraw(); } @@ -18306,13 +18136,11 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) */ - BoxItem.prototype.hide = function() { + PointItem.prototype.hide = function() { if (this.displayed) { - var dom = this.dom; - - 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); + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); + } this.top = null; this.left = null; @@ -18325,146 +18153,107 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - BoxItem.prototype.repositionX = function() { + PointItem.prototype.repositionX = function() { var start = this.conversion.toScreen(this.data.start); - var align = this.options.align; - var left; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; - // calculate left 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; - } - - // reposition box - box.style.left = this.left + 'px'; - - // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + this.left = start - this.props.dot.width; - // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + // reposition point + this.dom.point.style.left = this.left + 'px'; }; /** * Reposition the item vertically * @Override */ - BoxItem.prototype.repositionY = function() { - var orientation = this.options.orientation; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; + PointItem.prototype.repositionY = function() { + var orientation = this.options.orientation, + point = this.dom.point; if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; - - line.style.top = '0'; - line.style.height = (this.parent.top + this.top + 1) + 'px'; - line.style.bottom = ''; + point.style.top = this.top + 'px'; } - else { // orientation 'bottom' - var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty - var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - - box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; - line.style.top = (itemSetHeight - lineHeight) + 'px'; - line.style.bottom = '0'; + else { + point.style.top = (this.parent.height - this.top - this.height) + 'px'; } - - dot.style.top = (-this.props.dot.height / 2) + 'px'; }; - module.exports = BoxItem; + module.exports = PointItem; /***/ }, -/* 38 */ +/* 37 */ /***/ function(module, exports, __webpack_require__) { + var Hammer = __webpack_require__(19); var Item = __webpack_require__(35); /** - * @constructor PointItem + * @constructor RangeItem * @extends Item - * @param {Object} data Object containing parameters start + * @param {Object} data Object containing parameters start, end * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options - * // TODO: describe available options + * // TODO: describe options */ - function PointItem (data, conversion, options) { + function RangeItem (data, conversion, options) { this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, content: { - height: 0, - marginLeft: 0 + width: 0 } }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + throw new Error('Property "start" missing in item ' + data.id); + } + if (data.end == undefined) { + throw new Error('Property "end" missing in item ' + data.id); } } Item.call(this, data, conversion, options); } - PointItem.prototype = new Item (null, null, null); + RangeItem.prototype = new Item (null, null, null); + + RangeItem.prototype.baseClassName = 'item range'; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - PointItem.prototype.isVisible = function(range) { + RangeItem.prototype.isVisible = function(range) { // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + return (this.data.start < range.end) && (this.data.end > range.start); }; /** * Repaint the item */ - PointItem.prototype.redraw = function() { + RangeItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // background box - dom.point = document.createElement('div'); + // background box + dom.box = document.createElement('div'); // className is updated in redraw() - // contents box, right from the dot + // contents box dom.content = document.createElement('div'); dom.content.className = 'content'; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + dom.box.appendChild(dom.content); // attach this item as attribute - dom.point['timeline-item'] = this; + dom.box['timeline-item'] = this; this.dirty = true; } @@ -18473,12 +18262,12 @@ return /******/ (function(modules) { // webpackBootstrap if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.point.parentNode) { + if (!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.point); + foreground.appendChild(dom.box); } this.displayed = true; @@ -18488,41 +18277,35 @@ return /******/ (function(modules) { // webpackBootstrap // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); - this._updateTitle(this.dom.point); - this._updateDataAttributes(this.dom.point); - this._updateStyle(this.dom.point); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); // update class - var className = (this.data.className? ' ' + this.data.className : '') + + var className = (this.data.className ? (' ' + this.data.className) : '') + (this.selected ? ' selected' : ''); - dom.point.className = 'item point' + className; - dom.dot.className = 'item dot' + className; - - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; + dom.box.className = this.baseClassName + className; - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + // recalculate size + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; this.dirty = false; } - this._repaintDeleteButton(dom.point); + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); }; /** * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ - PointItem.prototype.show = function() { + RangeItem.prototype.show = function() { if (!this.displayed) { this.redraw(); } @@ -18530,11 +18313,14 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - PointItem.prototype.hide = function() { + RangeItem.prototype.hide = function() { if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); + var box = this.dom.box; + + if (box.parentNode) { + box.parentNode.removeChild(box); } this.top = null; @@ -18548,42 +18334,160 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - PointItem.prototype.repositionX = function() { + RangeItem.prototype.repositionX = function() { + var parentWidth = this.parent.width; var start = this.conversion.toScreen(this.data.start); + var end = this.conversion.toScreen(this.data.end); + var contentLeft; + var contentWidth; - this.left = start - this.props.dot.width; + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + var boxWidth = Math.max(end - start, 1); - // reposition point - this.dom.point.style.left = this.left + 'px'; + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; + + // Note: The calculation of width is an optimistic calculation, giving + // a width which will not change when moving the Timeline + // So no re-stacking needed, which is nicer for the eye; + } + else { + this.left = start; + this.width = boxWidth; + contentWidth = Math.min(end - start, this.props.content.width); + } + + this.dom.box.style.left = this.left + 'px'; + this.dom.box.style.width = boxWidth + 'px'; + + switch (this.options.align) { + case 'left': + this.dom.content.style.left = '0'; + break; + + case 'right': + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px'; + break; + + case 'center': + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px'; + break; + + default: // 'auto' + if (this.overflow) { + // when range exceeds left of the window, position the contents at the left of the visible area + contentLeft = Math.max(-start, 0); + } + else { + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - this.props.content.width - 2 * this.options.padding)); + // TODO: remove the need for options.padding. it's terrible. + } + else { + contentLeft = 0; + } + } + this.dom.content.style.left = contentLeft + 'px'; + } }; /** * Reposition the item vertically * @Override */ - PointItem.prototype.repositionY = function() { + RangeItem.prototype.repositionY = function() { var orientation = this.options.orientation, - point = this.dom.point; + box = this.dom.box; if (orientation == 'top') { - point.style.top = this.top + 'px'; + box.style.top = this.top + 'px'; } else { - point.style.top = (this.parent.height - this.top - this.height) + 'px'; + box.style.top = (this.parent.height - this.top - this.height) + 'px'; } }; - module.exports = PointItem; + /** + * Repaint a drag area on the left side of the range when the range is selected + * @protected + */ + RangeItem.prototype._repaintDragLeft = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { + // create and show drag area + var dragLeft = document.createElement('div'); + dragLeft.className = 'drag-left'; + dragLeft.dragLeftItem = this; + + // TODO: this should be redundant? + Hammer(dragLeft, { + preventDefault: true + }).on('drag', function () { + //console.log('drag left') + }); + + this.dom.box.appendChild(dragLeft); + this.dom.dragLeft = dragLeft; + } + else if (!this.selected && this.dom.dragLeft) { + // delete drag area + if (this.dom.dragLeft.parentNode) { + this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); + } + this.dom.dragLeft = null; + } + }; + + /** + * Repaint a drag area on the right side of the range when the range is selected + * @protected + */ + RangeItem.prototype._repaintDragRight = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { + // create and show drag area + var dragRight = document.createElement('div'); + dragRight.className = 'drag-right'; + dragRight.dragRightItem = this; + + // TODO: this should be redundant? + Hammer(dragRight, { + preventDefault: true + }).on('drag', function () { + //console.log('drag right') + }); + + this.dom.box.appendChild(dragRight); + this.dom.dragRight = dragRight; + } + else if (!this.selected && this.dom.dragRight) { + // delete drag area + if (this.dom.dragRight.parentNode) { + this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); + } + this.dom.dragRight = null; + } + }; + + module.exports = RangeItem; /***/ }, -/* 39 */ +/* 38 */ /***/ function(module, exports, __webpack_require__) { var Hammer = __webpack_require__(19); var Item = __webpack_require__(35); - var BackgroundGroup = __webpack_require__(36); - var RangeItem = __webpack_require__(34); + var BackgroundGroup = __webpack_require__(33); + var RangeItem = __webpack_require__(37); /** * @constructor BackgroundItem @@ -18789,8 +18693,358 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = BackgroundItem; +/***/ }, +/* 39 */ +/***/ function(module, exports, __webpack_require__) { + + var keycharm = __webpack_require__(40); + var Emitter = __webpack_require__(11); + var Hammer = __webpack_require__(19); + var util = __webpack_require__(1); + + /** + * Turn an element into an clickToUse element. + * When not active, the element has a transparent overlay. When the overlay is + * clicked, the mode is changed to active. + * When active, the element is displayed with a blue border around it, and + * the interactive contents of the element can be used. When clicked outside + * the element, the elements mode is changed to inactive. + * @param {Element} container + * @constructor + */ + function Activator(container) { + this.active = false; + + this.dom = { + container: container + }; + + this.dom.overlay = document.createElement('div'); + this.dom.overlay.className = 'overlay'; + + this.dom.container.appendChild(this.dom.overlay); + + this.hammer = Hammer(this.dom.overlay, {prevent_default: false}); + this.hammer.on('tap', this._onTapOverlay.bind(this)); + + // block all touch events (except tap) + var me = this; + var events = [ + 'touch', 'pinch', + 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + me.hammer.on(event, function (event) { + event.stopPropagation(); + }); + }); + + // attach a tap event to the window, in order to deactivate when clicking outside the timeline + this.windowHammer = Hammer(window, {prevent_default: false}); + this.windowHammer.on('tap', function (event) { + // deactivate when clicked outside the container + if (!_hasParent(event.target, container)) { + me.deactivate(); + } + }); + + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + this.keycharm = keycharm(); + + // keycharm listener only bounded when active) + this.escListener = this.deactivate.bind(this); + } + + // turn into an event emitter + Emitter(Activator.prototype); + + // The currently active activator + Activator.current = null; + + /** + * Destroy the activator. Cleans up all created DOM and event listeners + */ + Activator.prototype.destroy = function () { + this.deactivate(); + + // remove dom + this.dom.overlay.parentNode.removeChild(this.dom.overlay); + + // cleanup hammer instances + this.hammer = null; + this.windowHammer = null; + // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + }; + + /** + * Activate the element + * Overlay is hidden, element is decorated with a blue shadow border + */ + Activator.prototype.activate = function () { + // we allow only one active activator at a time + if (Activator.current) { + Activator.current.deactivate(); + } + Activator.current = this; + + this.active = true; + this.dom.overlay.style.display = 'none'; + util.addClassName(this.dom.container, 'vis-active'); + + this.emit('change'); + this.emit('activate'); + + // ugly hack: bind ESC after emitting the events, as the Network rebinds all + // keyboard events on a 'change' event + this.keycharm.bind('esc', this.escListener); + }; + + /** + * Deactivate the element + * Overlay is displayed on top of the element + */ + Activator.prototype.deactivate = function () { + this.active = false; + this.dom.overlay.style.display = ''; + util.removeClassName(this.dom.container, 'vis-active'); + this.keycharm.unbind('esc', this.escListener); + + this.emit('change'); + this.emit('deactivate'); + }; + + /** + * Handle a tap event: activate the container + * @param event + * @private + */ + Activator.prototype._onTapOverlay = function (event) { + // activate the container + this.activate(); + event.stopPropagation(); + }; + + /** + * Test whether the element has the requested parent element somewhere in + * its chain of parent nodes. + * @param {HTMLElement} element + * @param {HTMLElement} parent + * @returns {boolean} Returns true when the parent is found somewhere in the + * chain of parent nodes. + * @private + */ + function _hasParent(element, parent) { + while (element) { + if (element === parent) { + return true + } + element = element.parentNode; + } + return false; + } + + module.exports = Activator; + + /***/ }, /* 40 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/** + * Created by Alex on 11/6/2014. + */ + + // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 + // if the module has no dependencies, the above pattern can be simplified to + (function (root, factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.keycharm = factory(); + } + }(this, function () { + + function keycharm(options) { + var preventDefault = options && options.preventDefault || false; + + var _bound = {keydown:{}, keyup:{}}; + var _keys = {}; + var i; + + // a - z + for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} + // A - Z + for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} + // 0 - 9 + for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} + // F1 - F12 + for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} + // num0 - num9 + for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} + + // numpad misc + _keys['num*'] = {code:106, shift: false}; + _keys['num+'] = {code:107, shift: false}; + _keys['num-'] = {code:109, shift: false}; + _keys['num/'] = {code:111, shift: false}; + _keys['num.'] = {code:110, shift: false}; + // arrows + _keys['left'] = {code:37, shift: false}; + _keys['up'] = {code:38, shift: false}; + _keys['right'] = {code:39, shift: false}; + _keys['down'] = {code:40, shift: false}; + // extra keys + _keys['space'] = {code:32, shift: false}; + _keys['enter'] = {code:13, shift: false}; + _keys['shift'] = {code:16, shift: undefined}; + _keys['esc'] = {code:27, shift: false}; + _keys['backspace'] = {code:8, shift: false}; + _keys['tab'] = {code:9, shift: false}; + _keys['ctrl'] = {code:17, shift: false}; + _keys['alt'] = {code:18, shift: false}; + _keys['delete'] = {code:46, shift: false}; + _keys['pageup'] = {code:33, shift: false}; + _keys['pagedown'] = {code:34, shift: false}; + // symbols + _keys['='] = {code:187, shift: false}; + _keys['-'] = {code:189, shift: false}; + _keys[']'] = {code:221, shift: false}; + _keys['['] = {code:219, shift: false}; + + + + var down = function(event) {handleEvent(event,'keydown');}; + var up = function(event) {handleEvent(event,'keyup');}; + + // handle the actualy bound key with the event + var handleEvent = function(event,type) { + if (_bound[type][event.keyCode] !== undefined) { + var bound = _bound[type][event.keyCode]; + for (var i = 0; i < bound.length; i++) { + if (bound[i].shift === undefined) { + bound[i].fn(event); + } + else if (bound[i].shift == true && event.shiftKey == true) { + bound[i].fn(event); + } + else if (bound[i].shift == false && event.shiftKey == false) { + bound[i].fn(event); + } + } + + if (preventDefault == true) { + event.preventDefault(); + } + } + }; + + // bind a key to a callback + this.bind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (_bound[type][_keys[key].code] === undefined) { + _bound[type][_keys[key].code] = []; + } + _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); + }; + + + // bind all keys to a call back (demo purposes) + this.bindAll = function(callback, type) { + if (type === undefined) { + type = 'keydown'; + } + for (key in _keys) { + if (_keys.hasOwnProperty(key)) { + this.bind(key,callback,type); + } + } + } + + // get the key label from an event + this.getKey = function(event) { + for (key in _keys) { + if (_keys.hasOwnProperty(key)) { + if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { + return key; + } + else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { + return key; + } + else if (event.keyCode == _keys[key].code && key == 'shift') { + return key; + } + } + } + return "unknown key, currently not supported"; + }; + + // unbind either a specific callback from a key or all of them (by leaving callback undefined) + this.unbind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (callback !== undefined) { + var newBindings = []; + var bound = _bound[type][_keys[key].code] + for (var i = 0; i < bound.length; i++) { + if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { + newBindings.push(_bound[type][_keys[key].code][i]); + } + } + _bound[type][_keys[key].code] = newBindings; + } + else { + _bound[type][_keys[key].code] = []; + } + }; + + // reset all bound variables. + this.reset = function() { + _bound = {keydown:{}, keyup:{}}; + }; + + // unbind all listeners and reset all variables. + this.destroy = function() { + _bound = {keydown:{}, keyup:{}}; + window.removeEventListener('keydown', down, true); + window.removeEventListener('keyup', up, true); + }; + + // create listeners. + window.addEventListener('keydown',down,true); + window.addEventListener('keyup',up,true); + + // return the public functions. + return this; + } + + return keycharm; + })); + + + + +/***/ }, +/* 41 */ /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(11); @@ -18803,7 +19057,7 @@ return /******/ (function(modules) { // webpackBootstrap var TimeAxis = __webpack_require__(26); var CurrentTime = __webpack_require__(28); var CustomTime = __webpack_require__(30); - var LineGraph = __webpack_require__(41); + var LineGraph = __webpack_require__(42); /** * Create a timeline visualization @@ -19040,7 +19294,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 41 */ +/* 42 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -19048,10 +19302,10 @@ return /******/ (function(modules) { // webpackBootstrap var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); var Component = __webpack_require__(23); - var DataAxis = __webpack_require__(42); - var GraphGroup = __webpack_require__(44); - var Legend = __webpack_require__(48); - var BarGraphFunctions = __webpack_require__(47); + var DataAxis = __webpack_require__(43); + var GraphGroup = __webpack_require__(45); + var Legend = __webpack_require__(49); + var BarGraphFunctions = __webpack_require__(48); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -19736,7 +19990,7 @@ return /******/ (function(modules) { // webpackBootstrap var dataContainer = groupsData[groupIds[i]]; // optimization for sorted data if (group.options.sort == true) { - var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before')); + var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before')); for (j = guess; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { @@ -20003,13 +20257,13 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 42 */ +/* 43 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var Component = __webpack_require__(23); - var DataStep = __webpack_require__(43); + var DataStep = __webpack_require__(44); /** * A horizontal time axis @@ -20604,7 +20858,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 43 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { /** @@ -20872,14 +21126,14 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 44 */ +/* 45 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); - var Line = __webpack_require__(45); - var Bar = __webpack_require__(47); - var Points = __webpack_require__(46); + var Line = __webpack_require__(46); + var Bar = __webpack_require__(48); + var Points = __webpack_require__(47); /** * /** @@ -21077,14 +21331,14 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 45 */ +/* 46 */ /***/ function(module, exports, __webpack_require__) { /** * Created by Alex on 11/11/2014. */ var DOMutil = __webpack_require__(6); - var Points = __webpack_require__(46); + var Points = __webpack_require__(47); function Line(groupId, options) { this.groupId = groupId; @@ -21301,7 +21555,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 46 */ +/* 47 */ /***/ function(module, exports, __webpack_require__) { /** @@ -21349,14 +21603,14 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Points; /***/ }, -/* 47 */ +/* 48 */ /***/ function(module, exports, __webpack_require__) { /** * Created by Alex on 11/11/2014. */ var DOMutil = __webpack_require__(6); - var Points = __webpack_require__(46); + var Points = __webpack_require__(47); function Bargraph(groupId, options) { this.groupId = groupId; @@ -21583,7 +21837,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Bargraph; /***/ }, -/* 48 */ +/* 49 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -21793,25 +22047,153 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 49 */ +/* 50 */ +/***/ function(module, exports, __webpack_require__) { + + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + + /** + * Order items by their start data + * @param {Item[]} items + */ + exports.orderByStart = function(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 + */ + exports.orderByEnd = function(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 items such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be repositioned. If false (default), only + * items having a top===null will be re-stacked + */ + exports.stack = function(items, margin, force) { + var i, iMax; + + 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.stack && item.top === null) { + // initialize top position + item.top = margin.axis; + + 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 && other.stack && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the items above the colliding element + item.top = collidingItem.top + collidingItem.height + margin.item.vertical; + } + } while (collidingItem); + } + } + }; + + + /** + * Adjust vertical positions of the items without stacking them + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + */ + exports.nostack = function(items, margin, subgroups) { + var i, iMax, newTop; + + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + if (items[i].data.subgroup !== undefined) { + newTop = margin.axis; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } + } + } + items[i].top = newTop; + } + else { + items[i].top = margin.axis; + } + } + }; + + /** + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Item} a The first item + * @param {Item} b The second item + * @param {{horizontal: number, vertical: number}} margin + * An object containing a horizontal and vertical + * minimum required margin. + * @return {boolean} true if a and b collide, else false + */ + exports.collision = function(a, b, margin) { + return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && + (a.left + a.width + margin.horizontal - EPSILON) > b.left && + (a.top - margin.vertical + EPSILON) < (b.top + b.height) && + (a.top + a.height + margin.vertical - EPSILON) > b.top); + }; + + +/***/ }, +/* 51 */ /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(11); var Hammer = __webpack_require__(19); - var keycharm = __webpack_require__(50); + var keycharm = __webpack_require__(40); var util = __webpack_require__(1); var hammerUtil = __webpack_require__(22); var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); - var dotparser = __webpack_require__(51); - var gephiParser = __webpack_require__(52); - var Groups = __webpack_require__(53); - var Images = __webpack_require__(54); - var Node = __webpack_require__(55); - var Edge = __webpack_require__(56); - var Popup = __webpack_require__(57); - var MixinLoader = __webpack_require__(58); - var Activator = __webpack_require__(69); + var dotparser = __webpack_require__(52); + var gephiParser = __webpack_require__(53); + var Groups = __webpack_require__(54); + var Images = __webpack_require__(55); + var Node = __webpack_require__(56); + var Edge = __webpack_require__(57); + var Popup = __webpack_require__(58); + var MixinLoader = __webpack_require__(59); + var Activator = __webpack_require__(39); var locales = __webpack_require__(70); // Load custom shapes into CanvasRenderingContext2D @@ -24346,200 +24728,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 50 */ -/***/ function(module, exports, __webpack_require__) { - - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/** - * Created by Alex on 11/6/2014. - */ - - // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 - // if the module has no dependencies, the above pattern can be simplified to - (function (root, factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - root.keycharm = factory(); - } - }(this, function () { - - function keycharm(options) { - var preventDefault = options && options.preventDefault || false; - - var _bound = {keydown:{}, keyup:{}}; - var _keys = {}; - var i; - - // a - z - for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} - // A - Z - for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} - // 0 - 9 - for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} - // F1 - F12 - for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} - // num0 - num9 - for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} - - // numpad misc - _keys['num*'] = {code:106, shift: false}; - _keys['num+'] = {code:107, shift: false}; - _keys['num-'] = {code:109, shift: false}; - _keys['num/'] = {code:111, shift: false}; - _keys['num.'] = {code:110, shift: false}; - // arrows - _keys['left'] = {code:37, shift: false}; - _keys['up'] = {code:38, shift: false}; - _keys['right'] = {code:39, shift: false}; - _keys['down'] = {code:40, shift: false}; - // extra keys - _keys['space'] = {code:32, shift: false}; - _keys['enter'] = {code:13, shift: false}; - _keys['shift'] = {code:16, shift: undefined}; - _keys['esc'] = {code:27, shift: false}; - _keys['backspace'] = {code:8, shift: false}; - _keys['tab'] = {code:9, shift: false}; - _keys['ctrl'] = {code:17, shift: false}; - _keys['alt'] = {code:18, shift: false}; - _keys['delete'] = {code:46, shift: false}; - _keys['pageup'] = {code:33, shift: false}; - _keys['pagedown'] = {code:34, shift: false}; - // symbols - _keys['='] = {code:187, shift: false}; - _keys['-'] = {code:189, shift: false}; - _keys[']'] = {code:221, shift: false}; - _keys['['] = {code:219, shift: false}; - - - - var down = function(event) {handleEvent(event,'keydown');}; - var up = function(event) {handleEvent(event,'keyup');}; - - // handle the actualy bound key with the event - var handleEvent = function(event,type) { - if (_bound[type][event.keyCode] !== undefined) { - var bound = _bound[type][event.keyCode]; - for (var i = 0; i < bound.length; i++) { - if (bound[i].shift === undefined) { - bound[i].fn(event); - } - else if (bound[i].shift == true && event.shiftKey == true) { - bound[i].fn(event); - } - else if (bound[i].shift == false && event.shiftKey == false) { - bound[i].fn(event); - } - } - - if (preventDefault == true) { - event.preventDefault(); - } - } - }; - - // bind a key to a callback - this.bind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (_bound[type][_keys[key].code] === undefined) { - _bound[type][_keys[key].code] = []; - } - _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); - }; - - - // bind all keys to a call back (demo purposes) - this.bindAll = function(callback, type) { - if (type === undefined) { - type = 'keydown'; - } - for (key in _keys) { - if (_keys.hasOwnProperty(key)) { - this.bind(key,callback,type); - } - } - } - - // get the key label from an event - this.getKey = function(event) { - for (key in _keys) { - if (_keys.hasOwnProperty(key)) { - if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { - return key; - } - else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { - return key; - } - else if (event.keyCode == _keys[key].code && key == 'shift') { - return key; - } - } - } - return "unknown key, currently not supported"; - }; - - // unbind either a specific callback from a key or all of them (by leaving callback undefined) - this.unbind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (callback !== undefined) { - var newBindings = []; - var bound = _bound[type][_keys[key].code] - for (var i = 0; i < bound.length; i++) { - if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { - newBindings.push(_bound[type][_keys[key].code][i]); - } - } - _bound[type][_keys[key].code] = newBindings; - } - else { - _bound[type][_keys[key].code] = []; - } - }; - - // reset all bound variables. - this.reset = function() { - _bound = {keydown:{}, keyup:{}}; - }; - - // unbind all listeners and reset all variables. - this.destroy = function() { - _bound = {keydown:{}, keyup:{}}; - window.removeEventListener('keydown', down, true); - window.removeEventListener('keyup', up, true); - }; - - // create listeners. - window.addEventListener('keydown',down,true); - window.addEventListener('keyup',up,true); - - // return the public functions. - return this; - } - - return keycharm; - })); - - - - -/***/ }, -/* 51 */ +/* 52 */ /***/ function(module, exports, __webpack_require__) { /** @@ -25371,7 +25560,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 52 */ +/* 53 */ /***/ function(module, exports, __webpack_require__) { @@ -25436,7 +25625,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.parseGephi = parseGephi; /***/ }, -/* 53 */ +/* 54 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -25525,7 +25714,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 54 */ +/* 55 */ /***/ function(module, exports, __webpack_require__) { /** @@ -25583,7 +25772,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 55 */ +/* 56 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -26607,11 +26796,11 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 56 */ +/* 57 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var Node = __webpack_require__(55); + var Node = __webpack_require__(56); /** * @class Edge @@ -27800,7 +27989,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Edge; /***/ }, -/* 57 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { /** @@ -27946,16 +28135,16 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 58 */ +/* 59 */ /***/ function(module, exports, __webpack_require__) { - var PhysicsMixin = __webpack_require__(59); - var ClusterMixin = __webpack_require__(63); - var SectorsMixin = __webpack_require__(64); - var SelectionMixin = __webpack_require__(65); - var ManipulationMixin = __webpack_require__(66); - var NavigationMixin = __webpack_require__(67); - var HierarchicalLayoutMixin = __webpack_require__(68); + var PhysicsMixin = __webpack_require__(60); + var ClusterMixin = __webpack_require__(64); + var SectorsMixin = __webpack_require__(65); + var SelectionMixin = __webpack_require__(66); + var ManipulationMixin = __webpack_require__(67); + var NavigationMixin = __webpack_require__(68); + var HierarchicalLayoutMixin = __webpack_require__(69); /** * Load a mixin into the network object @@ -28150,13 +28339,13 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 59 */ +/* 60 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var RepulsionMixin = __webpack_require__(60); - var HierarchialRepulsionMixin = __webpack_require__(61); - var BarnesHutMixin = __webpack_require__(62); + var RepulsionMixin = __webpack_require__(61); + var HierarchialRepulsionMixin = __webpack_require__(62); + var BarnesHutMixin = __webpack_require__(63); /** * Toggling barnes Hut calculation on and off. @@ -28864,7 +29053,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 60 */ +/* 61 */ /***/ function(module, exports, __webpack_require__) { /** @@ -28928,7 +29117,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 61 */ +/* 62 */ /***/ function(module, exports, __webpack_require__) { /** @@ -29087,7 +29276,7 @@ return /******/ (function(modules) { // webpackBootstrap }; /***/ }, -/* 62 */ +/* 63 */ /***/ function(module, exports, __webpack_require__) { /** @@ -29492,7 +29681,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 63 */ +/* 64 */ /***/ function(module, exports, __webpack_require__) { /** @@ -30635,11 +30824,11 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 64 */ +/* 65 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var Node = __webpack_require__(55); + var Node = __webpack_require__(56); /** * Creation of the SectorMixin var. @@ -31194,10 +31383,10 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 65 */ +/* 66 */ /***/ function(module, exports, __webpack_require__) { - var Node = __webpack_require__(55); + var Node = __webpack_require__(56); /** * This function can be called from the _doInAllSectors function @@ -31908,12 +32097,12 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 66 */ +/* 67 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var Node = __webpack_require__(55); - var Edge = __webpack_require__(56); + var Node = __webpack_require__(56); + var Edge = __webpack_require__(57); /** * clears the toolbar div element of children @@ -32509,7 +32698,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 67 */ +/* 68 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -32691,7 +32880,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 68 */ +/* 69 */ /***/ function(module, exports, __webpack_require__) { exports._resetLevels = function() { @@ -33107,163 +33296,6 @@ return /******/ (function(modules) { // webpackBootstrap }; -/***/ }, -/* 69 */ -/***/ function(module, exports, __webpack_require__) { - - var keycharm = __webpack_require__(50); - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); - - /** - * Turn an element into an clickToUse element. - * When not active, the element has a transparent overlay. When the overlay is - * clicked, the mode is changed to active. - * When active, the element is displayed with a blue border around it, and - * the interactive contents of the element can be used. When clicked outside - * the element, the elements mode is changed to inactive. - * @param {Element} container - * @constructor - */ - function Activator(container) { - this.active = false; - - this.dom = { - container: container - }; - - this.dom.overlay = document.createElement('div'); - this.dom.overlay.className = 'overlay'; - - this.dom.container.appendChild(this.dom.overlay); - - this.hammer = Hammer(this.dom.overlay, {prevent_default: false}); - this.hammer.on('tap', this._onTapOverlay.bind(this)); - - // block all touch events (except tap) - var me = this; - var events = [ - 'touch', 'pinch', - 'doubletap', 'hold', - 'dragstart', 'drag', 'dragend', - 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (event) { - me.hammer.on(event, function (event) { - event.stopPropagation(); - }); - }); - - // attach a tap event to the window, in order to deactivate when clicking outside the timeline - this.windowHammer = Hammer(window, {prevent_default: false}); - this.windowHammer.on('tap', function (event) { - // deactivate when clicked outside the container - if (!_hasParent(event.target, container)) { - me.deactivate(); - } - }); - - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - this.keycharm = keycharm(); - - // keycharm listener only bounded when active) - this.escListener = this.deactivate.bind(this); - } - - // turn into an event emitter - Emitter(Activator.prototype); - - // The currently active activator - Activator.current = null; - - /** - * Destroy the activator. Cleans up all created DOM and event listeners - */ - Activator.prototype.destroy = function () { - this.deactivate(); - - // remove dom - this.dom.overlay.parentNode.removeChild(this.dom.overlay); - - // cleanup hammer instances - this.hammer = null; - this.windowHammer = null; - // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) - }; - - /** - * Activate the element - * Overlay is hidden, element is decorated with a blue shadow border - */ - Activator.prototype.activate = function () { - // we allow only one active activator at a time - if (Activator.current) { - Activator.current.deactivate(); - } - Activator.current = this; - - this.active = true; - this.dom.overlay.style.display = 'none'; - util.addClassName(this.dom.container, 'vis-active'); - - this.emit('change'); - this.emit('activate'); - - // ugly hack: bind ESC after emitting the events, as the Network rebinds all - // keyboard events on a 'change' event - this.keycharm.bind('esc', this.escListener); - }; - - /** - * Deactivate the element - * Overlay is displayed on top of the element - */ - Activator.prototype.deactivate = function () { - this.active = false; - this.dom.overlay.style.display = ''; - util.removeClassName(this.dom.container, 'vis-active'); - this.keycharm.unbind('esc', this.escListener); - - this.emit('change'); - this.emit('deactivate'); - }; - - /** - * Handle a tap event: activate the container - * @param event - * @private - */ - Activator.prototype._onTapOverlay = function (event) { - // activate the container - this.activate(); - event.stopPropagation(); - }; - - /** - * Test whether the element has the requested parent element somewhere in - * its chain of parent nodes. - * @param {HTMLElement} element - * @param {HTMLElement} parent - * @returns {boolean} Returns true when the parent is found somewhere in the - * chain of parent nodes. - * @private - */ - function _hasParent(element, parent) { - while (element) { - if (element === parent) { - return true - } - element = element.parentNode; - } - return false; - } - - module.exports = Activator; - - /***/ }, /* 70 */ /***/ function(module, exports, __webpack_require__) { diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 97452cfa..42476d32 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -26,7 +26,7 @@ function Group (groupId, data, itemSet) { this.items = {}; // items filtered by groupId of this group this.visibleItems = []; // items currently visible in window - this.orderedItems = { // items sorted by start and by end + this.orderedItems = { byStart: [], byEnd: [] }; @@ -361,6 +361,7 @@ Group.prototype.remove = function(item) { // TODO: also remove from ordered items? }; + /** * Remove an item from the corresponding DataSet * @param {Item} item @@ -369,35 +370,31 @@ Group.prototype.removeFromDataSet = function(item) { this.itemSet.removeItem(item.id); }; + /** * Reorder the items */ Group.prototype.order = function() { var array = util.toArray(this.items); - this.orderedItems.byStart = array; - this.orderedItems.byEnd = this._constructByEndArray(array); - - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); -}; - -/** - * Create an array containing all items being a range (having an end date) - * @param {Item[]} array - * @returns {RangeItem[]} - * @private - */ -Group.prototype._constructByEndArray = function(array) { + var startArray = []; var endArray = []; for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { + if (array[i].data.end !== undefined) { endArray.push(array[i]); } + startArray.push(array[i]); } - return endArray; + this.orderedItems = { + byStart: startArray, + byEnd: endArray + }; + + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; + /** * Update the visible items * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date @@ -406,79 +403,111 @@ Group.prototype._constructByEndArray = function(array) { * @return {Item[]} visibleItems The new visible items. * @private */ -Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { - var initialPosByStart, - newVisibleItems = [], - i; +Group.prototype._updateVisibleItems = function(orderedItems, oldVisibleItems, range) { + var visibleItems = []; + var visibleItemsLookup = {}; + var interval = (range.end - range.start) / 4; + var lowerBound = range.start - interval; + var upperBound = range.end + interval; + var item, i; + + // this function is used to do the binary search. + var searchFunction = function (value) { + if (value < lowerBound) {return -1;} + else if (value >= lowerBound && value <= upperBound) {return 0;} + else {return 1;} + } // first check if the items that were in view previously are still in view. - // this handles the case for the RangeItem that is both before and after the current one. - if (visibleItems.length > 0) { - for (i = 0; i < visibleItems.length; i++) { - this._checkIfVisible(visibleItems[i], newVisibleItems, range); + // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! + // also cleans up invisible items. + if (oldVisibleItems.length > 0) { + for (i = 0; i < oldVisibleItems.length; i++) { + item = oldVisibleItems[i]; + if (item.isVisible(range)) { + visibleItems.push(item); + visibleItemsLookup[item.id] = true; + } + else { + if (item.displayed) item.hide(); + } } } - // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime) - if (newVisibleItems.length == 0) { - initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); - } - else { - initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); - } + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data','start'); - // use visible search to find a visible RangeItem (only based on endTime) - var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); + // we do a binary search for the items that have defined end times. + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data','end'); - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByStart != -1) { - for (i = initialPosByStart; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - } + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. + this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { + return (item.data.start < lowerBound || item.data.start > upperBound); + }); - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByEnd != -1) { - for (i = initialPosByEnd; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. + this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { + return (item.data.end < lowerBound || item.data.end > upperBound); + }); + + + // finally, we reposition all the visible items. + for (i = 0; i < visibleItems.length; i++) { + item = visibleItems[i]; + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); } - return newVisibleItems; -}; + // debug + //console.log("new line") + //if (this.groupId == null) { + // for (i = 0; i < orderedItems.byStart.length; i++) { + // item = orderedItems.byStart[i].data; + // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") + // } + // for (i = 0; i < orderedItems.byEnd.length; i++) { + // item = orderedItems.byEnd[i].data; + // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") + // } + //} + return visibleItems; +}; +Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { + var item; + var i; -/** - * this function checks if an item is invisible. If it is NOT we make it visible - * and add it to the global visible items. If it is, return true. - * - * @param {Item} item - * @param {Item[]} visibleItems - * @param {{start:number, end:number}} range - * @returns {boolean} - * @private - */ -Group.prototype._checkIfInvisible = function(item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - item.repositionX(); - if (visibleItems.indexOf(item) == -1) { - visibleItems.push(item); + if (initialPos != -1) { + for (i = initialPos; i >= 0; i--) { + item = items[i]; + if (breakCondition(item)) { + break; + } + else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } } - return false; } - else { - if (item.displayed) item.hide(); - return true; + + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; + } + else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } } -}; + } +} + /** * this function is very similar to the _checkIfInvisible() but it does not @@ -503,4 +532,6 @@ Group.prototype._checkIfVisible = function(item, visibleItems, range) { } }; + + module.exports = Group; diff --git a/lib/timeline/component/LineGraph.js b/lib/timeline/component/LineGraph.js index 35215882..f5817169 100644 --- a/lib/timeline/component/LineGraph.js +++ b/lib/timeline/component/LineGraph.js @@ -691,7 +691,7 @@ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, var dataContainer = groupsData[groupIds[i]]; // optimization for sorted data if (group.options.sort == true) { - var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before')); + var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before')); for (j = guess; j < group.itemsData.length; j++) { item = group.itemsData[j]; if (item !== undefined) { diff --git a/lib/util.js b/lib/util.js index f3142cea..7294f406 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1141,14 +1141,14 @@ exports.mergeOptions = function (mergeTarget, options, option) { * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses * this function will then iterate in both directions over this sorted list to find all visible items. * - * @param {Item[]} orderedItems Items ordered by start - * @param {{start: number, end: number}} range + * @param {Item[]} orderedItems | Items ordered by start + * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher * @param {String} field * @param {String} field2 * @returns {number} * @private */ -exports.binarySearch = function(orderedItems, range, field, field2) { +exports.binarySearchCustom = function(orderedItems, searchFunction, field, field2) { var maxIterations = 10000; var iteration = 0; var low = 0; @@ -1158,12 +1158,13 @@ exports.binarySearch = function(orderedItems, range, field, field2) { var middle = Math.floor((low + high) / 2); var item = orderedItems[middle]; - if (item.isVisible(range)) { // jihaa, found a visible item! + var value = (field2 === undefined) ? item[field] : item[field][field2]; + + var searchResult = searchFunction(value); + if (searchResult == 0) { // jihaa, found a visible item! return middle; } - - var value = (field2 === undefined) ? item[field] : item[field][field2]; - if (value < range.start) { // it is too small --> increase low + else if (searchResult == -1) { // it is too small --> increase low low = middle + 1; } else { // it is too big --> decrease high @@ -1188,7 +1189,7 @@ exports.binarySearch = function(orderedItems, range, field, field2) { * @returns {number} * @private */ -exports.binarySearchGeneric = function(orderedItems, target, field, sidePreference) { +exports.binarySearchValue = function(orderedItems, target, field, sidePreference) { var maxIterations = 10000; var iteration = 0; var low = 0;