Return-Path: X-Original-To: apmail-climate-commits-archive@minotaur.apache.org Delivered-To: apmail-climate-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id EA6B310235 for ; Fri, 21 Jun 2013 07:50:55 +0000 (UTC) Received: (qmail 29915 invoked by uid 500); 21 Jun 2013 07:50:55 -0000 Delivered-To: apmail-climate-commits-archive@climate.apache.org Received: (qmail 29820 invoked by uid 500); 21 Jun 2013 07:50:52 -0000 Mailing-List: contact commits-help@climate.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@climate.incubator.apache.org Delivered-To: mailing list commits@climate.incubator.apache.org Received: (qmail 29784 invoked by uid 99); 21 Jun 2013 07:50:49 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 21 Jun 2013 07:50:49 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 21 Jun 2013 07:50:45 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id 3EAF92388A29; Fri, 21 Jun 2013 07:50:26 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1495306 [3/3] - in /incubator/climate/trunk/rcmet/src/main/ui/app: ./ css/ js/controllers/ js/directives/ lib/timeline/ Date: Fri, 21 Jun 2013 07:50:25 -0000 To: commits@climate.incubator.apache.org From: skhudiky@apache.org X-Mailer: svnmailer-1.0.8-patched Message-Id: <20130621075026.3EAF92388A29@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js URL: http://svn.apache.org/viewvc/incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js?rev=1495306&view=auto ============================================================================== --- incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js (added) +++ incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js Fri Jun 21 07:50:24 2013 @@ -0,0 +1,6381 @@ +/** + * @file timeline.js + * + * @brief + * The Timeline is an interactive visualization chart to visualize events in + * time, having a start and end date. + * You can freely move and zoom in the timeline by dragging + * and scrolling in the Timeline. Items are optionally dragable. The time + * scale on the axis is adjusted automatically, and supports scales ranging + * from milliseconds to years. + * + * Timeline is part of the CHAP Links library. + * + * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and + * Internet Explorer 6+. + * + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Almende B.V. + * + * @author Jos de Jong, + * @date 2013-04-18 + * @version 2.4.2 + */ + +/* + * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/) + * added to v2.4.1 with da_DK language by @bjarkebech + */ + +/* + * TODO + * + * Add zooming with pinching on Android + * + * Bug: when an item contains a javascript onclick or a link, this does not work + * when the item is not selected (when the item is being selected, + * it is redrawn, which cancels any onclick or link action) + * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly + * Bug: neglect items when they have no valid start/end, instead of throwing an error + * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically + * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;} + * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible + */ + +/** + * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, + * "links" + */ +if (typeof links === 'undefined') { + links = {}; + // important: do not use var, as "var links = {};" will overwrite + // the existing links variable value with undefined in IE8, IE7. +} + + +/** + * Ensure the variable google exists + */ +if (typeof google === 'undefined') { + google = undefined; + // important: do not use var, as "var google = undefined;" will overwrite + // the existing google variable value with undefined in IE8, IE7. +} + + + +// Internet Explorer 8 and older does not support Array.indexOf, +// so we define it here in that case +// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ +if(!Array.prototype.indexOf) { + Array.prototype.indexOf = function(obj){ + for(var i = 0; i < this.length; i++){ + if(this[i] == obj){ + return i; + } + } + return -1; + } +} + +// Internet Explorer 8 and older does not support Array.forEach, +// so we define it here in that case +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(fn, scope) { + for(var i = 0, len = this.length; i < len; ++i) { + fn.call(scope || this, this[i], i, this); + } + } +} + + +/** + * @constructor links.Timeline + * The timeline is a visualization chart to visualize events in time. + * + * The timeline is developed in javascript as a Google Visualization Chart. + * + * @param {Element} container The DOM element in which the Timeline will + * be created. Normally a div element. + */ +links.Timeline = function(container) { + if (!container) { + // this call was probably only for inheritance, no constructor-code is required + return; + } + + // create variables and set default values + this.dom = {}; + this.conversion = {}; + this.eventParams = {}; // stores parameters for mouse events + this.groups = []; + this.groupIndexes = {}; + this.items = []; + this.renderQueue = { + show: [], // Items made visible but not yet added to DOM + hide: [], // Items currently visible but not yet removed from DOM + update: [] // Items with changed data but not yet adjusted DOM + }; + this.renderedItems = []; // Items currently rendered in the DOM + this.clusterGenerator = new links.Timeline.ClusterGenerator(this); + this.currentClusters = []; + this.selection = undefined; // stores index and item which is currently selected + + this.listeners = {}; // event listener callbacks + + // Initialize sizes. + // Needed for IE (which gives an error when you try to set an undefined + // value in a style) + this.size = { + 'actualHeight': 0, + 'axis': { + 'characterMajorHeight': 0, + 'characterMajorWidth': 0, + 'characterMinorHeight': 0, + 'characterMinorWidth': 0, + 'height': 0, + 'labelMajorTop': 0, + 'labelMinorTop': 0, + 'line': 0, + 'lineMajorWidth': 0, + 'lineMinorHeight': 0, + 'lineMinorTop': 0, + 'lineMinorWidth': 0, + 'top': 0 + }, + 'contentHeight': 0, + 'contentLeft': 0, + 'contentWidth': 0, + 'frameHeight': 0, + 'frameWidth': 0, + 'groupsLeft': 0, + 'groupsWidth': 0, + 'items': { + 'top': 0 + } + }; + + this.dom.container = container; + + this.options = { + 'width': "100%", + 'height': "auto", + 'minHeight': 0, // minimal height in pixels + 'autoHeight': true, + + 'eventMargin': 10, // minimal margin between events + 'eventMarginAxis': 20, // minimal margin between events and the axis + 'dragAreaWidth': 10, // pixels + + 'min': undefined, + 'max': undefined, + 'zoomMin': 10, // milliseconds + 'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds + + 'moveable': true, + 'zoomable': true, + 'selectable': true, + 'editable': false, + 'snapEvents': true, + 'groupChangeable': true, + + 'showCurrentTime': true, // show a red bar displaying the current time + 'showCustomTime': false, // show a blue, draggable bar displaying a custom time + 'showMajorLabels': true, + 'showMinorLabels': true, + 'showNavigation': false, + 'showButtonNew': false, + 'groupsOnRight': false, + 'axisOnTop': false, + 'stackEvents': true, + 'animate': true, + 'animateZoom': true, + 'cluster': false, + 'style': 'box', + 'customStackOrder': false, //a function(a,b) for determining stackorder amongst a group of items. Essentially a comparator, -ve value for "a before b" and vice versa + + // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text. + 'locale': 'en', + 'MONTHS': new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"), + 'MONTHS_SHORT': new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"), + 'DAYS': new Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"), + 'DAYS_SHORT': new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"), + 'ZOOM_IN': "Zoom in", + 'ZOOM_OUT': "Zoom out", + 'MOVE_LEFT': "Move left", + 'MOVE_RIGHT': "Move right", + 'NEW': "New", + 'CREATE_NEW_EVENT': "Create new event" + }; + + this.clientTimeOffset = 0; // difference between client time and the time + // set via Timeline.setCurrentTime() + var dom = this.dom; + + // remove all elements from the container element. + while (dom.container.hasChildNodes()) { + dom.container.removeChild(dom.container.firstChild); + } + + // create a step for drawing the axis + this.step = new links.Timeline.StepDate(); + + // add standard item types + this.itemTypes = { + box: links.Timeline.ItemBox, + range: links.Timeline.ItemRange, + dot: links.Timeline.ItemDot + }; + + // initialize data + this.data = []; + this.firstDraw = true; + + // date interval must be initialized + this.setVisibleChartRange(undefined, undefined, false); + + // render for the first time + this.render(); + + // fire the ready event + var me = this; + setTimeout(function () { + me.trigger('ready'); + }, 0); +}; + + +/** + * Main drawing logic. This is the function that needs to be called + * in the html page, to draw the timeline. + * + * A data table with the events must be provided, and an options table. + * + * @param {google.visualization.DataTable} data + * The data containing the events for the timeline. + * Object DataTable is defined in + * google.visualization.DataTable + * @param {Object} options A name/value map containing settings for the + * timeline. Optional. + */ +links.Timeline.prototype.draw = function(data, options) { + this.setOptions(options); + + // read the data + this.setData(data); + + // set timer range. this will also redraw the timeline + if (options && (options.start || options.end)) { + this.setVisibleChartRange(options.start, options.end); + } + else if (this.firstDraw) { + this.setVisibleChartRangeAuto(); + } + + this.firstDraw = false; +}; + + +/** + * Set options for the timeline. + * Timeline must be redrawn afterwards + * @param {Object} options A name/value map containing settings for the + * timeline. Optional. + */ +links.Timeline.prototype.setOptions = function(options) { + if (options) { + // retrieve parameter values + for (var i in options) { + if (options.hasOwnProperty(i)) { + this.options[i] = options[i]; + } + } + + // prepare i18n dependent on set locale + if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') { + var localeOpts = links.locales[this.options.locale]; + if(localeOpts) { + for (var l in localeOpts) { + if (localeOpts.hasOwnProperty(l)) { + this.options[l] = localeOpts[l]; + } + } + } + } + + // check for deprecated options + if (options.showButtonAdd != undefined) { + this.options.showButtonNew = options.showButtonAdd; + console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead'); + } + if (options.intervalMin != undefined) { + this.options.zoomMin = options.intervalMin; + console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead'); + } + if (options.intervalMax != undefined) { + this.options.zoomMax = options.intervalMax; + console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead'); + } + + if (options.scale && options.step) { + this.step.setScale(options.scale, options.step); + } + } + + // validate options + this.options.autoHeight = (this.options.height === "auto"); +}; + +/** + * Add new type of items + * @param {String} typeName Name of new type + * @param {links.Timeline.Item} typeFactory Constructor of items + */ +links.Timeline.prototype.addItemType = function (typeName, typeFactory) { + this.itemTypes[typeName] = typeFactory; +}; + +/** + * Retrieve a map with the column indexes of the columns by column name. + * For example, the method returns the map + * { + * start: 0, + * end: 1, + * content: 2, + * group: undefined, + * className: undefined + * editable: undefined + * } + * @param {google.visualization.DataTable} dataTable + * @type {Object} map + */ +links.Timeline.mapColumnIds = function (dataTable) { + var cols = {}, + colMax = dataTable.getNumberOfColumns(), + allUndefined = true; + + // loop over the columns, and map the column id's to the column indexes + for (var col = 0; col < colMax; col++) { + var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); + cols[id] = col; + if (id == 'start' || id == 'end' || id == 'content' || + id == 'group' || id == 'className' || id == 'editable') { + allUndefined = false; + } + } + + // if no labels or ids are defined, + // use the default mapping for start, end, content + if (allUndefined) { + cols.start = 0; + cols.end = 1; + cols.content = 2; + } + + return cols; +}; + +/** + * Set data for the timeline + * @param {google.visualization.DataTable | Array} data + */ +links.Timeline.prototype.setData = function(data) { + // unselect any previously selected item + this.unselectItem(); + + if (!data) { + data = []; + } + + // clear all data + this.stackCancelAnimation(); + this.clearItems(); + this.data = data; + var items = this.items; + this.deleteGroups(); + + if (google && google.visualization && + data instanceof google.visualization.DataTable) { + // map the datatable columns + var cols = links.Timeline.mapColumnIds(data); + + // read DataTable + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + items.push(this.createItem({ + 'start': ((cols.start != undefined) ? data.getValue(row, cols.start) : undefined), + 'end': ((cols.end != undefined) ? data.getValue(row, cols.end) : undefined), + 'content': ((cols.content != undefined) ? data.getValue(row, cols.content) : undefined), + 'group': ((cols.group != undefined) ? data.getValue(row, cols.group) : undefined), + 'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined), + 'editable': ((cols.editable != undefined) ? data.getValue(row, cols.editable) : undefined) + })); + } + } + else if (links.Timeline.isArray(data)) { + // read JSON array + for (var row = 0, rows = data.length; row < rows; row++) { + var itemData = data[row]; + var item = this.createItem(itemData); + items.push(item); + } + } + else { + throw "Unknown data type. DataTable or Array expected."; + } + + // prepare data for clustering, by filtering and sorting by type + if (this.options.cluster) { + this.clusterGenerator.setData(this.items); + } + + this.render({ + animate: false + }); +}; + +/** + * Return the original data table. + * @return {google.visualization.DataTable | Array} data + */ +links.Timeline.prototype.getData = function () { + return this.data; +}; + + +/** + * Update the original data with changed start, end or group. + * + * @param {Number} index + * @param {Object} values An object containing some of the following parameters: + * {Date} start, + * {Date} end, + * {String} content, + * {String} group + */ +links.Timeline.prototype.updateData = function (index, values) { + var data = this.data, + prop; + + if (google && google.visualization && + data instanceof google.visualization.DataTable) { + // update the original google DataTable + var missingRows = (index + 1) - data.getNumberOfRows(); + if (missingRows > 0) { + data.addRows(missingRows); + } + + // map the column id's by name + var cols = links.Timeline.mapColumnIds(data); + + // merge all fields from the provided data into the current data + for (prop in values) { + if (values.hasOwnProperty(prop)) { + var col = cols[prop]; + if (col == undefined) { + // create new column + var value = values[prop]; + var valueType = 'string'; + if (typeof(value) == 'number') {valueType = 'number';} + else if (typeof(value) == 'boolean') {valueType = 'boolean';} + else if (value instanceof Date) {valueType = 'datetime';} + col = data.addColumn(valueType, prop); + } + data.setValue(index, col, values[prop]); + + // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number) + } + } + } + else if (links.Timeline.isArray(data)) { + // update the original JSON table + var row = data[index]; + if (row == undefined) { + row = {}; + data[index] = row; + } + + // merge all fields from the provided data into the current data + for (prop in values) { + if (values.hasOwnProperty(prop)) { + row[prop] = values[prop]; + + // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number) + } + } + } + else { + throw "Cannot update data, unknown type of data"; + } +}; + +/** + * Find the item index from a given HTML element + * If no item index is found, undefined is returned + * @param {Element} element + * @return {Number | undefined} index + */ +links.Timeline.prototype.getItemIndex = function(element) { + var e = element, + dom = this.dom, + frame = dom.items.frame, + items = this.items, + index = undefined; + + // try to find the frame where the items are located in + while (e.parentNode && e.parentNode !== frame) { + e = e.parentNode; + } + + if (e.parentNode === frame) { + // yes! we have found the parent element of all items + // retrieve its id from the array with items + for (var i = 0, iMax = items.length; i < iMax; i++) { + if (items[i].dom === e) { + index = i; + break; + } + } + } + + return index; +}; + +/** + * Set a new size for the timeline + * @param {string} width Width in pixels or percentage (for example "800px" + * or "50%") + * @param {string} height Height in pixels or percentage (for example "400px" + * or "30%") + */ +links.Timeline.prototype.setSize = function(width, height) { + if (width) { + this.options.width = width; + this.dom.frame.style.width = width; + } + if (height) { + this.options.height = height; + this.options.autoHeight = (this.options.height === "auto"); + if (height !== "auto" ) { + this.dom.frame.style.height = height; + } + } + + this.render({ + animate: false + }); +}; + + +/** + * Set a new value for the visible range int the timeline. + * Set start undefined to include everything from the earliest date to end. + * Set end undefined to include everything from start to the last date. + * Example usage: + * myTimeline.setVisibleChartRange(new Date("2010-08-22"), + * new Date("2010-09-13")); + * @param {Date} start The start date for the timeline. optional + * @param {Date} end The end date for the timeline. optional + * @param {boolean} redraw Optional. If true (default) the Timeline is + * directly redrawn + */ +links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) { + var range = {}; + if (!start || !end) { + // retrieve the date range of the items + range = this.getDataRange(true); + } + + if (!start) { + if (end) { + if (range.min && range.min.valueOf() < end.valueOf()) { + // start of the data + start = range.min; + } + else { + // 7 days before the end + start = new Date(end.valueOf()); + start.setDate(start.getDate() - 7); + } + } + else { + // default of 3 days ago + start = new Date(); + start.setDate(start.getDate() - 3); + } + } + + if (!end) { + if (range.max) { + // end of the data + end = range.max; + } + else { + // 7 days after start + end = new Date(start.valueOf()); + end.setDate(end.getDate() + 7); + } + } + + // prevent start Date <= end Date + if (end <= start) { + end = new Date(start.valueOf()); + end.setDate(end.getDate() + 7); + } + + // limit to the allowed range (don't let this do by applyRange, + // because that method will try to maintain the interval (end-start) + var min = this.options.min ? this.options.min : undefined; // date + if (min != undefined && start.valueOf() < min.valueOf()) { + start = new Date(min.valueOf()); // date + } + var max = this.options.max ? this.options.max : undefined; // date + if (max != undefined && end.valueOf() > max.valueOf()) { + end = new Date(max.valueOf()); // date + } + + this.applyRange(start, end); + + if (redraw == undefined || redraw == true) { + this.render({ + animate: false + }); // TODO: optimize, no reflow needed + } + else { + this.recalcConversion(); + } +}; + + +/** + * Change the visible chart range such that all items become visible + */ +links.Timeline.prototype.setVisibleChartRangeAuto = function() { + var range = this.getDataRange(true); + this.setVisibleChartRange(range.min, range.max); +}; + +/** + * Adjust the visible range such that the current time is located in the center + * of the timeline + */ +links.Timeline.prototype.setVisibleChartRangeNow = function() { + var now = new Date(); + + var diff = (this.end.valueOf() - this.start.valueOf()); + + var startNew = new Date(now.valueOf() - diff/2); + var endNew = new Date(startNew.valueOf() + diff); + this.setVisibleChartRange(startNew, endNew); +}; + + +/** + * Retrieve the current visible range in the timeline. + * @return {Object} An object with start and end properties + */ +links.Timeline.prototype.getVisibleChartRange = function() { + return { + 'start': new Date(this.start.valueOf()), + 'end': new Date(this.end.valueOf()) + }; +}; + +/** + * Get the date range of the items. + * @param {boolean} [withMargin] If true, 5% of whitespace is added to the + * left and right of the range. Default is false. + * @return {Object} range An object with parameters min and max. + * - {Date} min is the lowest start date of the items + * - {Date} max is the highest start or end date of the items + * If no data is available, the values of min and max + * will be undefined + */ +links.Timeline.prototype.getDataRange = function (withMargin) { + var items = this.items, + min = undefined, // number + max = undefined; // number + + if (items) { + for (var i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i], + start = item.start != undefined ? item.start.valueOf() : undefined, + end = item.end != undefined ? item.end.valueOf() : start; + + if (min != undefined && start != undefined) { + min = Math.min(min.valueOf(), start.valueOf()); + } + else { + min = start; + } + + if (max != undefined && end != undefined) { + max = Math.max(max, end); + } + else { + max = end; + } + } + } + + if (min && max && withMargin) { + // zoom out 5% such that you have a little white space on the left and right + var diff = (max - min); + min = min - diff * 0.05; + max = max + diff * 0.05; + } + + return { + 'min': min != undefined ? new Date(min) : undefined, + 'max': max != undefined ? new Date(max) : undefined + }; +}; + +/** + * Re-render (reflow and repaint) all components of the Timeline: frame, axis, + * items, ... + * @param {Object} [options] Available options: + * {boolean} renderTimesLeft Number of times the + * render may be repeated + * 5 times by default. + * {boolean} animate takes options.animate + * as default value + */ +links.Timeline.prototype.render = function(options) { + var frameResized = this.reflowFrame(); + var axisResized = this.reflowAxis(); + var groupsResized = this.reflowGroups(); + var itemsResized = this.reflowItems(); + var resized = (frameResized || axisResized || groupsResized || itemsResized); + + // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue). + // if (resized) { + var animate = this.options.animate; + if (options && options.animate != undefined) { + animate = options.animate; + } + + this.recalcConversion(); + this.clusterItems(); + this.filterItems(); + this.stackItems(animate); + + this.recalcItems(); + + // TODO: only repaint when resized or when filterItems or stackItems gave a change? + var needsReflow = this.repaint(); + + // re-render once when needed (prevent endless re-render loop) + if (needsReflow) { + var renderTimesLeft = options ? options.renderTimesLeft : undefined; + if (renderTimesLeft == undefined) { + renderTimesLeft = 5; + } + if (renderTimesLeft > 0) { + this.render({ + 'animate': options ? options.animate: undefined, + 'renderTimesLeft': (renderTimesLeft - 1) + }); + } + } +}; + +/** + * Repaint all components of the Timeline + * @return {boolean} needsReflow Returns true if the DOM is changed such that + * a reflow is needed. + */ +links.Timeline.prototype.repaint = function() { + var frameNeedsReflow = this.repaintFrame(); + var axisNeedsReflow = this.repaintAxis(); + var groupsNeedsReflow = this.repaintGroups(); + var itemsNeedsReflow = this.repaintItems(); + this.repaintCurrentTime(); + this.repaintCustomTime(); + + return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow); +}; + +/** + * Reflow the timeline frame + * @return {boolean} resized Returns true if any of the frame elements + * have been resized. + */ +links.Timeline.prototype.reflowFrame = function() { + var dom = this.dom, + options = this.options, + size = this.size, + resized = false; + + // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead + var frameWidth = dom.frame ? dom.frame.offsetWidth : 0, + frameHeight = dom.frame ? dom.frame.clientHeight : 0; + + resized = resized || (size.frameWidth !== frameWidth); + resized = resized || (size.frameHeight !== frameHeight); + size.frameWidth = frameWidth; + size.frameHeight = frameHeight; + + return resized; +}; + +/** + * repaint the Timeline frame + * @return {boolean} needsReflow Returns true if the DOM is changed such that + * a reflow is needed. + */ +links.Timeline.prototype.repaintFrame = function() { + var needsReflow = false, + dom = this.dom, + options = this.options, + size = this.size; + + // main frame + if (!dom.frame) { + dom.frame = document.createElement("DIV"); + dom.frame.className = "timeline-frame"; + dom.frame.style.position = "relative"; + dom.frame.style.overflow = "hidden"; + dom.container.appendChild(dom.frame); + needsReflow = true; + } + + var height = options.autoHeight ? + (size.actualHeight + "px") : + (options.height || "100%"); + var width = options.width || "100%"; + needsReflow = needsReflow || (dom.frame.style.height != height); + needsReflow = needsReflow || (dom.frame.style.width != width); + dom.frame.style.height = height; + dom.frame.style.width = width; + + // contents + if (!dom.content) { + // create content box where the axis and items will be created + dom.content = document.createElement("DIV"); + dom.content.style.position = "relative"; + dom.content.style.overflow = "hidden"; + dom.frame.appendChild(dom.content); + + var timelines = document.createElement("DIV"); + timelines.style.position = "absolute"; + timelines.style.left = "0px"; + timelines.style.top = "0px"; + timelines.style.height = "100%"; + timelines.style.width = "0px"; + dom.content.appendChild(timelines); + dom.contentTimelines = timelines; + + var params = this.eventParams, + me = this; + if (!params.onMouseDown) { + params.onMouseDown = function (event) {me.onMouseDown(event);}; + links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown); + } + if (!params.onTouchStart) { + params.onTouchStart = function (event) {me.onTouchStart(event);}; + links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart); + } + if (!params.onMouseWheel) { + params.onMouseWheel = function (event) {me.onMouseWheel(event);}; + links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel); + } + if (!params.onDblClick) { + params.onDblClick = function (event) {me.onDblClick(event);}; + links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick); + } + + needsReflow = true; + } + dom.content.style.left = size.contentLeft + "px"; + dom.content.style.top = "0px"; + dom.content.style.width = size.contentWidth + "px"; + dom.content.style.height = size.frameHeight + "px"; + + this.repaintNavigation(); + + return needsReflow; +}; + +/** + * Reflow the timeline axis. Calculate its height, width, positioning, etc... + * @return {boolean} resized returns true if the axis is resized + */ +links.Timeline.prototype.reflowAxis = function() { + var resized = false, + dom = this.dom, + options = this.options, + size = this.size, + axisDom = dom.axis; + + var characterMinorWidth = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0, + characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0, + characterMajorWidth = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0, + characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0, + axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) + + (options.showMajorLabels ? characterMajorHeight : 0); + + var axisTop = options.axisOnTop ? 0 : size.frameHeight - axisHeight, + axisLine = options.axisOnTop ? axisHeight : axisTop; + + resized = resized || (size.axis.top !== axisTop); + resized = resized || (size.axis.line !== axisLine); + resized = resized || (size.axis.height !== axisHeight); + size.axis.top = axisTop; + size.axis.line = axisLine; + size.axis.height = axisHeight; + size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + + (options.showMinorLabels ? characterMinorHeight : 0); + size.axis.labelMinorTop = options.axisOnTop ? + (options.showMajorLabels ? characterMajorHeight : 0) : + axisLine; + size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0; + size.axis.lineMinorHeight = options.showMajorLabels ? + size.frameHeight - characterMajorHeight: + size.frameHeight; + if (axisDom && axisDom.minorLines && axisDom.minorLines.length) { + size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth; + } + else { + size.axis.lineMinorWidth = 1; + } + if (axisDom && axisDom.majorLines && axisDom.majorLines.length) { + size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth; + } + else { + size.axis.lineMajorWidth = 1; + } + + resized = resized || (size.axis.characterMinorWidth !== characterMinorWidth); + resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight); + resized = resized || (size.axis.characterMajorWidth !== characterMajorWidth); + resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight); + size.axis.characterMinorWidth = characterMinorWidth; + size.axis.characterMinorHeight = characterMinorHeight; + size.axis.characterMajorWidth = characterMajorWidth; + size.axis.characterMajorHeight = characterMajorHeight; + + var contentHeight = Math.max(size.frameHeight - axisHeight, 0); + size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth; + size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0); + size.contentHeight = contentHeight; + + return resized; +}; + +/** + * Redraw the timeline axis with minor and major labels + * @return {boolean} needsReflow Returns true if the DOM is changed such + * that a reflow is needed. + */ +links.Timeline.prototype.repaintAxis = function() { + var needsReflow = false, + dom = this.dom, + options = this.options, + size = this.size, + step = this.step; + + var axis = dom.axis; + if (!axis) { + axis = {}; + dom.axis = axis; + } + if (!size.axis.properties) { + size.axis.properties = {}; + } + if (!axis.minorTexts) { + axis.minorTexts = []; + } + if (!axis.minorLines) { + axis.minorLines = []; + } + if (!axis.majorTexts) { + axis.majorTexts = []; + } + if (!axis.majorLines) { + axis.majorLines = []; + } + + if (!axis.frame) { + axis.frame = document.createElement("DIV"); + axis.frame.style.position = "absolute"; + axis.frame.style.left = "0px"; + axis.frame.style.top = "0px"; + dom.content.appendChild(axis.frame); + } + + // take axis offline + dom.content.removeChild(axis.frame); + + axis.frame.style.width = (size.contentWidth) + "px"; + axis.frame.style.height = (size.axis.height) + "px"; + + // the drawn axis is more wide than the actual visual part, such that + // the axis can be dragged without having to redraw it each time again. + var start = this.screenToTime(0); + var end = this.screenToTime(size.contentWidth); + + // calculate minimum step (in milliseconds) based on character size + if (size.axis.characterMinorWidth) { + this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) - + this.screenToTime(0); + + step.setRange(start, end, this.minimumStep); + } + + var charsNeedsReflow = this.repaintAxisCharacters(); + needsReflow = needsReflow || charsNeedsReflow; + + // The current labels on the axis will be re-used (much better performance), + // therefore, the repaintAxis method uses the mechanism with + // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and + // this.size.axis.properties is used. + this.repaintAxisStartOverwriting(); + + step.start(); + var xFirstMajorLabel = undefined; + var max = 0; + while (!step.end() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.timeToScreen(cur), + isMajor = step.isMajor(); + + if (options.showMinorLabels) { + this.repaintAxisMinorText(x, step.getLabelMinor(options)); + } + + if (isMajor && options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this.repaintAxisMajorText(x, step.getLabelMajor(options)); + } + this.repaintAxisMajorLine(x); + } + else { + this.repaintAxisMinorLine(x); + } + + step.next(); + } + + // create a major label on the left when needed + if (options.showMajorLabels) { + var leftTime = this.screenToTime(0), + leftText = this.step.getLabelMajor(options, leftTime), + width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation + + if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) { + this.repaintAxisMajorText(0, leftText, leftTime); + } + } + + // cleanup left over labels + this.repaintAxisEndOverwriting(); + + this.repaintAxisHorizontal(); + + // put axis online + dom.content.insertBefore(axis.frame, dom.content.firstChild); + + return needsReflow; +}; + +/** + * Create characters used to determine the size of text on the axis + * @return {boolean} needsReflow Returns true if the DOM is changed such that + * a reflow is needed. + */ +links.Timeline.prototype.repaintAxisCharacters = function () { + // calculate the width and height of a single character + // this is used to calculate the step size, and also the positioning of the + // axis + var needsReflow = false, + dom = this.dom, + axis = dom.axis, + text; + + if (!axis.characterMinor) { + text = document.createTextNode("0"); + var characterMinor = document.createElement("DIV"); + characterMinor.className = "timeline-axis-text timeline-axis-text-minor"; + characterMinor.appendChild(text); + characterMinor.style.position = "absolute"; + characterMinor.style.visibility = "hidden"; + characterMinor.style.paddingLeft = "0px"; + characterMinor.style.paddingRight = "0px"; + axis.frame.appendChild(characterMinor); + + axis.characterMinor = characterMinor; + needsReflow = true; + } + + if (!axis.characterMajor) { + text = document.createTextNode("0"); + var characterMajor = document.createElement("DIV"); + characterMajor.className = "timeline-axis-text timeline-axis-text-major"; + characterMajor.appendChild(text); + characterMajor.style.position = "absolute"; + characterMajor.style.visibility = "hidden"; + characterMajor.style.paddingLeft = "0px"; + characterMajor.style.paddingRight = "0px"; + axis.frame.appendChild(characterMajor); + + axis.characterMajor = characterMajor; + needsReflow = true; + } + + return needsReflow; +}; + +/** + * Initialize redraw of the axis. All existing labels and lines will be + * overwritten and reused. + */ +links.Timeline.prototype.repaintAxisStartOverwriting = function () { + var properties = this.size.axis.properties; + + properties.minorTextNum = 0; + properties.minorLineNum = 0; + properties.majorTextNum = 0; + properties.majorLineNum = 0; +}; + +/** + * End of overwriting HTML DOM elements of the axis. + * remaining elements will be removed + */ +links.Timeline.prototype.repaintAxisEndOverwriting = function () { + var dom = this.dom, + props = this.size.axis.properties, + frame = this.dom.axis.frame, + num; + + // remove leftovers + var minorTexts = dom.axis.minorTexts; + num = props.minorTextNum; + while (minorTexts.length > num) { + var minorText = minorTexts[num]; + frame.removeChild(minorText); + minorTexts.splice(num, 1); + } + + var minorLines = dom.axis.minorLines; + num = props.minorLineNum; + while (minorLines.length > num) { + var minorLine = minorLines[num]; + frame.removeChild(minorLine); + minorLines.splice(num, 1); + } + + var majorTexts = dom.axis.majorTexts; + num = props.majorTextNum; + while (majorTexts.length > num) { + var majorText = majorTexts[num]; + frame.removeChild(majorText); + majorTexts.splice(num, 1); + } + + var majorLines = dom.axis.majorLines; + num = props.majorLineNum; + while (majorLines.length > num) { + var majorLine = majorLines[num]; + frame.removeChild(majorLine); + majorLines.splice(num, 1); + } +}; + +/** + * Repaint the horizontal line and background of the axis + */ +links.Timeline.prototype.repaintAxisHorizontal = function() { + var axis = this.dom.axis, + size = this.size, + options = this.options; + + // line behind all axis elements (possibly having a background color) + var hasAxis = (options.showMinorLabels || options.showMajorLabels); + if (hasAxis) { + if (!axis.backgroundLine) { + // create the axis line background (for a background color or so) + var backgroundLine = document.createElement("DIV"); + backgroundLine.className = "timeline-axis"; + backgroundLine.style.position = "absolute"; + backgroundLine.style.left = "0px"; + backgroundLine.style.width = "100%"; + backgroundLine.style.border = "none"; + axis.frame.insertBefore(backgroundLine, axis.frame.firstChild); + + axis.backgroundLine = backgroundLine; + } + + if (axis.backgroundLine) { + axis.backgroundLine.style.top = size.axis.top + "px"; + axis.backgroundLine.style.height = size.axis.height + "px"; + } + } + else { + if (axis.backgroundLine) { + axis.frame.removeChild(axis.backgroundLine); + delete axis.backgroundLine; + } + } + + // line before all axis elements + if (hasAxis) { + if (axis.line) { + // put this line at the end of all childs + var line = axis.frame.removeChild(axis.line); + axis.frame.appendChild(line); + } + else { + // make the axis line + var line = document.createElement("DIV"); + line.className = "timeline-axis"; + line.style.position = "absolute"; + line.style.left = "0px"; + line.style.width = "100%"; + line.style.height = "0px"; + axis.frame.appendChild(line); + + axis.line = line; + } + + axis.line.style.top = size.axis.line + "px"; + } + else { + if (axis.line && axis.line.parentElement) { + axis.frame.removeChild(axis.line); + delete axis.line; + } + } +}; + +/** + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + */ +links.Timeline.prototype.repaintAxisMinorText = function (x, text) { + var size = this.size, + dom = this.dom, + props = size.axis.properties, + frame = dom.axis.frame, + minorTexts = dom.axis.minorTexts, + index = props.minorTextNum, + label; + + if (index < minorTexts.length) { + label = minorTexts[index] + } + else { + // create new label + var content = document.createTextNode(""); + label = document.createElement("DIV"); + label.appendChild(content); + label.className = "timeline-axis-text timeline-axis-text-minor"; + label.style.position = "absolute"; + + frame.appendChild(label); + + minorTexts.push(label); + } + + label.childNodes[0].nodeValue = text; + label.style.left = x + "px"; + label.style.top = size.axis.labelMinorTop + "px"; + //label.title = title; // TODO: this is a heavy operation + + props.minorTextNum++; +}; + +/** + * Create a minor line for the axis at position x + * @param {Number} x + */ +links.Timeline.prototype.repaintAxisMinorLine = function (x) { + var axis = this.size.axis, + dom = this.dom, + props = axis.properties, + frame = dom.axis.frame, + minorLines = dom.axis.minorLines, + index = props.minorLineNum, + line; + + if (index < minorLines.length) { + line = minorLines[index]; + } + else { + // create vertical line + line = document.createElement("DIV"); + line.className = "timeline-axis-grid timeline-axis-grid-minor"; + line.style.position = "absolute"; + line.style.width = "0px"; + + frame.appendChild(line); + minorLines.push(line); + } + + line.style.top = axis.lineMinorTop + "px"; + line.style.height = axis.lineMinorHeight + "px"; + line.style.left = (x - axis.lineMinorWidth/2) + "px"; + + props.minorLineNum++; +}; + +/** + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + */ +links.Timeline.prototype.repaintAxisMajorText = function (x, text) { + var size = this.size, + props = size.axis.properties, + frame = this.dom.axis.frame, + majorTexts = this.dom.axis.majorTexts, + index = props.majorTextNum, + label; + + if (index < majorTexts.length) { + label = majorTexts[index]; + } + else { + // create label + var content = document.createTextNode(text); + label = document.createElement("DIV"); + label.className = "timeline-axis-text timeline-axis-text-major"; + label.appendChild(content); + label.style.position = "absolute"; + label.style.top = "0px"; + + frame.appendChild(label); + majorTexts.push(label); + } + + label.childNodes[0].nodeValue = text; + label.style.top = size.axis.labelMajorTop + "px"; + label.style.left = x + "px"; + //label.title = title; // TODO: this is a heavy operation + + props.majorTextNum ++; +}; + +/** + * Create a Major line for the axis at position x + * @param {Number} x + */ +links.Timeline.prototype.repaintAxisMajorLine = function (x) { + var size = this.size, + props = size.axis.properties, + axis = this.size.axis, + frame = this.dom.axis.frame, + majorLines = this.dom.axis.majorLines, + index = props.majorLineNum, + line; + + if (index < majorLines.length) { + line = majorLines[index]; + } + else { + // create vertical line + line = document.createElement("DIV"); + line.className = "timeline-axis-grid timeline-axis-grid-major"; + line.style.position = "absolute"; + line.style.top = "0px"; + line.style.width = "0px"; + + frame.appendChild(line); + majorLines.push(line); + } + + line.style.left = (x - axis.lineMajorWidth/2) + "px"; + line.style.height = size.frameHeight + "px"; + + props.majorLineNum ++; +}; + +/** + * Reflow all items, retrieve their actual size + * @return {boolean} resized returns true if any of the items is resized + */ +links.Timeline.prototype.reflowItems = function() { + var resized = false, + i, + iMax, + group, + groups = this.groups, + renderedItems = this.renderedItems; + + if (groups) { // TODO: need to check if labels exists? + // loop through all groups to reset the items height + groups.forEach(function (group) { + group.itemsHeight = 0; + }); + } + + // loop through the width and height of all visible items + for (i = 0, iMax = renderedItems.length; i < iMax; i++) { + var item = renderedItems[i], + domItem = item.dom; + group = item.group; + + if (domItem) { + // TODO: move updating width and height into item.reflow + var width = domItem ? domItem.clientWidth : 0; + var height = domItem ? domItem.clientHeight : 0; + resized = resized || (item.width != width); + resized = resized || (item.height != height); + item.width = width; + item.height = height; + //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth + item.reflow(); + } + + if (group) { + group.itemsHeight = group.itemsHeight ? + Math.max(group.itemsHeight, item.height) : + item.height; + } + } + + return resized; +}; + +/** + * Recalculate item properties: + * - the height of each group. + * - the actualHeight, from the stacked items or the sum of the group heights + * @return {boolean} resized returns true if any of the items properties is + * changed + */ +links.Timeline.prototype.recalcItems = function () { + var resized = false, + i, + iMax, + item, + finalItem, + finalItems, + group, + groups = this.groups, + size = this.size, + options = this.options, + renderedItems = this.renderedItems; + + var actualHeight = 0; + if (groups.length == 0) { + // calculate actual height of the timeline when there are no groups + // but stacked items + if (options.autoHeight || options.cluster) { + var min = 0, + max = 0; + + if (this.stack && this.stack.finalItems) { + // adjust the offset of all finalItems when the actualHeight has been changed + finalItems = this.stack.finalItems; + finalItem = finalItems[0]; + if (finalItem && finalItem.top) { + min = finalItem.top; + max = finalItem.top + finalItem.height; + } + for (i = 1, iMax = finalItems.length; i < iMax; i++) { + finalItem = finalItems[i]; + min = Math.min(min, finalItem.top); + max = Math.max(max, finalItem.top + finalItem.height); + } + } + else { + item = renderedItems[0]; + if (item && item.top) { + min = item.top; + max = item.top + item.height; + } + for (i = 1, iMax = renderedItems.length; i < iMax; i++) { + item = renderedItems[i]; + if (item.top) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + } + } + } + + actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height; + if (actualHeight < options.minHeight) { + actualHeight = options.minHeight; + } + + if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) { + // adjust the offset of all items when the actualHeight has been changed + var diff = actualHeight - size.actualHeight; + if (this.stack && this.stack.finalItems) { + finalItems = this.stack.finalItems; + for (i = 0, iMax = finalItems.length; i < iMax; i++) { + finalItems[i].top += diff; + finalItems[i].item.top += diff; + } + } + else { + for (i = 0, iMax = renderedItems.length; i < iMax; i++) { + renderedItems[i].top += diff; + } + } + } + } + } + else { + // loop through all groups to get the height of each group, and the + // total height + actualHeight = size.axis.height + 2 * options.eventMarginAxis; + for (i = 0, iMax = groups.length; i < iMax; i++) { + group = groups[i]; + + var groupHeight = Math.max(group.labelHeight || 0, group.itemsHeight || 0); + resized = resized || (groupHeight != group.height); + group.height = groupHeight; + + actualHeight += groups[i].height + options.eventMargin; + } + + // calculate top positions of the group labels and lines + var eventMargin = options.eventMargin, + top = options.axisOnTop ? + options.eventMarginAxis + eventMargin/2 : + size.contentHeight - options.eventMarginAxis + eventMargin/ 2, + axisHeight = size.axis.height; + + for (i = 0, iMax = groups.length; i < iMax; i++) { + group = groups[i]; + if (options.axisOnTop) { + group.top = top + axisHeight; + group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2; + group.lineTop = top + axisHeight + group.height + eventMargin/2; + top += group.height + eventMargin; + } + else { + top -= group.height + eventMargin; + group.top = top; + group.labelTop = top + (group.height - group.labelHeight) / 2; + group.lineTop = top - eventMargin/2; + } + } + + // calculate top position of the visible items + for (i = 0, iMax = renderedItems.length; i < iMax; i++) { + item = renderedItems[i]; + group = item.group; + + if (group) { + item.top = group.top; + } + } + + resized = true; + } + + if (actualHeight < options.minHeight) { + actualHeight = options.minHeight; + } + resized = resized || (actualHeight != size.actualHeight); + size.actualHeight = actualHeight; + + return resized; +}; + +/** + * This method clears the (internal) array this.items in a safe way: neatly + * cleaning up the DOM, and accompanying arrays this.renderedItems and + * the created clusters. + */ +links.Timeline.prototype.clearItems = function() { + // add all visible items to the list to be hidden + var hideItems = this.renderQueue.hide; + this.renderedItems.forEach(function (item) { + hideItems.push(item); + }); + + // clear the cluster generator + this.clusterGenerator.clear(); + + // actually clear the items + this.items = []; +}; + +/** + * Repaint all items + * @return {boolean} needsReflow Returns true if the DOM is changed such that + * a reflow is needed. + */ +links.Timeline.prototype.repaintItems = function() { + var i, iMax, item, index; + + var needsReflow = false, + dom = this.dom, + size = this.size, + timeline = this, + renderedItems = this.renderedItems; + + if (!dom.items) { + dom.items = {}; + } + + // draw the frame containing the items + var frame = dom.items.frame; + if (!frame) { + frame = document.createElement("DIV"); + frame.style.position = "relative"; + dom.content.appendChild(frame); + dom.items.frame = frame; + } + + frame.style.left = "0px"; + frame.style.top = size.items.top + "px"; + frame.style.height = "0px"; + + // Take frame offline (for faster manipulation of the DOM) + dom.content.removeChild(frame); + + // process the render queue with changes + var queue = this.renderQueue; + var newImageUrls = []; + needsReflow = needsReflow || + (queue.show.length > 0) || + (queue.update.length > 0) || + (queue.hide.length > 0); // TODO: reflow needed on hide of items? + + while (item = queue.show.shift()) { + item.showDOM(frame); + item.getImageUrls(newImageUrls); + renderedItems.push(item); + } + while (item = queue.update.shift()) { + item.updateDOM(frame); + item.getImageUrls(newImageUrls); + index = this.renderedItems.indexOf(item); + if (index == -1) { + renderedItems.push(item); + } + } + while (item = queue.hide.shift()) { + item.hideDOM(frame); + index = this.renderedItems.indexOf(item); + if (index != -1) { + renderedItems.splice(index, 1); + } + } + + // reposition all visible items + renderedItems.forEach(function (item) { + item.updatePosition(timeline); + }); + + // redraw the delete button and dragareas of the selected item (if any) + this.repaintDeleteButton(); + this.repaintDragAreas(); + + // put frame online again + dom.content.appendChild(frame); + + if (newImageUrls.length) { + // retrieve all image sources from the items, and set a callback once + // all images are retrieved + var callback = function () { + timeline.render(); + }; + var sendCallbackWhenAlreadyLoaded = false; + links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded); + } + + return needsReflow; +}; + +/** + * Reflow the size of the groups + * @return {boolean} resized Returns true if any of the frame elements + * have been resized. + */ +links.Timeline.prototype.reflowGroups = function() { + var resized = false, + options = this.options, + size = this.size, + dom = this.dom; + + // calculate the groups width and height + // TODO: only update when data is changed! -> use an updateSeq + var groupsWidth = 0; + + // loop through all groups to get the labels width and height + var groups = this.groups; + var labels = this.dom.groups ? this.dom.groups.labels : []; + for (var i = 0, iMax = groups.length; i < iMax; i++) { + var group = groups[i]; + var label = labels[i]; + group.labelWidth = label ? label.clientWidth : 0; + group.labelHeight = label ? label.clientHeight : 0; + group.width = group.labelWidth; // TODO: group.width is redundant with labelWidth + + groupsWidth = Math.max(groupsWidth, group.width); + } + + // limit groupsWidth to the groups width in the options + if (options.groupsWidth !== undefined) { + groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0; + } + + // compensate for the border width. TODO: calculate the real border width + groupsWidth += 1; + + var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0; + resized = resized || (size.groupsWidth !== groupsWidth); + resized = resized || (size.groupsLeft !== groupsLeft); + size.groupsWidth = groupsWidth; + size.groupsLeft = groupsLeft; + + return resized; +}; + +/** + * Redraw the group labels + */ +links.Timeline.prototype.repaintGroups = function() { + var dom = this.dom, + timeline = this, + options = this.options, + size = this.size, + groups = this.groups; + + if (dom.groups === undefined) { + dom.groups = {}; + } + + var labels = dom.groups.labels; + if (!labels) { + labels = []; + dom.groups.labels = labels; + } + var labelLines = dom.groups.labelLines; + if (!labelLines) { + labelLines = []; + dom.groups.labelLines = labelLines; + } + var itemLines = dom.groups.itemLines; + if (!itemLines) { + itemLines = []; + dom.groups.itemLines = itemLines; + } + + // create the frame for holding the groups + var frame = dom.groups.frame; + if (!frame) { + frame = document.createElement("DIV"); + frame.className = "timeline-groups-axis"; + frame.style.position = "absolute"; + frame.style.overflow = "hidden"; + frame.style.top = "0px"; + frame.style.height = "100%"; + + dom.frame.appendChild(frame); + dom.groups.frame = frame; + } + + frame.style.left = size.groupsLeft + "px"; + frame.style.width = (options.groupsWidth !== undefined) ? + options.groupsWidth : + size.groupsWidth + "px"; + + // hide groups axis when there are no groups + if (groups.length == 0) { + frame.style.display = 'none'; + } + else { + frame.style.display = ''; + } + + // TODO: only create/update groups when data is changed. + + // create the items + var current = labels.length, + needed = groups.length; + + // overwrite existing group labels + for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) { + var group = groups[i]; + var label = labels[i]; + label.innerHTML = this.getGroupName(group); + label.style.display = ''; + } + + // append new items when needed + for (var i = current; i < needed; i++) { + var group = groups[i]; + + // create text label + var label = document.createElement("DIV"); + label.className = "timeline-groups-text"; + label.style.position = "absolute"; + if (options.groupsWidth === undefined) { + label.style.whiteSpace = "nowrap"; + } + label.innerHTML = this.getGroupName(group); + frame.appendChild(label); + labels[i] = label; + + // create the grid line between the group labels + var labelLine = document.createElement("DIV"); + labelLine.className = "timeline-axis-grid timeline-axis-grid-minor"; + labelLine.style.position = "absolute"; + labelLine.style.left = "0px"; + labelLine.style.width = "100%"; + labelLine.style.height = "0px"; + labelLine.style.borderTopStyle = "solid"; + frame.appendChild(labelLine); + labelLines[i] = labelLine; + + // create the grid line between the items + var itemLine = document.createElement("DIV"); + itemLine.className = "timeline-axis-grid timeline-axis-grid-minor"; + itemLine.style.position = "absolute"; + itemLine.style.left = "0px"; + itemLine.style.width = "100%"; + itemLine.style.height = "0px"; + itemLine.style.borderTopStyle = "solid"; + dom.content.insertBefore(itemLine, dom.content.firstChild); + itemLines[i] = itemLine; + } + + // remove redundant items from the DOM when needed + for (var i = needed; i < current; i++) { + var label = labels[i], + labelLine = labelLines[i], + itemLine = itemLines[i]; + + frame.removeChild(label); + frame.removeChild(labelLine); + dom.content.removeChild(itemLine); + } + labels.splice(needed, current - needed); + labelLines.splice(needed, current - needed); + itemLines.splice(needed, current - needed); + + frame.style.borderStyle = options.groupsOnRight ? + "none none none solid" : + "none solid none none"; + + // position the groups + for (var i = 0, iMax = groups.length; i < iMax; i++) { + var group = groups[i], + label = labels[i], + labelLine = labelLines[i], + itemLine = itemLines[i]; + + label.style.top = group.labelTop + "px"; + labelLine.style.top = group.lineTop + "px"; + itemLine.style.top = group.lineTop + "px"; + itemLine.style.width = size.contentWidth + "px"; + } + + if (!dom.groups.background) { + // create the axis grid line background + var background = document.createElement("DIV"); + background.className = "timeline-axis"; + background.style.position = "absolute"; + background.style.left = "0px"; + background.style.width = "100%"; + background.style.border = "none"; + + frame.appendChild(background); + dom.groups.background = background; + } + dom.groups.background.style.top = size.axis.top + 'px'; + dom.groups.background.style.height = size.axis.height + 'px'; + + if (!dom.groups.line) { + // create the axis grid line + var line = document.createElement("DIV"); + line.className = "timeline-axis"; + line.style.position = "absolute"; + line.style.left = "0px"; + line.style.width = "100%"; + line.style.height = "0px"; + + frame.appendChild(line); + dom.groups.line = line; + } + dom.groups.line.style.top = size.axis.line + 'px'; + + // create a callback when there are images which are not yet loaded + // TODO: more efficiently load images in the groups + if (dom.groups.frame && groups.length) { + var imageUrls = []; + links.imageloader.filterImageUrls(dom.groups.frame, imageUrls); + if (imageUrls.length) { + // retrieve all image sources from the items, and set a callback once + // all images are retrieved + var callback = function () { + timeline.render(); + }; + var sendCallbackWhenAlreadyLoaded = false; + links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded); + } + } +}; + + +/** + * Redraw the current time bar + */ +links.Timeline.prototype.repaintCurrentTime = function() { + var options = this.options, + dom = this.dom, + size = this.size; + + if (!options.showCurrentTime) { + if (dom.currentTime) { + dom.contentTimelines.removeChild(dom.currentTime); + delete dom.currentTime; + } + + return; + } + + if (!dom.currentTime) { + // create the current time bar + var currentTime = document.createElement("DIV"); + currentTime.className = "timeline-currenttime"; + currentTime.style.position = "absolute"; + currentTime.style.top = "0px"; + currentTime.style.height = "100%"; + + dom.contentTimelines.appendChild(currentTime); + dom.currentTime = currentTime; + } + + var now = new Date(); + var nowOffset = new Date(now.valueOf() + this.clientTimeOffset); + var x = this.timeToScreen(nowOffset); + + var visible = (x > -size.contentWidth && x < 2 * size.contentWidth); + dom.currentTime.style.display = visible ? '' : 'none'; + dom.currentTime.style.left = x + "px"; + dom.currentTime.title = "Current time: " + nowOffset; + + // start a timer to adjust for the new time + if (this.currentTimeTimer != undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } + var timeline = this; + var onTimeout = function() { + timeline.repaintCurrentTime(); + }; + // the time equal to the width of one pixel, divided by 2 for more smoothness + var interval = 1 / this.conversion.factor / 2; + if (interval < 30) interval = 30; + this.currentTimeTimer = setTimeout(onTimeout, interval); +}; + +/** + * Redraw the custom time bar + */ +links.Timeline.prototype.repaintCustomTime = function() { + var options = this.options, + dom = this.dom, + size = this.size; + + if (!options.showCustomTime) { + if (dom.customTime) { + dom.contentTimelines.removeChild(dom.customTime); + delete dom.customTime; + } + + return; + } + + if (!dom.customTime) { + var customTime = document.createElement("DIV"); + customTime.className = "timeline-customtime"; + customTime.style.position = "absolute"; + customTime.style.top = "0px"; + customTime.style.height = "100%"; + + var drag = document.createElement("DIV"); + drag.style.position = "relative"; + drag.style.top = "0px"; + drag.style.left = "-10px"; + drag.style.height = "100%"; + drag.style.width = "20px"; + customTime.appendChild(drag); + + dom.contentTimelines.appendChild(customTime); + dom.customTime = customTime; + + // initialize parameter + this.customTime = new Date(); + } + + var x = this.timeToScreen(this.customTime), + visible = (x > -size.contentWidth && x < 2 * size.contentWidth); + dom.customTime.style.display = visible ? '' : 'none'; + dom.customTime.style.left = x + "px"; + dom.customTime.title = "Time: " + this.customTime; +}; + + +/** + * Redraw the delete button, on the top right of the currently selected item + * if there is no item selected, the button is hidden. + */ +links.Timeline.prototype.repaintDeleteButton = function () { + var timeline = this, + dom = this.dom, + frame = dom.items.frame; + + var deleteButton = dom.items.deleteButton; + if (!deleteButton) { + // create a delete button + deleteButton = document.createElement("DIV"); + deleteButton.className = "timeline-navigation-delete"; + deleteButton.style.position = "absolute"; + + frame.appendChild(deleteButton); + dom.items.deleteButton = deleteButton; + } + + var index = this.selection ? this.selection.index : -1, + item = this.selection ? this.items[index] : undefined; + if (item && item.rendered && this.isEditable(item)) { + var right = item.getRight(this), + top = item.top; + + deleteButton.style.left = right + 'px'; + deleteButton.style.top = top + 'px'; + deleteButton.style.display = ''; + frame.removeChild(deleteButton); + frame.appendChild(deleteButton); + } + else { + deleteButton.style.display = 'none'; + } +}; + + +/** + * Redraw the drag areas. When an item (ranges only) is selected, + * it gets a drag area on the left and right side, to change its width + */ +links.Timeline.prototype.repaintDragAreas = function () { + var timeline = this, + options = this.options, + dom = this.dom, + frame = this.dom.items.frame; + + // create left drag area + var dragLeft = dom.items.dragLeft; + if (!dragLeft) { + dragLeft = document.createElement("DIV"); + dragLeft.className="timeline-event-range-drag-left"; + dragLeft.style.position = "absolute"; + + frame.appendChild(dragLeft); + dom.items.dragLeft = dragLeft; + } + + // create right drag area + var dragRight = dom.items.dragRight; + if (!dragRight) { + dragRight = document.createElement("DIV"); + dragRight.className="timeline-event-range-drag-right"; + dragRight.style.position = "absolute"; + + frame.appendChild(dragRight); + dom.items.dragRight = dragRight; + } + + // reposition left and right drag area + var index = this.selection ? this.selection.index : -1, + item = this.selection ? this.items[index] : undefined; + if (item && item.rendered && this.isEditable(item) && + (item instanceof links.Timeline.ItemRange)) { + var left = this.timeToScreen(item.start), + right = this.timeToScreen(item.end), + top = item.top, + height = item.height; + + dragLeft.style.left = left + 'px'; + dragLeft.style.top = top + 'px'; + dragLeft.style.width = options.dragAreaWidth + "px"; + dragLeft.style.height = height + 'px'; + dragLeft.style.display = ''; + frame.removeChild(dragLeft); + frame.appendChild(dragLeft); + + dragRight.style.left = (right - options.dragAreaWidth) + 'px'; + dragRight.style.top = top + 'px'; + dragRight.style.width = options.dragAreaWidth + "px"; + dragRight.style.height = height + 'px'; + dragRight.style.display = ''; + frame.removeChild(dragRight); + frame.appendChild(dragRight); + } + else { + dragLeft.style.display = 'none'; + dragRight.style.display = 'none'; + } +}; + +/** + * Create the navigation buttons for zooming and moving + */ +links.Timeline.prototype.repaintNavigation = function () { + var timeline = this, + options = this.options, + dom = this.dom, + frame = dom.frame, + navBar = dom.navBar; + + if (!navBar) { + var showButtonNew = options.showButtonNew && options.editable; + var showNavigation = options.showNavigation && (options.zoomable || options.moveable); + if (showNavigation || showButtonNew) { + // create a navigation bar containing the navigation buttons + navBar = document.createElement("DIV"); + navBar.style.position = "absolute"; + navBar.className = "timeline-navigation"; + if (options.groupsOnRight) { + navBar.style.left = '10px'; + } + else { + navBar.style.right = '10px'; + } + if (options.axisOnTop) { + navBar.style.bottom = '10px'; + } + else { + navBar.style.top = '10px'; + } + dom.navBar = navBar; + frame.appendChild(navBar); + } + + if (showButtonNew) { + // create a new in button + navBar.addButton = document.createElement("DIV"); + navBar.addButton.className = "timeline-navigation-new"; + + navBar.addButton.title = options.CREATE_NEW_EVENT; + var onAdd = function(event) { + links.Timeline.preventDefault(event); + links.Timeline.stopPropagation(event); + + // create a new event at the center of the frame + var w = timeline.size.contentWidth; + var x = w / 2; + var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width + var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width + if (options.snapEvents) { + timeline.step.snap(xstart); + timeline.step.snap(xend); + } + + var content = options.NEW; + var group = timeline.groups.length ? timeline.groups[0].content : undefined; + var preventRender = true; + timeline.addItem({ + 'start': xstart, + 'end': xend, + 'content': content, + 'group': group + }, preventRender); + var index = (timeline.items.length - 1); + timeline.selectItem(index); + + timeline.applyAdd = true; + + // fire an add event. + // Note that the change can be canceled from within an event listener if + // this listener calls the method cancelAdd(). + timeline.trigger('add'); + + if (timeline.applyAdd) { + // render and select the item + timeline.render({animate: false}); + timeline.selectItem(index); + } + else { + // undo an add + timeline.deleteItem(index); + } + }; + links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd); + navBar.appendChild(navBar.addButton); + } + + if (showButtonNew && showNavigation) { + // create a separator line + navBar.addButton.style.borderRightWidth = "1px"; + navBar.addButton.style.borderRightStyle = "solid"; + } + + if (showNavigation) { + if (options.zoomable) { + // create a zoom in button + navBar.zoomInButton = document.createElement("DIV"); + navBar.zoomInButton.className = "timeline-navigation-zoom-in"; + navBar.zoomInButton.title = this.options.ZOOM_IN; + var onZoomIn = function(event) { + links.Timeline.preventDefault(event); + links.Timeline.stopPropagation(event); + timeline.zoom(0.4); + timeline.trigger("rangechange"); + timeline.trigger("rangechanged"); + }; + links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn); + navBar.appendChild(navBar.zoomInButton); + + // create a zoom out button + navBar.zoomOutButton = document.createElement("DIV"); + navBar.zoomOutButton.className = "timeline-navigation-zoom-out"; + navBar.zoomOutButton.title = this.options.ZOOM_OUT; + var onZoomOut = function(event) { + links.Timeline.preventDefault(event); + links.Timeline.stopPropagation(event); + timeline.zoom(-0.4); + timeline.trigger("rangechange"); + timeline.trigger("rangechanged"); + }; + links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut); + navBar.appendChild(navBar.zoomOutButton); + } + + if (options.moveable) { + // create a move left button + navBar.moveLeftButton = document.createElement("DIV"); + navBar.moveLeftButton.className = "timeline-navigation-move-left"; + navBar.moveLeftButton.title = this.options.MOVE_LEFT; + var onMoveLeft = function(event) { + links.Timeline.preventDefault(event); + links.Timeline.stopPropagation(event); + timeline.move(-0.2); + timeline.trigger("rangechange"); + timeline.trigger("rangechanged"); + }; + links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft); + navBar.appendChild(navBar.moveLeftButton); + + // create a move right button + navBar.moveRightButton = document.createElement("DIV"); + navBar.moveRightButton.className = "timeline-navigation-move-right"; + navBar.moveRightButton.title = this.options.MOVE_RIGHT; + var onMoveRight = function(event) { + links.Timeline.preventDefault(event); + links.Timeline.stopPropagation(event); + timeline.move(0.2); + timeline.trigger("rangechange"); + timeline.trigger("rangechanged"); + }; + links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight); + navBar.appendChild(navBar.moveRightButton); + } + } + } +}; + + +/** + * Set current time. This function can be used to set the time in the client + * timeline equal with the time on a server. + * @param {Date} time + */ +links.Timeline.prototype.setCurrentTime = function(time) { + var now = new Date(); + this.clientTimeOffset = (time.valueOf() - now.valueOf()); + + this.repaintCurrentTime(); +}; + +/** + * Get current time. The time can have an offset from the real time, when + * the current time has been changed via the method setCurrentTime. + * @return {Date} time + */ +links.Timeline.prototype.getCurrentTime = function() { + var now = new Date(); + return new Date(now.valueOf() + this.clientTimeOffset); +}; + + +/** + * Set custom time. + * The custom time bar can be used to display events in past or future. + * @param {Date} time + */ +links.Timeline.prototype.setCustomTime = function(time) { + this.customTime = new Date(time.valueOf()); + this.repaintCustomTime(); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +links.Timeline.prototype.getCustomTime = function() { + return new Date(this.customTime.valueOf()); +}; + +/** + * Set a custom scale. Autoscaling will be disabled. + * For example setScale(SCALE.MINUTES, 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {links.Timeline.StepDate.SCALE} scale + * A scale. Choose from SCALE.MILLISECOND, + * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, + * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, + * SCALE.YEAR. + * @param {int} step A step size, by default 1. Choose for + * example 1, 2, 5, or 10. + */ +links.Timeline.prototype.setScale = function(scale, step) { + this.step.setScale(scale, step); + this.render(); // TODO: optimize: only reflow/repaint axis +}; + +/** + * Enable or disable autoscaling + * @param {boolean} enable If true or not defined, autoscaling is enabled. + * If false, autoscaling is disabled. + */ +links.Timeline.prototype.setAutoScale = function(enable) { + this.step.setAutoScale(enable); + this.render(); // TODO: optimize: only reflow/repaint axis +}; + +/** + * Redraw the timeline + * Reloads the (linked) data table and redraws the timeline when resized. + * See also the method checkResize + */ +links.Timeline.prototype.redraw = function() { + this.setData(this.data); +}; + + +/** + * Check if the timeline is resized, and if so, redraw the timeline. + * Useful when the webpage is resized. + */ +links.Timeline.prototype.checkResize = function() { + // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter + this.render(); +}; + +/** + * Check whether a given item is editable + * @param {links.Timeline.Item} item + * @return {boolean} editable + */ +links.Timeline.prototype.isEditable = function (item) { + if (item) { + if (item.editable != undefined) { + return item.editable; + } + else { + return this.options.editable; + } + } + return false; +}; + +/** + * Calculate the factor and offset to convert a position on screen to the + * corresponding date and vice versa. + * After the method calcConversionFactor is executed once, the methods screenToTime and + * timeToScreen can be used. + */ +links.Timeline.prototype.recalcConversion = function() { + this.conversion.offset = this.start.valueOf(); + this.conversion.factor = this.size.contentWidth / + (this.end.valueOf() - this.start.valueOf()); +}; + + +/** + * Convert a position on screen (pixels) to a datetime + * Before this method can be used, the method calcConversionFactor must be + * executed once. + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + */ +links.Timeline.prototype.screenToTime = function(x) { + var conversion = this.conversion; + return new Date(x / conversion.factor + conversion.offset); +}; + +/** + * Convert a datetime (Date object) into a position on the screen + * Before this method can be used, the method calcConversionFactor must be + * executed once. + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + */ +links.Timeline.prototype.timeToScreen = function(time) { + var conversion = this.conversion; + return (time.valueOf() - conversion.offset) * conversion.factor; +}; + + + +/** + * Event handler for touchstart event on mobile devices + */ +links.Timeline.prototype.onTouchStart = function(event) { + var params = this.eventParams, + me = this; + + if (params.touchDown) { + // if already moving, return + return; + } + + params.touchDown = true; + params.zoomed = false; + + this.onMouseDown(event); + + if (!params.onTouchMove) { + params.onTouchMove = function (event) {me.onTouchMove(event);}; + links.Timeline.addEventListener(document, "touchmove", params.onTouchMove); + } + if (!params.onTouchEnd) { + params.onTouchEnd = function (event) {me.onTouchEnd(event);}; + links.Timeline.addEventListener(document, "touchend", params.onTouchEnd); + } + + /* TODO + // check for double tap event + var delta = 500; // ms + var doubleTapStart = (new Date()).valueOf(); + var target = links.Timeline.getTarget(event); + var doubleTapItem = this.getItemIndex(target); + if (params.doubleTapStart && + (doubleTapStart - params.doubleTapStart) < delta && + doubleTapItem == params.doubleTapItem) { + delete params.doubleTapStart; + delete params.doubleTapItem; + me.onDblClick(event); + params.touchDown = false; + } + params.doubleTapStart = doubleTapStart; + params.doubleTapItem = doubleTapItem; + */ + // store timing for double taps + var target = links.Timeline.getTarget(event); + var item = this.getItemIndex(target); + params.doubleTapStartPrev = params.doubleTapStart; + params.doubleTapStart = (new Date()).valueOf(); + params.doubleTapItemPrev = params.doubleTapItem; + params.doubleTapItem = item; + + links.Timeline.preventDefault(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +links.Timeline.prototype.onTouchMove = function(event) { + var params = this.eventParams; + + if (event.scale && event.scale !== 1) { + params.zoomed = true; + } + + if (!params.zoomed) { + // move + this.onMouseMove(event); + } + else { + if (this.options.zoomable) { + // pinch + // TODO: pinch only supported on iPhone/iPad. Create something manually for Android? + params.zoomed = true; + + var scale = event.scale, + oldWidth = (params.end.valueOf() - params.start.valueOf()), + newWidth = oldWidth / scale, + diff = newWidth - oldWidth, + start = new Date(parseInt(params.start.valueOf() - diff/2)), + end = new Date(parseInt(params.end.valueOf() + diff/2)); + + // TODO: determine zoom-around-date from touch positions? + + this.setVisibleChartRange(start, end); + this.trigger("rangechange"); + } + } + + links.Timeline.preventDefault(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +links.Timeline.prototype.onTouchEnd = function(event) { + var params = this.eventParams; + var me = this; + params.touchDown = false; + + if (params.zoomed) { + this.trigger("rangechanged"); + } + + if (params.onTouchMove) { + links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove); + delete params.onTouchMove; + + } + if (params.onTouchEnd) { + links.Timeline.removeEventListener(document, "touchend", params.onTouchEnd); + delete params.onTouchEnd; + } + + this.onMouseUp(event); + + // check for double tap event + var delta = 500; // ms + var doubleTapEnd = (new Date()).valueOf(); + var target = links.Timeline.getTarget(event); + var doubleTapItem = this.getItemIndex(target); + if (params.doubleTapStartPrev && + (doubleTapEnd - params.doubleTapStartPrev) < delta && + params.doubleTapItem == params.doubleTapItemPrev) { + params.touchDown = true; + me.onDblClick(event); + params.touchDown = false; + } + + links.Timeline.preventDefault(event); +}; + + +/** + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) + */ +links.Timeline.prototype.onMouseDown = function(event) { + event = event || window.event; + + var params = this.eventParams, + options = this.options, + dom = this.dom; + + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!leftButtonDown && !params.touchDown) { + return; + } + + // get mouse position + params.mouseX = links.Timeline.getPageX(event); + params.mouseY = links.Timeline.getPageY(event); + params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content); + params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content); + params.previousLeft = 0; + params.previousOffset = 0; + + params.moved = false; + params.start = new Date(this.start.valueOf()); + params.end = new Date(this.end.valueOf()); + + params.target = links.Timeline.getTarget(event); [... 3780 lines stripped ...]