1 /**
  2  * @file timeline.js
  3  *
  4  * @brief
  5  * The Timeline is an interactive visualization chart to visualize events in
  6  * time, having a start and end date.
  7  * You can freely move and zoom in the timeline by dragging
  8  * and scrolling in the Timeline. Items are optionally dragable. The time
  9  * scale on the axis is adjusted automatically, and supports scales ranging
 10  * from milliseconds to years.
 11  *
 12  * Timeline is part of the CHAP Links library.
 13  *
 14  * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 15  * Internet Explorer 6+.
 16  *
 17  * @license
 18  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 19  * use this file except in compliance with the License. You may obtain a copy
 20  * of the License at
 21  *
 22  * http://www.apache.org/licenses/LICENSE-2.0
 23  *
 24  * Unless required by applicable law or agreed to in writing, software
 25  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 26  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 27  * License for the specific language governing permissions and limitations under
 28  * the License.
 29  *
 30  * Copyright (c) 2011-2015 Almende B.V.
 31  *
 32  * @author  Jos de Jong, <jos@almende.org>
 33  * @date    2015-03-04
 34  * @version 2.9.1
 35  */
 36 
 37 /*
 38  * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/)
 39  * added to v2.4.1 with da_DK language by @bjarkebech
 40  */
 41 
 42 /*
 43  * TODO
 44  *
 45  * Add zooming with pinching on Android
 46  *
 47  * Bug: when an item contains a javascript onclick or a link, this does not work
 48  *      when the item is not selected (when the item is being selected,
 49  *      it is redrawn, which cancels any onclick or link action)
 50  * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
 51  * Bug: neglect items when they have no valid start/end, instead of throwing an error
 52  * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
 53  * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
 54  * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 55  */
 56 
 57 /**
 58  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 59  * "links"
 60  */
 61 if (typeof links === 'undefined') {
 62     links = {};
 63     // important: do not use var, as "var links = {};" will overwrite
 64     //            the existing links variable value with undefined in IE8, IE7.
 65 }
 66 
 67 
 68 /**
 69  * Ensure the variable google exists
 70  */
 71 if (typeof google === 'undefined') {
 72     google = undefined;
 73     // important: do not use var, as "var google = undefined;" will overwrite
 74     //            the existing google variable value with undefined in IE8, IE7.
 75 }
 76 
 77 
 78 
 79 // Internet Explorer 8 and older does not support Array.indexOf,
 80 // so we define it here in that case
 81 // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
 82 if(!Array.prototype.indexOf) {
 83     Array.prototype.indexOf = function(obj){
 84         for(var i = 0; i < this.length; i++){
 85             if(this[i] == obj){
 86                 return i;
 87             }
 88         }
 89         return -1;
 90     }
 91 }
 92 
 93 // Internet Explorer 8 and older does not support Array.forEach,
 94 // so we define it here in that case
 95 // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
 96 if (!Array.prototype.forEach) {
 97     Array.prototype.forEach = function(fn, scope) {
 98         for(var i = 0, len = this.length; i < len; ++i) {
 99             fn.call(scope || this, this[i], i, this);
100         }
101     }
102 }
103 
104 
105 /**
106  * @constructor links.Timeline
107  * The timeline is a visualization chart to visualize events in time.
108  *
109  * The timeline is developed in javascript as a Google Visualization Chart.
110  *
111  * @param {Element} container   The DOM element in which the Timeline will
112  *                              be created. Normally a div element.
113  * @param {Object} options      A name/value map containing settings for the
114  *                              timeline. Optional.
115  */
116 links.Timeline = function(container, options) {
117     if (!container) {
118         // this call was probably only for inheritance, no constructor-code is required
119         return;
120     }
121 
122     // create variables and set default values
123     this.dom = {};
124     this.conversion = {};
125     this.eventParams = {}; // stores parameters for mouse events
126     this.groups = [];
127     this.groupIndexes = {};
128     this.items = [];
129     this.renderQueue = {
130         show: [],   // Items made visible but not yet added to DOM
131         hide: [],   // Items currently visible but not yet removed from DOM
132         update: []  // Items with changed data but not yet adjusted DOM
133     };
134     this.renderedItems = [];  // Items currently rendered in the DOM
135     this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
136     this.currentClusters = [];
137     this.selection = undefined; // stores index and item which is currently selected
138 
139     this.listeners = {}; // event listener callbacks
140 
141     // Initialize sizes.
142     // Needed for IE (which gives an error when you try to set an undefined
143     // value in a style)
144     this.size = {
145         'actualHeight': 0,
146         'axis': {
147             'characterMajorHeight': 0,
148             'characterMajorWidth': 0,
149             'characterMinorHeight': 0,
150             'characterMinorWidth': 0,
151             'height': 0,
152             'labelMajorTop': 0,
153             'labelMinorTop': 0,
154             'line': 0,
155             'lineMajorWidth': 0,
156             'lineMinorHeight': 0,
157             'lineMinorTop': 0,
158             'lineMinorWidth': 0,
159             'top': 0
160         },
161         'contentHeight': 0,
162         'contentLeft': 0,
163         'contentWidth': 0,
164         'frameHeight': 0,
165         'frameWidth': 0,
166         'groupsLeft': 0,
167         'groupsWidth': 0,
168         'items': {
169             'top': 0
170         }
171     };
172 
173     this.dom.container = container;
174 
175     //
176     // Let's set the default options first
177     //
178     this.options = {
179         'width': "100%",
180         'height': "auto",
181         'minHeight': 0,        // minimal height in pixels
182         'groupMinHeight': 0,
183         'autoHeight': true,
184 
185         'eventMargin': 10,     // minimal margin between events
186         'eventMarginAxis': 20, // minimal margin between events and the axis
187         'dragAreaWidth': 10,   // pixels
188 
189         'min': undefined,
190         'max': undefined,
191         'zoomMin': 10,     // milliseconds
192         'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
193 
194         'moveable': true,
195         'zoomable': true,
196         'selectable': true,
197         'unselectable': true,
198         'editable': false,
199         'snapEvents': true,
200         'groupsChangeable': true,
201         'timeChangeable': true,
202 
203         'showCurrentTime': true, // show a red bar displaying the current time
204         'showCustomTime': false, // show a blue, draggable bar displaying a custom time
205         'showMajorLabels': true,
206         'showMinorLabels': true,
207         'showNavigation': false,
208         'showButtonNew': false,
209         'groupsOnRight': false,
210         'groupsOrder' : true,
211         'axisOnTop': false,
212         'stackEvents': true,
213         'animate': true,
214         'animateZoom': true,
215         'cluster': false,
216         'clusterMaxItems': 5,
217         'style': 'box',
218         '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
219         
220         // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
221         'locale': 'en',
222         'MONTHS': ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
223         'MONTHS_SHORT': ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
224         'DAYS': ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
225         'DAYS_SHORT': ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
226         'ZOOM_IN': "Zoom in",
227         'ZOOM_OUT': "Zoom out",
228         'MOVE_LEFT': "Move left",
229         'MOVE_RIGHT': "Move right",
230         'NEW': "New",
231         'CREATE_NEW_EVENT': "Create new event"
232     };
233     
234     //
235     // Now we can set the givenproperties
236     //
237     this.setOptions(options);
238 
239     this.clientTimeOffset = 0;    // difference between client time and the time
240     // set via Timeline.setCurrentTime()
241     var dom = this.dom;
242 
243     // remove all elements from the container element.
244     while (dom.container.hasChildNodes()) {
245         dom.container.removeChild(dom.container.firstChild);
246     }
247 
248     // create a step for drawing the axis
249     this.step = new links.Timeline.StepDate();
250 
251     // add standard item types
252     this.itemTypes = {
253         box:           links.Timeline.ItemBox,
254         range:         links.Timeline.ItemRange,
255         floatingRange: links.Timeline.ItemFloatingRange,
256         dot:           links.Timeline.ItemDot
257     };
258 
259     // initialize data
260     this.data = [];
261     this.firstDraw = true;
262 
263     // date interval must be initialized
264     this.setVisibleChartRange(undefined, undefined, false);
265 
266     // render for the first time
267     this.render();
268 
269     // fire the ready event
270     var me = this;
271     setTimeout(function () {
272         me.trigger('ready');
273     }, 0);
274 };
275 
276 
277 /**
278  * Main drawing logic. This is the function that needs to be called
279  * in the html page, to draw the timeline.
280  *
281  * A data table with the events must be provided, and an options table.
282  *
283  * @param {google.visualization.DataTable}      data
284  *                                 The data containing the events for the timeline.
285  *                                 Object DataTable is defined in
286  *                                 google.visualization.DataTable
287  * @param {Object} options         A name/value map containing settings for the
288  *                                 timeline. Optional. The use of options here
289  *                                 is deprecated. Pass timeline options in the
290  *                                 constructor or use setOptions()
291  */
292 links.Timeline.prototype.draw = function(data, options) {
293     if (options) {
294         console.log("WARNING: Passing options in draw() is deprecated. Pass options to the constructur or use setOptions() instead!");       
295         this.setOptions(options);
296     }
297 
298     if (this.options.selectable) {
299         links.Timeline.addClassName(this.dom.frame, "timeline-selectable");
300     }
301 
302     // read the data
303     this.setData(data);
304 
305     if (this.firstDraw) {
306         this.setVisibleChartRangeAuto();
307     }
308 
309     this.firstDraw = false;
310 };
311 
312 
313 /**
314  * Set options for the timeline.
315  * Timeline must be redrawn afterwards
316  * @param {Object} options A name/value map containing settings for the
317  *                                 timeline. Optional.
318  */
319 links.Timeline.prototype.setOptions = function(options) {
320     if (options) {
321         // retrieve parameter values
322         for (var i in options) {
323             if (options.hasOwnProperty(i)) {
324                 this.options[i] = options[i];
325             }
326         }
327 
328         // prepare i18n dependent on set locale
329         if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
330             var localeOpts = links.locales[this.options.locale];
331             if(localeOpts) {
332                 for (var l in localeOpts) {
333                     if (localeOpts.hasOwnProperty(l)) {
334                         this.options[l] = localeOpts[l];
335                     }
336                 }
337             }
338         }
339 
340         // check for deprecated options
341         if (options.showButtonAdd != undefined) {
342             this.options.showButtonNew = options.showButtonAdd;
343             console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
344         }
345         if (options.intervalMin != undefined) {
346             this.options.zoomMin = options.intervalMin;
347             console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
348         }
349         if (options.intervalMax != undefined) {
350             this.options.zoomMax = options.intervalMax;
351             console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
352         }
353 
354         if (options.scale && options.step) {
355             this.step.setScale(options.scale, options.step);
356         }
357     }
358 
359     // validate options
360     this.options.autoHeight = (this.options.height === "auto");
361 };
362 
363 /**
364  * Get options for the timeline.
365  *
366  * @return the options object
367  */
368 links.Timeline.prototype.getOptions = function() {
369     return this.options;
370 };
371 
372 /**
373  * Add new type of items
374  * @param {String} typeName  Name of new type
375  * @param {links.Timeline.Item} typeFactory Constructor of items
376  */
377 links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
378     this.itemTypes[typeName] = typeFactory;
379 };
380 
381 /**
382  * Retrieve a map with the column indexes of the columns by column name.
383  * For example, the method returns the map
384  *     {
385  *         start: 0,
386  *         end: 1,
387  *         content: 2,
388  *         group: undefined,
389  *         className: undefined
390  *         editable: undefined
391  *         type: undefined
392  *     }
393  * @param {google.visualization.DataTable} dataTable
394  * @type {Object} map
395  */
396 links.Timeline.mapColumnIds = function (dataTable) {
397     var cols = {},
398         colCount = dataTable.getNumberOfColumns(),
399         allUndefined = true;
400 
401     // loop over the columns, and map the column id's to the column indexes
402     for (var col = 0; col < colCount; col++) {
403         var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
404         cols[id] = col;
405         if (id == 'start' || id == 'end' || id == 'content' || id == 'group' ||
406             id == 'className' || id == 'editable' || id == 'type') {
407             allUndefined = false;
408         }
409     }
410 
411     // if no labels or ids are defined, use the default mapping
412     // for start, end, content, group, className, editable, type
413     if (allUndefined) {
414         cols.start = 0;
415         cols.end = 1;
416         cols.content = 2;
417         if (colCount > 3) {cols.group = 3}
418         if (colCount > 4) {cols.className = 4}
419         if (colCount > 5) {cols.editable = 5}
420         if (colCount > 6) {cols.type = 6}
421     }
422 
423     return cols;
424 };
425 
426 /**
427  * Set data for the timeline
428  * @param {google.visualization.DataTable | Array} data
429  */
430 links.Timeline.prototype.setData = function(data) {
431     // unselect any previously selected item
432     this.unselectItem();
433 
434     if (!data) {
435         data = [];
436     }
437 
438     // clear all data
439     this.stackCancelAnimation();
440     this.clearItems();
441     this.data = data;
442     var items = this.items;
443     this.deleteGroups();
444 
445     if (google && google.visualization &&
446         data instanceof google.visualization.DataTable) {
447         // map the datatable columns
448         var cols = links.Timeline.mapColumnIds(data);
449 
450         // read DataTable
451         for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
452             items.push(this.createItem({
453                 'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
454                 'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
455                 'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
456                 'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
457                 'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
458                 'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined),
459                 'type':      ((cols.type != undefined)      ? data.getValue(row, cols.type)      : undefined)
460             }));
461         }
462     }
463     else if (links.Timeline.isArray(data)) {
464         // read JSON array
465         for (var row = 0, rows = data.length; row < rows; row++) {
466             var itemData = data[row];
467             var item = this.createItem(itemData);
468             items.push(item);
469         }
470     }
471     else {
472         throw "Unknown data type. DataTable or Array expected.";
473     }
474 
475     // prepare data for clustering, by filtering and sorting by type
476     if (this.options.cluster) {
477         this.clusterGenerator.setData(this.items);
478     }
479 
480     this.render({
481         animate: false
482     });
483 };
484 
485 /**
486  * Return the original data table.
487  * @return {google.visualization.DataTable | Array} data
488  */
489 links.Timeline.prototype.getData = function  () {
490     return this.data;
491 };
492 
493 
494 /**
495  * Update the original data with changed start, end or group.
496  *
497  * @param {Number} index
498  * @param {Object} values   An object containing some of the following parameters:
499  *                          {Date} start,
500  *                          {Date} end,
501  *                          {String} content,
502  *                          {String} group
503  */
504 links.Timeline.prototype.updateData = function  (index, values) {
505     var data = this.data,
506         prop;
507 
508     if (google && google.visualization &&
509         data instanceof google.visualization.DataTable) {
510         // update the original google DataTable
511         var missingRows = (index + 1) - data.getNumberOfRows();
512         if (missingRows > 0) {
513             data.addRows(missingRows);
514         }
515 
516         // map the column id's by name
517         var cols = links.Timeline.mapColumnIds(data);
518 
519         // merge all fields from the provided data into the current data
520         for (prop in values) {
521             if (values.hasOwnProperty(prop)) {
522                 var col = cols[prop];
523                 if (col == undefined) {
524                     // create new column
525                     var value = values[prop];
526                     var valueType = 'string';
527                     if (typeof(value) == 'number')       {valueType = 'number';}
528                     else if (typeof(value) == 'boolean') {valueType = 'boolean';}
529                     else if (value instanceof Date)      {valueType = 'datetime';}
530                     col = data.addColumn(valueType, prop);
531                 }
532                 data.setValue(index, col, values[prop]);
533 
534                 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
535             }
536         }
537     }
538     else if (links.Timeline.isArray(data)) {
539         // update the original JSON table
540         var row = data[index];
541         if (row == undefined) {
542             row = {};
543             data[index] = row;
544         }
545 
546         // merge all fields from the provided data into the current data
547         for (prop in values) {
548             if (values.hasOwnProperty(prop)) {
549                 row[prop] = values[prop];
550 
551                 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
552             }
553         }
554     }
555     else {
556         throw "Cannot update data, unknown type of data";
557     }
558 };
559 
560 /**
561  * Find the item index from a given HTML element
562  * If no item index is found, undefined is returned
563  * @param {Element} element
564  * @return {Number | undefined} index
565  */
566 links.Timeline.prototype.getItemIndex = function(element) {
567     var e = element,
568         dom = this.dom,
569         frame = dom.items.frame,
570         items = this.items,
571         index = undefined;
572 
573     // try to find the frame where the items are located in
574     while (e.parentNode && e.parentNode !== frame) {
575         e = e.parentNode;
576     }
577 
578     if (e.parentNode === frame) {
579         // yes! we have found the parent element of all items
580         // retrieve its id from the array with items
581         for (var i = 0, iMax = items.length; i < iMax; i++) {
582             if (items[i].dom === e) {
583                 index = i;
584                 break;
585             }
586         }
587     }
588 
589     return index;
590 };
591 
592 
593 /**
594  * Find the cluster index from a given HTML element
595  * If no cluster index is found, undefined is returned
596  * @param {Element} element
597  * @return {Number | undefined} index
598  */
599 links.Timeline.prototype.getClusterIndex = function(element) {
600     var e = element,
601         dom = this.dom,
602         frame = dom.items.frame,
603         clusters = this.clusters,
604         index = undefined;
605 
606     if (this.clusters) {
607         // try to find the frame where the clusters are located in
608         while (e.parentNode && e.parentNode !== frame) {
609             e = e.parentNode;
610         }
611 
612         if (e.parentNode === frame) {
613             // yes! we have found the parent element of all clusters
614             // retrieve its id from the array with clusters
615             for (var i = 0, iMax = clusters.length; i < iMax; i++) {
616                 if (clusters[i].dom === e) {
617                     index = i;
618                     break;
619                 }
620             }
621         }
622     }
623 
624     return index;
625 };
626 
627 /**
628  * Find all elements within the start and end range
629  * If no element is found, returns an empty array
630  * @param start time
631  * @param end time
632  * @return Array itemsInRange
633  */
634 links.Timeline.prototype.getVisibleItems = function  (start, end) {
635     var items = this.items;
636     var itemsInRange = [];
637 
638     if (items) {
639         for (var i = 0, iMax = items.length; i < iMax; i++) {
640             var item = items[i];
641             if (item.end) {
642                 // Time range object // NH use getLeft and getRight here
643                 if (start <= item.start && item.end <= end) {
644                     itemsInRange.push({"row": i});
645                 }
646             } else {
647                 // Point object
648                 if (start <= item.start && item.start <= end) {
649                     itemsInRange.push({"row": i});
650                 }
651             }
652         }
653     }
654 
655     //     var sel = [];
656     // if (this.selection) {
657     //     sel.push({"row": this.selection.index});
658     // }
659     // return sel;
660 
661     return itemsInRange;
662 };
663 
664 
665 /**
666  * Set a new size for the timeline
667  * @param {string} width   Width in pixels or percentage (for example "800px"
668  *                         or "50%")
669  * @param {string} height  Height in pixels or percentage  (for example "400px"
670  *                         or "30%")
671  */
672 links.Timeline.prototype.setSize = function(width, height) {
673     if (width) {
674         this.options.width = width;
675         this.dom.frame.style.width = width;
676     }
677     if (height) {
678         this.options.height = height;
679         this.options.autoHeight = (this.options.height === "auto");
680         if (height !==  "auto" ) {
681             this.dom.frame.style.height = height;
682         }
683     }
684 
685     this.render({
686         animate: false
687     });
688 };
689 
690 
691 /**
692  * Set a new value for the visible range int the timeline.
693  * Set start undefined to include everything from the earliest date to end.
694  * Set end undefined to include everything from start to the last date.
695  * Example usage:
696  *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
697  *                                    new Date("2010-09-13"));
698  * @param {Date}   start     The start date for the timeline. optional
699  * @param {Date}   end       The end date for the timeline. optional
700  * @param {boolean} redraw   Optional. If true (default) the Timeline is
701  *                           directly redrawn
702  */
703 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
704     var range = {};
705     if (!start || !end) {
706         // retrieve the date range of the items
707         range = this.getDataRange(true);
708     }
709 
710     if (!start) {
711         if (end) {
712             if (range.min && range.min.valueOf() < end.valueOf()) {
713                 // start of the data
714                 start = range.min;
715             }
716             else {
717                 // 7 days before the end
718                 start = new Date(end.valueOf());
719                 start.setDate(start.getDate() - 7);
720             }
721         }
722         else {
723             // default of 3 days ago
724             start = new Date();
725             start.setDate(start.getDate() - 3);
726         }
727     }
728 
729     if (!end) {
730         if (range.max) {
731             // end of the data
732             end = range.max;
733         }
734         else {
735             // 7 days after start
736             end = new Date(start.valueOf());
737             end.setDate(end.getDate() + 7);
738         }
739     }
740 
741     // prevent start Date <= end Date
742     if (end <= start) {
743         end = new Date(start.valueOf());
744         end.setDate(end.getDate() + 7);
745     }
746 
747     // limit to the allowed range (don't let this do by applyRange,
748     // because that method will try to maintain the interval (end-start)
749     var min = this.options.min ? this.options.min : undefined; // date
750     if (min != undefined && start.valueOf() < min.valueOf()) {
751         start = new Date(min.valueOf()); // date
752     }
753     var max = this.options.max ? this.options.max : undefined; // date
754     if (max != undefined && end.valueOf() > max.valueOf()) {
755         end = new Date(max.valueOf()); // date
756     }
757 
758     this.applyRange(start, end);
759 
760     if (redraw == undefined || redraw == true) {
761         this.render({
762             animate: false
763         });  // TODO: optimize, no reflow needed
764     }
765     else {
766         this.recalcConversion();
767     }
768 };
769 
770 
771 /**
772  * Change the visible chart range such that all items become visible
773  */
774 links.Timeline.prototype.setVisibleChartRangeAuto = function() {
775     var range = this.getDataRange(true);
776     this.setVisibleChartRange(range.min, range.max);
777 };
778 
779 /**
780  * Adjust the visible range such that the current time is located in the center
781  * of the timeline
782  */
783 links.Timeline.prototype.setVisibleChartRangeNow = function() {
784     var now = new Date();
785 
786     var diff = (this.end.valueOf() - this.start.valueOf());
787 
788     var startNew = new Date(now.valueOf() - diff/2);
789     var endNew = new Date(startNew.valueOf() + diff);
790     this.setVisibleChartRange(startNew, endNew);
791 };
792 
793 
794 /**
795  * Retrieve the current visible range in the timeline.
796  * @return {Object} An object with start and end properties
797  */
798 links.Timeline.prototype.getVisibleChartRange = function() {
799     return {
800         'start': new Date(this.start.valueOf()),
801         'end': new Date(this.end.valueOf())
802     };
803 };
804 
805 /**
806  * Get the date range of the items.
807  * @param {boolean} [withMargin]  If true, 5% of whitespace is added to the
808  *                                left and right of the range. Default is false.
809  * @return {Object} range    An object with parameters min and max.
810  *                           - {Date} min is the lowest start date of the items
811  *                           - {Date} max is the highest start or end date of the items
812  *                           If no data is available, the values of min and max
813  *                           will be undefined
814  */
815 links.Timeline.prototype.getDataRange = function (withMargin) {
816     var items = this.items,
817         min = undefined, // number
818         max = undefined; // number
819 
820     if (items) {
821         for (var i = 0, iMax = items.length; i < iMax; i++) {
822             var item = items[i],
823                 start = item.start != undefined ? item.start.valueOf() : undefined,
824                 end   = item.end != undefined   ? item.end.valueOf() : start;
825 
826             if (start != undefined) {
827                 min = (min != undefined) ? Math.min(min.valueOf(), start.valueOf()) : start;
828             }
829 
830             if (end != undefined) {
831                 max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end;
832             }
833         }
834     }
835 
836     if (min && max && withMargin) {
837         // zoom out 5% such that you have a little white space on the left and right
838         var diff = (max - min);
839         min = min - diff * 0.05;
840         max = max + diff * 0.05;
841     }
842 
843     return {
844         'min': min != undefined ? new Date(min) : undefined,
845         'max': max != undefined ? new Date(max) : undefined
846     };
847 };
848 
849 /**
850  * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
851  * items, ...
852  * @param {Object} [options]  Available options:
853  *                            {boolean} renderTimesLeft   Number of times the
854  *                                                        render may be repeated
855  *                                                        5 times by default.
856  *                            {boolean} animate           takes options.animate
857  *                                                        as default value
858  */
859 links.Timeline.prototype.render = function(options) {
860     var frameResized = this.reflowFrame();
861     var axisResized = this.reflowAxis();
862     var groupsResized = this.reflowGroups();
863     var itemsResized = this.reflowItems();
864     var resized = (frameResized || axisResized || groupsResized || itemsResized);
865 
866     // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue).
867     // if (resized) {
868     var animate = this.options.animate;
869     if (options && options.animate != undefined) {
870         animate = options.animate;
871     }
872 
873     this.recalcConversion();
874     this.clusterItems();
875     this.filterItems();
876     this.stackItems(animate);
877     this.recalcItems();
878 
879     // TODO: only repaint when resized or when filterItems or stackItems gave a change?
880     var needsReflow = this.repaint();
881 
882     // re-render once when needed (prevent endless re-render loop)
883     if (needsReflow) {
884         var renderTimesLeft = options ? options.renderTimesLeft : undefined;
885         if (renderTimesLeft == undefined) {
886             renderTimesLeft = 5;
887         }
888         if (renderTimesLeft > 0) {
889             this.render({
890                 'animate': options ? options.animate: undefined,
891                 'renderTimesLeft': (renderTimesLeft - 1)
892             });
893         }
894     }
895 };
896 
897 /**
898  * Repaint all components of the Timeline
899  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
900  *                                 a reflow is needed.
901  */
902 links.Timeline.prototype.repaint = function() {
903     var frameNeedsReflow = this.repaintFrame();
904     var axisNeedsReflow  = this.repaintAxis();
905     var groupsNeedsReflow  = this.repaintGroups();
906     var itemsNeedsReflow = this.repaintItems();
907     this.repaintCurrentTime();
908     this.repaintCustomTime();
909 
910     return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
911 };
912 
913 /**
914  * Reflow the timeline frame
915  * @return {boolean} resized    Returns true if any of the frame elements
916  *                              have been resized.
917  */
918 links.Timeline.prototype.reflowFrame = function() {
919     var dom = this.dom,
920         options = this.options,
921         size = this.size,
922         resized = false;
923 
924     // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
925     var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
926         frameHeight = dom.frame ? dom.frame.clientHeight : 0;
927 
928     resized = resized || (size.frameWidth !== frameWidth);
929     resized = resized || (size.frameHeight !== frameHeight);
930     size.frameWidth = frameWidth;
931     size.frameHeight = frameHeight;
932 
933     return resized;
934 };
935 
936 /**
937  * repaint the Timeline frame
938  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
939  *                                 a reflow is needed.
940  */
941 links.Timeline.prototype.repaintFrame = function() {
942     var needsReflow = false,
943         dom = this.dom,
944         options = this.options,
945         size = this.size;
946 
947     // main frame
948     if (!dom.frame) {
949         dom.frame = document.createElement("DIV");
950         dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all";
951         dom.container.appendChild(dom.frame);
952         needsReflow = true;
953     }
954 
955     var height = options.autoHeight ?
956         (size.actualHeight + "px") :
957         (options.height || "100%");
958     var width  = options.width || "100%";
959     needsReflow = needsReflow || (dom.frame.style.height != height);
960     needsReflow = needsReflow || (dom.frame.style.width != width);
961     dom.frame.style.height = height;
962     dom.frame.style.width = width;
963 
964     // contents
965     if (!dom.content) {
966         // create content box where the axis and items will be created
967         dom.content = document.createElement("DIV");
968         dom.content.className = "timeline-content";
969         dom.frame.appendChild(dom.content);
970 
971         var timelines = document.createElement("DIV");
972         timelines.style.position = "absolute";
973         timelines.style.left = "0px";
974         timelines.style.top = "0px";
975         timelines.style.height = "100%";
976         timelines.style.width = "0px";
977         dom.content.appendChild(timelines);
978         dom.contentTimelines = timelines;
979 
980         var params = this.eventParams,
981             me = this;
982         if (!params.onMouseDown) {
983             params.onMouseDown = function (event) {me.onMouseDown(event);};
984             links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
985         }
986         if (!params.onTouchStart) {
987             params.onTouchStart = function (event) {me.onTouchStart(event);};
988             links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
989         }
990         if (!params.onMouseWheel) {
991             params.onMouseWheel = function (event) {me.onMouseWheel(event);};
992             links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
993         }
994         if (!params.onDblClick) {
995             params.onDblClick = function (event) {me.onDblClick(event);};
996             links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
997         }
998 
999         needsReflow = true;
1000     }
1001     dom.content.style.left = size.contentLeft + "px";
1002     dom.content.style.top = "0px";
1003     dom.content.style.width = size.contentWidth + "px";
1004     dom.content.style.height = size.frameHeight + "px";
1005 
1006     this.repaintNavigation();
1007 
1008     return needsReflow;
1009 };
1010 
1011 /**
1012  * Reflow the timeline axis. Calculate its height, width, positioning, etc...
1013  * @return {boolean} resized    returns true if the axis is resized
1014  */
1015 links.Timeline.prototype.reflowAxis = function() {
1016     var resized = false,
1017         dom = this.dom,
1018         options = this.options,
1019         size = this.size,
1020         axisDom = dom.axis;
1021 
1022     var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
1023         characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
1024         characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
1025         characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
1026         axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
1027             (options.showMajorLabels ? characterMajorHeight : 0);
1028 
1029     var axisTop  = options.axisOnTop ? 0 : size.frameHeight - axisHeight,
1030         axisLine = options.axisOnTop ? axisHeight : axisTop;
1031 
1032     resized = resized || (size.axis.top !== axisTop);
1033     resized = resized || (size.axis.line !== axisLine);
1034     resized = resized || (size.axis.height !== axisHeight);
1035     size.axis.top = axisTop;
1036     size.axis.line = axisLine;
1037     size.axis.height = axisHeight;
1038     size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
1039         (options.showMinorLabels ? characterMinorHeight : 0);
1040     size.axis.labelMinorTop = options.axisOnTop ?
1041         (options.showMajorLabels ? characterMajorHeight : 0) :
1042         axisLine;
1043     size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
1044     size.axis.lineMinorHeight = options.showMajorLabels ?
1045         size.frameHeight - characterMajorHeight:
1046         size.frameHeight;
1047     if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
1048         size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
1049     }
1050     else {
1051         size.axis.lineMinorWidth = 1;
1052     }
1053     if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
1054         size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
1055     }
1056     else {
1057         size.axis.lineMajorWidth = 1;
1058     }
1059 
1060     resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
1061     resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
1062     resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
1063     resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
1064     size.axis.characterMinorWidth  = characterMinorWidth;
1065     size.axis.characterMinorHeight = characterMinorHeight;
1066     size.axis.characterMajorWidth  = characterMajorWidth;
1067     size.axis.characterMajorHeight = characterMajorHeight;
1068 
1069     var contentHeight = Math.max(size.frameHeight - axisHeight, 0);
1070     size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth;
1071     size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0);
1072     size.contentHeight = contentHeight;
1073 
1074     return resized;
1075 };
1076 
1077 /**
1078  * Redraw the timeline axis with minor and major labels
1079  * @return {boolean} needsReflow     Returns true if the DOM is changed such
1080  *                                   that a reflow is needed.
1081  */
1082 links.Timeline.prototype.repaintAxis = function() {
1083     var needsReflow = false,
1084         dom = this.dom,
1085         options = this.options,
1086         size = this.size,
1087         step = this.step;
1088 
1089     var axis = dom.axis;
1090     if (!axis) {
1091         axis = {};
1092         dom.axis = axis;
1093     }
1094     if (!size.axis.properties) {
1095         size.axis.properties = {};
1096     }
1097     if (!axis.minorTexts) {
1098         axis.minorTexts = [];
1099     }
1100     if (!axis.minorLines) {
1101         axis.minorLines = [];
1102     }
1103     if (!axis.majorTexts) {
1104         axis.majorTexts = [];
1105     }
1106     if (!axis.majorLines) {
1107         axis.majorLines = [];
1108     }
1109 
1110     if (!axis.frame) {
1111         axis.frame = document.createElement("DIV");
1112         axis.frame.style.position = "absolute";
1113         axis.frame.style.left = "0px";
1114         axis.frame.style.top = "0px";
1115         dom.content.appendChild(axis.frame);
1116     }
1117 
1118     // take axis offline
1119     dom.content.removeChild(axis.frame);
1120 
1121     axis.frame.style.width = (size.contentWidth) + "px";
1122     axis.frame.style.height = (size.axis.height) + "px";
1123 
1124     // the drawn axis is more wide than the actual visual part, such that
1125     // the axis can be dragged without having to redraw it each time again.
1126     var start = this.screenToTime(0);
1127     var end = this.screenToTime(size.contentWidth);
1128 
1129     // calculate minimum step (in milliseconds) based on character size
1130     if (size.axis.characterMinorWidth) {
1131         this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) -
1132             this.screenToTime(0);
1133 
1134         step.setRange(start, end, this.minimumStep);
1135     }
1136 
1137     var charsNeedsReflow = this.repaintAxisCharacters();
1138     needsReflow = needsReflow || charsNeedsReflow;
1139 
1140     // The current labels on the axis will be re-used (much better performance),
1141     // therefore, the repaintAxis method uses the mechanism with
1142     // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and
1143     // this.size.axis.properties is used.
1144     this.repaintAxisStartOverwriting();
1145 
1146     step.start();
1147     var xFirstMajorLabel = undefined;
1148     var max = 0;
1149     while (!step.end() && max < 1000) {
1150         max++;
1151         var cur = step.getCurrent(),
1152             x = this.timeToScreen(cur),
1153             isMajor = step.isMajor();
1154 
1155         if (options.showMinorLabels) {
1156             this.repaintAxisMinorText(x, step.getLabelMinor(options));
1157         }
1158 
1159         if (isMajor && options.showMajorLabels) {
1160             if (x > 0) {
1161                 if (xFirstMajorLabel == undefined) {
1162                     xFirstMajorLabel = x;
1163                 }
1164                 this.repaintAxisMajorText(x, step.getLabelMajor(options));
1165             }
1166             this.repaintAxisMajorLine(x);
1167         }
1168         else {
1169             this.repaintAxisMinorLine(x);
1170         }
1171 
1172         step.next();
1173     }
1174 
1175     // create a major label on the left when needed
1176     if (options.showMajorLabels) {
1177         var leftTime = this.screenToTime(0),
1178             leftText = this.step.getLabelMajor(options, leftTime),
1179             width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation
1180 
1181         if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
1182             this.repaintAxisMajorText(0, leftText, leftTime);
1183         }
1184     }
1185 
1186     // cleanup left over labels
1187     this.repaintAxisEndOverwriting();
1188 
1189     this.repaintAxisHorizontal();
1190 
1191     // put axis online
1192     dom.content.insertBefore(axis.frame, dom.content.firstChild);
1193 
1194     return needsReflow;
1195 };
1196 
1197 /**
1198  * Create characters used to determine the size of text on the axis
1199  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1200  *                                 a reflow is needed.
1201  */
1202 links.Timeline.prototype.repaintAxisCharacters = function () {
1203     // calculate the width and height of a single character
1204     // this is used to calculate the step size, and also the positioning of the
1205     // axis
1206     var needsReflow = false,
1207         dom = this.dom,
1208         axis = dom.axis,
1209         text;
1210 
1211     if (!axis.characterMinor) {
1212         text = document.createTextNode("0");
1213         var characterMinor = document.createElement("DIV");
1214         characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
1215         characterMinor.appendChild(text);
1216         characterMinor.style.position = "absolute";
1217         characterMinor.style.visibility = "hidden";
1218         characterMinor.style.paddingLeft = "0px";
1219         characterMinor.style.paddingRight = "0px";
1220         axis.frame.appendChild(characterMinor);
1221 
1222         axis.characterMinor = characterMinor;
1223         needsReflow = true;
1224     }
1225 
1226     if (!axis.characterMajor) {
1227         text = document.createTextNode("0");
1228         var characterMajor = document.createElement("DIV");
1229         characterMajor.className = "timeline-axis-text timeline-axis-text-major";
1230         characterMajor.appendChild(text);
1231         characterMajor.style.position = "absolute";
1232         characterMajor.style.visibility = "hidden";
1233         characterMajor.style.paddingLeft = "0px";
1234         characterMajor.style.paddingRight = "0px";
1235         axis.frame.appendChild(characterMajor);
1236 
1237         axis.characterMajor = characterMajor;
1238         needsReflow = true;
1239     }
1240 
1241     return needsReflow;
1242 };
1243 
1244 /**
1245  * Initialize redraw of the axis. All existing labels and lines will be
1246  * overwritten and reused.
1247  */
1248 links.Timeline.prototype.repaintAxisStartOverwriting = function () {
1249     var properties = this.size.axis.properties;
1250 
1251     properties.minorTextNum = 0;
1252     properties.minorLineNum = 0;
1253     properties.majorTextNum = 0;
1254     properties.majorLineNum = 0;
1255 };
1256 
1257 /**
1258  * End of overwriting HTML DOM elements of the axis.
1259  * remaining elements will be removed
1260  */
1261 links.Timeline.prototype.repaintAxisEndOverwriting = function () {
1262     var dom = this.dom,
1263         props = this.size.axis.properties,
1264         frame = this.dom.axis.frame,
1265         num;
1266 
1267     // remove leftovers
1268     var minorTexts = dom.axis.minorTexts;
1269     num = props.minorTextNum;
1270     while (minorTexts.length > num) {
1271         var minorText = minorTexts[num];
1272         frame.removeChild(minorText);
1273         minorTexts.splice(num, 1);
1274     }
1275 
1276     var minorLines = dom.axis.minorLines;
1277     num = props.minorLineNum;
1278     while (minorLines.length > num) {
1279         var minorLine = minorLines[num];
1280         frame.removeChild(minorLine);
1281         minorLines.splice(num, 1);
1282     }
1283 
1284     var majorTexts = dom.axis.majorTexts;
1285     num = props.majorTextNum;
1286     while (majorTexts.length > num) {
1287         var majorText = majorTexts[num];
1288         frame.removeChild(majorText);
1289         majorTexts.splice(num, 1);
1290     }
1291 
1292     var majorLines = dom.axis.majorLines;
1293     num = props.majorLineNum;
1294     while (majorLines.length > num) {
1295         var majorLine = majorLines[num];
1296         frame.removeChild(majorLine);
1297         majorLines.splice(num, 1);
1298     }
1299 };
1300 
1301 /**
1302  * Repaint the horizontal line and background of the axis
1303  */
1304 links.Timeline.prototype.repaintAxisHorizontal = function() {
1305     var axis = this.dom.axis,
1306         size = this.size,
1307         options = this.options;
1308 
1309     // line behind all axis elements (possibly having a background color)
1310     var hasAxis = (options.showMinorLabels || options.showMajorLabels);
1311     if (hasAxis) {
1312         if (!axis.backgroundLine) {
1313             // create the axis line background (for a background color or so)
1314             var backgroundLine = document.createElement("DIV");
1315             backgroundLine.className = "timeline-axis";
1316             backgroundLine.style.position = "absolute";
1317             backgroundLine.style.left = "0px";
1318             backgroundLine.style.width = "100%";
1319             backgroundLine.style.border = "none";
1320             axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
1321 
1322             axis.backgroundLine = backgroundLine;
1323         }
1324 
1325         if (axis.backgroundLine) {
1326             axis.backgroundLine.style.top = size.axis.top + "px";
1327             axis.backgroundLine.style.height = size.axis.height + "px";
1328         }
1329     }
1330     else {
1331         if (axis.backgroundLine) {
1332             axis.frame.removeChild(axis.backgroundLine);
1333             delete axis.backgroundLine;
1334         }
1335     }
1336 
1337     // line before all axis elements
1338     if (hasAxis) {
1339         if (axis.line) {
1340             // put this line at the end of all childs
1341             var line = axis.frame.removeChild(axis.line);
1342             axis.frame.appendChild(line);
1343         }
1344         else {
1345             // make the axis line
1346             var line = document.createElement("DIV");
1347             line.className = "timeline-axis";
1348             line.style.position = "absolute";
1349             line.style.left = "0px";
1350             line.style.width = "100%";
1351             line.style.height = "0px";
1352             axis.frame.appendChild(line);
1353 
1354             axis.line = line;
1355         }
1356 
1357         axis.line.style.top = size.axis.line + "px";
1358     }
1359     else {
1360         if (axis.line && axis.line.parentElement) {
1361             axis.frame.removeChild(axis.line);
1362             delete axis.line;
1363         }
1364     }
1365 };
1366 
1367 /**
1368  * Create a minor label for the axis at position x
1369  * @param {Number} x
1370  * @param {String} text
1371  */
1372 links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
1373     var size = this.size,
1374         dom = this.dom,
1375         props = size.axis.properties,
1376         frame = dom.axis.frame,
1377         minorTexts = dom.axis.minorTexts,
1378         index = props.minorTextNum,
1379         label;
1380 
1381     if (index < minorTexts.length) {
1382         label = minorTexts[index]
1383     }
1384     else {
1385         // create new label
1386         var content = document.createTextNode("");
1387         label = document.createElement("DIV");
1388         label.appendChild(content);
1389         label.className = "timeline-axis-text timeline-axis-text-minor";
1390         label.style.position = "absolute";
1391 
1392         frame.appendChild(label);
1393 
1394         minorTexts.push(label);
1395     }
1396 
1397     label.childNodes[0].nodeValue = text;
1398     label.style.left = x + "px";
1399     label.style.top  = size.axis.labelMinorTop + "px";
1400     //label.title = title;  // TODO: this is a heavy operation
1401 
1402     props.minorTextNum++;
1403 };
1404 
1405 /**
1406  * Create a minor line for the axis at position x
1407  * @param {Number} x
1408  */
1409 links.Timeline.prototype.repaintAxisMinorLine = function (x) {
1410     var axis = this.size.axis,
1411         dom = this.dom,
1412         props = axis.properties,
1413         frame = dom.axis.frame,
1414         minorLines = dom.axis.minorLines,
1415         index = props.minorLineNum,
1416         line;
1417 
1418     if (index < minorLines.length) {
1419         line = minorLines[index];
1420     }
1421     else {
1422         // create vertical line
1423         line = document.createElement("DIV");
1424         line.className = "timeline-axis-grid timeline-axis-grid-minor";
1425         line.style.position = "absolute";
1426         line.style.width = "0px";
1427 
1428         frame.appendChild(line);
1429         minorLines.push(line);
1430     }
1431 
1432     line.style.top = axis.lineMinorTop + "px";
1433     line.style.height = axis.lineMinorHeight + "px";
1434     line.style.left = (x - axis.lineMinorWidth/2) + "px";
1435 
1436     props.minorLineNum++;
1437 };
1438 
1439 /**
1440  * Create a Major label for the axis at position x
1441  * @param {Number} x
1442  * @param {String} text
1443  */
1444 links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
1445     var size = this.size,
1446         props = size.axis.properties,
1447         frame = this.dom.axis.frame,
1448         majorTexts = this.dom.axis.majorTexts,
1449         index = props.majorTextNum,
1450         label;
1451 
1452     if (index < majorTexts.length) {
1453         label = majorTexts[index];
1454     }
1455     else {
1456         // create label
1457         var content = document.createTextNode(text);
1458         label = document.createElement("DIV");
1459         label.className = "timeline-axis-text timeline-axis-text-major";
1460         label.appendChild(content);
1461         label.style.position = "absolute";
1462         label.style.top = "0px";
1463 
1464         frame.appendChild(label);
1465         majorTexts.push(label);
1466     }
1467 
1468     label.childNodes[0].nodeValue = text;
1469     label.style.top = size.axis.labelMajorTop + "px";
1470     label.style.left = x + "px";
1471     //label.title = title; // TODO: this is a heavy operation
1472 
1473     props.majorTextNum ++;
1474 };
1475 
1476 /**
1477  * Create a Major line for the axis at position x
1478  * @param {Number} x
1479  */
1480 links.Timeline.prototype.repaintAxisMajorLine = function (x) {
1481     var size = this.size,
1482         props = size.axis.properties,
1483         axis = this.size.axis,
1484         frame = this.dom.axis.frame,
1485         majorLines = this.dom.axis.majorLines,
1486         index = props.majorLineNum,
1487         line;
1488 
1489     if (index < majorLines.length) {
1490         line = majorLines[index];
1491     }
1492     else {
1493         // create vertical line
1494         line = document.createElement("DIV");
1495         line.className = "timeline-axis-grid timeline-axis-grid-major";
1496         line.style.position = "absolute";
1497         line.style.top = "0px";
1498         line.style.width = "0px";
1499 
1500         frame.appendChild(line);
1501         majorLines.push(line);
1502     }
1503 
1504     line.style.left = (x - axis.lineMajorWidth/2) + "px";
1505     line.style.height = size.frameHeight + "px";
1506 
1507     props.majorLineNum ++;
1508 };
1509 
1510 /**
1511  * Reflow all items, retrieve their actual size
1512  * @return {boolean} resized    returns true if any of the items is resized
1513  */
1514 links.Timeline.prototype.reflowItems = function() {
1515     var resized = false,
1516         i,
1517         iMax,
1518         group,
1519         groups = this.groups,
1520         renderedItems = this.renderedItems;
1521 
1522     if (groups) { // TODO: need to check if labels exists?
1523         // loop through all groups to reset the items height
1524         groups.forEach(function (group) {
1525             group.itemsHeight = group.labelHeight || 0;
1526         });
1527     }
1528 
1529     // loop through the width and height of all visible items
1530     for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1531         var item = renderedItems[i],
1532             domItem = item.dom;
1533         group = item.group;
1534 
1535         if (domItem) {
1536             // TODO: move updating width and height into item.reflow
1537             var width = domItem ? domItem.clientWidth : 0;
1538             var height = domItem ? domItem.clientHeight : 0;
1539             resized = resized || (item.width != width);
1540             resized = resized || (item.height != height);
1541             item.width = width;
1542             item.height = height;
1543             //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
1544             item.reflow();
1545         }
1546 
1547         if (group) {
1548             group.itemsHeight = Math.max(this.options.groupMinHeight,group.itemsHeight ?
1549                 Math.max(group.itemsHeight, item.height) :
1550                 item.height);
1551         }
1552     }
1553 
1554     return resized;
1555 };
1556 
1557 /**
1558  * Recalculate item properties:
1559  * - the height of each group.
1560  * - the actualHeight, from the stacked items or the sum of the group heights
1561  * @return {boolean} resized    returns true if any of the items properties is
1562  *                              changed
1563  */
1564 links.Timeline.prototype.recalcItems = function () {
1565     var resized = false,
1566         i,
1567         iMax,
1568         item,
1569         finalItem,
1570         finalItems,
1571         group,
1572         groups = this.groups,
1573         size = this.size,
1574         options = this.options,
1575         renderedItems = this.renderedItems;
1576 
1577     var actualHeight = 0;
1578     if (groups.length == 0) {
1579         // calculate actual height of the timeline when there are no groups
1580         // but stacked items
1581         if (options.autoHeight || options.cluster) {
1582             var min = 0,
1583                 max = 0;
1584 
1585             if (this.stack && this.stack.finalItems) {
1586                 // adjust the offset of all finalItems when the actualHeight has been changed
1587                 finalItems = this.stack.finalItems;
1588                 finalItem = finalItems[0];
1589                 if (finalItem && finalItem.top) {
1590                     min = finalItem.top;
1591                     max = finalItem.top + finalItem.height;
1592                 }
1593                 for (i = 1, iMax = finalItems.length; i < iMax; i++) {
1594                     finalItem = finalItems[i];
1595                     min = Math.min(min, finalItem.top);
1596                     max = Math.max(max, finalItem.top + finalItem.height);
1597                 }
1598             }
1599             else {
1600                 item = renderedItems[0];
1601                 if (item && item.top) {
1602                     min = item.top;
1603                     max = item.top + item.height;
1604                 }
1605                 for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
1606                     item = renderedItems[i];
1607                     if (item.top) {
1608                         min = Math.min(min, item.top);
1609                         max = Math.max(max, (item.top + item.height));
1610                     }
1611                 }
1612             }
1613 
1614             actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height;
1615             if (actualHeight < options.minHeight) {
1616                 actualHeight = options.minHeight;
1617             }
1618 
1619             if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
1620                 // adjust the offset of all items when the actualHeight has been changed
1621                 var diff = actualHeight - size.actualHeight;
1622                 if (this.stack && this.stack.finalItems) {
1623                     finalItems = this.stack.finalItems;
1624                     for (i = 0, iMax = finalItems.length; i < iMax; i++) {
1625                         finalItems[i].top += diff;
1626                         finalItems[i].item.top += diff;
1627                     }
1628                 }
1629                 else {
1630                     for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
1631                         renderedItems[i].top += diff;
1632                     }
1633                 }
1634             }
1635         }
1636     }
1637     else {
1638         // loop through all groups to get the height of each group, and the
1639         // total height
1640         actualHeight = size.axis.height + 2 * options.eventMarginAxis;
1641         for (i = 0, iMax = groups.length; i < iMax; i++) {
1642             group = groups[i];
1643 
1644             //
1645             // TODO: Do we want to apply a max height? how ?
1646             //
1647             var groupHeight = group.itemsHeight;
1648             resized = resized || (groupHeight != group.height);
1649             group.height = Math.max(groupHeight, options.groupMinHeight);
1650 
1651             actualHeight += groups[i].height + options.eventMargin;
1652         }
1653 
1654         // calculate top positions of the group labels and lines
1655         var eventMargin = options.eventMargin,
1656             top = options.axisOnTop ?
1657                 options.eventMarginAxis + eventMargin/2 :
1658                 size.contentHeight - options.eventMarginAxis + eventMargin/ 2,
1659             axisHeight = size.axis.height;
1660 
1661         for (i = 0, iMax = groups.length; i < iMax; i++) {
1662             group = groups[i];
1663             if (options.axisOnTop) {
1664                 group.top = top + axisHeight;
1665                 group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
1666                 group.lineTop = top + axisHeight + group.height + eventMargin/2;
1667                 top += group.height + eventMargin;
1668             }
1669             else {
1670                 top -= group.height + eventMargin;
1671                 group.top = top;
1672                 group.labelTop = top + (group.height - group.labelHeight) / 2;
1673                 group.lineTop = top - eventMargin/2;
1674             }
1675         }
1676 
1677         resized = true;
1678     }
1679 
1680     if (actualHeight < options.minHeight) {
1681         actualHeight = options.minHeight;
1682     }
1683     resized = resized || (actualHeight != size.actualHeight);
1684     size.actualHeight = actualHeight;
1685 
1686     return resized;
1687 };
1688 
1689 /**
1690  * This method clears the (internal) array this.items in a safe way: neatly
1691  * cleaning up the DOM, and accompanying arrays this.renderedItems and
1692  * the created clusters.
1693  */
1694 links.Timeline.prototype.clearItems = function() {
1695     // add all visible items to the list to be hidden
1696     var hideItems = this.renderQueue.hide;
1697     this.renderedItems.forEach(function (item) {
1698         hideItems.push(item);
1699     });
1700 
1701     // clear the cluster generator
1702     this.clusterGenerator.clear();
1703 
1704     // actually clear the items
1705     this.items = [];
1706 };
1707 
1708 /**
1709  * Repaint all items
1710  * @return {boolean} needsReflow   Returns true if the DOM is changed such that
1711  *                                 a reflow is needed.
1712  */
1713 links.Timeline.prototype.repaintItems = function() {
1714     var i, iMax, item, index;
1715 
1716     var needsReflow = false,
1717         dom = this.dom,
1718         size = this.size,
1719         timeline = this,
1720         renderedItems = this.renderedItems;
1721 
1722     if (!dom.items) {
1723         dom.items = {};
1724     }
1725 
1726     // draw the frame containing the items
1727     var frame = dom.items.frame;
1728     if (!frame) {
1729         frame = document.createElement("DIV");
1730         frame.style.position = "relative";
1731         dom.content.appendChild(frame);
1732         dom.items.frame = frame;
1733     }
1734 
1735     frame.style.left = "0px";
1736     frame.style.top = size.items.top + "px";
1737     frame.style.height = "0px";
1738 
1739     // Take frame offline (for faster manipulation of the DOM)
1740     dom.content.removeChild(frame);
1741 
1742     // process the render queue with changes
1743     var queue = this.renderQueue;
1744     var newImageUrls = [];
1745     needsReflow = needsReflow ||
1746         (queue.show.length > 0) ||
1747         (queue.update.length > 0) ||
1748         (queue.hide.length > 0);   // TODO: reflow needed on hide of items?
1749 
1750     while (item = queue.show.shift()) {
1751         item.showDOM(frame);
1752         item.getImageUrls(newImageUrls);
1753         renderedItems.push(item);
1754     }
1755     while (item = queue.update.shift()) {
1756         item.updateDOM(frame);
1757         item.getImageUrls(newImageUrls);
1758         index = this.renderedItems.indexOf(item);
1759         if (index == -1) {
1760             renderedItems.push(item);
1761         }
1762     }
1763     while (item = queue.hide.shift()) {
1764         item.hideDOM(frame);
1765         index = this.renderedItems.indexOf(item);
1766         if (index != -1) {
1767             renderedItems.splice(index, 1);
1768         }
1769     }
1770 
1771     // reposition all visible items
1772     renderedItems.forEach(function (item) {
1773         item.updatePosition(timeline);
1774     });
1775 
1776     // redraw the delete button and dragareas of the selected item (if any)
1777     this.repaintDeleteButton();
1778     this.repaintDragAreas();
1779 
1780     // put frame online again
1781     dom.content.appendChild(frame);
1782 
1783     if (newImageUrls.length) {
1784         // retrieve all image sources from the items, and set a callback once
1785         // all images are retrieved
1786         var callback = function () {
1787             timeline.render();
1788         };
1789         var sendCallbackWhenAlreadyLoaded = false;
1790         links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
1791     }
1792 
1793     return needsReflow;
1794 };
1795 
1796 /**
1797  * Reflow the size of the groups
1798  * @return {boolean} resized    Returns true if any of the frame elements
1799  *                              have been resized.
1800  */
1801 links.Timeline.prototype.reflowGroups = function() {
1802     var resized = false,
1803         options = this.options,
1804         size = this.size,
1805         dom = this.dom;
1806 
1807     // calculate the groups width and height
1808     // TODO: only update when data is changed! -> use an updateSeq
1809     var groupsWidth = 0;
1810 
1811     // loop through all groups to get the labels width and height
1812     var groups = this.groups;
1813     var labels = this.dom.groups ? this.dom.groups.labels : [];
1814     for (var i = 0, iMax = groups.length; i < iMax; i++) {
1815         var group = groups[i];
1816         var label = labels[i];
1817         group.labelWidth  = label ? label.clientWidth : 0;
1818         group.labelHeight = label ? label.clientHeight : 0;
1819         group.width = group.labelWidth;  // TODO: group.width is redundant with labelWidth
1820 
1821         groupsWidth = Math.max(groupsWidth, group.width);
1822     }
1823 
1824     // limit groupsWidth to the groups width in the options
1825     if (options.groupsWidth !== undefined) {
1826         groupsWidth = dom.groups && dom.groups.frame ? dom.groups.frame.clientWidth : 0;
1827     }
1828 
1829     // compensate for the border width. TODO: calculate the real border width
1830     groupsWidth += 1;
1831 
1832     var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
1833     resized = resized || (size.groupsWidth !== groupsWidth);
1834     resized = resized || (size.groupsLeft !== groupsLeft);
1835     size.groupsWidth = groupsWidth;
1836     size.groupsLeft = groupsLeft;
1837 
1838     return resized;
1839 };
1840 
1841 /**
1842  * Redraw the group labels
1843  */
1844 links.Timeline.prototype.repaintGroups = function() {
1845     var dom = this.dom,
1846         timeline = this,
1847         options = this.options,
1848         size = this.size,
1849         groups = this.groups;
1850 
1851     if (dom.groups === undefined) {
1852         dom.groups = {};
1853     }
1854 
1855     var labels = dom.groups.labels;
1856     if (!labels) {
1857         labels = [];
1858         dom.groups.labels = labels;
1859     }
1860     var labelLines = dom.groups.labelLines;
1861     if (!labelLines) {
1862         labelLines = [];
1863         dom.groups.labelLines = labelLines;
1864     }
1865     var itemLines = dom.groups.itemLines;
1866     if (!itemLines) {
1867         itemLines = [];
1868         dom.groups.itemLines = itemLines;
1869     }
1870 
1871     // create the frame for holding the groups
1872     var frame = dom.groups.frame;
1873     if (!frame) {
1874         frame =  document.createElement("DIV");
1875         frame.className = "timeline-groups-axis";
1876         frame.style.position = "absolute";
1877         frame.style.overflow = "hidden";
1878         frame.style.top = "0px";
1879         frame.style.height = "100%";
1880 
1881         dom.frame.appendChild(frame);
1882         dom.groups.frame = frame;
1883     }
1884 
1885     frame.style.left = size.groupsLeft + "px";
1886     frame.style.width = (options.groupsWidth !== undefined) ?
1887         options.groupsWidth :
1888         size.groupsWidth + "px";
1889 
1890     // hide groups axis when there are no groups
1891     if (groups.length == 0) {
1892         frame.style.display = 'none';
1893     }
1894     else {
1895         frame.style.display = '';
1896     }
1897 
1898     // TODO: only create/update groups when data is changed.
1899 
1900     // create the items
1901     var current = labels.length,
1902         needed = groups.length;
1903 
1904     // overwrite existing group labels
1905     for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
1906         var group = groups[i];
1907         var label = labels[i];
1908         label.innerHTML = this.getGroupName(group);
1909         label.style.display = '';
1910     }
1911 
1912     // append new items when needed
1913     for (var i = current; i < needed; i++) {
1914         var group = groups[i];
1915 
1916         // create text label
1917         var label = document.createElement("DIV");
1918         label.className = "timeline-groups-text";
1919         label.style.position = "absolute";
1920         if (options.groupsWidth === undefined) {
1921             label.style.whiteSpace = "nowrap";
1922         }
1923         label.innerHTML = this.getGroupName(group);
1924         frame.appendChild(label);
1925         labels[i] = label;
1926 
1927         // create the grid line between the group labels
1928         var labelLine = document.createElement("DIV");
1929         labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1930         labelLine.style.position = "absolute";
1931         labelLine.style.left = "0px";
1932         labelLine.style.width = "100%";
1933         labelLine.style.height = "0px";
1934         labelLine.style.borderTopStyle = "solid";
1935         frame.appendChild(labelLine);
1936         labelLines[i] = labelLine;
1937 
1938         // create the grid line between the items
1939         var itemLine = document.createElement("DIV");
1940         itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1941         itemLine.style.position = "absolute";
1942         itemLine.style.left = "0px";
1943         itemLine.style.width = "100%";
1944         itemLine.style.height = "0px";
1945         itemLine.style.borderTopStyle = "solid";
1946         dom.content.insertBefore(itemLine, dom.content.firstChild);
1947         itemLines[i] = itemLine;
1948     }
1949 
1950     // remove redundant items from the DOM when needed
1951     for (var i = needed; i < current; i++) {
1952         var label = labels[i],
1953             labelLine = labelLines[i],
1954             itemLine = itemLines[i];
1955 
1956         frame.removeChild(label);
1957         frame.removeChild(labelLine);
1958         dom.content.removeChild(itemLine);
1959     }
1960     labels.splice(needed, current - needed);
1961     labelLines.splice(needed, current - needed);
1962     itemLines.splice(needed, current - needed);
1963 
1964     links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft');
1965 
1966     // position the groups
1967     for (var i = 0, iMax = groups.length; i < iMax; i++) {
1968         var group = groups[i],
1969             label = labels[i],
1970             labelLine = labelLines[i],
1971             itemLine = itemLines[i];
1972 
1973         label.style.top = group.labelTop + "px";
1974         labelLine.style.top = group.lineTop + "px";
1975         itemLine.style.top = group.lineTop + "px";
1976         itemLine.style.width = size.contentWidth + "px";
1977     }
1978 
1979     if (!dom.groups.background) {
1980         // create the axis grid line background
1981         var background = document.createElement("DIV");
1982         background.className = "timeline-axis";
1983         background.style.position = "absolute";
1984         background.style.left = "0px";
1985         background.style.width = "100%";
1986         background.style.border = "none";
1987 
1988         frame.appendChild(background);
1989         dom.groups.background = background;
1990     }
1991     dom.groups.background.style.top = size.axis.top + 'px';
1992     dom.groups.background.style.height = size.axis.height + 'px';
1993 
1994     if (!dom.groups.line) {
1995         // create the axis grid line
1996         var line = document.createElement("DIV");
1997         line.className = "timeline-axis";
1998         line.style.position = "absolute";
1999         line.style.left = "0px";
2000         line.style.width = "100%";
2001         line.style.height = "0px";
2002 
2003         frame.appendChild(line);
2004         dom.groups.line = line;
2005     }
2006     dom.groups.line.style.top = size.axis.line + 'px';
2007 
2008     // create a callback when there are images which are not yet loaded
2009     // TODO: more efficiently load images in the groups
2010     if (dom.groups.frame && groups.length) {
2011         var imageUrls = [];
2012         links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
2013         if (imageUrls.length) {
2014             // retrieve all image sources from the items, and set a callback once
2015             // all images are retrieved
2016             var callback = function () {
2017                 timeline.render();
2018             };
2019             var sendCallbackWhenAlreadyLoaded = false;
2020             links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
2021         }
2022     }
2023 };
2024 
2025 
2026 /**
2027  * Redraw the current time bar
2028  */
2029 links.Timeline.prototype.repaintCurrentTime = function() {
2030     var options = this.options,
2031         dom = this.dom,
2032         size = this.size;
2033 
2034     if (!options.showCurrentTime) {
2035         if (dom.currentTime) {
2036             dom.contentTimelines.removeChild(dom.currentTime);
2037             delete dom.currentTime;
2038         }
2039 
2040         return;
2041     }
2042 
2043     if (!dom.currentTime) {
2044         // create the current time bar
2045         var currentTime = document.createElement("DIV");
2046         currentTime.className = "timeline-currenttime";
2047         currentTime.style.position = "absolute";
2048         currentTime.style.top = "0px";
2049         currentTime.style.height = "100%";
2050 
2051         dom.contentTimelines.appendChild(currentTime);
2052         dom.currentTime = currentTime;
2053     }
2054 
2055     var now = new Date();
2056     var nowOffset = new Date(now.valueOf() + this.clientTimeOffset);
2057     var x = this.timeToScreen(nowOffset);
2058 
2059     var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
2060     dom.currentTime.style.display = visible ? '' : 'none';
2061     dom.currentTime.style.left = x + "px";
2062     dom.currentTime.title = "Current time: " + nowOffset;
2063 
2064     // start a timer to adjust for the new time
2065     if (this.currentTimeTimer != undefined) {
2066         clearTimeout(this.currentTimeTimer);
2067         delete this.currentTimeTimer;
2068     }
2069     var timeline = this;
2070     var onTimeout = function() {
2071         timeline.repaintCurrentTime();
2072     };
2073     // the time equal to the width of one pixel, divided by 2 for more smoothness
2074     var interval = 1 / this.conversion.factor / 2;
2075     if (interval < 30) interval = 30;
2076     this.currentTimeTimer = setTimeout(onTimeout, interval);
2077 };
2078 
2079 /**
2080  * Redraw the custom time bar
2081  */
2082 links.Timeline.prototype.repaintCustomTime = function() {
2083     var options = this.options,
2084         dom = this.dom,
2085         size = this.size;
2086 
2087     if (!options.showCustomTime) {
2088         if (dom.customTime) {
2089             dom.contentTimelines.removeChild(dom.customTime);
2090             delete dom.customTime;
2091         }
2092 
2093         return;
2094     }
2095 
2096     if (!dom.customTime) {
2097         var customTime = document.createElement("DIV");
2098         customTime.className = "timeline-customtime";
2099         customTime.style.position = "absolute";
2100         customTime.style.top = "0px";
2101         customTime.style.height = "100%";
2102 
2103         var drag = document.createElement("DIV");
2104         drag.style.position = "relative";
2105         drag.style.top = "0px";
2106         drag.style.left = "-10px";
2107         drag.style.height = "100%";
2108         drag.style.width = "20px";
2109         customTime.appendChild(drag);
2110 
2111         dom.contentTimelines.appendChild(customTime);
2112         dom.customTime = customTime;
2113 
2114         // initialize parameter
2115         this.customTime = new Date();
2116     }
2117 
2118     var x = this.timeToScreen(this.customTime),
2119         visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
2120     dom.customTime.style.display = visible ? '' : 'none';
2121     dom.customTime.style.left = x + "px";
2122     dom.customTime.title = "Time: " + this.customTime;
2123 };
2124 
2125 
2126 /**
2127  * Redraw the delete button, on the top right of the currently selected item
2128  * if there is no item selected, the button is hidden.
2129  */
2130 links.Timeline.prototype.repaintDeleteButton = function () {
2131     var timeline = this,
2132         dom = this.dom,
2133         frame = dom.items.frame;
2134 
2135     var deleteButton = dom.items.deleteButton;
2136     if (!deleteButton) {
2137         // create a delete button
2138         deleteButton = document.createElement("DIV");
2139         deleteButton.className = "timeline-navigation-delete";
2140         deleteButton.style.position = "absolute";
2141 
2142         frame.appendChild(deleteButton);
2143         dom.items.deleteButton = deleteButton;
2144     }
2145 
2146     var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
2147         item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
2148     if (item && item.rendered && this.isEditable(item)) {
2149         var right = item.getRight(this),
2150             top = item.top;
2151 
2152         deleteButton.style.left = right + 'px';
2153         deleteButton.style.top = top + 'px';
2154         deleteButton.style.display = '';
2155         frame.removeChild(deleteButton);
2156         frame.appendChild(deleteButton);
2157     }
2158     else {
2159         deleteButton.style.display = 'none';
2160     }
2161 };
2162 
2163 
2164 /**
2165  * Redraw the drag areas. When an item (ranges only) is selected,
2166  * it gets a drag area on the left and right side, to change its width
2167  */
2168 links.Timeline.prototype.repaintDragAreas = function () {
2169     var timeline = this,
2170         options = this.options,
2171         dom = this.dom,
2172         frame = this.dom.items.frame;
2173 
2174     // create left drag area
2175     var dragLeft = dom.items.dragLeft;
2176     if (!dragLeft) {
2177         dragLeft = document.createElement("DIV");
2178         dragLeft.className="timeline-event-range-drag-left";
2179         dragLeft.style.position = "absolute";
2180 
2181         frame.appendChild(dragLeft);
2182         dom.items.dragLeft = dragLeft;
2183     }
2184 
2185     // create right drag area
2186     var dragRight = dom.items.dragRight;
2187     if (!dragRight) {
2188         dragRight = document.createElement("DIV");
2189         dragRight.className="timeline-event-range-drag-right";
2190         dragRight.style.position = "absolute";
2191 
2192         frame.appendChild(dragRight);
2193         dom.items.dragRight = dragRight;
2194     }
2195 
2196     // reposition left and right drag area
2197     var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1,
2198         item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined;
2199     if (item && item.rendered && this.isEditable(item) &&
2200         (item instanceof links.Timeline.ItemRange || item instanceof links.Timeline.ItemFloatingRange)) {
2201         var left = item.getLeft(this), // NH change to getLeft
2202             right = item.getRight(this), // NH change to getRight
2203             top = item.top,
2204             height = item.height;
2205 
2206         dragLeft.style.left = left + 'px';
2207         dragLeft.style.top = top + 'px';
2208         dragLeft.style.width = options.dragAreaWidth + "px";
2209         dragLeft.style.height = height + 'px';
2210         dragLeft.style.display = '';
2211         frame.removeChild(dragLeft);
2212         frame.appendChild(dragLeft);
2213 
2214         dragRight.style.left = (right - options.dragAreaWidth) + 'px';
2215         dragRight.style.top = top + 'px';
2216         dragRight.style.width = options.dragAreaWidth + "px";
2217         dragRight.style.height = height + 'px';
2218         dragRight.style.display = '';
2219         frame.removeChild(dragRight);
2220         frame.appendChild(dragRight);
2221     }
2222     else {
2223         dragLeft.style.display = 'none';
2224         dragRight.style.display = 'none';
2225     }
2226 };
2227 
2228 /**
2229  * Create the navigation buttons for zooming and moving
2230  */
2231 links.Timeline.prototype.repaintNavigation = function () {
2232     var timeline = this,
2233         options = this.options,
2234         dom = this.dom,
2235         frame = dom.frame,
2236         navBar = dom.navBar;
2237 
2238     if (!navBar) {
2239         var showButtonNew = options.showButtonNew && options.editable;
2240         var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
2241         if (showNavigation || showButtonNew) {
2242             // create a navigation bar containing the navigation buttons
2243             navBar = document.createElement("DIV");
2244             navBar.style.position = "absolute";
2245             navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all";
2246             if (options.groupsOnRight) {
2247                 navBar.style.left = '10px';
2248             }
2249             else {
2250                 navBar.style.right = '10px';
2251             }
2252             if (options.axisOnTop) {
2253                 navBar.style.bottom = '10px';
2254             }
2255             else {
2256                 navBar.style.top = '10px';
2257             }
2258             dom.navBar = navBar;
2259             frame.appendChild(navBar);
2260         }
2261 
2262         if (showButtonNew) {
2263             // create a new in button
2264             navBar.addButton = document.createElement("DIV");
2265             navBar.addButton.className = "timeline-navigation-new";
2266             navBar.addButton.title = options.CREATE_NEW_EVENT;
2267             var addIconSpan = document.createElement("SPAN");
2268             addIconSpan.className = "ui-icon ui-icon-circle-plus";
2269             navBar.addButton.appendChild(addIconSpan);
2270 
2271             var onAdd = function(event) {
2272                 links.Timeline.preventDefault(event);
2273                 links.Timeline.stopPropagation(event);
2274 
2275                 // create a new event at the center of the frame
2276                 var w = timeline.size.contentWidth;
2277                 var x = w / 2;
2278                 var xstart = timeline.screenToTime(x);
2279                 if (options.snapEvents) {
2280                     timeline.step.snap(xstart);
2281                 }
2282 
2283                 var content = options.NEW;
2284                 var group = timeline.groups.length ? timeline.groups[0].content : undefined;
2285                 var preventRender = true;
2286                 timeline.addItem({
2287                     'start': xstart,
2288                     'content': content,
2289                     'group': group
2290                 }, preventRender);
2291                 var index = (timeline.items.length - 1);
2292                 timeline.selectItem(index);
2293 
2294                 timeline.applyAdd = true;
2295 
2296                 // fire an add event.
2297                 // Note that the change can be canceled from within an event listener if
2298                 // this listener calls the method cancelAdd().
2299                 timeline.trigger('add');
2300 
2301                 if (timeline.applyAdd) {
2302                     // render and select the item
2303                     timeline.render({animate: false});
2304                     timeline.selectItem(index);
2305                 }
2306                 else {
2307                     // undo an add
2308                     timeline.deleteItem(index);
2309                 }
2310             };
2311             links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
2312             navBar.appendChild(navBar.addButton);
2313         }
2314 
2315         if (showButtonNew && showNavigation) {
2316             // create a separator line
2317             links.Timeline.addClassName(navBar.addButton, 'timeline-navigation-new-line');
2318         }
2319 
2320         if (showNavigation) {
2321             if (options.zoomable) {
2322                 // create a zoom in button
2323                 navBar.zoomInButton = document.createElement("DIV");
2324                 navBar.zoomInButton.className = "timeline-navigation-zoom-in";
2325                 navBar.zoomInButton.title = this.options.ZOOM_IN;
2326                 var ziIconSpan = document.createElement("SPAN");
2327                 ziIconSpan.className = "ui-icon ui-icon-circle-zoomin";
2328                 navBar.zoomInButton.appendChild(ziIconSpan);
2329 
2330                 var onZoomIn = function(event) {
2331                     links.Timeline.preventDefault(event);
2332                     links.Timeline.stopPropagation(event);
2333                     timeline.zoom(0.4);
2334                     timeline.trigger("rangechange");
2335                     timeline.trigger("rangechanged");
2336                 };
2337                 links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
2338                 navBar.appendChild(navBar.zoomInButton);
2339 
2340                 // create a zoom out button
2341                 navBar.zoomOutButton = document.createElement("DIV");
2342                 navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
2343                 navBar.zoomOutButton.title = this.options.ZOOM_OUT;
2344                 var zoIconSpan = document.createElement("SPAN");
2345                 zoIconSpan.className = "ui-icon ui-icon-circle-zoomout";
2346                 navBar.zoomOutButton.appendChild(zoIconSpan);
2347 
2348                 var onZoomOut = function(event) {
2349                     links.Timeline.preventDefault(event);
2350                     links.Timeline.stopPropagation(event);
2351                     timeline.zoom(-0.4);
2352                     timeline.trigger("rangechange");
2353                     timeline.trigger("rangechanged");
2354                 };
2355                 links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
2356                 navBar.appendChild(navBar.zoomOutButton);
2357             }
2358 
2359             if (options.moveable) {
2360                 // create a move left button
2361                 navBar.moveLeftButton = document.createElement("DIV");
2362                 navBar.moveLeftButton.className = "timeline-navigation-move-left";
2363                 navBar.moveLeftButton.title = this.options.MOVE_LEFT;
2364                 var mlIconSpan = document.createElement("SPAN");
2365                 mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w";
2366                 navBar.moveLeftButton.appendChild(mlIconSpan);
2367 
2368                 var onMoveLeft = function(event) {
2369                     links.Timeline.preventDefault(event);
2370                     links.Timeline.stopPropagation(event);
2371                     timeline.move(-0.2);
2372                     timeline.trigger("rangechange");
2373                     timeline.trigger("rangechanged");
2374                 };
2375                 links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
2376                 navBar.appendChild(navBar.moveLeftButton);
2377 
2378                 // create a move right button
2379                 navBar.moveRightButton = document.createElement("DIV");
2380                 navBar.moveRightButton.className = "timeline-navigation-move-right";
2381                 navBar.moveRightButton.title = this.options.MOVE_RIGHT;
2382                 var mrIconSpan = document.createElement("SPAN");
2383                 mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e";
2384                 navBar.moveRightButton.appendChild(mrIconSpan);
2385 
2386                 var onMoveRight = function(event) {
2387                     links.Timeline.preventDefault(event);
2388                     links.Timeline.stopPropagation(event);
2389                     timeline.move(0.2);
2390                     timeline.trigger("rangechange");
2391                     timeline.trigger("rangechanged");
2392                 };
2393                 links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
2394                 navBar.appendChild(navBar.moveRightButton);
2395             }
2396         }
2397     }
2398 };
2399 
2400 
2401 /**
2402  * Set current time. This function can be used to set the time in the client
2403  * timeline equal with the time on a server.
2404  * @param {Date} time
2405  */
2406 links.Timeline.prototype.setCurrentTime = function(time) {
2407     var now = new Date();
2408     this.clientTimeOffset = (time.valueOf() - now.valueOf());
2409 
2410     this.repaintCurrentTime();
2411 };
2412 
2413 /**
2414  * Get current time. The time can have an offset from the real time, when
2415  * the current time has been changed via the method setCurrentTime.
2416  * @return {Date} time
2417  */
2418 links.Timeline.prototype.getCurrentTime = function() {
2419     var now = new Date();
2420     return new Date(now.valueOf() + this.clientTimeOffset);
2421 };
2422 
2423 
2424 /**
2425  * Set custom time.
2426  * The custom time bar can be used to display events in past or future.
2427  * @param {Date} time
2428  */
2429 links.Timeline.prototype.setCustomTime = function(time) {
2430     this.customTime = new Date(time.valueOf());
2431     this.repaintCustomTime();
2432 };
2433 
2434 /**
2435  * Retrieve the current custom time.
2436  * @return {Date} customTime
2437  */
2438 links.Timeline.prototype.getCustomTime = function() {
2439     return new Date(this.customTime.valueOf());
2440 };
2441 
2442 /**
2443  * Set a custom scale. Autoscaling will be disabled.
2444  * For example setScale(SCALE.MINUTES, 5) will result
2445  * in minor steps of 5 minutes, and major steps of an hour.
2446  *
2447  * @param {links.Timeline.StepDate.SCALE} scale
2448  *                               A scale. Choose from SCALE.MILLISECOND,
2449  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
2450  *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
2451  *                               SCALE.YEAR.
2452  * @param {int}        step   A step size, by default 1. Choose for
2453  *                               example 1, 2, 5, or 10.
2454  */
2455 links.Timeline.prototype.setScale = function(scale, step) {
2456     this.step.setScale(scale, step);
2457     this.render(); // TODO: optimize: only reflow/repaint axis
2458 };
2459 
2460 /**
2461  * Enable or disable autoscaling
2462  * @param {boolean} enable  If true or not defined, autoscaling is enabled.
2463  *                          If false, autoscaling is disabled.
2464  */
2465 links.Timeline.prototype.setAutoScale = function(enable) {
2466     this.step.setAutoScale(enable);
2467     this.render(); // TODO: optimize: only reflow/repaint axis
2468 };
2469 
2470 /**
2471  * Redraw the timeline
2472  * Reloads the (linked) data table and redraws the timeline when resized.
2473  * See also the method checkResize
2474  */
2475 links.Timeline.prototype.redraw = function() {
2476     this.setData(this.data);
2477 };
2478 
2479 
2480 /**
2481  * Check if the timeline is resized, and if so, redraw the timeline.
2482  * Useful when the webpage is resized.
2483  */
2484 links.Timeline.prototype.checkResize = function() {
2485     // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter
2486     this.render();
2487 };
2488 
2489 /**
2490  * Check whether a given item is editable
2491  * @param {links.Timeline.Item} item
2492  * @return {boolean} editable
2493  */
2494 links.Timeline.prototype.isEditable = function (item) {
2495     if (item) {
2496         if (item.editable != undefined) {
2497             return item.editable;
2498         }
2499         else {
2500             return this.options.editable;
2501         }
2502     }
2503     return false;
2504 };
2505 
2506 /**
2507  * Calculate the factor and offset to convert a position on screen to the
2508  * corresponding date and vice versa.
2509  * After the method calcConversionFactor is executed once, the methods screenToTime and
2510  * timeToScreen can be used.
2511  */
2512 links.Timeline.prototype.recalcConversion = function() {
2513     this.conversion.offset = this.start.valueOf();
2514     this.conversion.factor = this.size.contentWidth /
2515         (this.end.valueOf() - this.start.valueOf());
2516 };
2517 
2518 
2519 /**
2520  * Convert a position on screen (pixels) to a datetime
2521  * Before this method can be used, the method calcConversionFactor must be
2522  * executed once.
2523  * @param {int}     x    Position on the screen in pixels
2524  * @return {Date}   time The datetime the corresponds with given position x
2525  */
2526 links.Timeline.prototype.screenToTime = function(x) {
2527     var conversion = this.conversion;
2528     return new Date(x / conversion.factor + conversion.offset);
2529 };
2530 
2531 /**
2532  * Convert a datetime (Date object) into a position on the screen
2533  * Before this method can be used, the method calcConversionFactor must be
2534  * executed once.
2535  * @param {Date}   time A date
2536  * @return {int}   x    The position on the screen in pixels which corresponds
2537  *                      with the given date.
2538  */
2539 links.Timeline.prototype.timeToScreen = function(time) {
2540     var conversion = this.conversion;
2541     return (time.valueOf() - conversion.offset) * conversion.factor;
2542 };
2543 
2544 
2545 
2546 /**
2547  * Event handler for touchstart event on mobile devices
2548  */
2549 links.Timeline.prototype.onTouchStart = function(event) {
2550     var params = this.eventParams,
2551         me = this;
2552 
2553     if (params.touchDown) {
2554         // if already moving, return
2555         return;
2556     }
2557 
2558     params.touchDown = true;
2559     params.zoomed = false;
2560 
2561     this.onMouseDown(event);
2562 
2563     if (!params.onTouchMove) {
2564         params.onTouchMove = function (event) {me.onTouchMove(event);};
2565         links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
2566     }
2567     if (!params.onTouchEnd) {
2568         params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
2569         links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
2570     }
2571 
2572     /* TODO
2573      // check for double tap event
2574      var delta = 500; // ms
2575      var doubleTapStart = (new Date()).valueOf();
2576      var target = links.Timeline.getTarget(event);
2577      var doubleTapItem = this.getItemIndex(target);
2578      if (params.doubleTapStart &&
2579      (doubleTapStart - params.doubleTapStart) < delta &&
2580      doubleTapItem == params.doubleTapItem) {
2581      delete params.doubleTapStart;
2582      delete params.doubleTapItem;
2583      me.onDblClick(event);
2584      params.touchDown = false;
2585      }
2586      params.doubleTapStart = doubleTapStart;
2587      params.doubleTapItem = doubleTapItem;
2588      */
2589     // store timing for double taps
2590     var target = links.Timeline.getTarget(event);
2591     var item = this.getItemIndex(target);
2592     params.doubleTapStartPrev = params.doubleTapStart;
2593     params.doubleTapStart = (new Date()).valueOf();
2594     params.doubleTapItemPrev = params.doubleTapItem;
2595     params.doubleTapItem = item;
2596 
2597     links.Timeline.preventDefault(event);
2598 };
2599 
2600 /**
2601  * Event handler for touchmove event on mobile devices
2602  */
2603 links.Timeline.prototype.onTouchMove = function(event) {
2604     var params = this.eventParams;
2605 
2606     if (event.scale && event.scale !== 1) {
2607         params.zoomed = true;
2608     }
2609 
2610     if (!params.zoomed) {
2611         // move
2612         this.onMouseMove(event);
2613     }
2614     else {
2615         if (this.options.zoomable) {
2616             // pinch
2617             // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
2618             params.zoomed = true;
2619 
2620             var scale = event.scale,
2621                 oldWidth = (params.end.valueOf() - params.start.valueOf()),
2622                 newWidth = oldWidth / scale,
2623                 diff = newWidth - oldWidth,
2624                 start = new Date(parseInt(params.start.valueOf() - diff/2)),
2625                 end = new Date(parseInt(params.end.valueOf() + diff/2));
2626 
2627             // TODO: determine zoom-around-date from touch positions?
2628 
2629             this.setVisibleChartRange(start, end);
2630             this.trigger("rangechange");
2631         }
2632     }
2633 
2634     links.Timeline.preventDefault(event);
2635 };
2636 
2637 /**
2638  * Event handler for touchend event on mobile devices
2639  */
2640 links.Timeline.prototype.onTouchEnd = function(event) {
2641     var params = this.eventParams;
2642     var me = this;
2643     params.touchDown = false;
2644 
2645     if (params.zoomed) {
2646         this.trigger("rangechanged");
2647     }
2648 
2649     if (params.onTouchMove) {
2650         links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
2651         delete params.onTouchMove;
2652 
2653     }
2654     if (params.onTouchEnd) {
2655         links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
2656         delete params.onTouchEnd;
2657     }
2658 
2659     this.onMouseUp(event);
2660 
2661     // check for double tap event
2662     var delta = 500; // ms
2663     var doubleTapEnd = (new Date()).valueOf();
2664     var target = links.Timeline.getTarget(event);
2665     var doubleTapItem = this.getItemIndex(target);
2666     if (params.doubleTapStartPrev &&
2667         (doubleTapEnd - params.doubleTapStartPrev) < delta &&
2668         params.doubleTapItem == params.doubleTapItemPrev) {
2669         params.touchDown = true;
2670         me.onDblClick(event);
2671         params.touchDown = false;
2672     }
2673 
2674     links.Timeline.preventDefault(event);
2675 };
2676 
2677 
2678 /**
2679  * Start a moving operation inside the provided parent element
2680  * @param {Event} event       The event that occurred (required for
2681  *                             retrieving the  mouse position)
2682  */
2683 links.Timeline.prototype.onMouseDown = function(event) {
2684     event = event || window.event;
2685 
2686     var params = this.eventParams,
2687         options = this.options,
2688         dom = this.dom;
2689 
2690     // only react on left mouse button down
2691     var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2692     if (!leftButtonDown && !params.touchDown) {
2693         return;
2694     }
2695 
2696     // get mouse position
2697     params.mouseX = links.Timeline.getPageX(event);
2698     params.mouseY = links.Timeline.getPageY(event);
2699     params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
2700     params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
2701     params.previousLeft = 0;
2702     params.previousOffset = 0;
2703 
2704     params.moved = false;
2705     params.start = new Date(this.start.valueOf());
2706     params.end = new Date(this.end.valueOf());
2707 
2708     params.target = links.Timeline.getTarget(event);
2709     var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined;
2710     var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined;
2711     params.itemDragLeft = (params.target === dragLeft);
2712     params.itemDragRight = (params.target === dragRight);
2713 
2714     if (params.itemDragLeft || params.itemDragRight) {
2715         params.itemIndex = (this.selection && this.selection.index !== undefined) ? this.selection.index : undefined;
2716         delete params.clusterIndex;
2717     }
2718     else {
2719         params.itemIndex = this.getItemIndex(params.target);
2720         params.clusterIndex = this.getClusterIndex(params.target);
2721     }
2722 
2723     params.customTime = (params.target === dom.customTime ||
2724         params.target.parentNode === dom.customTime) ?
2725         this.customTime :
2726         undefined;
2727 
2728     params.addItem = (options.editable && event.ctrlKey);
2729     if (params.addItem) {
2730         // create a new event at the current mouse position
2731         var x = params.mouseX - params.frameLeft;
2732         var y = params.mouseY - params.frameTop;
2733 
2734         var xstart = this.screenToTime(x);
2735         if (options.snapEvents) {
2736             this.step.snap(xstart);
2737         }
2738         var xend = new Date(xstart.valueOf());
2739         var content = options.NEW;
2740         var group = this.getGroupFromHeight(y);
2741         this.addItem({
2742             'start': xstart,
2743             'end': xend,
2744             'content': content,
2745             'group': this.getGroupName(group)
2746         });
2747         params.itemIndex = (this.items.length - 1);
2748         delete params.clusterIndex;
2749         this.selectItem(params.itemIndex);
2750         params.itemDragRight = true;
2751     }
2752 
2753     var item = this.items[params.itemIndex];
2754     var isSelected = this.isSelected(params.itemIndex);
2755     params.editItem = isSelected && this.isEditable(item);
2756     if (params.editItem) {
2757         params.itemStart = item.start;
2758         params.itemEnd = item.end;
2759         params.itemGroup = item.group;
2760         params.itemLeft = item.getLeft(this); // NH Use item.getLeft here
2761         params.itemRight = item.getRight(this); // NH Use item.getRight here
2762     }
2763     else {
2764         this.dom.frame.style.cursor = 'move';
2765     }
2766     if (!params.touchDown) {
2767         // add event listeners to handle moving the contents
2768         // we store the function onmousemove and onmouseup in the timeline, so we can
2769         // remove the eventlisteners lateron in the function mouseUp()
2770         var me = this;
2771         if (!params.onMouseMove) {
2772             params.onMouseMove = function (event) {me.onMouseMove(event);};
2773             links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
2774         }
2775         if (!params.onMouseUp) {
2776             params.onMouseUp = function (event) {me.onMouseUp(event);};
2777             links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
2778         }
2779 
2780         links.Timeline.preventDefault(event);
2781     }
2782 };
2783 
2784 
2785 /**
2786  * Perform moving operating.
2787  * This function activated from within the funcion links.Timeline.onMouseDown().
2788  * @param {Event}   event  Well, eehh, the event
2789  */
2790 links.Timeline.prototype.onMouseMove = function (event) {
2791     event = event || window.event;
2792 
2793     var params = this.eventParams,
2794         size = this.size,
2795         dom = this.dom,
2796         options = this.options;
2797 
2798     // calculate change in mouse position
2799     var mouseX = links.Timeline.getPageX(event);
2800     var mouseY = links.Timeline.getPageY(event);
2801 
2802     if (params.mouseX == undefined) {
2803         params.mouseX = mouseX;
2804     }
2805     if (params.mouseY == undefined) {
2806         params.mouseY = mouseY;
2807     }
2808 
2809     var diffX = mouseX - params.mouseX;
2810     var diffY = mouseY - params.mouseY;
2811 
2812     // if mouse movement is big enough, register it as a "moved" event
2813     if (Math.abs(diffX) >= 1) {
2814         params.moved = true;
2815     }
2816 
2817     if (params.customTime) {
2818         var x = this.timeToScreen(params.customTime);
2819         var xnew = x + diffX;
2820         this.customTime = this.screenToTime(xnew);
2821         this.repaintCustomTime();
2822 
2823         // fire a timechange event
2824         this.trigger('timechange');
2825     }
2826     else if (params.editItem) {
2827         var item = this.items[params.itemIndex],
2828             left,
2829             right;
2830 
2831         if (params.itemDragLeft && options.timeChangeable) {
2832             // move the start of the item
2833             left = params.itemLeft + diffX;
2834             right = params.itemRight;
2835 
2836             item.start = this.screenToTime(left);
2837             if (options.snapEvents) {
2838                 this.step.snap(item.start);
2839                 left = this.timeToScreen(item.start);
2840             }
2841 
2842             if (left > right) {
2843                 left = right;
2844                 item.start = this.screenToTime(left);
2845             }
2846           this.trigger('change');
2847         }
2848         else if (params.itemDragRight && options.timeChangeable) {
2849             // move the end of the item
2850             left = params.itemLeft;
2851             right = params.itemRight + diffX;
2852 
2853             item.end = this.screenToTime(right);
2854             if (options.snapEvents) {
2855                 this.step.snap(item.end);
2856                 right = this.timeToScreen(item.end);
2857             }
2858 
2859             if (right < left) {
2860                 right = left;
2861                 item.end = this.screenToTime(right);
2862             }
2863           this.trigger('change');
2864         }
2865         else if (options.timeChangeable) {
2866             // move the item
2867             left = params.itemLeft + diffX;
2868             item.start = this.screenToTime(left);
2869             if (options.snapEvents) {
2870                 this.step.snap(item.start);
2871                 left = this.timeToScreen(item.start);
2872             }
2873 
2874             if (item.end) {
2875                 right = left + (params.itemRight - params.itemLeft);
2876                 item.end = this.screenToTime(right);
2877             }
2878             this.trigger('change');
2879         }
2880 
2881         item.setPosition(left, right);
2882 
2883         var dragging = params.itemDragLeft || params.itemDragRight;
2884         if (this.groups.length && !dragging) {
2885             // move item from one group to another when needed
2886             var y = mouseY - params.frameTop;
2887             var group = this.getGroupFromHeight(y);
2888             if (options.groupsChangeable && item.group !== group) {
2889                 // move item to the other group
2890                 var index = this.items.indexOf(item);
2891                 this.changeItem(index, {'group': this.getGroupName(group)});
2892             }
2893             else {
2894                 this.repaintDeleteButton();
2895                 this.repaintDragAreas();
2896             }
2897         }
2898         else {
2899             // TODO: does not work well in FF, forces redraw with every mouse move it seems
2900             this.render(); // TODO: optimize, only redraw the items?
2901             // Note: when animate==true, no redraw is needed here, its done by stackItems animation
2902         }
2903     }
2904     else if (options.moveable) {
2905         var interval = (params.end.valueOf() - params.start.valueOf());
2906         var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval);
2907         var newStart = new Date(params.start.valueOf() + diffMillisecs);
2908         var newEnd = new Date(params.end.valueOf() + diffMillisecs);
2909         this.applyRange(newStart, newEnd);
2910         // if the applied range is moved due to a fixed min or max,
2911         // change the diffMillisecs accordingly
2912         var appliedDiff = (this.start.valueOf() - newStart.valueOf());
2913         if (appliedDiff) {
2914             diffMillisecs += appliedDiff;
2915         }
2916 
2917         this.recalcConversion();
2918 
2919         // move the items by changing the left position of their frame.
2920         // this is much faster than repositioning all elements individually via the
2921         // repaintFrame() function (which is done once at mouseup)
2922         // note that we round diffX to prevent wrong positioning on millisecond scale
2923         var previousLeft = params.previousLeft || 0;
2924         var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
2925         var previousOffset = params.previousOffset || 0;
2926         var frameOffset = previousOffset + (currentLeft - previousLeft);
2927         var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset;
2928 
2929         dom.items.frame.style.left = (frameLeft) + "px";
2930 
2931         // read the left again from DOM (IE8- rounds the value)
2932         params.previousOffset = frameOffset;
2933         params.previousLeft = parseFloat(dom.items.frame.style.left) || frameLeft;
2934 
2935         this.repaintCurrentTime();
2936         this.repaintCustomTime();
2937         this.repaintAxis();
2938 
2939         // fire a rangechange event
2940         this.trigger('rangechange');
2941     }
2942 
2943     links.Timeline.preventDefault(event);
2944 };
2945 
2946 
2947 /**
2948  * Stop moving operating.
2949  * This function activated from within the funcion links.Timeline.onMouseDown().
2950  * @param {event}  event   The event
2951  */
2952 links.Timeline.prototype.onMouseUp = function (event) {
2953     var params = this.eventParams,
2954         options = this.options;
2955 
2956     event = event || window.event;
2957 
2958     this.dom.frame.style.cursor = 'auto';
2959 
2960     // remove event listeners here, important for Safari
2961     if (params.onMouseMove) {
2962         links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
2963         delete params.onMouseMove;
2964     }
2965     if (params.onMouseUp) {
2966         links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
2967         delete params.onMouseUp;
2968     }
2969     //links.Timeline.preventDefault(event);
2970 
2971     if (params.customTime) {
2972         // fire a timechanged event
2973         this.trigger('timechanged');
2974     }
2975     else if (params.editItem) {
2976         var item = this.items[params.itemIndex];
2977 
2978         if (params.moved || params.addItem) {
2979             this.applyChange = true;
2980             this.applyAdd = true;
2981 
2982             this.updateData(params.itemIndex, {
2983                 'start': item.start,
2984                 'end': item.end
2985             });
2986 
2987             // fire an add or changed event.
2988             // Note that the change can be canceled from within an event listener if
2989             // this listener calls the method cancelChange().
2990             this.trigger(params.addItem ? 'add' : 'changed');
2991             
2992             //retrieve item data again to include changes made to it in the triggered event handlers
2993             item = this.items[params.itemIndex];
2994 
2995             if (params.addItem) {
2996                 if (this.applyAdd) {
2997                     this.updateData(params.itemIndex, {
2998                         'start': item.start,
2999                         'end': item.end,
3000                         'content': item.content,
3001                         'group': this.getGroupName(item.group)
3002                     });
3003                 }
3004                 else {
3005                     // undo an add
3006                     this.deleteItem(params.itemIndex);
3007                 }
3008             }
3009             else {
3010                 if (this.applyChange) {
3011                     this.updateData(params.itemIndex, {
3012                         'start': item.start,
3013                         'end': item.end
3014                     });
3015                 }
3016                 else {
3017                     // undo a change
3018                     delete this.applyChange;
3019                     delete this.applyAdd;
3020 
3021                     var item = this.items[params.itemIndex],
3022                         domItem = item.dom;
3023 
3024                     item.start = params.itemStart;
3025                     item.end = params.itemEnd;
3026                     item.group = params.itemGroup;
3027                     // TODO: original group should be restored too
3028                     item.setPosition(params.itemLeft, params.itemRight);
3029 
3030                     this.updateData(params.itemIndex, {
3031                         'start': params.itemStart,
3032                         'end': params.itemEnd
3033                     });
3034                 }
3035             }
3036 
3037             // prepare data for clustering, by filtering and sorting by type
3038             if (this.options.cluster) {
3039                 this.clusterGenerator.updateData();
3040             }
3041 
3042             this.render();
3043         }
3044     }
3045     else {
3046         if (!params.moved && !params.zoomed) {
3047             // mouse did not move -> user has selected an item
3048 
3049             if (params.target === this.dom.items.deleteButton) {
3050                 // delete item
3051                 if (this.selection && this.selection.index !== undefined) {
3052                     this.confirmDeleteItem(this.selection.index);
3053                 }
3054             }
3055             else if (options.selectable) {
3056                 // select/unselect item
3057                 if (params.itemIndex != undefined) {
3058                     if (!this.isSelected(params.itemIndex)) {
3059                         this.selectItem(params.itemIndex);
3060                         this.trigger('select');
3061                     }
3062                 }
3063                 else if(params.clusterIndex != undefined) {
3064                     this.selectCluster(params.clusterIndex);
3065                     this.trigger('select');
3066                 }
3067                 else {
3068                     if (options.unselectable) {
3069                         this.unselectItem();
3070                         this.trigger('select');
3071                     }
3072                 }
3073             }
3074         }
3075         else {
3076             // timeline is moved
3077             // TODO: optimize: no need to reflow and cluster again?
3078             this.render();
3079 
3080             if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
3081                 // fire a rangechanged event
3082                 this.trigger('rangechanged');
3083             }
3084         }
3085     }
3086 };
3087 
3088 /**
3089  * Double click event occurred for an item
3090  * @param {Event}  event
3091  */
3092 links.Timeline.prototype.onDblClick = function (event) {
3093     var params = this.eventParams,
3094         options = this.options,
3095         dom = this.dom,
3096         size = this.size;
3097     event = event || window.event;
3098 
3099     if (params.itemIndex != undefined) {
3100         var item = this.items[params.itemIndex];
3101         if (item && this.isEditable(item)) {
3102             // fire the edit event
3103             this.trigger('edit');
3104         }
3105     }
3106     else {
3107         if (options.editable) {
3108             // create a new item
3109 
3110             // get mouse position
3111             params.mouseX = links.Timeline.getPageX(event);
3112             params.mouseY = links.Timeline.getPageY(event);
3113             var x = params.mouseX - links.Timeline.getAbsoluteLeft(dom.content);
3114             var y = params.mouseY - links.Timeline.getAbsoluteTop(dom.content);
3115 
3116             // create a new event at the current mouse position
3117             var xstart = this.screenToTime(x);
3118             if (options.snapEvents) {
3119                 this.step.snap(xstart);
3120             }
3121 
3122             var content = options.NEW;
3123             var group = this.getGroupFromHeight(y);   // (group may be undefined)
3124             var preventRender = true;
3125             this.addItem({
3126                 'start': xstart,
3127                 'content': content,
3128                 'group': this.getGroupName(group)
3129             }, preventRender);
3130             params.itemIndex = (this.items.length - 1);
3131             this.selectItem(params.itemIndex);
3132 
3133             this.applyAdd = true;
3134 
3135             // fire an add event.
3136             // Note that the change can be canceled from within an event listener if
3137             // this listener calls the method cancelAdd().
3138             this.trigger('add');
3139 
3140             if (this.applyAdd) {
3141                 // render and select the item
3142                 this.render({animate: false});
3143                 this.selectItem(params.itemIndex);
3144             }
3145             else {
3146                 // undo an add
3147                 this.deleteItem(params.itemIndex);
3148             }
3149         }
3150     }
3151 
3152     links.Timeline.preventDefault(event);
3153 };
3154 
3155 
3156 /**
3157  * Event handler for mouse wheel event, used to zoom the timeline
3158  * Code from http://adomas.org/javascript-mouse-wheel/
3159  * @param {Event}  event   The event
3160  */
3161 links.Timeline.prototype.onMouseWheel = function(event) {
3162     if (!this.options.zoomable)
3163         return;
3164 
3165     if (!event) { /* For IE. */
3166         event = window.event;
3167     }
3168 
3169     // retrieve delta
3170     var delta = 0;
3171     if (event.wheelDelta) { /* IE/Opera. */
3172         delta = event.wheelDelta/120;
3173     } else if (event.detail) { /* Mozilla case. */
3174         // In Mozilla, sign of delta is different than in IE.
3175         // Also, delta is multiple of 3.
3176         delta = -event.detail/3;
3177     }
3178 
3179     // If delta is nonzero, handle it.
3180     // Basically, delta is now positive if wheel was scrolled up,
3181     // and negative, if wheel was scrolled down.
3182     if (delta) {
3183         // TODO: on FireFox, the window is not redrawn within repeated scroll-events
3184         // -> use a delayed redraw? Make a zoom queue?
3185 
3186         var timeline = this;
3187         var zoom = function () {
3188             // perform the zoom action. Delta is normally 1 or -1
3189             var zoomFactor = delta / 5.0;
3190             var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
3191             var mouseX = links.Timeline.getPageX(event);
3192             var zoomAroundDate =
3193                 (mouseX != undefined && frameLeft != undefined) ?
3194                     timeline.screenToTime(mouseX - frameLeft) :
3195                     undefined;
3196 
3197             timeline.zoom(zoomFactor, zoomAroundDate);
3198 
3199             // fire a rangechange and a rangechanged event
3200             timeline.trigger("rangechange");
3201             timeline.trigger("rangechanged");
3202         };
3203 
3204         var scroll = function () {
3205             // Scroll the timeline
3206             timeline.move(delta * -0.2);
3207             timeline.trigger("rangechange");
3208             timeline.trigger("rangechanged");
3209         };
3210 
3211         if (event.shiftKey) {
3212             scroll();
3213         }
3214         else {
3215             zoom();
3216         }
3217     }
3218 
3219     // Prevent default actions caused by mouse wheel.
3220     // That might be ugly, but we handle scrolls somehow
3221     // anyway, so don't bother here...
3222     links.Timeline.preventDefault(event);
3223 };
3224 
3225 
3226 /**
3227  * Zoom the timeline the given zoomfactor in or out. Start and end date will
3228  * be adjusted, and the timeline will be redrawn. You can optionally give a
3229  * date around which to zoom.
3230  * For example, try zoomfactor = 0.1 or -0.1
3231  * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
3232  *                                 negative value will zoom out
3233  * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
3234  */
3235 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
3236     // if zoomAroundDate is not provided, take it half between start Date and end Date
3237     if (zoomAroundDate == undefined) {
3238         zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
3239     }
3240 
3241     // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
3242     // result in a start>=end )
3243     if (zoomFactor >= 1) {
3244         zoomFactor = 0.9;
3245     }
3246     if (zoomFactor <= -1) {
3247         zoomFactor = -0.9;
3248     }
3249 
3250     // adjust a negative factor such that zooming in with 0.1 equals zooming
3251     // out with a factor -0.1
3252     if (zoomFactor < 0) {
3253         zoomFactor = zoomFactor / (1 + zoomFactor);
3254     }
3255 
3256     // zoom start Date and end Date relative to the zoomAroundDate
3257     var startDiff = (this.start.valueOf() - zoomAroundDate);
3258     var endDiff = (this.end.valueOf() - zoomAroundDate);
3259 
3260     // calculate new dates
3261     var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
3262     var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
3263 
3264     // only zoom in when interval is larger than minimum interval (to prevent
3265     // sliding to left/right when having reached the minimum zoom level)
3266     var interval = (newEnd.valueOf() - newStart.valueOf());
3267     var zoomMin = Number(this.options.zoomMin) || 10;
3268     if (zoomMin < 10) {
3269         zoomMin = 10;
3270     }
3271     if (interval >= zoomMin) {
3272         this.applyRange(newStart, newEnd, zoomAroundDate);
3273         this.render({
3274             animate: this.options.animate && this.options.animateZoom
3275         });
3276     }
3277 };
3278 
3279 /**
3280  * Move the timeline the given movefactor to the left or right. Start and end
3281  * date will be adjusted, and the timeline will be redrawn.
3282  * For example, try moveFactor = 0.1 or -0.1
3283  * @param {Number}  moveFactor      Moving amount. Positive value will move right,
3284  *                                 negative value will move left
3285  */
3286 links.Timeline.prototype.move = function(moveFactor) {
3287     // zoom start Date and end Date relative to the zoomAroundDate
3288     var diff = (this.end.valueOf() - this.start.valueOf());
3289 
3290     // apply new dates
3291     var newStart = new Date(this.start.valueOf() + diff * moveFactor);
3292     var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
3293     this.applyRange(newStart, newEnd);
3294 
3295     this.render(); // TODO: optimize, no need to reflow, only to recalc conversion and repaint
3296 };
3297 
3298 /**
3299  * Apply a visible range. The range is limited to feasible maximum and minimum
3300  * range.
3301  * @param {Date} start
3302  * @param {Date} end
3303  * @param {Date}   zoomAroundDate  Optional. Date around which will be zoomed.
3304  */
3305 links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) {
3306     // calculate new start and end value
3307     var startValue = start.valueOf(); // number
3308     var endValue = end.valueOf();     // number
3309     var interval = (endValue - startValue);
3310 
3311     // determine maximum and minimum interval
3312     var options = this.options;
3313     var year = 1000 * 60 * 60 * 24 * 365;
3314     var zoomMin = Number(options.zoomMin) || 10;
3315     if (zoomMin < 10) {
3316         zoomMin = 10;
3317     }
3318     var zoomMax = Number(options.zoomMax) || 10000 * year;
3319     if (zoomMax > 10000 * year) {
3320         zoomMax = 10000 * year;
3321     }
3322     if (zoomMax < zoomMin) {
3323         zoomMax = zoomMin;
3324     }
3325 
3326     // determine min and max date value
3327     var min = options.min ? options.min.valueOf() : undefined; // number
3328     var max = options.max ? options.max.valueOf() : undefined; // number
3329     if (min != undefined && max != undefined) {
3330         if (min >= max) {
3331             // empty range
3332             var day = 1000 * 60 * 60 * 24;
3333             max = min + day;
3334         }
3335         if (zoomMax > (max - min)) {
3336             zoomMax = (max - min);
3337         }
3338         if (zoomMin > (max - min)) {
3339             zoomMin = (max - min);
3340         }
3341     }
3342 
3343     // prevent empty interval
3344     if (startValue >= endValue) {
3345         endValue += 1000 * 60 * 60 * 24;
3346     }
3347 
3348     // prevent too small scale
3349     // TODO: IE has problems with milliseconds
3350     if (interval < zoomMin) {
3351         var diff = (zoomMin - interval);
3352         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3353         startValue -= Math.round(diff * f);
3354         endValue   += Math.round(diff * (1 - f));
3355     }
3356 
3357     // prevent too large scale
3358     if (interval > zoomMax) {
3359         var diff = (interval - zoomMax);
3360         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
3361         startValue += Math.round(diff * f);
3362         endValue   -= Math.round(diff * (1 - f));
3363     }
3364 
3365     // prevent to small start date
3366     if (min != undefined) {
3367         var diff = (startValue - min);
3368         if (diff < 0) {
3369             startValue -= diff;
3370             endValue -= diff;
3371         }
3372     }
3373 
3374     // prevent to large end date
3375     if (max != undefined) {
3376         var diff = (max - endValue);
3377         if (diff < 0) {
3378             startValue += diff;
3379             endValue += diff;
3380         }
3381     }
3382 
3383     // apply new dates
3384     this.start = new Date(startValue);
3385     this.end = new Date(endValue);
3386 };
3387 
3388 /**
3389  * Delete an item after a confirmation.
3390  * The deletion can be cancelled by executing .cancelDelete() during the
3391  * triggered event 'delete'.
3392  * @param {int} index   Index of the item to be deleted
3393  */
3394 links.Timeline.prototype.confirmDeleteItem = function(index) {
3395     this.applyDelete = true;
3396 
3397     // select the event to be deleted
3398     if (!this.isSelected(index)) {
3399         this.selectItem(index);
3400     }
3401 
3402     // fire a delete event trigger.
3403     // Note that the delete event can be canceled from within an event listener if
3404     // this listener calls the method cancelChange().
3405     this.trigger('delete');
3406 
3407     if (this.applyDelete) {
3408         this.deleteItem(index);
3409     }
3410 
3411     delete this.applyDelete;
3412 };
3413 
3414 /**
3415  * Delete an item
3416  * @param {int} index   Index of the item to be deleted
3417  * @param {boolean} [preventRender=false]   Do not re-render timeline if true
3418  *                                          (optimization for multiple delete)
3419  */
3420 links.Timeline.prototype.deleteItem = function(index, preventRender) {
3421     if (index >= this.items.length) {
3422         throw "Cannot delete row, index out of range";
3423     }
3424 
3425     if (this.selection && this.selection.index !== undefined) {
3426         // adjust the selection
3427         if (this.selection.index == index) {
3428             // item to be deleted is selected
3429             this.unselectItem();
3430         }
3431         else if (this.selection.index > index) {
3432             // update selection index
3433             this.selection.index--;
3434         }
3435     }
3436 
3437     // actually delete the item and remove it from the DOM
3438     var item = this.items.splice(index, 1)[0];
3439     this.renderQueue.hide.push(item);
3440 
3441     // delete the row in the original data table
3442     if (this.data) {
3443         if (google && google.visualization &&
3444             this.data instanceof google.visualization.DataTable) {
3445             this.data.removeRow(index);
3446         }
3447         else if (links.Timeline.isArray(this.data)) {
3448             this.data.splice(index, 1);
3449         }
3450         else {
3451             throw "Cannot delete row from data, unknown data type";
3452         }
3453     }
3454 
3455     // prepare data for clustering, by filtering and sorting by type
3456     if (this.options.cluster) {
3457         this.clusterGenerator.updateData();
3458     }
3459 
3460     if (!preventRender) {
3461         this.render();
3462     }
3463 };
3464 
3465 
3466 /**
3467  * Delete all items
3468  */
3469 links.Timeline.prototype.deleteAllItems = function() {
3470     this.unselectItem();
3471 
3472     // delete the loaded items
3473     this.clearItems();
3474 
3475     // delete the groups
3476     this.deleteGroups();
3477 
3478     // empty original data table
3479     if (this.data) {
3480         if (google && google.visualization &&
3481             this.data instanceof google.visualization.DataTable) {
3482             this.data.removeRows(0, this.data.getNumberOfRows());
3483         }
3484         else if (links.Timeline.isArray(this.data)) {
3485             this.data.splice(0, this.data.length);
3486         }
3487         else {
3488             throw "Cannot delete row from data, unknown data type";
3489         }
3490     }
3491 
3492     // prepare data for clustering, by filtering and sorting by type
3493     if (this.options.cluster) {
3494         this.clusterGenerator.updateData();
3495     }
3496 
3497     this.render();
3498 };
3499 
3500 
3501 /**
3502  * Find the group from a given height in the timeline
3503  * @param {Number} height   Height in the timeline
3504  * @return {Object | undefined} group   The group object, or undefined if out
3505  *                                      of range
3506  */
3507 links.Timeline.prototype.getGroupFromHeight = function(height) {
3508     var i,
3509         group,
3510         groups = this.groups;
3511 
3512     if (groups.length) {
3513         if (this.options.axisOnTop) {
3514             for (i = groups.length - 1; i >= 0; i--) {
3515                 group = groups[i];
3516                 if (height > group.top) {
3517                     return group;
3518                 }
3519             }
3520         }
3521         else {
3522             for (i = 0; i < groups.length; i++) {
3523                 group = groups[i];
3524                 if (height > group.top) {
3525                     return group;
3526                 }
3527             }
3528         }
3529 
3530         return group; // return the last group
3531     }
3532 
3533     return undefined;
3534 };
3535 
3536 /**
3537  * @constructor links.Timeline.Item
3538  * @param {Object} data       Object containing parameters start, end
3539  *                            content, group, type, editable.
3540  * @param {Object} [options]  Options to set initial property values
3541  *                                {Number} top
3542  *                                {Number} left
3543  *                                {Number} width
3544  *                                {Number} height
3545  */
3546 links.Timeline.Item = function (data, options) {
3547     if (data) {
3548         /* TODO: use parseJSONDate as soon as it is tested and working (in two directions)
3549          this.start = links.Timeline.parseJSONDate(data.start);
3550          this.end = links.Timeline.parseJSONDate(data.end);
3551          */
3552         this.start = data.start;
3553         this.end = data.end;
3554         this.content = data.content;
3555         this.className = data.className;
3556         this.editable = data.editable;
3557         this.group = data.group;
3558         this.type = data.type;
3559     }
3560     this.top = 0;
3561     this.left = 0;
3562     this.width = 0;
3563     this.height = 0;
3564     this.lineWidth = 0;
3565     this.dotWidth = 0;
3566     this.dotHeight = 0;
3567 
3568     this.rendered = false; // true when the item is draw in the Timeline DOM
3569 
3570     if (options) {
3571         // override the default properties
3572         for (var option in options) {
3573             if (options.hasOwnProperty(option)) {
3574                 this[option] = options[option];
3575             }
3576         }
3577     }
3578 
3579 };
3580 
3581 
3582 
3583 /**
3584  * Reflow the Item: retrieve its actual size from the DOM
3585  * @return {boolean} resized    returns true if the axis is resized
3586  */
3587 links.Timeline.Item.prototype.reflow = function () {
3588     // Should be implemented by sub-prototype
3589     return false;
3590 };
3591 
3592 /**
3593  * Append all image urls present in the items DOM to the provided array
3594  * @param {String[]} imageUrls
3595  */
3596 links.Timeline.Item.prototype.getImageUrls = function (imageUrls) {
3597     if (this.dom) {
3598         links.imageloader.filterImageUrls(this.dom, imageUrls);
3599     }
3600 };
3601 
3602 /**
3603  * Select the item
3604  */
3605 links.Timeline.Item.prototype.select = function () {
3606     // Should be implemented by sub-prototype
3607 };
3608 
3609 /**
3610  * Unselect the item
3611  */
3612 links.Timeline.Item.prototype.unselect = function () {
3613     // Should be implemented by sub-prototype
3614 };
3615 
3616 /**
3617  * Creates the DOM for the item, depending on its type
3618  * @return {Element | undefined}
3619  */
3620 links.Timeline.Item.prototype.createDOM = function () {
3621     // Should be implemented by sub-prototype
3622 };
3623 
3624 /**
3625  * Append the items DOM to the given HTML container. If items DOM does not yet
3626  * exist, it will be created first.
3627  * @param {Element} container
3628  */
3629 links.Timeline.Item.prototype.showDOM = function (container) {
3630     // Should be implemented by sub-prototype
3631 };
3632 
3633 /**
3634  * Remove the items DOM from the current HTML container
3635  * @param {Element} container
3636  */
3637 links.Timeline.Item.prototype.hideDOM = function (container) {
3638     // Should be implemented by sub-prototype
3639 };
3640 
3641 /**
3642  * Update the DOM of the item. This will update the content and the classes
3643  * of the item
3644  */
3645 links.Timeline.Item.prototype.updateDOM = function () {
3646     // Should be implemented by sub-prototype
3647 };
3648 
3649 /**
3650  * Reposition the item, recalculate its left, top, and width, using the current
3651  * range of the timeline and the timeline options.
3652  * @param {links.Timeline} timeline
3653  */
3654 links.Timeline.Item.prototype.updatePosition = function (timeline) {
3655     // Should be implemented by sub-prototype
3656 };
3657 
3658 /**
3659  * Check if the item is drawn in the timeline (i.e. the DOM of the item is
3660  * attached to the frame. You may also just request the parameter item.rendered
3661  * @return {boolean} rendered
3662  */
3663 links.Timeline.Item.prototype.isRendered = function () {
3664     return this.rendered;
3665 };
3666 
3667 /**
3668  * Check if the item is located in the visible area of the timeline, and
3669  * not part of a cluster
3670  * @param {Date} start
3671  * @param {Date} end
3672  * @return {boolean} visible
3673  */
3674 links.Timeline.Item.prototype.isVisible = function (start, end) {
3675     // Should be implemented by sub-prototype
3676     return false;
3677 };
3678 
3679 /**
3680  * Reposition the item
3681  * @param {Number} left
3682  * @param {Number} right
3683  */
3684 links.Timeline.Item.prototype.setPosition = function (left, right) {
3685     // Should be implemented by sub-prototype
3686 };
3687 
3688 /**
3689  * Calculate the left position of the item
3690  * @param {links.Timeline} timeline
3691  * @return {Number} left
3692  */
3693 links.Timeline.Item.prototype.getLeft = function (timeline) {
3694     // Should be implemented by sub-prototype
3695     return 0;
3696 };
3697 
3698 /**
3699  * Calculate the right position of the item
3700  * @param {links.Timeline} timeline
3701  * @return {Number} right
3702  */
3703 links.Timeline.Item.prototype.getRight = function (timeline) {
3704     // Should be implemented by sub-prototype
3705     return 0;
3706 };
3707 
3708 /**
3709  * Calculate the width of the item
3710  * @param {links.Timeline} timeline
3711  * @return {Number} width
3712  */
3713 links.Timeline.Item.prototype.getWidth = function (timeline) {
3714     // Should be implemented by sub-prototype
3715     return this.width || 0; // last rendered width
3716 };
3717 
3718 
3719 /**
3720  * @constructor links.Timeline.ItemBox
3721  * @extends links.Timeline.Item
3722  * @param {Object} data       Object containing parameters start, end
3723  *                            content, group, type, className, editable.
3724  * @param {Object} [options]  Options to set initial property values
3725  *                                {Number} top
3726  *                                {Number} left
3727  *                                {Number} width
3728  *                                {Number} height
3729  */
3730 links.Timeline.ItemBox = function (data, options) {
3731     links.Timeline.Item.call(this, data, options);
3732 };
3733 
3734 links.Timeline.ItemBox.prototype = new links.Timeline.Item();
3735 
3736 /**
3737  * Reflow the Item: retrieve its actual size from the DOM
3738  * @return {boolean} resized    returns true if the axis is resized
3739  * @override
3740  */
3741 links.Timeline.ItemBox.prototype.reflow = function () {
3742     var dom = this.dom,
3743         dotHeight = dom.dot.offsetHeight,
3744         dotWidth = dom.dot.offsetWidth,
3745         lineWidth = dom.line.offsetWidth,
3746         resized = (
3747             (this.dotHeight != dotHeight) ||
3748                 (this.dotWidth != dotWidth) ||
3749                 (this.lineWidth != lineWidth)
3750             );
3751 
3752     this.dotHeight = dotHeight;
3753     this.dotWidth = dotWidth;
3754     this.lineWidth = lineWidth;
3755 
3756     return resized;
3757 };
3758 
3759 /**
3760  * Select the item
3761  * @override
3762  */
3763 links.Timeline.ItemBox.prototype.select = function () {
3764     var dom = this.dom;
3765     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
3766     links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active');
3767     links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active');
3768 };
3769 
3770 /**
3771  * Unselect the item
3772  * @override
3773  */
3774 links.Timeline.ItemBox.prototype.unselect = function () {
3775     var dom = this.dom;
3776     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
3777     links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active');
3778     links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active');
3779 };
3780 
3781 /**
3782  * Creates the DOM for the item, depending on its type
3783  * @return {Element | undefined}
3784  * @override
3785  */
3786 links.Timeline.ItemBox.prototype.createDOM = function () {
3787     // background box
3788     var divBox = document.createElement("DIV");
3789     divBox.style.position = "absolute";
3790     divBox.style.left = this.left + "px";
3791     divBox.style.top = this.top + "px";
3792 
3793     // contents box (inside the background box). used for making margins
3794     var divContent = document.createElement("DIV");
3795     divContent.className = "timeline-event-content";
3796     divContent.innerHTML = this.content;
3797     divBox.appendChild(divContent);
3798 
3799     // line to axis
3800     var divLine = document.createElement("DIV");
3801     divLine.style.position = "absolute";
3802     divLine.style.width = "0px";
3803     // important: the vertical line is added at the front of the list of elements,
3804     // so it will be drawn behind all boxes and ranges
3805     divBox.line = divLine;
3806 
3807     // dot on axis
3808     var divDot = document.createElement("DIV");
3809     divDot.style.position = "absolute";
3810     divDot.style.width  = "0px";
3811     divDot.style.height = "0px";
3812     divBox.dot = divDot;
3813 
3814     this.dom = divBox;
3815     this.updateDOM();
3816 
3817     return divBox;
3818 };
3819 
3820 /**
3821  * Append the items DOM to the given HTML container. If items DOM does not yet
3822  * exist, it will be created first.
3823  * @param {Element} container
3824  * @override
3825  */
3826 links.Timeline.ItemBox.prototype.showDOM = function (container) {
3827     var dom = this.dom;
3828     if (!dom) {
3829         dom = this.createDOM();
3830     }
3831 
3832     if (dom.parentNode != container) {
3833         if (dom.parentNode) {
3834             // container is changed. remove from old container
3835             this.hideDOM();
3836         }
3837 
3838         // append to this container
3839         container.appendChild(dom);
3840         container.insertBefore(dom.line, container.firstChild);
3841         // Note: line must be added in front of the this,
3842         //       such that it stays below all this
3843         container.appendChild(dom.dot);
3844         this.rendered = true;
3845     }
3846 };
3847 
3848 /**
3849  * Remove the items DOM from the current HTML container, but keep the DOM in
3850  * memory
3851  * @override
3852  */
3853 links.Timeline.ItemBox.prototype.hideDOM = function () {
3854     var dom = this.dom;
3855     if (dom) {
3856         if (dom.parentNode) {
3857             dom.parentNode.removeChild(dom);
3858         }
3859         if (dom.line && dom.line.parentNode) {
3860             dom.line.parentNode.removeChild(dom.line);
3861         }
3862         if (dom.dot && dom.dot.parentNode) {
3863             dom.dot.parentNode.removeChild(dom.dot);
3864         }
3865         this.rendered = false;
3866     }
3867 };
3868 
3869 /**
3870  * Update the DOM of the item. This will update the content and the classes
3871  * of the item
3872  * @override
3873  */
3874 links.Timeline.ItemBox.prototype.updateDOM = function () {
3875     var divBox = this.dom;
3876     if (divBox) {
3877         var divLine = divBox.line;
3878         var divDot = divBox.dot;
3879 
3880         // update contents
3881         divBox.firstChild.innerHTML = this.content;
3882 
3883         // update class
3884         divBox.className = "timeline-event timeline-event-box ui-widget ui-state-default";
3885         divLine.className = "timeline-event timeline-event-line ui-widget ui-state-default";
3886         divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
3887 
3888         if (this.isCluster) {
3889             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
3890             links.Timeline.addClassName(divLine, 'timeline-event-cluster ui-widget-header');
3891             links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
3892         }
3893 
3894         // add item specific class name when provided
3895         if (this.className) {
3896             links.Timeline.addClassName(divBox, this.className);
3897             links.Timeline.addClassName(divLine, this.className);
3898             links.Timeline.addClassName(divDot, this.className);
3899         }
3900 
3901         // TODO: apply selected className?
3902     }
3903 };
3904 
3905 /**
3906  * Reposition the item, recalculate its left, top, and width, using the current
3907  * range of the timeline and the timeline options.
3908  * @param {links.Timeline} timeline
3909  * @override
3910  */
3911 links.Timeline.ItemBox.prototype.updatePosition = function (timeline) {
3912     var dom = this.dom;
3913     if (dom) {
3914         var left = timeline.timeToScreen(this.start),
3915             axisOnTop = timeline.options.axisOnTop,
3916             axisTop = timeline.size.axis.top,
3917             axisHeight = timeline.size.axis.height,
3918             boxAlign = (timeline.options.box && timeline.options.box.align) ?
3919                 timeline.options.box.align : undefined;
3920 
3921         dom.style.top = this.top + "px";
3922         if (boxAlign == 'right') {
3923             dom.style.left = (left - this.width) + "px";
3924         }
3925         else if (boxAlign == 'left') {
3926             dom.style.left = (left) + "px";
3927         }
3928         else { // default or 'center'
3929             dom.style.left = (left - this.width/2) + "px";
3930         }
3931 
3932         var line = dom.line;
3933         var dot = dom.dot;
3934         line.style.left = (left - this.lineWidth/2) + "px";
3935         dot.style.left = (left - this.dotWidth/2) + "px";
3936         if (axisOnTop) {
3937             line.style.top = axisHeight + "px";
3938             line.style.height = Math.max(this.top - axisHeight, 0) + "px";
3939             dot.style.top = (axisHeight - this.dotHeight/2) + "px";
3940         }
3941         else {
3942             line.style.top = (this.top + this.height) + "px";
3943             line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px";
3944             dot.style.top = (axisTop - this.dotHeight/2) + "px";
3945         }
3946     }
3947 };
3948 
3949 /**
3950  * Check if the item is visible in the timeline, and not part of a cluster
3951  * @param {Date} start
3952  * @param {Date} end
3953  * @return {Boolean} visible
3954  * @override
3955  */
3956 links.Timeline.ItemBox.prototype.isVisible = function (start, end) {
3957     if (this.cluster) {
3958         return false;
3959     }
3960 
3961     return (this.start > start) && (this.start < end);
3962 };
3963 
3964 /**
3965  * Reposition the item
3966  * @param {Number} left
3967  * @param {Number} right
3968  * @override
3969  */
3970 links.Timeline.ItemBox.prototype.setPosition = function (left, right) {
3971     var dom = this.dom;
3972 
3973     dom.style.left = (left - this.width / 2) + "px";
3974     dom.line.style.left = (left - this.lineWidth / 2) + "px";
3975     dom.dot.style.left = (left - this.dotWidth / 2) + "px";
3976 
3977     if (this.group) {
3978         this.top = this.group.top;
3979         dom.style.top = this.top + 'px';
3980     }
3981 };
3982 
3983 /**
3984  * Calculate the left position of the item
3985  * @param {links.Timeline} timeline
3986  * @return {Number} left
3987  * @override
3988  */
3989 links.Timeline.ItemBox.prototype.getLeft = function (timeline) {
3990     var boxAlign = (timeline.options.box && timeline.options.box.align) ?
3991         timeline.options.box.align : undefined;
3992 
3993     var left = timeline.timeToScreen(this.start);
3994     if (boxAlign == 'right') {
3995         left = left - width;
3996     }
3997     else { // default or 'center'
3998         left = (left - this.width / 2);
3999     }
4000 
4001     return left;
4002 };
4003 
4004 /**
4005  * Calculate the right position of the item
4006  * @param {links.Timeline} timeline
4007  * @return {Number} right
4008  * @override
4009  */
4010 links.Timeline.ItemBox.prototype.getRight = function (timeline) {
4011     var boxAlign = (timeline.options.box && timeline.options.box.align) ?
4012         timeline.options.box.align : undefined;
4013 
4014     var left = timeline.timeToScreen(this.start);
4015     var right;
4016     if (boxAlign == 'right') {
4017         right = left;
4018     }
4019     else if (boxAlign == 'left') {
4020         right = (left + this.width);
4021     }
4022     else { // default or 'center'
4023         right = (left + this.width / 2);
4024     }
4025 
4026     return right;
4027 };
4028 
4029 /**
4030  * @constructor links.Timeline.ItemRange
4031  * @extends links.Timeline.Item
4032  * @param {Object} data       Object containing parameters start, end
4033  *                            content, group, type, className, editable.
4034  * @param {Object} [options]  Options to set initial property values
4035  *                                {Number} top
4036  *                                {Number} left
4037  *                                {Number} width
4038  *                                {Number} height
4039  */
4040 links.Timeline.ItemRange = function (data, options) {
4041     links.Timeline.Item.call(this, data, options);
4042 };
4043 
4044 links.Timeline.ItemRange.prototype = new links.Timeline.Item();
4045 
4046 /**
4047  * Select the item
4048  * @override
4049  */
4050 links.Timeline.ItemRange.prototype.select = function () {
4051     var dom = this.dom;
4052     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4053 };
4054 
4055 /**
4056  * Unselect the item
4057  * @override
4058  */
4059 links.Timeline.ItemRange.prototype.unselect = function () {
4060     var dom = this.dom;
4061     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4062 };
4063 
4064 /**
4065  * Creates the DOM for the item, depending on its type
4066  * @return {Element | undefined}
4067  * @override
4068  */
4069 links.Timeline.ItemRange.prototype.createDOM = function () {
4070     // background box
4071     var divBox = document.createElement("DIV");
4072     divBox.style.position = "absolute";
4073 
4074     // contents box
4075     var divContent = document.createElement("DIV");
4076     divContent.className = "timeline-event-content";
4077     divBox.appendChild(divContent);
4078 
4079     this.dom = divBox;
4080     this.updateDOM();
4081 
4082     return divBox;
4083 };
4084 
4085 /**
4086  * Append the items DOM to the given HTML container. If items DOM does not yet
4087  * exist, it will be created first.
4088  * @param {Element} container
4089  * @override
4090  */
4091 links.Timeline.ItemRange.prototype.showDOM = function (container) {
4092     var dom = this.dom;
4093     if (!dom) {
4094         dom = this.createDOM();
4095     }
4096 
4097     if (dom.parentNode != container) {
4098         if (dom.parentNode) {
4099             // container changed. remove the item from the old container
4100             this.hideDOM();
4101         }
4102 
4103         // append to the new container
4104         container.appendChild(dom);
4105         this.rendered = true;
4106     }
4107 };
4108 
4109 /**
4110  * Remove the items DOM from the current HTML container
4111  * The DOM will be kept in memory
4112  * @override
4113  */
4114 links.Timeline.ItemRange.prototype.hideDOM = function () {
4115     var dom = this.dom;
4116     if (dom) {
4117         if (dom.parentNode) {
4118             dom.parentNode.removeChild(dom);
4119         }
4120         this.rendered = false;
4121     }
4122 };
4123 
4124 /**
4125  * Update the DOM of the item. This will update the content and the classes
4126  * of the item
4127  * @override
4128  */
4129 links.Timeline.ItemRange.prototype.updateDOM = function () {
4130     var divBox = this.dom;
4131     if (divBox) {
4132         // update contents
4133         divBox.firstChild.innerHTML = this.content;
4134 
4135         // update class
4136         divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
4137 
4138         if (this.isCluster) {
4139             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4140         }
4141 
4142         // add item specific class name when provided
4143         if (this.className) {
4144             links.Timeline.addClassName(divBox, this.className);
4145         }
4146 
4147         // TODO: apply selected className?
4148     }
4149 };
4150 
4151 /**
4152  * Reposition the item, recalculate its left, top, and width, using the current
4153  * range of the timeline and the timeline options. *
4154  * @param {links.Timeline} timeline
4155  * @override
4156  */
4157 links.Timeline.ItemRange.prototype.updatePosition = function (timeline) {
4158     var dom = this.dom;
4159     if (dom) {
4160         var contentWidth = timeline.size.contentWidth,
4161             left = timeline.timeToScreen(this.start),
4162             right = timeline.timeToScreen(this.end);
4163 
4164         // limit the width of the this, as browsers cannot draw very wide divs
4165         if (left < -contentWidth) {
4166             left = -contentWidth;
4167         }
4168         if (right > 2 * contentWidth) {
4169             right = 2 * contentWidth;
4170         }
4171 
4172         dom.style.top = this.top + "px";
4173         dom.style.left = left + "px";
4174         //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
4175         dom.style.width = Math.max(right - left, 1) + "px";
4176     }
4177 };
4178 
4179 /**
4180  * Check if the item is visible in the timeline, and not part of a cluster
4181  * @param {Number} start
4182  * @param {Number} end
4183  * @return {boolean} visible
4184  * @override
4185  */
4186 links.Timeline.ItemRange.prototype.isVisible = function (start, end) {
4187     if (this.cluster) {
4188         return false;
4189     }
4190 
4191     return (this.end > start)
4192         && (this.start < end);
4193 };
4194 
4195 /**
4196  * Reposition the item
4197  * @param {Number} left
4198  * @param {Number} right
4199  * @override
4200  */
4201 links.Timeline.ItemRange.prototype.setPosition = function (left, right) {
4202     var dom = this.dom;
4203 
4204     dom.style.left = left + 'px';
4205     dom.style.width = (right - left) + 'px';
4206 
4207     if (this.group) {
4208         this.top = this.group.top;
4209         dom.style.top = this.top + 'px';
4210     }
4211 };
4212 
4213 /**
4214  * Calculate the left position of the item
4215  * @param {links.Timeline} timeline
4216  * @return {Number} left
4217  * @override
4218  */
4219 links.Timeline.ItemRange.prototype.getLeft = function (timeline) {
4220     return timeline.timeToScreen(this.start);
4221 };
4222 
4223 /**
4224  * Calculate the right position of the item
4225  * @param {links.Timeline} timeline
4226  * @return {Number} right
4227  * @override
4228  */
4229 links.Timeline.ItemRange.prototype.getRight = function (timeline) {
4230     return timeline.timeToScreen(this.end);
4231 };
4232 
4233 /**
4234  * Calculate the width of the item
4235  * @param {links.Timeline} timeline
4236  * @return {Number} width
4237  * @override
4238  */
4239 links.Timeline.ItemRange.prototype.getWidth = function (timeline) {
4240     return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start);
4241 };
4242 
4243 /**
4244  * @constructor links.Timeline.ItemFloatingRange
4245  * @extends links.Timeline.Item
4246  * @param {Object} data       Object containing parameters start, end
4247  *                            content, group, type, className, editable.
4248  * @param {Object} [options]  Options to set initial property values
4249  *                                {Number} top
4250  *                                {Number} left
4251  *                                {Number} width
4252  *                                {Number} height
4253  */
4254 links.Timeline.ItemFloatingRange = function (data, options) {
4255     links.Timeline.Item.call(this, data, options);
4256 };
4257 
4258 links.Timeline.ItemFloatingRange.prototype = new links.Timeline.Item();
4259 
4260 /**
4261  * Select the item
4262  * @override
4263  */
4264 links.Timeline.ItemFloatingRange.prototype.select = function () {
4265     var dom = this.dom;
4266     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4267 };
4268 
4269 /**
4270  * Unselect the item
4271  * @override
4272  */
4273 links.Timeline.ItemFloatingRange.prototype.unselect = function () {
4274     var dom = this.dom;
4275     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4276 };
4277 
4278 /**
4279  * Creates the DOM for the item, depending on its type
4280  * @return {Element | undefined}
4281  * @override
4282  */
4283 links.Timeline.ItemFloatingRange.prototype.createDOM = function () {
4284     // background box
4285     var divBox = document.createElement("DIV");
4286     divBox.style.position = "absolute";
4287 
4288     // contents box
4289     var divContent = document.createElement("DIV");
4290     divContent.className = "timeline-event-content";
4291     divBox.appendChild(divContent);
4292 
4293     this.dom = divBox;
4294     this.updateDOM();
4295 
4296     return divBox;
4297 };
4298 
4299 /**
4300  * Append the items DOM to the given HTML container. If items DOM does not yet
4301  * exist, it will be created first.
4302  * @param {Element} container
4303  * @override
4304  */
4305 links.Timeline.ItemFloatingRange.prototype.showDOM = function (container) {
4306     var dom = this.dom;
4307     if (!dom) {
4308         dom = this.createDOM();
4309     }
4310 
4311     if (dom.parentNode != container) {
4312         if (dom.parentNode) {
4313             // container changed. remove the item from the old container
4314             this.hideDOM();
4315         }
4316 
4317         // append to the new container
4318         container.appendChild(dom);
4319         this.rendered = true;
4320     }
4321 };
4322 
4323 /**
4324  * Remove the items DOM from the current HTML container
4325  * The DOM will be kept in memory
4326  * @override
4327  */
4328 links.Timeline.ItemFloatingRange.prototype.hideDOM = function () {
4329     var dom = this.dom;
4330     if (dom) {
4331         if (dom.parentNode) {
4332             dom.parentNode.removeChild(dom);
4333         }
4334         this.rendered = false;
4335     }
4336 };
4337 
4338 /**
4339  * Update the DOM of the item. This will update the content and the classes
4340  * of the item
4341  * @override
4342  */
4343 links.Timeline.ItemFloatingRange.prototype.updateDOM = function () {
4344     var divBox = this.dom;
4345     if (divBox) {
4346         // update contents
4347         divBox.firstChild.innerHTML = this.content;
4348 
4349         // update class
4350         divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default";
4351 
4352         if (this.isCluster) {
4353             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4354         }
4355 
4356         // add item specific class name when provided
4357         if (this.className) {
4358             links.Timeline.addClassName(divBox, this.className);
4359         }
4360 
4361         // TODO: apply selected className?
4362     }
4363 };
4364 
4365 /**
4366  * Reposition the item, recalculate its left, top, and width, using the current
4367  * range of the timeline and the timeline options. *
4368  * @param {links.Timeline} timeline
4369  * @override
4370  */
4371 links.Timeline.ItemFloatingRange.prototype.updatePosition = function (timeline) {
4372     var dom = this.dom;
4373     if (dom) {
4374         var contentWidth = timeline.size.contentWidth,
4375             left = this.getLeft(timeline), // NH use getLeft
4376             right = this.getRight(timeline); // NH use getRight;
4377 
4378         // limit the width of the this, as browsers cannot draw very wide divs
4379         if (left < -contentWidth) {
4380             left = -contentWidth;
4381         }
4382         if (right > 2 * contentWidth) {
4383             right = 2 * contentWidth;
4384         }
4385 
4386         dom.style.top = this.top + "px";
4387         dom.style.left = left + "px";
4388         //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth
4389         dom.style.width = Math.max(right - left, 1) + "px";
4390     }
4391 };
4392 
4393 /**
4394  * Check if the item is visible in the timeline, and not part of a cluster
4395  * @param {Number} start
4396  * @param {Number} end
4397  * @return {boolean} visible
4398  * @override
4399  */
4400 links.Timeline.ItemFloatingRange.prototype.isVisible = function (start, end) {
4401     if (this.cluster) {
4402         return false;
4403     }
4404 
4405 	// NH check for no end value
4406 	if (this.end && this.start) {
4407 		return (this.end > start)
4408 			&& (this.start < end);
4409 	} else if (this.start) {
4410 		return (this.start < end);
4411 	} else if (this.end) {
4412         return (this.end > start);
4413     } else {return true;}
4414 };
4415 
4416 /**
4417  * Reposition the item
4418  * @param {Number} left
4419  * @param {Number} right
4420  * @override
4421  */
4422 links.Timeline.ItemFloatingRange.prototype.setPosition = function (left, right) {
4423     var dom = this.dom;
4424 
4425     dom.style.left = left + 'px';
4426     dom.style.width = (right - left) + 'px';
4427 
4428     if (this.group) {
4429         this.top = this.group.top;
4430         dom.style.top = this.top + 'px';
4431     }
4432 };
4433 
4434 /**
4435  * Calculate the left position of the item
4436  * @param {links.Timeline} timeline
4437  * @return {Number} left
4438  * @override
4439  */
4440 links.Timeline.ItemFloatingRange.prototype.getLeft = function (timeline) {
4441     // NH check for no start value
4442 	if (this.start) {
4443 		return timeline.timeToScreen(this.start);
4444 	} else {
4445 		return 0;
4446 	}
4447 };
4448 
4449 /**
4450  * Calculate the right position of the item
4451  * @param {links.Timeline} timeline
4452  * @return {Number} right
4453  * @override
4454  */
4455 links.Timeline.ItemFloatingRange.prototype.getRight = function (timeline) {
4456     // NH check for no end value
4457 	if (this.end) {
4458 		return timeline.timeToScreen(this.end);
4459 	} else {
4460 		return timeline.size.contentWidth;
4461 	}
4462 };
4463 
4464 /**
4465  * Calculate the width of the item
4466  * @param {links.Timeline} timeline
4467  * @return {Number} width
4468  * @override
4469  */
4470 links.Timeline.ItemFloatingRange.prototype.getWidth = function (timeline) {
4471     return this.getRight(timeline) - this.getLeft(timeline);
4472 };
4473 
4474 /**
4475  * @constructor links.Timeline.ItemDot
4476  * @extends links.Timeline.Item
4477  * @param {Object} data       Object containing parameters start, end
4478  *                            content, group, type, className, editable.
4479  * @param {Object} [options]  Options to set initial property values
4480  *                                {Number} top
4481  *                                {Number} left
4482  *                                {Number} width
4483  *                                {Number} height
4484  */
4485 links.Timeline.ItemDot = function (data, options) {
4486     links.Timeline.Item.call(this, data, options);
4487 };
4488 
4489 links.Timeline.ItemDot.prototype = new links.Timeline.Item();
4490 
4491 /**
4492  * Reflow the Item: retrieve its actual size from the DOM
4493  * @return {boolean} resized    returns true if the axis is resized
4494  * @override
4495  */
4496 links.Timeline.ItemDot.prototype.reflow = function () {
4497     var dom = this.dom,
4498         dotHeight = dom.dot.offsetHeight,
4499         dotWidth = dom.dot.offsetWidth,
4500         contentHeight = dom.content.offsetHeight,
4501         resized = (
4502             (this.dotHeight != dotHeight) ||
4503                 (this.dotWidth != dotWidth) ||
4504                 (this.contentHeight != contentHeight)
4505             );
4506 
4507     this.dotHeight = dotHeight;
4508     this.dotWidth = dotWidth;
4509     this.contentHeight = contentHeight;
4510 
4511     return resized;
4512 };
4513 
4514 /**
4515  * Select the item
4516  * @override
4517  */
4518 links.Timeline.ItemDot.prototype.select = function () {
4519     var dom = this.dom;
4520     links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active');
4521 };
4522 
4523 /**
4524  * Unselect the item
4525  * @override
4526  */
4527 links.Timeline.ItemDot.prototype.unselect = function () {
4528     var dom = this.dom;
4529     links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active');
4530 };
4531 
4532 /**
4533  * Creates the DOM for the item, depending on its type
4534  * @return {Element | undefined}
4535  * @override
4536  */
4537 links.Timeline.ItemDot.prototype.createDOM = function () {
4538     // background box
4539     var divBox = document.createElement("DIV");
4540     divBox.style.position = "absolute";
4541 
4542     // contents box, right from the dot
4543     var divContent = document.createElement("DIV");
4544     divContent.className = "timeline-event-content";
4545     divBox.appendChild(divContent);
4546 
4547     // dot at start
4548     var divDot = document.createElement("DIV");
4549     divDot.style.position = "absolute";
4550     divDot.style.width = "0px";
4551     divDot.style.height = "0px";
4552     divBox.appendChild(divDot);
4553 
4554     divBox.content = divContent;
4555     divBox.dot = divDot;
4556 
4557     this.dom = divBox;
4558     this.updateDOM();
4559 
4560     return divBox;
4561 };
4562 
4563 /**
4564  * Append the items DOM to the given HTML container. If items DOM does not yet
4565  * exist, it will be created first.
4566  * @param {Element} container
4567  * @override
4568  */
4569 links.Timeline.ItemDot.prototype.showDOM = function (container) {
4570     var dom = this.dom;
4571     if (!dom) {
4572         dom = this.createDOM();
4573     }
4574 
4575     if (dom.parentNode != container) {
4576         if (dom.parentNode) {
4577             // container changed. remove it from old container first
4578             this.hideDOM();
4579         }
4580 
4581         // append to container
4582         container.appendChild(dom);
4583         this.rendered = true;
4584     }
4585 };
4586 
4587 /**
4588  * Remove the items DOM from the current HTML container
4589  * @override
4590  */
4591 links.Timeline.ItemDot.prototype.hideDOM = function () {
4592     var dom = this.dom;
4593     if (dom) {
4594         if (dom.parentNode) {
4595             dom.parentNode.removeChild(dom);
4596         }
4597         this.rendered = false;
4598     }
4599 };
4600 
4601 /**
4602  * Update the DOM of the item. This will update the content and the classes
4603  * of the item
4604  * @override
4605  */
4606 links.Timeline.ItemDot.prototype.updateDOM = function () {
4607     if (this.dom) {
4608         var divBox = this.dom;
4609         var divDot = divBox.dot;
4610 
4611         // update contents
4612         divBox.firstChild.innerHTML = this.content;
4613 
4614         // update classes
4615         divBox.className = "timeline-event-dot-container";
4616         divDot.className  = "timeline-event timeline-event-dot ui-widget ui-state-default";
4617 
4618         if (this.isCluster) {
4619             links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header');
4620             links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header');
4621         }
4622 
4623         // add item specific class name when provided
4624         if (this.className) {
4625             links.Timeline.addClassName(divBox, this.className);
4626             links.Timeline.addClassName(divDot, this.className);
4627         }
4628 
4629         // TODO: apply selected className?
4630     }
4631 };
4632 
4633 /**
4634  * Reposition the item, recalculate its left, top, and width, using the current
4635  * range of the timeline and the timeline options. *
4636  * @param {links.Timeline} timeline
4637  * @override
4638  */
4639 links.Timeline.ItemDot.prototype.updatePosition = function (timeline) {
4640     var dom = this.dom;
4641     if (dom) {
4642         var left = timeline.timeToScreen(this.start);
4643 
4644         dom.style.top = this.top + "px";
4645         dom.style.left = (left - this.dotWidth / 2) + "px";
4646 
4647         dom.content.style.marginLeft = (1.5 * this.dotWidth) + "px";
4648         //dom.content.style.marginRight = (0.5 * this.dotWidth) + "px"; // TODO
4649         dom.dot.style.top = ((this.height - this.dotHeight) / 2) + "px";
4650     }
4651 };
4652 
4653 /**
4654  * Check if the item is visible in the timeline, and not part of a cluster.
4655  * @param {Date} start
4656  * @param {Date} end
4657  * @return {boolean} visible
4658  * @override
4659  */
4660 links.Timeline.ItemDot.prototype.isVisible = function (start, end) {
4661     if (this.cluster) {
4662         return false;
4663     }
4664 
4665     return (this.start > start)
4666         && (this.start < end);
4667 };
4668 
4669 /**
4670  * Reposition the item
4671  * @param {Number} left
4672  * @param {Number} right
4673  * @override
4674  */
4675 links.Timeline.ItemDot.prototype.setPosition = function (left, right) {
4676     var dom = this.dom;
4677 
4678     dom.style.left = (left - this.dotWidth / 2) + "px";
4679 
4680     if (this.group) {
4681         this.top = this.group.top;
4682         dom.style.top = this.top + 'px';
4683     }
4684 };
4685 
4686 /**
4687  * Calculate the left position of the item
4688  * @param {links.Timeline} timeline
4689  * @return {Number} left
4690  * @override
4691  */
4692 links.Timeline.ItemDot.prototype.getLeft = function (timeline) {
4693     return timeline.timeToScreen(this.start);
4694 };
4695 
4696 /**
4697  * Calculate the right position of the item
4698  * @param {links.Timeline} timeline
4699  * @return {Number} right
4700  * @override
4701  */
4702 links.Timeline.ItemDot.prototype.getRight = function (timeline) {
4703     return timeline.timeToScreen(this.start) + this.width;
4704 };
4705 
4706 /**
4707  * Retrieve the properties of an item.
4708  * @param {Number} index
4709  * @return {Object} itemData    Object containing item properties:<br>
4710  *                              {Date} start (required),
4711  *                              {Date} end (optional),
4712  *                              {String} content (required),
4713  *                              {String} group (optional),
4714  *                              {String} className (optional)
4715  *                              {boolean} editable (optional)
4716  *                              {String} type (optional)
4717  */
4718 links.Timeline.prototype.getItem = function (index) {
4719     if (index >= this.items.length) {
4720         throw "Cannot get item, index out of range";
4721     }
4722 
4723     // take the original data as start, includes foreign fields
4724     var data = this.data,
4725         itemData;
4726     if (google && google.visualization &&
4727         data instanceof google.visualization.DataTable) {
4728         // map the datatable columns
4729         var cols = links.Timeline.mapColumnIds(data);
4730 
4731         itemData = {};
4732         for (var col in cols) {
4733             if (cols.hasOwnProperty(col)) {
4734                 itemData[col] = this.data.getValue(index, cols[col]);
4735             }
4736         }
4737     }
4738     else if (links.Timeline.isArray(this.data)) {
4739         // read JSON array
4740         itemData = links.Timeline.clone(this.data[index]);
4741     }
4742     else {
4743         throw "Unknown data type. DataTable or Array expected.";
4744     }
4745 
4746     // override the data with current settings of the item (should be the same)
4747     var item = this.items[index];
4748 
4749     itemData.start = new Date(item.start.valueOf());
4750     if (item.end) {
4751         itemData.end = new Date(item.end.valueOf());
4752     }
4753     itemData.content = item.content;
4754     if (item.group) {
4755         itemData.group = this.getGroupName(item.group);
4756     }
4757     if (item.className) {
4758         itemData.className = item.className;
4759     }
4760     if (typeof item.editable !== 'undefined') {
4761         itemData.editable = item.editable;
4762     }
4763     if (item.type) {
4764         itemData.type = item.type;
4765     }
4766 
4767     return itemData;
4768 };
4769 
4770 
4771 /**
4772  * Retrieve the properties of a cluster.
4773  * @param {Number} index
4774  * @return {Object} clusterdata    Object containing cluster properties:<br>
4775  *                              {Date} start (required),
4776  *                              {String} type (optional)
4777  *                              {Array} array with item data as is in getItem()
4778  */
4779 links.Timeline.prototype.getCluster = function (index) {
4780     if (index >= this.clusters.length) {
4781         throw "Cannot get cluster, index out of range";
4782     }
4783 
4784     var clusterData = {},
4785         cluster = this.clusters[index],
4786         clusterItems = cluster.items;
4787     
4788     clusterData.start = new Date(cluster.start.valueOf());
4789     if (cluster.type) {
4790         clusterData.type = cluster.type;
4791     }
4792 
4793     // push cluster item data
4794     clusterData.items = [];
4795     for(var i = 0; i < clusterItems.length; i++){
4796         for(var j = 0; j < this.items.length; j++){
4797             // TODO could be nicer to be able to have the item index into the cluster
4798             if(this.items[j] == clusterItems[i])
4799             {
4800                 clusterData.items.push(this.getItem(j));
4801                 break;
4802             }
4803 
4804         }
4805     }
4806 
4807     return clusterData;
4808 };
4809 
4810 /**
4811  * Add a new item.
4812  * @param {Object} itemData     Object containing item properties:<br>
4813  *                              {Date} start (required),
4814  *                              {Date} end (optional),
4815  *