You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@climate.apache.org by sk...@apache.org on 2013/06/21 09:50:25 UTC
svn commit: r1495306 [3/3] - in
/incubator/climate/trunk/rcmet/src/main/ui/app: ./ css/ js/controllers/
js/directives/ lib/timeline/
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, <jo...@almende.org>
+ * @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 ...]