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 * {String} content (required), 4816 * {String} group (optional) 4817 * {String} className (optional) 4818 * {Boolean} editable (optional) 4819 * {String} type (optional) 4820 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4821 */ 4822 links.Timeline.prototype.addItem = function (itemData, preventRender) { 4823 var itemsData = [ 4824 itemData 4825 ]; 4826 4827 this.addItems(itemsData, preventRender); 4828 }; 4829 4830 /** 4831 * Add new items. 4832 * @param {Array} itemsData An array containing Objects. 4833 * The objects must have the following parameters: 4834 * {Date} start, 4835 * {Date} end, 4836 * {String} content with text or HTML code, 4837 * {String} group (optional) 4838 * {String} className (optional) 4839 * {String} editable (optional) 4840 * {String} type (optional) 4841 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4842 */ 4843 links.Timeline.prototype.addItems = function (itemsData, preventRender) { 4844 var timeline = this, 4845 items = this.items; 4846 4847 // append the items 4848 itemsData.forEach(function (itemData) { 4849 var index = items.length; 4850 items.push(timeline.createItem(itemData)); 4851 timeline.updateData(index, itemData); 4852 4853 // note: there is no need to add the item to the renderQueue, that 4854 // will be done when this.render() is executed and all items are 4855 // filtered again. 4856 }); 4857 4858 // prepare data for clustering, by filtering and sorting by type 4859 if (this.options.cluster) { 4860 this.clusterGenerator.updateData(); 4861 } 4862 4863 if (!preventRender) { 4864 this.render({ 4865 animate: false 4866 }); 4867 } 4868 }; 4869 4870 /** 4871 * Create an item object, containing all needed parameters 4872 * @param {Object} itemData Object containing parameters start, end 4873 * content, group. 4874 * @return {Object} item 4875 */ 4876 links.Timeline.prototype.createItem = function(itemData) { 4877 var type = itemData.type || (itemData.end ? 'range' : this.options.style); 4878 var data = links.Timeline.clone(itemData); 4879 data.type = type; 4880 data.group = this.getGroup(itemData.group); 4881 // TODO: optimize this, when creating an item, all data is copied twice... 4882 4883 // TODO: is initialTop needed? 4884 var initialTop, 4885 options = this.options; 4886 if (options.axisOnTop) { 4887 initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2; 4888 } 4889 else { 4890 initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2; 4891 } 4892 4893 if (type in this.itemTypes) { 4894 return new this.itemTypes[type](data, {'top': initialTop}) 4895 } 4896 4897 console.log('ERROR: Unknown event type "' + type + '"'); 4898 return new links.Timeline.Item(data, { 4899 'top': initialTop 4900 }); 4901 }; 4902 4903 /** 4904 * Edit an item 4905 * @param {Number} index 4906 * @param {Object} itemData Object containing item properties:<br> 4907 * {Date} start (required), 4908 * {Date} end (optional), 4909 * {String} content (required), 4910 * {String} group (optional) 4911 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4912 */ 4913 links.Timeline.prototype.changeItem = function (index, itemData, preventRender) { 4914 var oldItem = this.items[index]; 4915 if (!oldItem) { 4916 throw "Cannot change item, index out of range"; 4917 } 4918 4919 // replace item, merge the changes 4920 var newItem = this.createItem({ 4921 'start': itemData.hasOwnProperty('start') ? itemData.start : oldItem.start, 4922 'end': itemData.hasOwnProperty('end') ? itemData.end : oldItem.end, 4923 'content': itemData.hasOwnProperty('content') ? itemData.content : oldItem.content, 4924 'group': itemData.hasOwnProperty('group') ? itemData.group : this.getGroupName(oldItem.group), 4925 'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className, 4926 'editable': itemData.hasOwnProperty('editable') ? itemData.editable : oldItem.editable, 4927 'type': itemData.hasOwnProperty('type') ? itemData.type : oldItem.type 4928 }); 4929 this.items[index] = newItem; 4930 4931 // append the changes to the render queue 4932 this.renderQueue.hide.push(oldItem); 4933 this.renderQueue.show.push(newItem); 4934 4935 // update the original data table 4936 this.updateData(index, itemData); 4937 4938 // prepare data for clustering, by filtering and sorting by type 4939 if (this.options.cluster) { 4940 this.clusterGenerator.updateData(); 4941 } 4942 4943 if (!preventRender) { 4944 // redraw timeline 4945 this.render({ 4946 animate: false 4947 }); 4948 4949 if (this.selection && this.selection.index == index) { 4950 newItem.select(); 4951 } 4952 } 4953 }; 4954 4955 /** 4956 * Delete all groups 4957 */ 4958 links.Timeline.prototype.deleteGroups = function () { 4959 this.groups = []; 4960 this.groupIndexes = {}; 4961 }; 4962 4963 4964 /** 4965 * Get a group by the group name. When the group does not exist, 4966 * it will be created. 4967 * @param {String} groupName the name of the group 4968 * @return {Object} groupObject 4969 */ 4970 links.Timeline.prototype.getGroup = function (groupName) { 4971 var groups = this.groups, 4972 groupIndexes = this.groupIndexes, 4973 groupObj = undefined; 4974 4975 var groupIndex = groupIndexes[groupName]; 4976 if (groupIndex == undefined && groupName != undefined) { // not null or undefined 4977 groupObj = { 4978 'content': groupName, 4979 'labelTop': 0, 4980 'lineTop': 0 4981 // note: this object will lateron get addition information, 4982 // such as height and width of the group 4983 }; 4984 groups.push(groupObj); 4985 // sort the groups 4986 if (this.options.groupsOrder == true) { 4987 groups = groups.sort(function (a, b) { 4988 if (a.content > b.content) { 4989 return 1; 4990 } 4991 if (a.content < b.content) { 4992 return -1; 4993 } 4994 return 0; 4995 }); 4996 } else if (typeof(this.options.groupsOrder) == "function") { 4997 groups = groups.sort(this.options.groupsOrder) 4998 } 4999 5000 // rebuilt the groupIndexes 5001 for (var i = 0, iMax = groups.length; i < iMax; i++) { 5002 groupIndexes[groups[i].content] = i; 5003 } 5004 } 5005 else { 5006 groupObj = groups[groupIndex]; 5007 } 5008 5009 return groupObj; 5010 }; 5011 5012 /** 5013 * Get the group name from a group object. 5014 * @param {Object} groupObj 5015 * @return {String} groupName the name of the group, or undefined when group 5016 * was not provided 5017 */ 5018 links.Timeline.prototype.getGroupName = function (groupObj) { 5019 return groupObj ? groupObj.content : undefined; 5020 }; 5021 5022 /** 5023 * Cancel a change item 5024 * This method can be called insed an event listener which catches the "change" 5025 * event. The changed event position will be undone. 5026 */ 5027 links.Timeline.prototype.cancelChange = function () { 5028 this.applyChange = false; 5029 }; 5030 5031 /** 5032 * Cancel deletion of an item 5033 * This method can be called insed an event listener which catches the "delete" 5034 * event. Deletion of the event will be undone. 5035 */ 5036 links.Timeline.prototype.cancelDelete = function () { 5037 this.applyDelete = false; 5038 }; 5039 5040 5041 /** 5042 * Cancel creation of a new item 5043 * This method can be called insed an event listener which catches the "new" 5044 * event. Creation of the new the event will be undone. 5045 */ 5046 links.Timeline.prototype.cancelAdd = function () { 5047 this.applyAdd = false; 5048 }; 5049 5050 5051 /** 5052 * Select an event. The visible chart range will be moved such that the selected 5053 * event is placed in the middle. 5054 * For example selection = [{row: 5}]; 5055 * @param {Array} selection An array with a column row, containing the row 5056 * number (the id) of the event to be selected. 5057 * @return {boolean} true if selection is succesfully set, else false. 5058 */ 5059 links.Timeline.prototype.setSelection = function(selection) { 5060 if (selection != undefined && selection.length > 0) { 5061 if (selection[0].row != undefined) { 5062 var index = selection[0].row; 5063 if (this.items[index]) { 5064 var item = this.items[index]; 5065 this.selectItem(index); 5066 5067 // move the visible chart range to the selected event. 5068 var start = item.start; 5069 var end = item.end; 5070 var middle; // number 5071 if (end != undefined) { 5072 middle = (end.valueOf() + start.valueOf()) / 2; 5073 } else { 5074 middle = start.valueOf(); 5075 } 5076 var diff = (this.end.valueOf() - this.start.valueOf()), 5077 newStart = new Date(middle - diff/2), 5078 newEnd = new Date(middle + diff/2); 5079 5080 this.setVisibleChartRange(newStart, newEnd); 5081 5082 return true; 5083 } 5084 } 5085 } 5086 else { 5087 // unselect current selection 5088 this.unselectItem(); 5089 } 5090 return false; 5091 }; 5092 5093 /** 5094 * Retrieve the currently selected event 5095 * @return {Array} sel An array with a column row, containing the row number 5096 * of the selected event. If there is no selection, an 5097 * empty array is returned. 5098 */ 5099 links.Timeline.prototype.getSelection = function() { 5100 var sel = []; 5101 if (this.selection) { 5102 if(this.selection.index !== undefined) 5103 { 5104 sel.push({"row": this.selection.index}); 5105 } else { 5106 sel.push({"cluster": this.selection.cluster}); 5107 } 5108 } 5109 return sel; 5110 }; 5111 5112 5113 /** 5114 * Select an item by its index 5115 * @param {Number} index 5116 */ 5117 links.Timeline.prototype.selectItem = function(index) { 5118 this.unselectItem(); 5119 5120 this.selection = undefined; 5121 5122 if (this.items[index] != undefined) { 5123 var item = this.items[index], 5124 domItem = item.dom; 5125 5126 this.selection = { 5127 'index': index 5128 }; 5129 5130 if (item && item.dom) { 5131 // TODO: move adjusting the domItem to the item itself 5132 if (this.isEditable(item)) { 5133 item.dom.style.cursor = 'move'; 5134 } 5135 item.select(); 5136 } 5137 this.repaintDeleteButton(); 5138 this.repaintDragAreas(); 5139 } 5140 }; 5141 5142 /** 5143 * Select an cluster by its index 5144 * @param {Number} index 5145 */ 5146 links.Timeline.prototype.selectCluster = function(index) { 5147 this.unselectItem(); 5148 5149 this.selection = undefined; 5150 5151 if (this.clusters[index] != undefined) { 5152 this.selection = { 5153 'cluster': index 5154 }; 5155 this.repaintDeleteButton(); 5156 this.repaintDragAreas(); 5157 } 5158 }; 5159 5160 /** 5161 * Check if an item is currently selected 5162 * @param {Number} index 5163 * @return {boolean} true if row is selected, else false 5164 */ 5165 links.Timeline.prototype.isSelected = function (index) { 5166 return (this.selection && this.selection.index == index); 5167 }; 5168 5169 /** 5170 * Unselect the currently selected event (if any) 5171 */ 5172 links.Timeline.prototype.unselectItem = function() { 5173 if (this.selection && this.selection.index !== undefined) { 5174 var item = this.items[this.selection.index]; 5175 5176 if (item && item.dom) { 5177 var domItem = item.dom; 5178 domItem.style.cursor = ''; 5179 item.unselect(); 5180 } 5181 5182 this.selection = undefined; 5183 this.repaintDeleteButton(); 5184 this.repaintDragAreas(); 5185 } 5186 }; 5187 5188 5189 /** 5190 * Stack the items such that they don't overlap. The items will have a minimal 5191 * distance equal to options.eventMargin. 5192 * @param {boolean | undefined} animate if animate is true, the items are 5193 * moved to their new position animated 5194 * defaults to false. 5195 */ 5196 links.Timeline.prototype.stackItems = function(animate) { 5197 if (animate == undefined) { 5198 animate = false; 5199 } 5200 5201 // calculate the order and final stack position of the items 5202 var stack = this.stack; 5203 if (!stack) { 5204 stack = {}; 5205 this.stack = stack; 5206 } 5207 stack.sortedItems = this.stackOrder(this.renderedItems); 5208 stack.finalItems = this.stackCalculateFinal(stack.sortedItems); 5209 5210 if (animate || stack.timer) { 5211 // move animated to the final positions 5212 var timeline = this; 5213 var step = function () { 5214 var arrived = timeline.stackMoveOneStep(stack.sortedItems, 5215 stack.finalItems); 5216 5217 timeline.repaint(); 5218 5219 if (!arrived) { 5220 stack.timer = setTimeout(step, 30); 5221 } 5222 else { 5223 delete stack.timer; 5224 } 5225 }; 5226 5227 if (!stack.timer) { 5228 stack.timer = setTimeout(step, 30); 5229 } 5230 } 5231 else { 5232 // move immediately to the final positions 5233 this.stackMoveToFinal(stack.sortedItems, stack.finalItems); 5234 } 5235 }; 5236 5237 /** 5238 * Cancel any running animation 5239 */ 5240 links.Timeline.prototype.stackCancelAnimation = function() { 5241 if (this.stack && this.stack.timer) { 5242 clearTimeout(this.stack.timer); 5243 delete this.stack.timer; 5244 } 5245 }; 5246 5247 links.Timeline.prototype.getItemsByGroup = function(items) { 5248 var itemsByGroup = {}; 5249 for (var i = 0; i < items.length; ++i) { 5250 var item = items[i]; 5251 var group = "undefined"; 5252 5253 if (item.group) { 5254 if (item.group.content) { 5255 group = item.group.content; 5256 } else { 5257 group = item.group; 5258 } 5259 } 5260 5261 if (!itemsByGroup[group]) { 5262 itemsByGroup[group] = []; 5263 } 5264 5265 itemsByGroup[group].push(item); 5266 } 5267 5268 return itemsByGroup; 5269 }; 5270 5271 /** 5272 * Order the items in the array this.items. The default order is determined via: 5273 * - Ranges go before boxes and dots. 5274 * - The item with the oldest start time goes first 5275 * If a custom function has been provided via the stackorder option, then this will be used. 5276 * @param {Array} items Array with items 5277 * @return {Array} sortedItems Array with sorted items 5278 */ 5279 links.Timeline.prototype.stackOrder = function(items) { 5280 // TODO: store the sorted items, to have less work later on 5281 var sortedItems = items.concat([]); 5282 5283 //if a customer stack order function exists, use it. 5284 var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b) 5285 { 5286 if ((a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && 5287 !(b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { 5288 return -1; 5289 } 5290 5291 if (!(a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && 5292 (b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { 5293 return 1; 5294 } 5295 5296 return (a.left - b.left); 5297 }; 5298 5299 sortedItems.sort(f); 5300 5301 return sortedItems; 5302 }; 5303 5304 /** 5305 * Adjust vertical positions of the events such that they don't overlap each 5306 * other. 5307 * @param {timeline.Item[]} items 5308 * @return {Object[]} finalItems 5309 */ 5310 links.Timeline.prototype.stackCalculateFinal = function(items) { 5311 var size = this.size, 5312 options = this.options, 5313 axisOnTop = options.axisOnTop, 5314 eventMargin = options.eventMargin, 5315 eventMarginAxis = options.eventMarginAxis, 5316 groupBase = (axisOnTop) 5317 ? size.axis.height + eventMarginAxis + eventMargin/2 5318 : size.contentHeight - eventMarginAxis - eventMargin/2, 5319 groupedItems, groupFinalItems, finalItems = []; 5320 5321 groupedItems = this.getItemsByGroup(items); 5322 5323 // 5324 // groupedItems contains all items by group, plus it may contain an 5325 // additional "undefined" group which contains all items with no group. We 5326 // first process the grouped items, and then the ungrouped 5327 // 5328 for (j = 0; j<this.groups.length; ++j) { 5329 var group = this.groups[j]; 5330 5331 if (!groupedItems[group.content]) { 5332 if (axisOnTop) { 5333 groupBase += options.groupMinHeight + eventMargin; 5334 } else { 5335 groupBase -= (options.groupMinHeight + eventMargin); 5336 } 5337 continue; 5338 } 5339 5340 // initialize final positions and fill finalItems 5341 groupFinalItems = this.finalItemsPosition(groupedItems[group.content], groupBase, group); 5342 groupFinalItems.forEach(function(item) { 5343 finalItems.push(item); 5344 }); 5345 5346 if (axisOnTop) { 5347 groupBase += group.itemsHeight + eventMargin; 5348 } else { 5349 groupBase -= (group.itemsHeight + eventMargin); 5350 } 5351 } 5352 5353 // 5354 // Ungrouped items' turn now! 5355 // 5356 if (groupedItems["undefined"]) { 5357 // initialize final positions and fill finalItems 5358 groupFinalItems = this.finalItemsPosition(groupedItems["undefined"], groupBase); 5359 groupFinalItems.forEach(function(item) { 5360 finalItems.push(item); 5361 }); 5362 } 5363 5364 return finalItems; 5365 }; 5366 5367 links.Timeline.prototype.finalItemsPosition = function(items, groupBase, group) { 5368 var i, 5369 iMax, 5370 options = this.options, 5371 axisOnTop = options.axisOnTop, 5372 eventMargin = options.eventMargin, 5373 groupFinalItems; 5374 5375 // initialize final positions and fill finalItems 5376 groupFinalItems = this.initialItemsPosition(items, groupBase); 5377 5378 // calculate new, non-overlapping positions 5379 for (i = 0, iMax = groupFinalItems.length; i < iMax; i++) { 5380 var finalItem = groupFinalItems[i]; 5381 var collidingItem = null; 5382 5383 if (this.options.stackEvents) { 5384 do { 5385 // TODO: optimize checking for overlap. when there is a gap without items, 5386 // you only need to check for items from the next item on, not from zero 5387 collidingItem = this.stackItemsCheckOverlap(groupFinalItems, i, 0, i-1); 5388 if (collidingItem != null) { 5389 // There is a collision. Reposition the event above the colliding element 5390 if (axisOnTop) { 5391 finalItem.top = collidingItem.top + collidingItem.height + eventMargin; 5392 } 5393 else { 5394 finalItem.top = collidingItem.top - finalItem.height - eventMargin; 5395 } 5396 finalItem.bottom = finalItem.top + finalItem.height; 5397 } 5398 } while (collidingItem); 5399 } 5400 5401 if (group) { 5402 if (axisOnTop) { 5403 group.itemsHeight = (group.itemsHeight) 5404 ? Math.max(group.itemsHeight, finalItem.bottom - groupBase) 5405 : finalItem.height + eventMargin; 5406 } else { 5407 group.itemsHeight = (group.itemsHeight) 5408 ? Math.max(group.itemsHeight, groupBase - finalItem.top) 5409 : finalItem.height + eventMargin; 5410 } 5411 } 5412 } 5413 5414 return groupFinalItems; 5415 }; 5416 5417 links.Timeline.prototype.initialItemsPosition = function(items, groupBase) { 5418 var options = this.options, 5419 axisOnTop = options.axisOnTop, 5420 finalItems = []; 5421 5422 for (var i = 0, iMax = items.length; i < iMax; ++i) { 5423 var item = items[i], 5424 top, 5425 bottom, 5426 height = item.height, 5427 width = item.getWidth(this), 5428 right = item.getRight(this), 5429 left = right - width; 5430 5431 top = (axisOnTop) ? groupBase 5432 : groupBase - height; 5433 5434 bottom = top + height; 5435 5436 finalItems.push({ 5437 'left': left, 5438 'top': top, 5439 'right': right, 5440 'bottom': bottom, 5441 'height': height, 5442 'item': item 5443 }); 5444 } 5445 5446 return finalItems; 5447 }; 5448 5449 /** 5450 * Move the events one step in the direction of their final positions 5451 * @param {Array} currentItems Array with the real items and their current 5452 * positions 5453 * @param {Array} finalItems Array with objects containing the final 5454 * positions of the items 5455 * @return {boolean} arrived True if all items have reached their final 5456 * location, else false 5457 */ 5458 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) { 5459 var arrived = true; 5460 5461 // apply new positions animated 5462 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 5463 var finalItem = finalItems[i], 5464 item = finalItem.item; 5465 5466 var topNow = parseInt(item.top); 5467 var topFinal = parseInt(finalItem.top); 5468 var diff = (topFinal - topNow); 5469 if (diff) { 5470 var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1); 5471 if (Math.abs(diff) > 4) step = diff / 4; 5472 var topNew = parseInt(topNow + step); 5473 5474 if (topNew != topFinal) { 5475 arrived = false; 5476 } 5477 5478 item.top = topNew; 5479 item.bottom = item.top + item.height; 5480 } 5481 else { 5482 item.top = finalItem.top; 5483 item.bottom = finalItem.bottom; 5484 } 5485 5486 item.left = finalItem.left; 5487 item.right = finalItem.right; 5488 } 5489 5490 return arrived; 5491 }; 5492 5493 5494 5495 /** 5496 * Move the events from their current position to the final position 5497 * @param {Array} currentItems Array with the real items and their current 5498 * positions 5499 * @param {Array} finalItems Array with objects containing the final 5500 * positions of the items 5501 */ 5502 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) { 5503 // Put the events directly at there final position 5504 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 5505 var finalItem = finalItems[i], 5506 current = finalItem.item; 5507 5508 current.left = finalItem.left; 5509 current.top = finalItem.top; 5510 current.right = finalItem.right; 5511 current.bottom = finalItem.bottom; 5512 } 5513 }; 5514 5515 5516 5517 /** 5518 * Check if the destiny position of given item overlaps with any 5519 * of the other items from index itemStart to itemEnd. 5520 * @param {Array} items Array with items 5521 * @param {int} itemIndex Number of the item to be checked for overlap 5522 * @param {int} itemStart First item to be checked. 5523 * @param {int} itemEnd Last item to be checked. 5524 * @return {Object} colliding item, or undefined when no collisions 5525 */ 5526 links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex, 5527 itemStart, itemEnd) { 5528 var eventMargin = this.options.eventMargin, 5529 collision = this.collision; 5530 5531 // we loop from end to start, as we suppose that the chance of a 5532 // collision is larger for items at the end, so check these first. 5533 var item1 = items[itemIndex]; 5534 for (var i = itemEnd; i >= itemStart; i--) { 5535 var item2 = items[i]; 5536 if (collision(item1, item2, eventMargin)) { 5537 if (i != itemIndex) { 5538 return item2; 5539 } 5540 } 5541 } 5542 5543 return undefined; 5544 }; 5545 5546 /** 5547 * Test if the two provided items collide 5548 * The items must have parameters left, right, top, and bottom. 5549 * @param {Element} item1 The first item 5550 * @param {Element} item2 The second item 5551 * @param {Number} margin A minimum required margin. Optional. 5552 * If margin is provided, the two items will be 5553 * marked colliding when they overlap or 5554 * when the margin between the two is smaller than 5555 * the requested margin. 5556 * @return {boolean} true if item1 and item2 collide, else false 5557 */ 5558 links.Timeline.prototype.collision = function(item1, item2, margin) { 5559 // set margin if not specified 5560 if (margin == undefined) { 5561 margin = 0; 5562 } 5563 5564 // calculate if there is overlap (collision) 5565 return (item1.left - margin < item2.right && 5566 item1.right + margin > item2.left && 5567 item1.top - margin < item2.bottom && 5568 item1.bottom + margin > item2.top); 5569 }; 5570 5571 5572 /** 5573 * fire an event 5574 * @param {String} event The name of an event, for example "rangechange" or "edit" 5575 */ 5576 links.Timeline.prototype.trigger = function (event) { 5577 // built up properties 5578 var properties = null; 5579 switch (event) { 5580 case 'rangechange': 5581 case 'rangechanged': 5582 properties = { 5583 'start': new Date(this.start.valueOf()), 5584 'end': new Date(this.end.valueOf()) 5585 }; 5586 break; 5587 5588 case 'timechange': 5589 case 'timechanged': 5590 properties = { 5591 'time': new Date(this.customTime.valueOf()) 5592 }; 5593 break; 5594 } 5595 5596 // trigger the links event bus 5597 links.events.trigger(this, event, properties); 5598 5599 // trigger the google event bus 5600 if (google && google.visualization) { 5601 google.visualization.events.trigger(this, event, properties); 5602 } 5603 }; 5604 5605 5606 /** 5607 * Cluster the events 5608 */ 5609 links.Timeline.prototype.clusterItems = function () { 5610 if (!this.options.cluster) { 5611 return; 5612 } 5613 5614 var clusters = this.clusterGenerator.getClusters(this.conversion.factor, this.options.clusterMaxItems); 5615 if (this.clusters != clusters) { 5616 // cluster level changed 5617 var queue = this.renderQueue; 5618 5619 // remove the old clusters from the scene 5620 if (this.clusters) { 5621 this.clusters.forEach(function (cluster) { 5622 queue.hide.push(cluster); 5623 5624 // unlink the items 5625 cluster.items.forEach(function (item) { 5626 item.cluster = undefined; 5627 }); 5628 }); 5629 } 5630 5631 // append the new clusters 5632 clusters.forEach(function (cluster) { 5633 // don't add to the queue.show here, will be done in .filterItems() 5634 5635 // link all items to the cluster 5636 cluster.items.forEach(function (item) { 5637 item.cluster = cluster; 5638 }); 5639 }); 5640 5641 this.clusters = clusters; 5642 } 5643 }; 5644 5645 /** 5646 * Filter the visible events 5647 */ 5648 links.Timeline.prototype.filterItems = function () { 5649 var queue = this.renderQueue, 5650 window = (this.end - this.start), 5651 start = new Date(this.start.valueOf() - window), 5652 end = new Date(this.end.valueOf() + window); 5653 5654 function filter (arr) { 5655 arr.forEach(function (item) { 5656 var rendered = item.rendered; 5657 var visible = item.isVisible(start, end); 5658 if (rendered != visible) { 5659 if (rendered) { 5660 queue.hide.push(item); // item is rendered but no longer visible 5661 } 5662 if (visible && (queue.show.indexOf(item) == -1)) { 5663 queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered 5664 } 5665 } 5666 }); 5667 } 5668 5669 // filter all items and all clusters 5670 filter(this.items); 5671 if (this.clusters) { 5672 filter(this.clusters); 5673 } 5674 }; 5675 5676 /** ------------------------------------------------------------------------ **/ 5677 5678 /** 5679 * @constructor links.Timeline.ClusterGenerator 5680 * Generator which creates clusters of items, based on the visible range in 5681 * the Timeline. There is a set of cluster levels which is cached. 5682 * @param {links.Timeline} timeline 5683 */ 5684 links.Timeline.ClusterGenerator = function (timeline) { 5685 this.timeline = timeline; 5686 this.clear(); 5687 }; 5688 5689 /** 5690 * Clear all cached clusters and data, and initialize all variables 5691 */ 5692 links.Timeline.ClusterGenerator.prototype.clear = function () { 5693 // cache containing created clusters for each cluster level 5694 this.items = []; 5695 this.groups = {}; 5696 this.clearCache(); 5697 }; 5698 5699 /** 5700 * Clear the cached clusters 5701 */ 5702 links.Timeline.ClusterGenerator.prototype.clearCache = function () { 5703 // cache containing created clusters for each cluster level 5704 this.cache = {}; 5705 this.cacheLevel = -1; 5706 this.cache[this.cacheLevel] = []; 5707 }; 5708 5709 /** 5710 * Set the items to be clustered. 5711 * This will clear cached clusters. 5712 * @param {Item[]} items 5713 * @param {Object} [options] Available options: 5714 * {boolean} applyOnChangedLevel 5715 * If true (default), the changed data is applied 5716 * as soon the cluster level changes. If false, 5717 * The changed data is applied immediately 5718 */ 5719 links.Timeline.ClusterGenerator.prototype.setData = function (items, options) { 5720 this.items = items || []; 5721 this.dataChanged = true; 5722 this.applyOnChangedLevel = true; 5723 if (options && options.applyOnChangedLevel) { 5724 this.applyOnChangedLevel = options.applyOnChangedLevel; 5725 } 5726 // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup 5727 }; 5728 5729 /** 5730 * Update the current data set: clear cache, and recalculate the clustering for 5731 * the current level 5732 */ 5733 links.Timeline.ClusterGenerator.prototype.updateData = function () { 5734 this.dataChanged = true; 5735 this.applyOnChangedLevel = false; 5736 }; 5737 5738 /** 5739 * Filter the items per group. 5740 * @private 5741 */ 5742 links.Timeline.ClusterGenerator.prototype.filterData = function () { 5743 // filter per group 5744 var items = this.items || []; 5745 var groups = {}; 5746 this.groups = groups; 5747 5748 // split the items per group 5749 items.forEach(function (item) { 5750 // put the item in the correct group 5751 var groupName = item.group ? item.group.content : ''; 5752 var group = groups[groupName]; 5753 if (!group) { 5754 group = []; 5755 groups[groupName] = group; 5756 } 5757 group.push(item); 5758 5759 // calculate the center of the item 5760 if (item.start) { 5761 if (item.end) { 5762 // range 5763 item.center = (item.start.valueOf() + item.end.valueOf()) / 2; 5764 } 5765 else { 5766 // box, dot 5767 item.center = item.start.valueOf(); 5768 } 5769 } 5770 }); 5771 5772 // sort the items per group 5773 for (var groupName in groups) { 5774 if (groups.hasOwnProperty(groupName)) { 5775 groups[groupName].sort(function (a, b) { 5776 return (a.center - b.center); 5777 }); 5778 } 5779 } 5780 5781 this.dataChanged = false; 5782 }; 5783 5784 /** 5785 * Cluster the events which are too close together 5786 * @param {Number} scale The scale of the current window, 5787 * defined as (windowWidth / (endDate - startDate)) 5788 * @return {Item[]} clusters 5789 */ 5790 links.Timeline.ClusterGenerator.prototype.getClusters = function (scale, maxItems) { 5791 var level = -1, 5792 granularity = 2, // TODO: what granularity is needed for the cluster levels? 5793 timeWindow = 0; // milliseconds 5794 5795 if (scale > 0) { 5796 level = Math.round(Math.log(100 / scale) / Math.log(granularity)); 5797 timeWindow = Math.pow(granularity, level); 5798 } 5799 5800 // clear the cache when and re-filter the data when needed. 5801 if (this.dataChanged) { 5802 var levelChanged = (level != this.cacheLevel); 5803 var applyDataNow = this.applyOnChangedLevel ? levelChanged : true; 5804 if (applyDataNow) { 5805 // TODO: currently drawn clusters should be removed! mark them as invisible? 5806 this.clearCache(); 5807 this.filterData(); 5808 // console.log('clustergenerator: cache cleared...'); // TODO: cleanup 5809 } 5810 } 5811 5812 this.cacheLevel = level; 5813 var clusters = this.cache[level]; 5814 if (!clusters) { 5815 // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup 5816 clusters = []; 5817 5818 // TODO: spit this method, it is too large 5819 for (var groupName in this.groups) { 5820 if (this.groups.hasOwnProperty(groupName)) { 5821 var items = this.groups[groupName]; 5822 var iMax = items.length; 5823 var i = 0; 5824 while (i < iMax) { 5825 // find all items around current item, within the timeWindow 5826 var item = items[i]; 5827 var neighbors = 1; // start at 1, to include itself) 5828 5829 // loop through items left from the current item 5830 var j = i - 1; 5831 while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) { 5832 if (!items[j].cluster) { 5833 neighbors++; 5834 } 5835 j--; 5836 } 5837 5838 // loop through items right from the current item 5839 var k = i + 1; 5840 while (k < items.length && (items[k].center - item.center) < timeWindow / 2) { 5841 neighbors++; 5842 k++; 5843 } 5844 5845 // loop through the created clusters 5846 var l = clusters.length - 1; 5847 while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) { 5848 if (item.group == clusters[l].group) { 5849 neighbors++; 5850 } 5851 l--; 5852 } 5853 5854 // aggregate until the number of items is within maxItems 5855 if (neighbors > maxItems) { 5856 // too busy in this window. 5857 var num = neighbors - maxItems + 1; 5858 var clusterItems = []; 5859 5860 // append the items to the cluster, 5861 // and calculate the average start for the cluster 5862 var avg = undefined; // number. average of all start dates 5863 var min = undefined; // number. minimum of all start dates 5864 var max = undefined; // number. maximum of all start and end dates 5865 var containsRanges = false; 5866 var count = 0; 5867 var m = i; 5868 while (clusterItems.length < num && m < items.length) { 5869 var p = items[m]; 5870 var start = p.start.valueOf(); 5871 var end = p.end ? p.end.valueOf() : p.start.valueOf(); 5872 clusterItems.push(p); 5873 if (count) { 5874 // calculate new average (use fractions to prevent overflow) 5875 avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center; 5876 } 5877 else { 5878 avg = p.center; 5879 } 5880 min = (min != undefined) ? Math.min(min, start) : start; 5881 max = (max != undefined) ? Math.max(max, end) : end; 5882 containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange || p instanceof links.Timeline.ItemFloatingRange); 5883 count++; 5884 m++; 5885 } 5886 5887 var cluster; 5888 var title = 'Cluster containing ' + count + 5889 ' events. Zoom in to see the individual events.'; 5890 var content = '<div title="' + title + '">' + count + ' events</div>'; 5891 var group = item.group ? item.group.content : undefined; 5892 if (containsRanges) { 5893 // boxes and/or ranges 5894 cluster = this.timeline.createItem({ 5895 'start': new Date(min), 5896 'end': new Date(max), 5897 'content': content, 5898 'group': group 5899 }); 5900 } 5901 else { 5902 // boxes only 5903 cluster = this.timeline.createItem({ 5904 'start': new Date(avg), 5905 'content': content, 5906 'group': group 5907 }); 5908 } 5909 cluster.isCluster = true; 5910 cluster.items = clusterItems; 5911 cluster.items.forEach(function (item) { 5912 item.cluster = cluster; 5913 }); 5914 5915 clusters.push(cluster); 5916 i += num; 5917 } 5918 else { 5919 delete item.cluster; 5920 i += 1; 5921 } 5922 } 5923 } 5924 } 5925 5926 this.cache[level] = clusters; 5927 } 5928 5929 return clusters; 5930 }; 5931 5932 5933 /** ------------------------------------------------------------------------ **/ 5934 5935 5936 /** 5937 * Event listener (singleton) 5938 */ 5939 links.events = links.events || { 5940 'listeners': [], 5941 5942 /** 5943 * Find a single listener by its object 5944 * @param {Object} object 5945 * @return {Number} index -1 when not found 5946 */ 5947 'indexOf': function (object) { 5948 var listeners = this.listeners; 5949 for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { 5950 var listener = listeners[i]; 5951 if (listener && listener.object == object) { 5952 return i; 5953 } 5954 } 5955 return -1; 5956 }, 5957 5958 /** 5959 * Add an event listener 5960 * @param {Object} object 5961 * @param {String} event The name of an event, for example 'select' 5962 * @param {function} callback The callback method, called when the 5963 * event takes place 5964 */ 5965 'addListener': function (object, event, callback) { 5966 var index = this.indexOf(object); 5967 var listener = this.listeners[index]; 5968 if (!listener) { 5969 listener = { 5970 'object': object, 5971 'events': {} 5972 }; 5973 this.listeners.push(listener); 5974 } 5975 5976 var callbacks = listener.events[event]; 5977 if (!callbacks) { 5978 callbacks = []; 5979 listener.events[event] = callbacks; 5980 } 5981 5982 // add the callback if it does not yet exist 5983 if (callbacks.indexOf(callback) == -1) { 5984 callbacks.push(callback); 5985 } 5986 }, 5987 5988 /** 5989 * Remove an event listener 5990 * @param {Object} object 5991 * @param {String} event The name of an event, for example 'select' 5992 * @param {function} callback The registered callback method 5993 */ 5994 'removeListener': function (object, event, callback) { 5995 var index = this.indexOf(object); 5996 var listener = this.listeners[index]; 5997 if (listener) { 5998 var callbacks = listener.events[event]; 5999 if (callbacks) { 6000 var index = callbacks.indexOf(callback); 6001 if (index != -1) { 6002 callbacks.splice(index, 1); 6003 } 6004 6005 // remove the array when empty 6006 if (callbacks.length == 0) { 6007 delete listener.events[event]; 6008 } 6009 } 6010 6011 // count the number of registered events. remove listener when empty 6012 var count = 0; 6013 var events = listener.events; 6014 for (var e in events) { 6015 if (events.hasOwnProperty(e)) { 6016 count++; 6017 } 6018 } 6019 if (count == 0) { 6020 delete this.listeners[index]; 6021 } 6022 } 6023 }, 6024 6025 /** 6026 * Remove all registered event listeners 6027 */ 6028 'removeAllListeners': function () { 6029 this.listeners = []; 6030 }, 6031 6032 /** 6033 * Trigger an event. All registered event handlers will be called 6034 * @param {Object} object 6035 * @param {String} event 6036 * @param {Object} properties (optional) 6037 */ 6038 'trigger': function (object, event, properties) { 6039 var index = this.indexOf(object); 6040 var listener = this.listeners[index]; 6041 if (listener) { 6042 var callbacks = listener.events[event]; 6043 if (callbacks) { 6044 for (var i = 0, iMax = callbacks.length; i < iMax; i++) { 6045 callbacks[i](properties); 6046 } 6047 } 6048 } 6049 } 6050 }; 6051 6052 6053 /** ------------------------------------------------------------------------ **/ 6054 6055 /** 6056 * @constructor links.Timeline.StepDate 6057 * The class StepDate is an iterator for dates. You provide a start date and an 6058 * end date. The class itself determines the best scale (step size) based on the 6059 * provided start Date, end Date, and minimumStep. 6060 * 6061 * If minimumStep is provided, the step size is chosen as close as possible 6062 * to the minimumStep but larger than minimumStep. If minimumStep is not 6063 * provided, the scale is set to 1 DAY. 6064 * The minimumStep should correspond with the onscreen size of about 6 characters 6065 * 6066 * Alternatively, you can set a scale by hand. 6067 * After creation, you can initialize the class by executing start(). Then you 6068 * can iterate from the start date to the end date via next(). You can check if 6069 * the end date is reached with the function end(). After each step, you can 6070 * retrieve the current date via get(). 6071 * The class step has scales ranging from milliseconds, seconds, minutes, hours, 6072 * days, to years. 6073 * 6074 * Version: 1.2 6075 * 6076 * @param {Date} start The start date, for example new Date(2010, 9, 21) 6077 * or new Date(2010, 9, 21, 23, 45, 00) 6078 * @param {Date} end The end date 6079 * @param {Number} minimumStep Optional. Minimum step size in milliseconds 6080 */ 6081 links.Timeline.StepDate = function(start, end, minimumStep) { 6082 6083 // variables 6084 this.current = new Date(); 6085 this._start = new Date(); 6086 this._end = new Date(); 6087 6088 this.autoScale = true; 6089 this.scale = links.Timeline.StepDate.SCALE.DAY; 6090 this.step = 1; 6091 6092 // initialize the range 6093 this.setRange(start, end, minimumStep); 6094 }; 6095 6096 /// enum scale 6097 links.Timeline.StepDate.SCALE = { 6098 MILLISECOND: 1, 6099 SECOND: 2, 6100 MINUTE: 3, 6101 HOUR: 4, 6102 DAY: 5, 6103 WEEKDAY: 6, 6104 MONTH: 7, 6105 YEAR: 8 6106 }; 6107 6108 6109 /** 6110 * Set a new range 6111 * If minimumStep is provided, the step size is chosen as close as possible 6112 * to the minimumStep but larger than minimumStep. If minimumStep is not 6113 * provided, the scale is set to 1 DAY. 6114 * The minimumStep should correspond with the onscreen size of about 6 characters 6115 * @param {Date} start The start date and time. 6116 * @param {Date} end The end date and time. 6117 * @param {int} minimumStep Optional. Minimum step size in milliseconds 6118 */ 6119 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) { 6120 if (!(start instanceof Date) || !(end instanceof Date)) { 6121 //throw "No legal start or end date in method setRange"; 6122 return; 6123 } 6124 6125 this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); 6126 this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); 6127 6128 if (this.autoScale) { 6129 this.setMinimumStep(minimumStep); 6130 } 6131 }; 6132 6133 /** 6134 * Set the step iterator to the start date. 6135 */ 6136 links.Timeline.StepDate.prototype.start = function() { 6137 this.current = new Date(this._start.valueOf()); 6138 this.roundToMinor(); 6139 }; 6140 6141 /** 6142 * Round the current date to the first minor date value 6143 * This must be executed once when the current date is set to start Date 6144 */ 6145 links.Timeline.StepDate.prototype.roundToMinor = function() { 6146 // round to floor 6147 // IMPORTANT: we have no breaks in this switch! (this is no bug) 6148 //noinspection FallthroughInSwitchStatementJS 6149 switch (this.scale) { 6150 case links.Timeline.StepDate.SCALE.YEAR: 6151 this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); 6152 this.current.setMonth(0); 6153 case links.Timeline.StepDate.SCALE.MONTH: this.current.setDate(1); 6154 case links.Timeline.StepDate.SCALE.DAY: // intentional fall through 6155 case links.Timeline.StepDate.SCALE.WEEKDAY: this.current.setHours(0); 6156 case links.Timeline.StepDate.SCALE.HOUR: this.current.setMinutes(0); 6157 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setSeconds(0); 6158 case links.Timeline.StepDate.SCALE.SECOND: this.current.setMilliseconds(0); 6159 //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds 6160 } 6161 6162 if (this.step != 1) { 6163 // round down to the first minor value that is a multiple of the current step size 6164 switch (this.scale) { 6165 case links.Timeline.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; 6166 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; 6167 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; 6168 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; 6169 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6170 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; 6171 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; 6172 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; 6173 default: break; 6174 } 6175 } 6176 }; 6177 6178 /** 6179 * Check if the end date is reached 6180 * @return {boolean} true if the current date has passed the end date 6181 */ 6182 links.Timeline.StepDate.prototype.end = function () { 6183 return (this.current.valueOf() > this._end.valueOf()); 6184 }; 6185 6186 /** 6187 * Do the next step 6188 */ 6189 links.Timeline.StepDate.prototype.next = function() { 6190 var prev = this.current.valueOf(); 6191 6192 // Two cases, needed to prevent issues with switching daylight savings 6193 // (end of March and end of October) 6194 if (this.current.getMonth() < 6) { 6195 switch (this.scale) { 6196 case links.Timeline.StepDate.SCALE.MILLISECOND: 6197 6198 this.current = new Date(this.current.valueOf() + this.step); break; 6199 case links.Timeline.StepDate.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; 6200 case links.Timeline.StepDate.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; 6201 case links.Timeline.StepDate.SCALE.HOUR: 6202 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); 6203 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) 6204 var h = this.current.getHours(); 6205 this.current.setHours(h - (h % this.step)); 6206 break; 6207 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6208 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 6209 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 6210 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 6211 default: break; 6212 } 6213 } 6214 else { 6215 switch (this.scale) { 6216 case links.Timeline.StepDate.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; 6217 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; 6218 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; 6219 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; 6220 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6221 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 6222 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 6223 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 6224 default: break; 6225 } 6226 } 6227 6228 if (this.step != 1) { 6229 // round down to the correct major value 6230 switch (this.scale) { 6231 case links.Timeline.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; 6232 case links.Timeline.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; 6233 case links.Timeline.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; 6234 case links.Timeline.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; 6235 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6236 case links.Timeline.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; 6237 case links.Timeline.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; 6238 case links.Timeline.StepDate.SCALE.YEAR: break; // nothing to do for year 6239 default: break; 6240 } 6241 } 6242 6243 // safety mechanism: if current time is still unchanged, move to the end 6244 if (this.current.valueOf() == prev) { 6245 this.current = new Date(this._end.valueOf()); 6246 } 6247 }; 6248 6249 6250 /** 6251 * Get the current datetime 6252 * @return {Date} current The current date 6253 */ 6254 links.Timeline.StepDate.prototype.getCurrent = function() { 6255 return this.current; 6256 }; 6257 6258 /** 6259 * Set a custom scale. Autoscaling will be disabled. 6260 * For example setScale(SCALE.MINUTES, 5) will result 6261 * in minor steps of 5 minutes, and major steps of an hour. 6262 * 6263 * @param {links.Timeline.StepDate.SCALE} newScale 6264 * A scale. Choose from SCALE.MILLISECOND, 6265 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 6266 * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, 6267 * SCALE.YEAR. 6268 * @param {Number} newStep A step size, by default 1. Choose for 6269 * example 1, 2, 5, or 10. 6270 */ 6271 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) { 6272 this.scale = newScale; 6273 6274 if (newStep > 0) { 6275 this.step = newStep; 6276 } 6277 6278 this.autoScale = false; 6279 }; 6280 6281 /** 6282 * Enable or disable autoscaling 6283 * @param {boolean} enable If true, autoascaling is set true 6284 */ 6285 links.Timeline.StepDate.prototype.setAutoScale = function (enable) { 6286 this.autoScale = enable; 6287 }; 6288 6289 6290 /** 6291 * Automatically determine the scale that bests fits the provided minimum step 6292 * @param {Number} minimumStep The minimum step size in milliseconds 6293 */ 6294 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) { 6295 if (minimumStep == undefined) { 6296 return; 6297 } 6298 6299 var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); 6300 var stepMonth = (1000 * 60 * 60 * 24 * 30); 6301 var stepDay = (1000 * 60 * 60 * 24); 6302 var stepHour = (1000 * 60 * 60); 6303 var stepMinute = (1000 * 60); 6304 var stepSecond = (1000); 6305 var stepMillisecond= (1); 6306 6307 // find the smallest step that is larger than the provided minimumStep 6308 if (stepYear*1000 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;} 6309 if (stepYear*500 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 500;} 6310 if (stepYear*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 100;} 6311 if (stepYear*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 50;} 6312 if (stepYear*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 10;} 6313 if (stepYear*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 5;} 6314 if (stepYear > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1;} 6315 if (stepMonth*3 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 3;} 6316 if (stepMonth > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 1;} 6317 if (stepDay*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 5;} 6318 if (stepDay*2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 2;} 6319 if (stepDay > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1;} 6320 if (stepDay/2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY; this.step = 1;} 6321 if (stepHour*4 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 4;} 6322 if (stepHour > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 1;} 6323 if (stepMinute*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;} 6324 if (stepMinute*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;} 6325 if (stepMinute*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;} 6326 if (stepMinute > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;} 6327 if (stepSecond*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 15;} 6328 if (stepSecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 10;} 6329 if (stepSecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 5;} 6330 if (stepSecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 1;} 6331 if (stepMillisecond*200 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;} 6332 if (stepMillisecond*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;} 6333 if (stepMillisecond*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;} 6334 if (stepMillisecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;} 6335 if (stepMillisecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;} 6336 if (stepMillisecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;} 6337 }; 6338 6339 /** 6340 * Snap a date to a rounded value. The snap intervals are dependent on the 6341 * current scale and step. 6342 * @param {Date} date the date to be snapped 6343 */ 6344 links.Timeline.StepDate.prototype.snap = function(date) { 6345 if (this.scale == links.Timeline.StepDate.SCALE.YEAR) { 6346 var year = date.getFullYear() + Math.round(date.getMonth() / 12); 6347 date.setFullYear(Math.round(year / this.step) * this.step); 6348 date.setMonth(0); 6349 date.setDate(0); 6350 date.setHours(0); 6351 date.setMinutes(0); 6352 date.setSeconds(0); 6353 date.setMilliseconds(0); 6354 } 6355 else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) { 6356 if (date.getDate() > 15) { 6357 date.setDate(1); 6358 date.setMonth(date.getMonth() + 1); 6359 // important: first set Date to 1, after that change the month. 6360 } 6361 else { 6362 date.setDate(1); 6363 } 6364 6365 date.setHours(0); 6366 date.setMinutes(0); 6367 date.setSeconds(0); 6368 date.setMilliseconds(0); 6369 } 6370 else if (this.scale == links.Timeline.StepDate.SCALE.DAY || 6371 this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) { 6372 switch (this.step) { 6373 case 5: 6374 case 2: 6375 date.setHours(Math.round(date.getHours() / 24) * 24); break; 6376 default: 6377 date.setHours(Math.round(date.getHours() / 12) * 12); break; 6378 } 6379 date.setMinutes(0); 6380 date.setSeconds(0); 6381 date.setMilliseconds(0); 6382 } 6383 else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) { 6384 switch (this.step) { 6385 case 4: 6386 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; 6387 default: 6388 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; 6389 } 6390 date.setSeconds(0); 6391 date.setMilliseconds(0); 6392 } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) { 6393 switch (this.step) { 6394 case 15: 6395 case 10: 6396 date.setMinutes(Math.round(date.getMinutes() / 5) * 5); 6397 date.setSeconds(0); 6398 break; 6399 case 5: 6400 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; 6401 default: 6402 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; 6403 } 6404 date.setMilliseconds(0); 6405 } 6406 else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) { 6407 switch (this.step) { 6408 case 15: 6409 case 10: 6410 date.setSeconds(Math.round(date.getSeconds() / 5) * 5); 6411 date.setMilliseconds(0); 6412 break; 6413 case 5: 6414 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; 6415 default: 6416 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; 6417 } 6418 } 6419 else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) { 6420 var step = this.step > 5 ? this.step / 2 : 1; 6421 date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); 6422 } 6423 }; 6424 6425 /** 6426 * Check if the current step is a major step (for example when the step 6427 * is DAY, a major step is each first day of the MONTH) 6428 * @return {boolean} true if current date is major, else false. 6429 */ 6430 links.Timeline.StepDate.prototype.isMajor = function() { 6431 switch (this.scale) { 6432 case links.Timeline.StepDate.SCALE.MILLISECOND: 6433 return (this.current.getMilliseconds() == 0); 6434 case links.Timeline.StepDate.SCALE.SECOND: 6435 return (this.current.getSeconds() == 0); 6436 case links.Timeline.StepDate.SCALE.MINUTE: 6437 return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); 6438 // Note: this is no bug. Major label is equal for both minute and hour scale 6439 case links.Timeline.StepDate.SCALE.HOUR: 6440 return (this.current.getHours() == 0); 6441 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6442 case links.Timeline.StepDate.SCALE.DAY: 6443 return (this.current.getDate() == 1); 6444 case links.Timeline.StepDate.SCALE.MONTH: 6445 return (this.current.getMonth() == 0); 6446 case links.Timeline.StepDate.SCALE.YEAR: 6447 return false; 6448 default: 6449 return false; 6450 } 6451 }; 6452 6453 6454 /** 6455 * Returns formatted text for the minor axislabel, depending on the current 6456 * date and the scale. For example when scale is MINUTE, the current time is 6457 * formatted as "hh:mm". 6458 * @param {Object} options 6459 * @param {Date} [date] custom date. if not provided, current date is taken 6460 */ 6461 links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) { 6462 if (date == undefined) { 6463 date = this.current; 6464 } 6465 6466 switch (this.scale) { 6467 case links.Timeline.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds()); 6468 case links.Timeline.StepDate.SCALE.SECOND: return String(date.getSeconds()); 6469 case links.Timeline.StepDate.SCALE.MINUTE: 6470 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 6471 case links.Timeline.StepDate.SCALE.HOUR: 6472 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 6473 case links.Timeline.StepDate.SCALE.WEEKDAY: return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate(); 6474 case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate()); 6475 case links.Timeline.StepDate.SCALE.MONTH: return options.MONTHS_SHORT[date.getMonth()]; // month is zero based 6476 case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear()); 6477 default: return ""; 6478 } 6479 }; 6480 6481 6482 /** 6483 * Returns formatted text for the major axislabel, depending on the current 6484 * date and the scale. For example when scale is MINUTE, the major scale is 6485 * hours, and the hour will be formatted as "hh". 6486 * @param {Object} options 6487 * @param {Date} [date] custom date. if not provided, current date is taken 6488 */ 6489 links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) { 6490 if (date == undefined) { 6491 date = this.current; 6492 } 6493 6494 switch (this.scale) { 6495 case links.Timeline.StepDate.SCALE.MILLISECOND: 6496 return this.addZeros(date.getHours(), 2) + ":" + 6497 this.addZeros(date.getMinutes(), 2) + ":" + 6498 this.addZeros(date.getSeconds(), 2); 6499 case links.Timeline.StepDate.SCALE.SECOND: 6500 return date.getDate() + " " + 6501 options.MONTHS[date.getMonth()] + " " + 6502 this.addZeros(date.getHours(), 2) + ":" + 6503 this.addZeros(date.getMinutes(), 2); 6504 case links.Timeline.StepDate.SCALE.MINUTE: 6505 return options.DAYS[date.getDay()] + " " + 6506 date.getDate() + " " + 6507 options.MONTHS[date.getMonth()] + " " + 6508 date.getFullYear(); 6509 case links.Timeline.StepDate.SCALE.HOUR: 6510 return options.DAYS[date.getDay()] + " " + 6511 date.getDate() + " " + 6512 options.MONTHS[date.getMonth()] + " " + 6513 date.getFullYear(); 6514 case links.Timeline.StepDate.SCALE.WEEKDAY: 6515 case links.Timeline.StepDate.SCALE.DAY: 6516 return options.MONTHS[date.getMonth()] + " " + 6517 date.getFullYear(); 6518 case links.Timeline.StepDate.SCALE.MONTH: 6519 return String(date.getFullYear()); 6520 default: 6521 return ""; 6522 } 6523 }; 6524 6525 /** 6526 * Add leading zeros to the given value to match the desired length. 6527 * For example addZeros(123, 5) returns "00123" 6528 * @param {int} value A value 6529 * @param {int} len Desired final length 6530 * @return {string} value with leading zeros 6531 */ 6532 links.Timeline.StepDate.prototype.addZeros = function(value, len) { 6533 var str = "" + value; 6534 while (str.length < len) { 6535 str = "0" + str; 6536 } 6537 return str; 6538 }; 6539 6540 6541 6542 /** ------------------------------------------------------------------------ **/ 6543 6544 /** 6545 * Image Loader service. 6546 * can be used to get a callback when a certain image is loaded 6547 * 6548 */ 6549 links.imageloader = (function () { 6550 var urls = {}; // the loaded urls 6551 var callbacks = {}; // the urls currently being loaded. Each key contains 6552 // an array with callbacks 6553 6554 /** 6555 * Check if an image url is loaded 6556 * @param {String} url 6557 * @return {boolean} loaded True when loaded, false when not loaded 6558 * or when being loaded 6559 */ 6560 function isLoaded (url) { 6561 if (urls[url] == true) { 6562 return true; 6563 } 6564 6565 var image = new Image(); 6566 image.src = url; 6567 if (image.complete) { 6568 return true; 6569 } 6570 6571 return false; 6572 } 6573 6574 /** 6575 * Check if an image url is being loaded 6576 * @param {String} url 6577 * @return {boolean} loading True when being loaded, false when not loading 6578 * or when already loaded 6579 */ 6580 function isLoading (url) { 6581 return (callbacks[url] != undefined); 6582 } 6583 6584 /** 6585 * Load given image url 6586 * @param {String} url 6587 * @param {function} callback 6588 * @param {boolean} sendCallbackWhenAlreadyLoaded optional 6589 */ 6590 function load (url, callback, sendCallbackWhenAlreadyLoaded) { 6591 if (sendCallbackWhenAlreadyLoaded == undefined) { 6592 sendCallbackWhenAlreadyLoaded = true; 6593 } 6594 6595 if (isLoaded(url)) { 6596 if (sendCallbackWhenAlreadyLoaded) { 6597 callback(url); 6598 } 6599 return; 6600 } 6601 6602 if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) { 6603 return; 6604 } 6605 6606 var c = callbacks[url]; 6607 if (!c) { 6608 var image = new Image(); 6609 image.src = url; 6610 6611 c = []; 6612 callbacks[url] = c; 6613 6614 image.onload = function (event) { 6615 urls[url] = true; 6616 delete callbacks[url]; 6617 6618 for (var i = 0; i < c.length; i++) { 6619 c[i](url); 6620 } 6621 } 6622 } 6623 6624 if (c.indexOf(callback) == -1) { 6625 c.push(callback); 6626 } 6627 } 6628 6629 /** 6630 * Load a set of images, and send a callback as soon as all images are 6631 * loaded 6632 * @param {String[]} urls 6633 * @param {function } callback 6634 * @param {boolean} sendCallbackWhenAlreadyLoaded 6635 */ 6636 function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) { 6637 // list all urls which are not yet loaded 6638 var urlsLeft = []; 6639 urls.forEach(function (url) { 6640 if (!isLoaded(url)) { 6641 urlsLeft.push(url); 6642 } 6643 }); 6644 6645 if (urlsLeft.length) { 6646 // there are unloaded images 6647 var countLeft = urlsLeft.length; 6648 urlsLeft.forEach(function (url) { 6649 load(url, function () { 6650 countLeft--; 6651 if (countLeft == 0) { 6652 // done! 6653 callback(); 6654 } 6655 }, sendCallbackWhenAlreadyLoaded); 6656 }); 6657 } 6658 else { 6659 // we are already done! 6660 if (sendCallbackWhenAlreadyLoaded) { 6661 callback(); 6662 } 6663 } 6664 } 6665 6666 /** 6667 * Recursively retrieve all image urls from the images located inside a given 6668 * HTML element 6669 * @param {Node} elem 6670 * @param {String[]} urls Urls will be added here (no duplicates) 6671 */ 6672 function filterImageUrls (elem, urls) { 6673 var child = elem.firstChild; 6674 while (child) { 6675 if (child.tagName == 'IMG') { 6676 var url = child.src; 6677 if (urls.indexOf(url) == -1) { 6678 urls.push(url); 6679 } 6680 } 6681 6682 filterImageUrls(child, urls); 6683 6684 child = child.nextSibling; 6685 } 6686 } 6687 6688 return { 6689 'isLoaded': isLoaded, 6690 'isLoading': isLoading, 6691 'load': load, 6692 'loadAll': loadAll, 6693 'filterImageUrls': filterImageUrls 6694 }; 6695 })(); 6696 6697 6698 /** ------------------------------------------------------------------------ **/ 6699 6700 6701 /** 6702 * Add and event listener. Works for all browsers 6703 * @param {Element} element An html element 6704 * @param {string} action The action, for example "click", 6705 * without the prefix "on" 6706 * @param {function} listener The callback function to be executed 6707 * @param {boolean} useCapture 6708 */ 6709 links.Timeline.addEventListener = function (element, action, listener, useCapture) { 6710 if (element.addEventListener) { 6711 if (useCapture === undefined) 6712 useCapture = false; 6713 6714 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 6715 action = "DOMMouseScroll"; // For Firefox 6716 } 6717 6718 element.addEventListener(action, listener, useCapture); 6719 } else { 6720 element.attachEvent("on" + action, listener); // IE browsers 6721 } 6722 }; 6723 6724 /** 6725 * Remove an event listener from an element 6726 * @param {Element} element An html dom element 6727 * @param {string} action The name of the event, for example "mousedown" 6728 * @param {function} listener The listener function 6729 * @param {boolean} useCapture 6730 */ 6731 links.Timeline.removeEventListener = function(element, action, listener, useCapture) { 6732 if (element.removeEventListener) { 6733 // non-IE browsers 6734 if (useCapture === undefined) 6735 useCapture = false; 6736 6737 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 6738 action = "DOMMouseScroll"; // For Firefox 6739 } 6740 6741 element.removeEventListener(action, listener, useCapture); 6742 } else { 6743 // IE browsers 6744 element.detachEvent("on" + action, listener); 6745 } 6746 }; 6747 6748 6749 /** 6750 * Get HTML element which is the target of the event 6751 * @param {Event} event 6752 * @return {Element} target element 6753 */ 6754 links.Timeline.getTarget = function (event) { 6755 // code from http://www.quirksmode.org/js/events_properties.html 6756 if (!event) { 6757 event = window.event; 6758 } 6759 6760 var target; 6761 6762 if (event.target) { 6763 target = event.target; 6764 } 6765 else if (event.srcElement) { 6766 target = event.srcElement; 6767 } 6768 6769 if (target.nodeType != undefined && target.nodeType == 3) { 6770 // defeat Safari bug 6771 target = target.parentNode; 6772 } 6773 6774 return target; 6775 }; 6776 6777 /** 6778 * Stop event propagation 6779 */ 6780 links.Timeline.stopPropagation = function (event) { 6781 if (!event) 6782 event = window.event; 6783 6784 if (event.stopPropagation) { 6785 event.stopPropagation(); // non-IE browsers 6786 } 6787 else { 6788 event.cancelBubble = true; // IE browsers 6789 } 6790 }; 6791 6792 6793 /** 6794 * Cancels the event if it is cancelable, without stopping further propagation of the event. 6795 */ 6796 links.Timeline.preventDefault = function (event) { 6797 if (!event) 6798 event = window.event; 6799 6800 if (event.preventDefault) { 6801 event.preventDefault(); // non-IE browsers 6802 } 6803 else { 6804 event.returnValue = false; // IE browsers 6805 } 6806 }; 6807 6808 6809 /** 6810 * Retrieve the absolute left value of a DOM element 6811 * @param {Element} elem A dom element, for example a div 6812 * @return {number} left The absolute left position of this element 6813 * in the browser page. 6814 */ 6815 links.Timeline.getAbsoluteLeft = function(elem) { 6816 var doc = document.documentElement; 6817 var body = document.body; 6818 6819 var left = elem.offsetLeft; 6820 var e = elem.offsetParent; 6821 while (e != null && e != body && e != doc) { 6822 left += e.offsetLeft; 6823 left -= e.scrollLeft; 6824 e = e.offsetParent; 6825 } 6826 return left; 6827 }; 6828 6829 /** 6830 * Retrieve the absolute top value of a DOM element 6831 * @param {Element} elem A dom element, for example a div 6832 * @return {number} top The absolute top position of this element 6833 * in the browser page. 6834 */ 6835 links.Timeline.getAbsoluteTop = function(elem) { 6836 var doc = document.documentElement; 6837 var body = document.body; 6838 6839 var top = elem.offsetTop; 6840 var e = elem.offsetParent; 6841 while (e != null && e != body && e != doc) { 6842 top += e.offsetTop; 6843 top -= e.scrollTop; 6844 e = e.offsetParent; 6845 } 6846 return top; 6847 }; 6848 6849 /** 6850 * Get the absolute, vertical mouse position from an event. 6851 * @param {Event} event 6852 * @return {Number} pageY 6853 */ 6854 links.Timeline.getPageY = function (event) { 6855 if (('targetTouches' in event) && event.targetTouches.length) { 6856 event = event.targetTouches[0]; 6857 } 6858 6859 if ('pageY' in event) { 6860 return event.pageY; 6861 } 6862 6863 // calculate pageY from clientY 6864 var clientY = event.clientY; 6865 var doc = document.documentElement; 6866 var body = document.body; 6867 return clientY + 6868 ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - 6869 ( doc && doc.clientTop || body && body.clientTop || 0 ); 6870 }; 6871 6872 /** 6873 * Get the absolute, horizontal mouse position from an event. 6874 * @param {Event} event 6875 * @return {Number} pageX 6876 */ 6877 links.Timeline.getPageX = function (event) { 6878 if (('targetTouches' in event) && event.targetTouches.length) { 6879 event = event.targetTouches[0]; 6880 } 6881 6882 if ('pageX' in event) { 6883 return event.pageX; 6884 } 6885 6886 // calculate pageX from clientX 6887 var clientX = event.clientX; 6888 var doc = document.documentElement; 6889 var body = document.body; 6890 return clientX + 6891 ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 6892 ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 6893 }; 6894 6895 /** 6896 * Adds one or more className's to the given elements style 6897 * @param {Element} elem 6898 * @param {String} className 6899 */ 6900 links.Timeline.addClassName = function(elem, className) { 6901 var classes = elem.className.split(' '); 6902 var classesToAdd = className.split(' '); 6903 6904 var added = false; 6905 for (var i=0; i<classesToAdd.length; i++) { 6906 if (classes.indexOf(classesToAdd[i]) == -1) { 6907 classes.push(classesToAdd[i]); // add the class to the array 6908 added = true; 6909 } 6910 } 6911 6912 if (added) { 6913 elem.className = classes.join(' '); 6914 } 6915 }; 6916 6917 /** 6918 * Removes one or more className's from the given elements style 6919 * @param {Element} elem 6920 * @param {String} className 6921 */ 6922 links.Timeline.removeClassName = function(elem, className) { 6923 var classes = elem.className.split(' '); 6924 var classesToRemove = className.split(' '); 6925 6926 var removed = false; 6927 for (var i=0; i<classesToRemove.length; i++) { 6928 var index = classes.indexOf(classesToRemove[i]); 6929 if (index != -1) { 6930 classes.splice(index, 1); // remove the class from the array 6931 removed = true; 6932 } 6933 } 6934 6935 if (removed) { 6936 elem.className = classes.join(' '); 6937 } 6938 }; 6939 6940 /** 6941 * Check if given object is a Javascript Array 6942 * @param {*} obj 6943 * @return {Boolean} isArray true if the given object is an array 6944 */ 6945 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni 6946 links.Timeline.isArray = function (obj) { 6947 if (obj instanceof Array) { 6948 return true; 6949 } 6950 return (Object.prototype.toString.call(obj) === '[object Array]'); 6951 }; 6952 6953 /** 6954 * Shallow clone an object 6955 * @param {Object} object 6956 * @return {Object} clone 6957 */ 6958 links.Timeline.clone = function (object) { 6959 var clone = {}; 6960 for (var prop in object) { 6961 if (object.hasOwnProperty(prop)) { 6962 clone[prop] = object[prop]; 6963 } 6964 } 6965 return clone; 6966 }; 6967 6968 /** 6969 * parse a JSON date 6970 * @param {Date | String | Number} date Date object to be parsed. Can be: 6971 * - a Date object like new Date(), 6972 * - a long like 1356970529389, 6973 * an ISO String like "2012-12-31T16:16:07.213Z", 6974 * or a .Net Date string like 6975 * "\/Date(1356970529389)\/" 6976 * @return {Date} parsedDate 6977 */ 6978 links.Timeline.parseJSONDate = function (date) { 6979 if (date == undefined) { 6980 return undefined; 6981 } 6982 6983 //test for date 6984 if (date instanceof Date) { 6985 return date; 6986 } 6987 6988 // test for MS format. 6989 // FIXME: will fail on a Number 6990 var m = date.match(/\/Date\((-?\d+)([-\+]?\d{2})?(\d{2})?\)\//i); 6991 if (m) { 6992 var offset = m[2] 6993 ? (3600000 * m[2]) // hrs offset 6994 + (60000 * m[3] * (m[2] / Math.abs(m[2]))) // mins offset 6995 : 0; 6996 6997 return new Date( 6998 (1 * m[1]) // ticks 6999 + offset 7000 ); 7001 } 7002 7003 // failing that, try to parse whatever we've got. 7004 return Date.parse(date); 7005 }; 7006