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