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-2014 Almende B.V. 31 * 32 * @author Jos de Jong, <jos@almende.org> 33 * @date 2014-07-28 34 * @version 2.9.0 35 */ 36 37 /* 38 * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/) 39 * added to v2.4.1 with da_DK language by @bjarkebech 40 */ 41 42 /* 43 * TODO 44 * 45 * Add zooming with pinching on Android 46 * 47 * Bug: when an item contains a javascript onclick or a link, this does not work 48 * when the item is not selected (when the item is being selected, 49 * it is redrawn, which cancels any onclick or link action) 50 * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly 51 * Bug: neglect items when they have no valid start/end, instead of throwing an error 52 * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically 53 * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;} 54 * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible 55 */ 56 57 /** 58 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, 59 * "links" 60 */ 61 if (typeof links === 'undefined') { 62 links = {}; 63 // important: do not use var, as "var links = {};" will overwrite 64 // the existing links variable value with undefined in IE8, IE7. 65 } 66 67 68 /** 69 * Ensure the variable google exists 70 */ 71 if (typeof google === 'undefined') { 72 google = undefined; 73 // important: do not use var, as "var google = undefined;" will overwrite 74 // the existing google variable value with undefined in IE8, IE7. 75 } 76 77 78 79 // Internet Explorer 8 and older does not support Array.indexOf, 80 // so we define it here in that case 81 // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ 82 if(!Array.prototype.indexOf) { 83 Array.prototype.indexOf = function(obj){ 84 for(var i = 0; i < this.length; i++){ 85 if(this[i] == obj){ 86 return i; 87 } 88 } 89 return -1; 90 } 91 } 92 93 // Internet Explorer 8 and older does not support Array.forEach, 94 // so we define it here in that case 95 // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach 96 if (!Array.prototype.forEach) { 97 Array.prototype.forEach = function(fn, scope) { 98 for(var i = 0, len = this.length; i < len; ++i) { 99 fn.call(scope || this, this[i], i, this); 100 } 101 } 102 } 103 104 105 /** 106 * @constructor links.Timeline 107 * The timeline is a visualization chart to visualize events in time. 108 * 109 * The timeline is developed in javascript as a Google Visualization Chart. 110 * 111 * @param {Element} container The DOM element in which the Timeline will 112 * be created. Normally a div element. 113 * @param {Object} options A name/value map containing settings for the 114 * timeline. Optional. 115 */ 116 links.Timeline = function(container, options) { 117 if (!container) { 118 // this call was probably only for inheritance, no constructor-code is required 119 return; 120 } 121 122 // create variables and set default values 123 this.dom = {}; 124 this.conversion = {}; 125 this.eventParams = {}; // stores parameters for mouse events 126 this.groups = []; 127 this.groupIndexes = {}; 128 this.items = []; 129 this.renderQueue = { 130 show: [], // Items made visible but not yet added to DOM 131 hide: [], // Items currently visible but not yet removed from DOM 132 update: [] // Items with changed data but not yet adjusted DOM 133 }; 134 this.renderedItems = []; // Items currently rendered in the DOM 135 this.clusterGenerator = new links.Timeline.ClusterGenerator(this); 136 this.currentClusters = []; 137 this.selection = undefined; // stores index and item which is currently selected 138 139 this.listeners = {}; // event listener callbacks 140 141 // Initialize sizes. 142 // Needed for IE (which gives an error when you try to set an undefined 143 // value in a style) 144 this.size = { 145 'actualHeight': 0, 146 'axis': { 147 'characterMajorHeight': 0, 148 'characterMajorWidth': 0, 149 'characterMinorHeight': 0, 150 'characterMinorWidth': 0, 151 'height': 0, 152 'labelMajorTop': 0, 153 'labelMinorTop': 0, 154 'line': 0, 155 'lineMajorWidth': 0, 156 'lineMinorHeight': 0, 157 'lineMinorTop': 0, 158 'lineMinorWidth': 0, 159 'top': 0 160 }, 161 'contentHeight': 0, 162 'contentLeft': 0, 163 'contentWidth': 0, 164 'frameHeight': 0, 165 'frameWidth': 0, 166 'groupsLeft': 0, 167 'groupsWidth': 0, 168 'items': { 169 'top': 0 170 } 171 }; 172 173 this.dom.container = container; 174 175 // 176 // Let's set the default options first 177 // 178 this.options = { 179 'width': "100%", 180 'height': "auto", 181 'minHeight': 0, // minimal height in pixels 182 'groupMinHeight': 0, 183 'autoHeight': true, 184 185 'eventMargin': 10, // minimal margin between events 186 'eventMarginAxis': 20, // minimal margin between events and the axis 187 'dragAreaWidth': 10, // pixels 188 189 'min': undefined, 190 'max': undefined, 191 'zoomMin': 10, // milliseconds 192 'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds 193 194 'moveable': true, 195 'zoomable': true, 196 'selectable': true, 197 'unselectable': true, 198 'editable': false, 199 'snapEvents': true, 200 'groupChangeable': true, 201 'timeChangeable': true, 202 203 'showCurrentTime': true, // show a red bar displaying the current time 204 'showCustomTime': false, // show a blue, draggable bar displaying a custom time 205 'showMajorLabels': true, 206 'showMinorLabels': true, 207 'showNavigation': false, 208 'showButtonNew': false, 209 'groupsOnRight': false, 210 'groupsOrder' : true, 211 'axisOnTop': false, 212 'stackEvents': true, 213 'animate': true, 214 'animateZoom': true, 215 'cluster': false, 216 'clusterMaxItems': 5, 217 'style': 'box', 218 'customStackOrder': false, //a function(a,b) for determining stackorder amongst a group of items. Essentially a comparator, -ve value for "a before b" and vice versa 219 220 // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text. 221 'locale': 'en', 222 'MONTHS': ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], 223 'MONTHS_SHORT': ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], 224 'DAYS': ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], 225 'DAYS_SHORT': ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], 226 'ZOOM_IN': "Zoom in", 227 'ZOOM_OUT': "Zoom out", 228 'MOVE_LEFT': "Move left", 229 'MOVE_RIGHT': "Move right", 230 'NEW': "New", 231 'CREATE_NEW_EVENT': "Create new event" 232 }; 233 234 // 235 // Now we can set the givenproperties 236 // 237 this.setOptions(options); 238 239 this.clientTimeOffset = 0; // difference between client time and the time 240 // set via Timeline.setCurrentTime() 241 var dom = this.dom; 242 243 // remove all elements from the container element. 244 while (dom.container.hasChildNodes()) { 245 dom.container.removeChild(dom.container.firstChild); 246 } 247 248 // create a step for drawing the axis 249 this.step = new links.Timeline.StepDate(); 250 251 // add standard item types 252 this.itemTypes = { 253 box: links.Timeline.ItemBox, 254 range: links.Timeline.ItemRange, 255 floatingRange: links.Timeline.ItemFloatingRange, 256 dot: links.Timeline.ItemDot 257 }; 258 259 // initialize data 260 this.data = []; 261 this.firstDraw = true; 262 263 // date interval must be initialized 264 this.setVisibleChartRange(undefined, undefined, false); 265 266 // render for the first time 267 this.render(); 268 269 // fire the ready event 270 var me = this; 271 setTimeout(function () { 272 me.trigger('ready'); 273 }, 0); 274 }; 275 276 277 /** 278 * Main drawing logic. This is the function that needs to be called 279 * in the html page, to draw the timeline. 280 * 281 * A data table with the events must be provided, and an options table. 282 * 283 * @param {google.visualization.DataTable} data 284 * The data containing the events for the timeline. 285 * Object DataTable is defined in 286 * google.visualization.DataTable 287 * @param {Object} options A name/value map containing settings for the 288 * timeline. Optional. The use of options here 289 * is deprecated. Pass timeline options in the 290 * constructor or use setOptions() 291 */ 292 links.Timeline.prototype.draw = function(data, options) { 293 if (options) { 294 console.log("WARNING: Passing options in draw() is deprecated. Pass options to the constructur or use setOptions() instead!"); 295 } 296 this.setOptions(options); 297 298 if (this.options.selectable) { 299 links.Timeline.addClassName(this.dom.frame, "timeline-selectable"); 300 } 301 302 // read the data 303 this.setData(data); 304 305 // set timer range. this will also redraw the timeline 306 if (options && (options.start || options.end)) { 307 this.setVisibleChartRange(options.start, options.end); 308 } 309 else if (this.firstDraw) { 310 this.setVisibleChartRangeAuto(); 311 } 312 313 this.firstDraw = false; 314 }; 315 316 317 /** 318 * Set options for the timeline. 319 * Timeline must be redrawn afterwards 320 * @param {Object} options A name/value map containing settings for the 321 * timeline. Optional. 322 */ 323 links.Timeline.prototype.setOptions = function(options) { 324 if (options) { 325 // retrieve parameter values 326 for (var i in options) { 327 if (options.hasOwnProperty(i)) { 328 this.options[i] = options[i]; 329 } 330 } 331 332 // prepare i18n dependent on set locale 333 if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') { 334 var localeOpts = links.locales[this.options.locale]; 335 if(localeOpts) { 336 for (var l in localeOpts) { 337 if (localeOpts.hasOwnProperty(l)) { 338 this.options[l] = localeOpts[l]; 339 } 340 } 341 } 342 } 343 344 // check for deprecated options 345 if (options.showButtonAdd != undefined) { 346 this.options.showButtonNew = options.showButtonAdd; 347 console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead'); 348 } 349 if (options.intervalMin != undefined) { 350 this.options.zoomMin = options.intervalMin; 351 console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead'); 352 } 353 if (options.intervalMax != undefined) { 354 this.options.zoomMax = options.intervalMax; 355 console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead'); 356 } 357 358 if (options.scale && options.step) { 359 this.step.setScale(options.scale, options.step); 360 } 361 } 362 363 // validate options 364 this.options.autoHeight = (this.options.height === "auto"); 365 }; 366 367 /** 368 * Get options for the timeline. 369 * 370 * @return the options object 371 */ 372 links.Timeline.prototype.getOptions = function() { 373 return this.options; 374 }; 375 376 /** 377 * Add new type of items 378 * @param {String} typeName Name of new type 379 * @param {links.Timeline.Item} typeFactory Constructor of items 380 */ 381 links.Timeline.prototype.addItemType = function (typeName, typeFactory) { 382 this.itemTypes[typeName] = typeFactory; 383 }; 384 385 /** 386 * Retrieve a map with the column indexes of the columns by column name. 387 * For example, the method returns the map 388 * { 389 * start: 0, 390 * end: 1, 391 * content: 2, 392 * group: undefined, 393 * className: undefined 394 * editable: undefined 395 * type: undefined 396 * } 397 * @param {google.visualization.DataTable} dataTable 398 * @type {Object} map 399 */ 400 links.Timeline.mapColumnIds = function (dataTable) { 401 var cols = {}, 402 colCount = dataTable.getNumberOfColumns(), 403 allUndefined = true; 404 405 // loop over the columns, and map the column id's to the column indexes 406 for (var col = 0; col < colCount; col++) { 407 var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); 408 cols[id] = col; 409 if (id == 'start' || id == 'end' || id == 'content' || id == 'group' || 410 id == 'className' || id == 'editable' || id == 'type') { 411 allUndefined = false; 412 } 413 } 414 415 // if no labels or ids are defined, use the default mapping 416 // for start, end, content, group, className, editable, type 417 if (allUndefined) { 418 cols.start = 0; 419 cols.end = 1; 420 cols.content = 2; 421 if (colCount > 3) {cols.group = 3} 422 if (colCount > 4) {cols.className = 4} 423 if (colCount > 5) {cols.editable = 5} 424 if (colCount > 6) {cols.type = 6} 425 } 426 427 return cols; 428 }; 429 430 /** 431 * Set data for the timeline 432 * @param {google.visualization.DataTable | Array} data 433 */ 434 links.Timeline.prototype.setData = function(data) { 435 // unselect any previously selected item 436 this.unselectItem(); 437 438 if (!data) { 439 data = []; 440 } 441 442 // clear all data 443 this.stackCancelAnimation(); 444 this.clearItems(); 445 this.data = data; 446 var items = this.items; 447 this.deleteGroups(); 448 449 if (google && google.visualization && 450 data instanceof google.visualization.DataTable) { 451 // map the datatable columns 452 var cols = links.Timeline.mapColumnIds(data); 453 454 // read DataTable 455 for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { 456 items.push(this.createItem({ 457 'start': ((cols.start != undefined) ? data.getValue(row, cols.start) : undefined), 458 'end': ((cols.end != undefined) ? data.getValue(row, cols.end) : undefined), 459 'content': ((cols.content != undefined) ? data.getValue(row, cols.content) : undefined), 460 'group': ((cols.group != undefined) ? data.getValue(row, cols.group) : undefined), 461 'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined), 462 'editable': ((cols.editable != undefined) ? data.getValue(row, cols.editable) : undefined), 463 'type': ((cols.type != undefined) ? data.getValue(row, cols.type) : undefined) 464 })); 465 } 466 } 467 else if (links.Timeline.isArray(data)) { 468 // read JSON array 469 for (var row = 0, rows = data.length; row < rows; row++) { 470 var itemData = data[row]; 471 var item = this.createItem(itemData); 472 items.push(item); 473 } 474 } 475 else { 476 throw "Unknown data type. DataTable or Array expected."; 477 } 478 479 // prepare data for clustering, by filtering and sorting by type 480 if (this.options.cluster) { 481 this.clusterGenerator.setData(this.items); 482 } 483 484 this.render({ 485 animate: false 486 }); 487 }; 488 489 /** 490 * Return the original data table. 491 * @return {google.visualization.DataTable | Array} data 492 */ 493 links.Timeline.prototype.getData = function () { 494 return this.data; 495 }; 496 497 498 /** 499 * Update the original data with changed start, end or group. 500 * 501 * @param {Number} index 502 * @param {Object} values An object containing some of the following parameters: 503 * {Date} start, 504 * {Date} end, 505 * {String} content, 506 * {String} group 507 */ 508 links.Timeline.prototype.updateData = function (index, values) { 509 var data = this.data, 510 prop; 511 512 if (google && google.visualization && 513 data instanceof google.visualization.DataTable) { 514 // update the original google DataTable 515 var missingRows = (index + 1) - data.getNumberOfRows(); 516 if (missingRows > 0) { 517 data.addRows(missingRows); 518 } 519 520 // map the column id's by name 521 var cols = links.Timeline.mapColumnIds(data); 522 523 // merge all fields from the provided data into the current data 524 for (prop in values) { 525 if (values.hasOwnProperty(prop)) { 526 var col = cols[prop]; 527 if (col == undefined) { 528 // create new column 529 var value = values[prop]; 530 var valueType = 'string'; 531 if (typeof(value) == 'number') {valueType = 'number';} 532 else if (typeof(value) == 'boolean') {valueType = 'boolean';} 533 else if (value instanceof Date) {valueType = 'datetime';} 534 col = data.addColumn(valueType, prop); 535 } 536 data.setValue(index, col, values[prop]); 537 538 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number) 539 } 540 } 541 } 542 else if (links.Timeline.isArray(data)) { 543 // update the original JSON table 544 var row = data[index]; 545 if (row == undefined) { 546 row = {}; 547 data[index] = row; 548 } 549 550 // merge all fields from the provided data into the current data 551 for (prop in values) { 552 if (values.hasOwnProperty(prop)) { 553 row[prop] = values[prop]; 554 555 // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number) 556 } 557 } 558 } 559 else { 560 throw "Cannot update data, unknown type of data"; 561 } 562 }; 563 564 /** 565 * Find the item index from a given HTML element 566 * If no item index is found, undefined is returned 567 * @param {Element} element 568 * @return {Number | undefined} index 569 */ 570 links.Timeline.prototype.getItemIndex = function(element) { 571 var e = element, 572 dom = this.dom, 573 frame = dom.items.frame, 574 items = this.items, 575 index = undefined; 576 577 // try to find the frame where the items are located in 578 while (e.parentNode && e.parentNode !== frame) { 579 e = e.parentNode; 580 } 581 582 if (e.parentNode === frame) { 583 // yes! we have found the parent element of all items 584 // retrieve its id from the array with items 585 for (var i = 0, iMax = items.length; i < iMax; i++) { 586 if (items[i].dom === e) { 587 index = i; 588 break; 589 } 590 } 591 } 592 593 return index; 594 }; 595 596 597 /** 598 * Find the cluster index from a given HTML element 599 * If no cluster index is found, undefined is returned 600 * @param {Element} element 601 * @return {Number | undefined} index 602 */ 603 links.Timeline.prototype.getClusterIndex = function(element) { 604 var e = element, 605 dom = this.dom, 606 frame = dom.items.frame, 607 clusters = this.clusters, 608 index = undefined; 609 610 if (this.clusters) { 611 // try to find the frame where the clusters are located in 612 while (e.parentNode && e.parentNode !== frame) { 613 e = e.parentNode; 614 } 615 616 if (e.parentNode === frame) { 617 // yes! we have found the parent element of all clusters 618 // retrieve its id from the array with clusters 619 for (var i = 0, iMax = clusters.length; i < iMax; i++) { 620 if (clusters[i].dom === e) { 621 index = i; 622 break; 623 } 624 } 625 } 626 } 627 628 return index; 629 }; 630 631 /** 632 * Find all elements within the start and end range 633 * If no element is found, returns an empty array 634 * @param start time 635 * @param end time 636 * @return Array itemsInRange 637 */ 638 links.Timeline.prototype.getVisibleItems = function (start, end) { 639 var items = this.items; 640 var itemsInRange = []; 641 642 if (items) { 643 for (var i = 0, iMax = items.length; i < iMax; i++) { 644 var item = items[i]; 645 if (item.end) { 646 // Time range object // NH use getLeft and getRight here 647 if (start <= item.start && item.end <= end) { 648 itemsInRange.push({"row": i}); 649 } 650 } else { 651 // Point object 652 if (start <= item.start && item.start <= end) { 653 itemsInRange.push({"row": i}); 654 } 655 } 656 } 657 } 658 659 // var sel = []; 660 // if (this.selection) { 661 // sel.push({"row": this.selection.index}); 662 // } 663 // return sel; 664 665 return itemsInRange; 666 }; 667 668 669 /** 670 * Set a new size for the timeline 671 * @param {string} width Width in pixels or percentage (for example "800px" 672 * or "50%") 673 * @param {string} height Height in pixels or percentage (for example "400px" 674 * or "30%") 675 */ 676 links.Timeline.prototype.setSize = function(width, height) { 677 if (width) { 678 this.options.width = width; 679 this.dom.frame.style.width = width; 680 } 681 if (height) { 682 this.options.height = height; 683 this.options.autoHeight = (this.options.height === "auto"); 684 if (height !== "auto" ) { 685 this.dom.frame.style.height = height; 686 } 687 } 688 689 this.render({ 690 animate: false 691 }); 692 }; 693 694 695 /** 696 * Set a new value for the visible range int the timeline. 697 * Set start undefined to include everything from the earliest date to end. 698 * Set end undefined to include everything from start to the last date. 699 * Example usage: 700 * myTimeline.setVisibleChartRange(new Date("2010-08-22"), 701 * new Date("2010-09-13")); 702 * @param {Date} start The start date for the timeline. optional 703 * @param {Date} end The end date for the timeline. optional 704 * @param {boolean} redraw Optional. If true (default) the Timeline is 705 * directly redrawn 706 */ 707 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) { 708 var range = {}; 709 if (!start || !end) { 710 // retrieve the date range of the items 711 range = this.getDataRange(true); 712 } 713 714 if (!start) { 715 if (end) { 716 if (range.min && range.min.valueOf() < end.valueOf()) { 717 // start of the data 718 start = range.min; 719 } 720 else { 721 // 7 days before the end 722 start = new Date(end.valueOf()); 723 start.setDate(start.getDate() - 7); 724 } 725 } 726 else { 727 // default of 3 days ago 728 start = new Date(); 729 start.setDate(start.getDate() - 3); 730 } 731 } 732 733 if (!end) { 734 if (range.max) { 735 // end of the data 736 end = range.max; 737 } 738 else { 739 // 7 days after start 740 end = new Date(start.valueOf()); 741 end.setDate(end.getDate() + 7); 742 } 743 } 744 745 // prevent start Date <= end Date 746 if (end <= start) { 747 end = new Date(start.valueOf()); 748 end.setDate(end.getDate() + 7); 749 } 750 751 // limit to the allowed range (don't let this do by applyRange, 752 // because that method will try to maintain the interval (end-start) 753 var min = this.options.min ? this.options.min : undefined; // date 754 if (min != undefined && start.valueOf() < min.valueOf()) { 755 start = new Date(min.valueOf()); // date 756 } 757 var max = this.options.max ? this.options.max : undefined; // date 758 if (max != undefined && end.valueOf() > max.valueOf()) { 759 end = new Date(max.valueOf()); // date 760 } 761 762 this.applyRange(start, end); 763 764 if (redraw == undefined || redraw == true) { 765 this.render({ 766 animate: false 767 }); // TODO: optimize, no reflow needed 768 } 769 else { 770 this.recalcConversion(); 771 } 772 }; 773 774 775 /** 776 * Change the visible chart range such that all items become visible 777 */ 778 links.Timeline.prototype.setVisibleChartRangeAuto = function() { 779 var range = this.getDataRange(true); 780 this.setVisibleChartRange(range.min, range.max); 781 }; 782 783 /** 784 * Adjust the visible range such that the current time is located in the center 785 * of the timeline 786 */ 787 links.Timeline.prototype.setVisibleChartRangeNow = function() { 788 var now = new Date(); 789 790 var diff = (this.end.valueOf() - this.start.valueOf()); 791 792 var startNew = new Date(now.valueOf() - diff/2); 793 var endNew = new Date(startNew.valueOf() + diff); 794 this.setVisibleChartRange(startNew, endNew); 795 }; 796 797 798 /** 799 * Retrieve the current visible range in the timeline. 800 * @return {Object} An object with start and end properties 801 */ 802 links.Timeline.prototype.getVisibleChartRange = function() { 803 return { 804 'start': new Date(this.start.valueOf()), 805 'end': new Date(this.end.valueOf()) 806 }; 807 }; 808 809 /** 810 * Get the date range of the items. 811 * @param {boolean} [withMargin] If true, 5% of whitespace is added to the 812 * left and right of the range. Default is false. 813 * @return {Object} range An object with parameters min and max. 814 * - {Date} min is the lowest start date of the items 815 * - {Date} max is the highest start or end date of the items 816 * If no data is available, the values of min and max 817 * will be undefined 818 */ 819 links.Timeline.prototype.getDataRange = function (withMargin) { 820 var items = this.items, 821 min = undefined, // number 822 max = undefined; // number 823 824 if (items) { 825 for (var i = 0, iMax = items.length; i < iMax; i++) { 826 var item = items[i], 827 start = item.start != undefined ? item.start.valueOf() : undefined, 828 end = item.end != undefined ? item.end.valueOf() : start; 829 830 if (start != undefined) { 831 min = (min != undefined) ? Math.min(min.valueOf(), start.valueOf()) : start; 832 } 833 834 if (end != undefined) { 835 max = (max != undefined) ? Math.max(max.valueOf(), end.valueOf()) : end; 836 } 837 } 838 } 839 840 if (min && max && withMargin) { 841 // zoom out 5% such that you have a little white space on the left and right 842 var diff = (max - min); 843 min = min - diff * 0.05; 844 max = max + diff * 0.05; 845 } 846 847 return { 848 'min': min != undefined ? new Date(min) : undefined, 849 'max': max != undefined ? new Date(max) : undefined 850 }; 851 }; 852 853 /** 854 * Re-render (reflow and repaint) all components of the Timeline: frame, axis, 855 * items, ... 856 * @param {Object} [options] Available options: 857 * {boolean} renderTimesLeft Number of times the 858 * render may be repeated 859 * 5 times by default. 860 * {boolean} animate takes options.animate 861 * as default value 862 */ 863 links.Timeline.prototype.render = function(options) { 864 var frameResized = this.reflowFrame(); 865 var axisResized = this.reflowAxis(); 866 var groupsResized = this.reflowGroups(); 867 var itemsResized = this.reflowItems(); 868 var resized = (frameResized || axisResized || groupsResized || itemsResized); 869 870 // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue). 871 // if (resized) { 872 var animate = this.options.animate; 873 if (options && options.animate != undefined) { 874 animate = options.animate; 875 } 876 877 this.recalcConversion(); 878 this.clusterItems(); 879 this.filterItems(); 880 this.stackItems(animate); 881 this.recalcItems(); 882 883 // TODO: only repaint when resized or when filterItems or stackItems gave a change? 884 var needsReflow = this.repaint(); 885 886 // re-render once when needed (prevent endless re-render loop) 887 if (needsReflow) { 888 var renderTimesLeft = options ? options.renderTimesLeft : undefined; 889 if (renderTimesLeft == undefined) { 890 renderTimesLeft = 5; 891 } 892 if (renderTimesLeft > 0) { 893 this.render({ 894 'animate': options ? options.animate: undefined, 895 'renderTimesLeft': (renderTimesLeft - 1) 896 }); 897 } 898 } 899 }; 900 901 /** 902 * Repaint all components of the Timeline 903 * @return {boolean} needsReflow Returns true if the DOM is changed such that 904 * a reflow is needed. 905 */ 906 links.Timeline.prototype.repaint = function() { 907 var frameNeedsReflow = this.repaintFrame(); 908 var axisNeedsReflow = this.repaintAxis(); 909 var groupsNeedsReflow = this.repaintGroups(); 910 var itemsNeedsReflow = this.repaintItems(); 911 this.repaintCurrentTime(); 912 this.repaintCustomTime(); 913 914 return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow); 915 }; 916 917 /** 918 * Reflow the timeline frame 919 * @return {boolean} resized Returns true if any of the frame elements 920 * have been resized. 921 */ 922 links.Timeline.prototype.reflowFrame = function() { 923 var dom = this.dom, 924 options = this.options, 925 size = this.size, 926 resized = false; 927 928 // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead 929 var frameWidth = dom.frame ? dom.frame.offsetWidth : 0, 930 frameHeight = dom.frame ? dom.frame.clientHeight : 0; 931 932 resized = resized || (size.frameWidth !== frameWidth); 933 resized = resized || (size.frameHeight !== frameHeight); 934 size.frameWidth = frameWidth; 935 size.frameHeight = frameHeight; 936 937 return resized; 938 }; 939 940 /** 941 * repaint the Timeline frame 942 * @return {boolean} needsReflow Returns true if the DOM is changed such that 943 * a reflow is needed. 944 */ 945 links.Timeline.prototype.repaintFrame = function() { 946 var needsReflow = false, 947 dom = this.dom, 948 options = this.options, 949 size = this.size; 950 951 // main frame 952 if (!dom.frame) { 953 dom.frame = document.createElement("DIV"); 954 dom.frame.className = "timeline-frame ui-widget ui-widget-content ui-corner-all"; 955 dom.container.appendChild(dom.frame); 956 needsReflow = true; 957 } 958 959 var height = options.autoHeight ? 960 (size.actualHeight + "px") : 961 (options.height || "100%"); 962 var width = options.width || "100%"; 963 needsReflow = needsReflow || (dom.frame.style.height != height); 964 needsReflow = needsReflow || (dom.frame.style.width != width); 965 dom.frame.style.height = height; 966 dom.frame.style.width = width; 967 968 // contents 969 if (!dom.content) { 970 // create content box where the axis and items will be created 971 dom.content = document.createElement("DIV"); 972 dom.content.className = "timeline-content"; 973 dom.frame.appendChild(dom.content); 974 975 var timelines = document.createElement("DIV"); 976 timelines.style.position = "absolute"; 977 timelines.style.left = "0px"; 978 timelines.style.top = "0px"; 979 timelines.style.height = "100%"; 980 timelines.style.width = "0px"; 981 dom.content.appendChild(timelines); 982 dom.contentTimelines = timelines; 983 984 var params = this.eventParams, 985 me = this; 986 if (!params.onMouseDown) { 987 params.onMouseDown = function (event) {me.onMouseDown(event);}; 988 links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown); 989 } 990 if (!params.onTouchStart) { 991 params.onTouchStart = function (event) {me.onTouchStart(event);}; 992 links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart); 993 } 994 if (!params.onMouseWheel) { 995 params.onMouseWheel = function (event) {me.onMouseWheel(event);}; 996 links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel); 997 } 998 if (!params.onDblClick) { 999 params.onDblClick = function (event) {me.onDblClick(event);}; 1000 links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick); 1001 } 1002 1003 needsReflow = true; 1004 } 1005 dom.content.style.left = size.contentLeft + "px"; 1006 dom.content.style.top = "0px"; 1007 dom.content.style.width = size.contentWidth + "px"; 1008 dom.content.style.height = size.frameHeight + "px"; 1009 1010 this.repaintNavigation(); 1011 1012 return needsReflow; 1013 }; 1014 1015 /** 1016 * Reflow the timeline axis. Calculate its height, width, positioning, etc... 1017 * @return {boolean} resized returns true if the axis is resized 1018 */ 1019 links.Timeline.prototype.reflowAxis = function() { 1020 var resized = false, 1021 dom = this.dom, 1022 options = this.options, 1023 size = this.size, 1024 axisDom = dom.axis; 1025 1026 var characterMinorWidth = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0, 1027 characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0, 1028 characterMajorWidth = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0, 1029 characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0, 1030 axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) + 1031 (options.showMajorLabels ? characterMajorHeight : 0); 1032 1033 var axisTop = options.axisOnTop ? 0 : size.frameHeight - axisHeight, 1034 axisLine = options.axisOnTop ? axisHeight : axisTop; 1035 1036 resized = resized || (size.axis.top !== axisTop); 1037 resized = resized || (size.axis.line !== axisLine); 1038 resized = resized || (size.axis.height !== axisHeight); 1039 size.axis.top = axisTop; 1040 size.axis.line = axisLine; 1041 size.axis.height = axisHeight; 1042 size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + 1043 (options.showMinorLabels ? characterMinorHeight : 0); 1044 size.axis.labelMinorTop = options.axisOnTop ? 1045 (options.showMajorLabels ? characterMajorHeight : 0) : 1046 axisLine; 1047 size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0; 1048 size.axis.lineMinorHeight = options.showMajorLabels ? 1049 size.frameHeight - characterMajorHeight: 1050 size.frameHeight; 1051 if (axisDom && axisDom.minorLines && axisDom.minorLines.length) { 1052 size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth; 1053 } 1054 else { 1055 size.axis.lineMinorWidth = 1; 1056 } 1057 if (axisDom && axisDom.majorLines && axisDom.majorLines.length) { 1058 size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth; 1059 } 1060 else { 1061 size.axis.lineMajorWidth = 1; 1062 } 1063 1064 resized = resized || (size.axis.characterMinorWidth !== characterMinorWidth); 1065 resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight); 1066 resized = resized || (size.axis.characterMajorWidth !== characterMajorWidth); 1067 resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight); 1068 size.axis.characterMinorWidth = characterMinorWidth; 1069 size.axis.characterMinorHeight = characterMinorHeight; 1070 size.axis.characterMajorWidth = characterMajorWidth; 1071 size.axis.characterMajorHeight = characterMajorHeight; 1072 1073 var contentHeight = Math.max(size.frameHeight - axisHeight, 0); 1074 size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth; 1075 size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0); 1076 size.contentHeight = contentHeight; 1077 1078 return resized; 1079 }; 1080 1081 /** 1082 * Redraw the timeline axis with minor and major labels 1083 * @return {boolean} needsReflow Returns true if the DOM is changed such 1084 * that a reflow is needed. 1085 */ 1086 links.Timeline.prototype.repaintAxis = function() { 1087 var needsReflow = false, 1088 dom = this.dom, 1089 options = this.options, 1090 size = this.size, 1091 step = this.step; 1092 1093 var axis = dom.axis; 1094 if (!axis) { 1095 axis = {}; 1096 dom.axis = axis; 1097 } 1098 if (!size.axis.properties) { 1099 size.axis.properties = {}; 1100 } 1101 if (!axis.minorTexts) { 1102 axis.minorTexts = []; 1103 } 1104 if (!axis.minorLines) { 1105 axis.minorLines = []; 1106 } 1107 if (!axis.majorTexts) { 1108 axis.majorTexts = []; 1109 } 1110 if (!axis.majorLines) { 1111 axis.majorLines = []; 1112 } 1113 1114 if (!axis.frame) { 1115 axis.frame = document.createElement("DIV"); 1116 axis.frame.style.position = "absolute"; 1117 axis.frame.style.left = "0px"; 1118 axis.frame.style.top = "0px"; 1119 dom.content.appendChild(axis.frame); 1120 } 1121 1122 // take axis offline 1123 dom.content.removeChild(axis.frame); 1124 1125 axis.frame.style.width = (size.contentWidth) + "px"; 1126 axis.frame.style.height = (size.axis.height) + "px"; 1127 1128 // the drawn axis is more wide than the actual visual part, such that 1129 // the axis can be dragged without having to redraw it each time again. 1130 var start = this.screenToTime(0); 1131 var end = this.screenToTime(size.contentWidth); 1132 1133 // calculate minimum step (in milliseconds) based on character size 1134 if (size.axis.characterMinorWidth) { 1135 this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) - 1136 this.screenToTime(0); 1137 1138 step.setRange(start, end, this.minimumStep); 1139 } 1140 1141 var charsNeedsReflow = this.repaintAxisCharacters(); 1142 needsReflow = needsReflow || charsNeedsReflow; 1143 1144 // The current labels on the axis will be re-used (much better performance), 1145 // therefore, the repaintAxis method uses the mechanism with 1146 // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and 1147 // this.size.axis.properties is used. 1148 this.repaintAxisStartOverwriting(); 1149 1150 step.start(); 1151 var xFirstMajorLabel = undefined; 1152 var max = 0; 1153 while (!step.end() && max < 1000) { 1154 max++; 1155 var cur = step.getCurrent(), 1156 x = this.timeToScreen(cur), 1157 isMajor = step.isMajor(); 1158 1159 if (options.showMinorLabels) { 1160 this.repaintAxisMinorText(x, step.getLabelMinor(options)); 1161 } 1162 1163 if (isMajor && options.showMajorLabels) { 1164 if (x > 0) { 1165 if (xFirstMajorLabel == undefined) { 1166 xFirstMajorLabel = x; 1167 } 1168 this.repaintAxisMajorText(x, step.getLabelMajor(options)); 1169 } 1170 this.repaintAxisMajorLine(x); 1171 } 1172 else { 1173 this.repaintAxisMinorLine(x); 1174 } 1175 1176 step.next(); 1177 } 1178 1179 // create a major label on the left when needed 1180 if (options.showMajorLabels) { 1181 var leftTime = this.screenToTime(0), 1182 leftText = this.step.getLabelMajor(options, leftTime), 1183 width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation 1184 1185 if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) { 1186 this.repaintAxisMajorText(0, leftText, leftTime); 1187 } 1188 } 1189 1190 // cleanup left over labels 1191 this.repaintAxisEndOverwriting(); 1192 1193 this.repaintAxisHorizontal(); 1194 1195 // put axis online 1196 dom.content.insertBefore(axis.frame, dom.content.firstChild); 1197 1198 return needsReflow; 1199 }; 1200 1201 /** 1202 * Create characters used to determine the size of text on the axis 1203 * @return {boolean} needsReflow Returns true if the DOM is changed such that 1204 * a reflow is needed. 1205 */ 1206 links.Timeline.prototype.repaintAxisCharacters = function () { 1207 // calculate the width and height of a single character 1208 // this is used to calculate the step size, and also the positioning of the 1209 // axis 1210 var needsReflow = false, 1211 dom = this.dom, 1212 axis = dom.axis, 1213 text; 1214 1215 if (!axis.characterMinor) { 1216 text = document.createTextNode("0"); 1217 var characterMinor = document.createElement("DIV"); 1218 characterMinor.className = "timeline-axis-text timeline-axis-text-minor"; 1219 characterMinor.appendChild(text); 1220 characterMinor.style.position = "absolute"; 1221 characterMinor.style.visibility = "hidden"; 1222 characterMinor.style.paddingLeft = "0px"; 1223 characterMinor.style.paddingRight = "0px"; 1224 axis.frame.appendChild(characterMinor); 1225 1226 axis.characterMinor = characterMinor; 1227 needsReflow = true; 1228 } 1229 1230 if (!axis.characterMajor) { 1231 text = document.createTextNode("0"); 1232 var characterMajor = document.createElement("DIV"); 1233 characterMajor.className = "timeline-axis-text timeline-axis-text-major"; 1234 characterMajor.appendChild(text); 1235 characterMajor.style.position = "absolute"; 1236 characterMajor.style.visibility = "hidden"; 1237 characterMajor.style.paddingLeft = "0px"; 1238 characterMajor.style.paddingRight = "0px"; 1239 axis.frame.appendChild(characterMajor); 1240 1241 axis.characterMajor = characterMajor; 1242 needsReflow = true; 1243 } 1244 1245 return needsReflow; 1246 }; 1247 1248 /** 1249 * Initialize redraw of the axis. All existing labels and lines will be 1250 * overwritten and reused. 1251 */ 1252 links.Timeline.prototype.repaintAxisStartOverwriting = function () { 1253 var properties = this.size.axis.properties; 1254 1255 properties.minorTextNum = 0; 1256 properties.minorLineNum = 0; 1257 properties.majorTextNum = 0; 1258 properties.majorLineNum = 0; 1259 }; 1260 1261 /** 1262 * End of overwriting HTML DOM elements of the axis. 1263 * remaining elements will be removed 1264 */ 1265 links.Timeline.prototype.repaintAxisEndOverwriting = function () { 1266 var dom = this.dom, 1267 props = this.size.axis.properties, 1268 frame = this.dom.axis.frame, 1269 num; 1270 1271 // remove leftovers 1272 var minorTexts = dom.axis.minorTexts; 1273 num = props.minorTextNum; 1274 while (minorTexts.length > num) { 1275 var minorText = minorTexts[num]; 1276 frame.removeChild(minorText); 1277 minorTexts.splice(num, 1); 1278 } 1279 1280 var minorLines = dom.axis.minorLines; 1281 num = props.minorLineNum; 1282 while (minorLines.length > num) { 1283 var minorLine = minorLines[num]; 1284 frame.removeChild(minorLine); 1285 minorLines.splice(num, 1); 1286 } 1287 1288 var majorTexts = dom.axis.majorTexts; 1289 num = props.majorTextNum; 1290 while (majorTexts.length > num) { 1291 var majorText = majorTexts[num]; 1292 frame.removeChild(majorText); 1293 majorTexts.splice(num, 1); 1294 } 1295 1296 var majorLines = dom.axis.majorLines; 1297 num = props.majorLineNum; 1298 while (majorLines.length > num) { 1299 var majorLine = majorLines[num]; 1300 frame.removeChild(majorLine); 1301 majorLines.splice(num, 1); 1302 } 1303 }; 1304 1305 /** 1306 * Repaint the horizontal line and background of the axis 1307 */ 1308 links.Timeline.prototype.repaintAxisHorizontal = function() { 1309 var axis = this.dom.axis, 1310 size = this.size, 1311 options = this.options; 1312 1313 // line behind all axis elements (possibly having a background color) 1314 var hasAxis = (options.showMinorLabels || options.showMajorLabels); 1315 if (hasAxis) { 1316 if (!axis.backgroundLine) { 1317 // create the axis line background (for a background color or so) 1318 var backgroundLine = document.createElement("DIV"); 1319 backgroundLine.className = "timeline-axis"; 1320 backgroundLine.style.position = "absolute"; 1321 backgroundLine.style.left = "0px"; 1322 backgroundLine.style.width = "100%"; 1323 backgroundLine.style.border = "none"; 1324 axis.frame.insertBefore(backgroundLine, axis.frame.firstChild); 1325 1326 axis.backgroundLine = backgroundLine; 1327 } 1328 1329 if (axis.backgroundLine) { 1330 axis.backgroundLine.style.top = size.axis.top + "px"; 1331 axis.backgroundLine.style.height = size.axis.height + "px"; 1332 } 1333 } 1334 else { 1335 if (axis.backgroundLine) { 1336 axis.frame.removeChild(axis.backgroundLine); 1337 delete axis.backgroundLine; 1338 } 1339 } 1340 1341 // line before all axis elements 1342 if (hasAxis) { 1343 if (axis.line) { 1344 // put this line at the end of all childs 1345 var line = axis.frame.removeChild(axis.line); 1346 axis.frame.appendChild(line); 1347 } 1348 else { 1349 // make the axis line 1350 var line = document.createElement("DIV"); 1351 line.className = "timeline-axis"; 1352 line.style.position = "absolute"; 1353 line.style.left = "0px"; 1354 line.style.width = "100%"; 1355 line.style.height = "0px"; 1356 axis.frame.appendChild(line); 1357 1358 axis.line = line; 1359 } 1360 1361 axis.line.style.top = size.axis.line + "px"; 1362 } 1363 else { 1364 if (axis.line && axis.line.parentElement) { 1365 axis.frame.removeChild(axis.line); 1366 delete axis.line; 1367 } 1368 } 1369 }; 1370 1371 /** 1372 * Create a minor label for the axis at position x 1373 * @param {Number} x 1374 * @param {String} text 1375 */ 1376 links.Timeline.prototype.repaintAxisMinorText = function (x, text) { 1377 var size = this.size, 1378 dom = this.dom, 1379 props = size.axis.properties, 1380 frame = dom.axis.frame, 1381 minorTexts = dom.axis.minorTexts, 1382 index = props.minorTextNum, 1383 label; 1384 1385 if (index < minorTexts.length) { 1386 label = minorTexts[index] 1387 } 1388 else { 1389 // create new label 1390 var content = document.createTextNode(""); 1391 label = document.createElement("DIV"); 1392 label.appendChild(content); 1393 label.className = "timeline-axis-text timeline-axis-text-minor"; 1394 label.style.position = "absolute"; 1395 1396 frame.appendChild(label); 1397 1398 minorTexts.push(label); 1399 } 1400 1401 label.childNodes[0].nodeValue = text; 1402 label.style.left = x + "px"; 1403 label.style.top = size.axis.labelMinorTop + "px"; 1404 //label.title = title; // TODO: this is a heavy operation 1405 1406 props.minorTextNum++; 1407 }; 1408 1409 /** 1410 * Create a minor line for the axis at position x 1411 * @param {Number} x 1412 */ 1413 links.Timeline.prototype.repaintAxisMinorLine = function (x) { 1414 var axis = this.size.axis, 1415 dom = this.dom, 1416 props = axis.properties, 1417 frame = dom.axis.frame, 1418 minorLines = dom.axis.minorLines, 1419 index = props.minorLineNum, 1420 line; 1421 1422 if (index < minorLines.length) { 1423 line = minorLines[index]; 1424 } 1425 else { 1426 // create vertical line 1427 line = document.createElement("DIV"); 1428 line.className = "timeline-axis-grid timeline-axis-grid-minor"; 1429 line.style.position = "absolute"; 1430 line.style.width = "0px"; 1431 1432 frame.appendChild(line); 1433 minorLines.push(line); 1434 } 1435 1436 line.style.top = axis.lineMinorTop + "px"; 1437 line.style.height = axis.lineMinorHeight + "px"; 1438 line.style.left = (x - axis.lineMinorWidth/2) + "px"; 1439 1440 props.minorLineNum++; 1441 }; 1442 1443 /** 1444 * Create a Major label for the axis at position x 1445 * @param {Number} x 1446 * @param {String} text 1447 */ 1448 links.Timeline.prototype.repaintAxisMajorText = function (x, text) { 1449 var size = this.size, 1450 props = size.axis.properties, 1451 frame = this.dom.axis.frame, 1452 majorTexts = this.dom.axis.majorTexts, 1453 index = props.majorTextNum, 1454 label; 1455 1456 if (index < majorTexts.length) { 1457 label = majorTexts[index]; 1458 } 1459 else { 1460 // create label 1461 var content = document.createTextNode(text); 1462 label = document.createElement("DIV"); 1463 label.className = "timeline-axis-text timeline-axis-text-major"; 1464 label.appendChild(content); 1465 label.style.position = "absolute"; 1466 label.style.top = "0px"; 1467 1468 frame.appendChild(label); 1469 majorTexts.push(label); 1470 } 1471 1472 label.childNodes[0].nodeValue = text; 1473 label.style.top = size.axis.labelMajorTop + "px"; 1474 label.style.left = x + "px"; 1475 //label.title = title; // TODO: this is a heavy operation 1476 1477 props.majorTextNum ++; 1478 }; 1479 1480 /** 1481 * Create a Major line for the axis at position x 1482 * @param {Number} x 1483 */ 1484 links.Timeline.prototype.repaintAxisMajorLine = function (x) { 1485 var size = this.size, 1486 props = size.axis.properties, 1487 axis = this.size.axis, 1488 frame = this.dom.axis.frame, 1489 majorLines = this.dom.axis.majorLines, 1490 index = props.majorLineNum, 1491 line; 1492 1493 if (index < majorLines.length) { 1494 line = majorLines[index]; 1495 } 1496 else { 1497 // create vertical line 1498 line = document.createElement("DIV"); 1499 line.className = "timeline-axis-grid timeline-axis-grid-major"; 1500 line.style.position = "absolute"; 1501 line.style.top = "0px"; 1502 line.style.width = "0px"; 1503 1504 frame.appendChild(line); 1505 majorLines.push(line); 1506 } 1507 1508 line.style.left = (x - axis.lineMajorWidth/2) + "px"; 1509 line.style.height = size.frameHeight + "px"; 1510 1511 props.majorLineNum ++; 1512 }; 1513 1514 /** 1515 * Reflow all items, retrieve their actual size 1516 * @return {boolean} resized returns true if any of the items is resized 1517 */ 1518 links.Timeline.prototype.reflowItems = function() { 1519 var resized = false, 1520 i, 1521 iMax, 1522 group, 1523 groups = this.groups, 1524 renderedItems = this.renderedItems; 1525 1526 if (groups) { // TODO: need to check if labels exists? 1527 // loop through all groups to reset the items height 1528 groups.forEach(function (group) { 1529 group.itemsHeight = 0; 1530 }); 1531 } 1532 1533 // loop through the width and height of all visible items 1534 for (i = 0, iMax = renderedItems.length; i < iMax; i++) { 1535 var item = renderedItems[i], 1536 domItem = item.dom; 1537 group = item.group; 1538 1539 if (domItem) { 1540 // TODO: move updating width and height into item.reflow 1541 var width = domItem ? domItem.clientWidth : 0; 1542 var height = domItem ? domItem.clientHeight : 0; 1543 resized = resized || (item.width != width); 1544 resized = resized || (item.height != height); 1545 item.width = width; 1546 item.height = height; 1547 //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth 1548 item.reflow(); 1549 } 1550 1551 if (group) { 1552 group.itemsHeight = Math.max(this.options.groupMinHeight,group.itemsHeight ? 1553 Math.max(group.itemsHeight, item.height) : 1554 item.height); 1555 } 1556 } 1557 1558 return resized; 1559 }; 1560 1561 /** 1562 * Recalculate item properties: 1563 * - the height of each group. 1564 * - the actualHeight, from the stacked items or the sum of the group heights 1565 * @return {boolean} resized returns true if any of the items properties is 1566 * changed 1567 */ 1568 links.Timeline.prototype.recalcItems = function () { 1569 var resized = false, 1570 i, 1571 iMax, 1572 item, 1573 finalItem, 1574 finalItems, 1575 group, 1576 groups = this.groups, 1577 size = this.size, 1578 options = this.options, 1579 renderedItems = this.renderedItems; 1580 1581 var actualHeight = 0; 1582 if (groups.length == 0) { 1583 // calculate actual height of the timeline when there are no groups 1584 // but stacked items 1585 if (options.autoHeight || options.cluster) { 1586 var min = 0, 1587 max = 0; 1588 1589 if (this.stack && this.stack.finalItems) { 1590 // adjust the offset of all finalItems when the actualHeight has been changed 1591 finalItems = this.stack.finalItems; 1592 finalItem = finalItems[0]; 1593 if (finalItem && finalItem.top) { 1594 min = finalItem.top; 1595 max = finalItem.top + finalItem.height; 1596 } 1597 for (i = 1, iMax = finalItems.length; i < iMax; i++) { 1598 finalItem = finalItems[i]; 1599 min = Math.min(min, finalItem.top); 1600 max = Math.max(max, finalItem.top + finalItem.height); 1601 } 1602 } 1603 else { 1604 item = renderedItems[0]; 1605 if (item && item.top) { 1606 min = item.top; 1607 max = item.top + item.height; 1608 } 1609 for (i = 1, iMax = renderedItems.length; i < iMax; i++) { 1610 item = renderedItems[i]; 1611 if (item.top) { 1612 min = Math.min(min, item.top); 1613 max = Math.max(max, (item.top + item.height)); 1614 } 1615 } 1616 } 1617 1618 actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height; 1619 if (actualHeight < options.minHeight) { 1620 actualHeight = options.minHeight; 1621 } 1622 1623 if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) { 1624 // adjust the offset of all items when the actualHeight has been changed 1625 var diff = actualHeight - size.actualHeight; 1626 if (this.stack && this.stack.finalItems) { 1627 finalItems = this.stack.finalItems; 1628 for (i = 0, iMax = finalItems.length; i < iMax; i++) { 1629 finalItems[i].top += diff; 1630 finalItems[i].item.top += diff; 1631 } 1632 } 1633 else { 1634 for (i = 0, iMax = renderedItems.length; i < iMax; i++) { 1635 renderedItems[i].top += diff; 1636 } 1637 } 1638 } 1639 } 1640 } 1641 else { 1642 // loop through all groups to get the height of each group, and the 1643 // total height 1644 actualHeight = size.axis.height + 2 * options.eventMarginAxis; 1645 for (i = 0, iMax = groups.length; i < iMax; i++) { 1646 group = groups[i]; 1647 1648 // 1649 // TODO: Do we want to apply a max height? how ? 1650 // 1651 var groupHeight = group.itemsHeight; 1652 resized = resized || (groupHeight != group.height); 1653 group.height = Math.max(groupHeight, options.groupMinHeight); 1654 1655 actualHeight += groups[i].height + options.eventMargin; 1656 } 1657 1658 // calculate top positions of the group labels and lines 1659 var eventMargin = options.eventMargin, 1660 top = options.axisOnTop ? 1661 options.eventMarginAxis + eventMargin/2 : 1662 size.contentHeight - options.eventMarginAxis + eventMargin/ 2, 1663 axisHeight = size.axis.height; 1664 1665 for (i = 0, iMax = groups.length; i < iMax; i++) { 1666 group = groups[i]; 1667 if (options.axisOnTop) { 1668 group.top = top + axisHeight; 1669 group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2; 1670 group.lineTop = top + axisHeight + group.height + eventMargin/2; 1671 top += group.height + eventMargin; 1672 } 1673 else { 1674 top -= group.height + eventMargin; 1675 group.top = top; 1676 group.labelTop = top + (group.height - group.labelHeight) / 2; 1677 group.lineTop = top - eventMargin/2; 1678 } 1679 } 1680 1681 resized = true; 1682 } 1683 1684 if (actualHeight < options.minHeight) { 1685 actualHeight = options.minHeight; 1686 } 1687 resized = resized || (actualHeight != size.actualHeight); 1688 size.actualHeight = actualHeight; 1689 1690 return resized; 1691 }; 1692 1693 /** 1694 * This method clears the (internal) array this.items in a safe way: neatly 1695 * cleaning up the DOM, and accompanying arrays this.renderedItems and 1696 * the created clusters. 1697 */ 1698 links.Timeline.prototype.clearItems = function() { 1699 // add all visible items to the list to be hidden 1700 var hideItems = this.renderQueue.hide; 1701 this.renderedItems.forEach(function (item) { 1702 hideItems.push(item); 1703 }); 1704 1705 // clear the cluster generator 1706 this.clusterGenerator.clear(); 1707 1708 // actually clear the items 1709 this.items = []; 1710 }; 1711 1712 /** 1713 * Repaint all items 1714 * @return {boolean} needsReflow Returns true if the DOM is changed such that 1715 * a reflow is needed. 1716 */ 1717 links.Timeline.prototype.repaintItems = function() { 1718 var i, iMax, item, index; 1719 1720 var needsReflow = false, 1721 dom = this.dom, 1722 size = this.size, 1723 timeline = this, 1724 renderedItems = this.renderedItems; 1725 1726 if (!dom.items) { 1727 dom.items = {}; 1728 } 1729 1730 // draw the frame containing the items 1731 var frame = dom.items.frame; 1732 if (!frame) { 1733 frame = document.createElement("DIV"); 1734 frame.style.position = "relative"; 1735 dom.content.appendChild(frame); 1736 dom.items.frame = frame; 1737 } 1738 1739 frame.style.left = "0px"; 1740 frame.style.top = size.items.top + "px"; 1741 frame.style.height = "0px"; 1742 1743 // Take frame offline (for faster manipulation of the DOM) 1744 dom.content.removeChild(frame); 1745 1746 // process the render queue with changes 1747 var queue = this.renderQueue; 1748 var newImageUrls = []; 1749 needsReflow = needsReflow || 1750 (queue.show.length > 0) || 1751 (queue.update.length > 0) || 1752 (queue.hide.length > 0); // TODO: reflow needed on hide of items? 1753 1754 while (item = queue.show.shift()) { 1755 item.showDOM(frame); 1756 item.getImageUrls(newImageUrls); 1757 renderedItems.push(item); 1758 } 1759 while (item = queue.update.shift()) { 1760 item.updateDOM(frame); 1761 item.getImageUrls(newImageUrls); 1762 index = this.renderedItems.indexOf(item); 1763 if (index == -1) { 1764 renderedItems.push(item); 1765 } 1766 } 1767 while (item = queue.hide.shift()) { 1768 item.hideDOM(frame); 1769 index = this.renderedItems.indexOf(item); 1770 if (index != -1) { 1771 renderedItems.splice(index, 1); 1772 } 1773 } 1774 1775 // reposition all visible items 1776 renderedItems.forEach(function (item) { 1777 item.updatePosition(timeline); 1778 }); 1779 1780 // redraw the delete button and dragareas of the selected item (if any) 1781 this.repaintDeleteButton(); 1782 this.repaintDragAreas(); 1783 1784 // put frame online again 1785 dom.content.appendChild(frame); 1786 1787 if (newImageUrls.length) { 1788 // retrieve all image sources from the items, and set a callback once 1789 // all images are retrieved 1790 var callback = function () { 1791 timeline.render(); 1792 }; 1793 var sendCallbackWhenAlreadyLoaded = false; 1794 links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded); 1795 } 1796 1797 return needsReflow; 1798 }; 1799 1800 /** 1801 * Reflow the size of the groups 1802 * @return {boolean} resized Returns true if any of the frame elements 1803 * have been resized. 1804 */ 1805 links.Timeline.prototype.reflowGroups = function() { 1806 var resized = false, 1807 options = this.options, 1808 size = this.size, 1809 dom = this.dom; 1810 1811 // calculate the groups width and height 1812 // TODO: only update when data is changed! -> use an updateSeq 1813 var groupsWidth = 0; 1814 1815 // loop through all groups to get the labels width and height 1816 var groups = this.groups; 1817 var labels = this.dom.groups ? this.dom.groups.labels : []; 1818 for (var i = 0, iMax = groups.length; i < iMax; i++) { 1819 var group = groups[i]; 1820 var label = labels[i]; 1821 group.labelWidth = label ? label.clientWidth : 0; 1822 group.labelHeight = label ? label.clientHeight : 0; 1823 group.width = group.labelWidth; // TODO: group.width is redundant with labelWidth 1824 1825 groupsWidth = Math.max(groupsWidth, group.width); 1826 } 1827 1828 // limit groupsWidth to the groups width in the options 1829 if (options.groupsWidth !== undefined) { 1830 groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0; 1831 } 1832 1833 // compensate for the border width. TODO: calculate the real border width 1834 groupsWidth += 1; 1835 1836 var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0; 1837 resized = resized || (size.groupsWidth !== groupsWidth); 1838 resized = resized || (size.groupsLeft !== groupsLeft); 1839 size.groupsWidth = groupsWidth; 1840 size.groupsLeft = groupsLeft; 1841 1842 return resized; 1843 }; 1844 1845 /** 1846 * Redraw the group labels 1847 */ 1848 links.Timeline.prototype.repaintGroups = function() { 1849 var dom = this.dom, 1850 timeline = this, 1851 options = this.options, 1852 size = this.size, 1853 groups = this.groups; 1854 1855 if (dom.groups === undefined) { 1856 dom.groups = {}; 1857 } 1858 1859 var labels = dom.groups.labels; 1860 if (!labels) { 1861 labels = []; 1862 dom.groups.labels = labels; 1863 } 1864 var labelLines = dom.groups.labelLines; 1865 if (!labelLines) { 1866 labelLines = []; 1867 dom.groups.labelLines = labelLines; 1868 } 1869 var itemLines = dom.groups.itemLines; 1870 if (!itemLines) { 1871 itemLines = []; 1872 dom.groups.itemLines = itemLines; 1873 } 1874 1875 // create the frame for holding the groups 1876 var frame = dom.groups.frame; 1877 if (!frame) { 1878 frame = document.createElement("DIV"); 1879 frame.className = "timeline-groups-axis"; 1880 frame.style.position = "absolute"; 1881 frame.style.overflow = "hidden"; 1882 frame.style.top = "0px"; 1883 frame.style.height = "100%"; 1884 1885 dom.frame.appendChild(frame); 1886 dom.groups.frame = frame; 1887 } 1888 1889 frame.style.left = size.groupsLeft + "px"; 1890 frame.style.width = (options.groupsWidth !== undefined) ? 1891 options.groupsWidth : 1892 size.groupsWidth + "px"; 1893 1894 // hide groups axis when there are no groups 1895 if (groups.length == 0) { 1896 frame.style.display = 'none'; 1897 } 1898 else { 1899 frame.style.display = ''; 1900 } 1901 1902 // TODO: only create/update groups when data is changed. 1903 1904 // create the items 1905 var current = labels.length, 1906 needed = groups.length; 1907 1908 // overwrite existing group labels 1909 for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) { 1910 var group = groups[i]; 1911 var label = labels[i]; 1912 label.innerHTML = this.getGroupName(group); 1913 label.style.display = ''; 1914 } 1915 1916 // append new items when needed 1917 for (var i = current; i < needed; i++) { 1918 var group = groups[i]; 1919 1920 // create text label 1921 var label = document.createElement("DIV"); 1922 label.className = "timeline-groups-text"; 1923 label.style.position = "absolute"; 1924 if (options.groupsWidth === undefined) { 1925 label.style.whiteSpace = "nowrap"; 1926 } 1927 label.innerHTML = this.getGroupName(group); 1928 frame.appendChild(label); 1929 labels[i] = label; 1930 1931 // create the grid line between the group labels 1932 var labelLine = document.createElement("DIV"); 1933 labelLine.className = "timeline-axis-grid timeline-axis-grid-minor"; 1934 labelLine.style.position = "absolute"; 1935 labelLine.style.left = "0px"; 1936 labelLine.style.width = "100%"; 1937 labelLine.style.height = "0px"; 1938 labelLine.style.borderTopStyle = "solid"; 1939 frame.appendChild(labelLine); 1940 labelLines[i] = labelLine; 1941 1942 // create the grid line between the items 1943 var itemLine = document.createElement("DIV"); 1944 itemLine.className = "timeline-axis-grid timeline-axis-grid-minor"; 1945 itemLine.style.position = "absolute"; 1946 itemLine.style.left = "0px"; 1947 itemLine.style.width = "100%"; 1948 itemLine.style.height = "0px"; 1949 itemLine.style.borderTopStyle = "solid"; 1950 dom.content.insertBefore(itemLine, dom.content.firstChild); 1951 itemLines[i] = itemLine; 1952 } 1953 1954 // remove redundant items from the DOM when needed 1955 for (var i = needed; i < current; i++) { 1956 var label = labels[i], 1957 labelLine = labelLines[i], 1958 itemLine = itemLines[i]; 1959 1960 frame.removeChild(label); 1961 frame.removeChild(labelLine); 1962 dom.content.removeChild(itemLine); 1963 } 1964 labels.splice(needed, current - needed); 1965 labelLines.splice(needed, current - needed); 1966 itemLines.splice(needed, current - needed); 1967 1968 links.Timeline.addClassName(frame, options.groupsOnRight ? 'timeline-groups-axis-onright' : 'timeline-groups-axis-onleft'); 1969 1970 // position the groups 1971 for (var i = 0, iMax = groups.length; i < iMax; i++) { 1972 var group = groups[i], 1973 label = labels[i], 1974 labelLine = labelLines[i], 1975 itemLine = itemLines[i]; 1976 1977 label.style.top = group.labelTop + "px"; 1978 labelLine.style.top = group.lineTop + "px"; 1979 itemLine.style.top = group.lineTop + "px"; 1980 itemLine.style.width = size.contentWidth + "px"; 1981 } 1982 1983 if (!dom.groups.background) { 1984 // create the axis grid line background 1985 var background = document.createElement("DIV"); 1986 background.className = "timeline-axis"; 1987 background.style.position = "absolute"; 1988 background.style.left = "0px"; 1989 background.style.width = "100%"; 1990 background.style.border = "none"; 1991 1992 frame.appendChild(background); 1993 dom.groups.background = background; 1994 } 1995 dom.groups.background.style.top = size.axis.top + 'px'; 1996 dom.groups.background.style.height = size.axis.height + 'px'; 1997 1998 if (!dom.groups.line) { 1999 // create the axis grid line 2000 var line = document.createElement("DIV"); 2001 line.className = "timeline-axis"; 2002 line.style.position = "absolute"; 2003 line.style.left = "0px"; 2004 line.style.width = "100%"; 2005 line.style.height = "0px"; 2006 2007 frame.appendChild(line); 2008 dom.groups.line = line; 2009 } 2010 dom.groups.line.style.top = size.axis.line + 'px'; 2011 2012 // create a callback when there are images which are not yet loaded 2013 // TODO: more efficiently load images in the groups 2014 if (dom.groups.frame && groups.length) { 2015 var imageUrls = []; 2016 links.imageloader.filterImageUrls(dom.groups.frame, imageUrls); 2017 if (imageUrls.length) { 2018 // retrieve all image sources from the items, and set a callback once 2019 // all images are retrieved 2020 var callback = function () { 2021 timeline.render(); 2022 }; 2023 var sendCallbackWhenAlreadyLoaded = false; 2024 links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded); 2025 } 2026 } 2027 }; 2028 2029 2030 /** 2031 * Redraw the current time bar 2032 */ 2033 links.Timeline.prototype.repaintCurrentTime = function() { 2034 var options = this.options, 2035 dom = this.dom, 2036 size = this.size; 2037 2038 if (!options.showCurrentTime) { 2039 if (dom.currentTime) { 2040 dom.contentTimelines.removeChild(dom.currentTime); 2041 delete dom.currentTime; 2042 } 2043 2044 return; 2045 } 2046 2047 if (!dom.currentTime) { 2048 // create the current time bar 2049 var currentTime = document.createElement("DIV"); 2050 currentTime.className = "timeline-currenttime"; 2051 currentTime.style.position = "absolute"; 2052 currentTime.style.top = "0px"; 2053 currentTime.style.height = "100%"; 2054 2055 dom.contentTimelines.appendChild(currentTime); 2056 dom.currentTime = currentTime; 2057 } 2058 2059 var now = new Date(); 2060 var nowOffset = new Date(now.valueOf() + this.clientTimeOffset); 2061 var x = this.timeToScreen(nowOffset); 2062 2063 var visible = (x > -size.contentWidth && x < 2 * size.contentWidth); 2064 dom.currentTime.style.display = visible ? '' : 'none'; 2065 dom.currentTime.style.left = x + "px"; 2066 dom.currentTime.title = "Current time: " + nowOffset; 2067 2068 // start a timer to adjust for the new time 2069 if (this.currentTimeTimer != undefined) { 2070 clearTimeout(this.currentTimeTimer); 2071 delete this.currentTimeTimer; 2072 } 2073 var timeline = this; 2074 var onTimeout = function() { 2075 timeline.repaintCurrentTime(); 2076 }; 2077 // the time equal to the width of one pixel, divided by 2 for more smoothness 2078 var interval = 1 / this.conversion.factor / 2; 2079 if (interval < 30) interval = 30; 2080 this.currentTimeTimer = setTimeout(onTimeout, interval); 2081 }; 2082 2083 /** 2084 * Redraw the custom time bar 2085 */ 2086 links.Timeline.prototype.repaintCustomTime = function() { 2087 var options = this.options, 2088 dom = this.dom, 2089 size = this.size; 2090 2091 if (!options.showCustomTime) { 2092 if (dom.customTime) { 2093 dom.contentTimelines.removeChild(dom.customTime); 2094 delete dom.customTime; 2095 } 2096 2097 return; 2098 } 2099 2100 if (!dom.customTime) { 2101 var customTime = document.createElement("DIV"); 2102 customTime.className = "timeline-customtime"; 2103 customTime.style.position = "absolute"; 2104 customTime.style.top = "0px"; 2105 customTime.style.height = "100%"; 2106 2107 var drag = document.createElement("DIV"); 2108 drag.style.position = "relative"; 2109 drag.style.top = "0px"; 2110 drag.style.left = "-10px"; 2111 drag.style.height = "100%"; 2112 drag.style.width = "20px"; 2113 customTime.appendChild(drag); 2114 2115 dom.contentTimelines.appendChild(customTime); 2116 dom.customTime = customTime; 2117 2118 // initialize parameter 2119 this.customTime = new Date(); 2120 } 2121 2122 var x = this.timeToScreen(this.customTime), 2123 visible = (x > -size.contentWidth && x < 2 * size.contentWidth); 2124 dom.customTime.style.display = visible ? '' : 'none'; 2125 dom.customTime.style.left = x + "px"; 2126 dom.customTime.title = "Time: " + this.customTime; 2127 }; 2128 2129 2130 /** 2131 * Redraw the delete button, on the top right of the currently selected item 2132 * if there is no item selected, the button is hidden. 2133 */ 2134 links.Timeline.prototype.repaintDeleteButton = function () { 2135 var timeline = this, 2136 dom = this.dom, 2137 frame = dom.items.frame; 2138 2139 var deleteButton = dom.items.deleteButton; 2140 if (!deleteButton) { 2141 // create a delete button 2142 deleteButton = document.createElement("DIV"); 2143 deleteButton.className = "timeline-navigation-delete"; 2144 deleteButton.style.position = "absolute"; 2145 2146 frame.appendChild(deleteButton); 2147 dom.items.deleteButton = deleteButton; 2148 } 2149 2150 var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1, 2151 item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined; 2152 if (item && item.rendered && this.isEditable(item)) { 2153 var right = item.getRight(this), 2154 top = item.top; 2155 2156 deleteButton.style.left = right + 'px'; 2157 deleteButton.style.top = top + 'px'; 2158 deleteButton.style.display = ''; 2159 frame.removeChild(deleteButton); 2160 frame.appendChild(deleteButton); 2161 } 2162 else { 2163 deleteButton.style.display = 'none'; 2164 } 2165 }; 2166 2167 2168 /** 2169 * Redraw the drag areas. When an item (ranges only) is selected, 2170 * it gets a drag area on the left and right side, to change its width 2171 */ 2172 links.Timeline.prototype.repaintDragAreas = function () { 2173 var timeline = this, 2174 options = this.options, 2175 dom = this.dom, 2176 frame = this.dom.items.frame; 2177 2178 // create left drag area 2179 var dragLeft = dom.items.dragLeft; 2180 if (!dragLeft) { 2181 dragLeft = document.createElement("DIV"); 2182 dragLeft.className="timeline-event-range-drag-left"; 2183 dragLeft.style.position = "absolute"; 2184 2185 frame.appendChild(dragLeft); 2186 dom.items.dragLeft = dragLeft; 2187 } 2188 2189 // create right drag area 2190 var dragRight = dom.items.dragRight; 2191 if (!dragRight) { 2192 dragRight = document.createElement("DIV"); 2193 dragRight.className="timeline-event-range-drag-right"; 2194 dragRight.style.position = "absolute"; 2195 2196 frame.appendChild(dragRight); 2197 dom.items.dragRight = dragRight; 2198 } 2199 2200 // reposition left and right drag area 2201 var index = (this.selection && this.selection.index !== undefined) ? this.selection.index : -1, 2202 item = (this.selection && this.selection.index !== undefined) ? this.items[index] : undefined; 2203 if (item && item.rendered && this.isEditable(item) && 2204 (item instanceof links.Timeline.ItemRange || item instanceof links.Timeline.ItemFloatingRange)) { 2205 var left = item.getLeft(this), // NH change to getLeft 2206 right = item.getRight(this), // NH change to getRight 2207 top = item.top, 2208 height = item.height; 2209 2210 dragLeft.style.left = left + 'px'; 2211 dragLeft.style.top = top + 'px'; 2212 dragLeft.style.width = options.dragAreaWidth + "px"; 2213 dragLeft.style.height = height + 'px'; 2214 dragLeft.style.display = ''; 2215 frame.removeChild(dragLeft); 2216 frame.appendChild(dragLeft); 2217 2218 dragRight.style.left = (right - options.dragAreaWidth) + 'px'; 2219 dragRight.style.top = top + 'px'; 2220 dragRight.style.width = options.dragAreaWidth + "px"; 2221 dragRight.style.height = height + 'px'; 2222 dragRight.style.display = ''; 2223 frame.removeChild(dragRight); 2224 frame.appendChild(dragRight); 2225 } 2226 else { 2227 dragLeft.style.display = 'none'; 2228 dragRight.style.display = 'none'; 2229 } 2230 }; 2231 2232 /** 2233 * Create the navigation buttons for zooming and moving 2234 */ 2235 links.Timeline.prototype.repaintNavigation = function () { 2236 var timeline = this, 2237 options = this.options, 2238 dom = this.dom, 2239 frame = dom.frame, 2240 navBar = dom.navBar; 2241 2242 if (!navBar) { 2243 var showButtonNew = options.showButtonNew && options.editable; 2244 var showNavigation = options.showNavigation && (options.zoomable || options.moveable); 2245 if (showNavigation || showButtonNew) { 2246 // create a navigation bar containing the navigation buttons 2247 navBar = document.createElement("DIV"); 2248 navBar.style.position = "absolute"; 2249 navBar.className = "timeline-navigation ui-widget ui-state-highlight ui-corner-all"; 2250 if (options.groupsOnRight) { 2251 navBar.style.left = '10px'; 2252 } 2253 else { 2254 navBar.style.right = '10px'; 2255 } 2256 if (options.axisOnTop) { 2257 navBar.style.bottom = '10px'; 2258 } 2259 else { 2260 navBar.style.top = '10px'; 2261 } 2262 dom.navBar = navBar; 2263 frame.appendChild(navBar); 2264 } 2265 2266 if (showButtonNew) { 2267 // create a new in button 2268 navBar.addButton = document.createElement("DIV"); 2269 navBar.addButton.className = "timeline-navigation-new"; 2270 navBar.addButton.title = options.CREATE_NEW_EVENT; 2271 var addIconSpan = document.createElement("SPAN"); 2272 addIconSpan.className = "ui-icon ui-icon-circle-plus"; 2273 navBar.addButton.appendChild(addIconSpan); 2274 2275 var onAdd = function(event) { 2276 links.Timeline.preventDefault(event); 2277 links.Timeline.stopPropagation(event); 2278 2279 // create a new event at the center of the frame 2280 var w = timeline.size.contentWidth; 2281 var x = w / 2; 2282 var xstart = timeline.screenToTime(x); 2283 if (options.snapEvents) { 2284 timeline.step.snap(xstart); 2285 } 2286 2287 var content = options.NEW; 2288 var group = timeline.groups.length ? timeline.groups[0].content : undefined; 2289 var preventRender = true; 2290 timeline.addItem({ 2291 'start': xstart, 2292 'content': content, 2293 'group': group 2294 }, preventRender); 2295 var index = (timeline.items.length - 1); 2296 timeline.selectItem(index); 2297 2298 timeline.applyAdd = true; 2299 2300 // fire an add event. 2301 // Note that the change can be canceled from within an event listener if 2302 // this listener calls the method cancelAdd(). 2303 timeline.trigger('add'); 2304 2305 if (timeline.applyAdd) { 2306 // render and select the item 2307 timeline.render({animate: false}); 2308 timeline.selectItem(index); 2309 } 2310 else { 2311 // undo an add 2312 timeline.deleteItem(index); 2313 } 2314 }; 2315 links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd); 2316 navBar.appendChild(navBar.addButton); 2317 } 2318 2319 if (showButtonNew && showNavigation) { 2320 // create a separator line 2321 links.Timeline.addClassName(navBar.addButton, 'timeline-navigation-new-line'); 2322 } 2323 2324 if (showNavigation) { 2325 if (options.zoomable) { 2326 // create a zoom in button 2327 navBar.zoomInButton = document.createElement("DIV"); 2328 navBar.zoomInButton.className = "timeline-navigation-zoom-in"; 2329 navBar.zoomInButton.title = this.options.ZOOM_IN; 2330 var ziIconSpan = document.createElement("SPAN"); 2331 ziIconSpan.className = "ui-icon ui-icon-circle-zoomin"; 2332 navBar.zoomInButton.appendChild(ziIconSpan); 2333 2334 var onZoomIn = function(event) { 2335 links.Timeline.preventDefault(event); 2336 links.Timeline.stopPropagation(event); 2337 timeline.zoom(0.4); 2338 timeline.trigger("rangechange"); 2339 timeline.trigger("rangechanged"); 2340 }; 2341 links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn); 2342 navBar.appendChild(navBar.zoomInButton); 2343 2344 // create a zoom out button 2345 navBar.zoomOutButton = document.createElement("DIV"); 2346 navBar.zoomOutButton.className = "timeline-navigation-zoom-out"; 2347 navBar.zoomOutButton.title = this.options.ZOOM_OUT; 2348 var zoIconSpan = document.createElement("SPAN"); 2349 zoIconSpan.className = "ui-icon ui-icon-circle-zoomout"; 2350 navBar.zoomOutButton.appendChild(zoIconSpan); 2351 2352 var onZoomOut = function(event) { 2353 links.Timeline.preventDefault(event); 2354 links.Timeline.stopPropagation(event); 2355 timeline.zoom(-0.4); 2356 timeline.trigger("rangechange"); 2357 timeline.trigger("rangechanged"); 2358 }; 2359 links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut); 2360 navBar.appendChild(navBar.zoomOutButton); 2361 } 2362 2363 if (options.moveable) { 2364 // create a move left button 2365 navBar.moveLeftButton = document.createElement("DIV"); 2366 navBar.moveLeftButton.className = "timeline-navigation-move-left"; 2367 navBar.moveLeftButton.title = this.options.MOVE_LEFT; 2368 var mlIconSpan = document.createElement("SPAN"); 2369 mlIconSpan.className = "ui-icon ui-icon-circle-arrow-w"; 2370 navBar.moveLeftButton.appendChild(mlIconSpan); 2371 2372 var onMoveLeft = function(event) { 2373 links.Timeline.preventDefault(event); 2374 links.Timeline.stopPropagation(event); 2375 timeline.move(-0.2); 2376 timeline.trigger("rangechange"); 2377 timeline.trigger("rangechanged"); 2378 }; 2379 links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft); 2380 navBar.appendChild(navBar.moveLeftButton); 2381 2382 // create a move right button 2383 navBar.moveRightButton = document.createElement("DIV"); 2384 navBar.moveRightButton.className = "timeline-navigation-move-right"; 2385 navBar.moveRightButton.title = this.options.MOVE_RIGHT; 2386 var mrIconSpan = document.createElement("SPAN"); 2387 mrIconSpan.className = "ui-icon ui-icon-circle-arrow-e"; 2388 navBar.moveRightButton.appendChild(mrIconSpan); 2389 2390 var onMoveRight = function(event) { 2391 links.Timeline.preventDefault(event); 2392 links.Timeline.stopPropagation(event); 2393 timeline.move(0.2); 2394 timeline.trigger("rangechange"); 2395 timeline.trigger("rangechanged"); 2396 }; 2397 links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight); 2398 navBar.appendChild(navBar.moveRightButton); 2399 } 2400 } 2401 } 2402 }; 2403 2404 2405 /** 2406 * Set current time. This function can be used to set the time in the client 2407 * timeline equal with the time on a server. 2408 * @param {Date} time 2409 */ 2410 links.Timeline.prototype.setCurrentTime = function(time) { 2411 var now = new Date(); 2412 this.clientTimeOffset = (time.valueOf() - now.valueOf()); 2413 2414 this.repaintCurrentTime(); 2415 }; 2416 2417 /** 2418 * Get current time. The time can have an offset from the real time, when 2419 * the current time has been changed via the method setCurrentTime. 2420 * @return {Date} time 2421 */ 2422 links.Timeline.prototype.getCurrentTime = function() { 2423 var now = new Date(); 2424 return new Date(now.valueOf() + this.clientTimeOffset); 2425 }; 2426 2427 2428 /** 2429 * Set custom time. 2430 * The custom time bar can be used to display events in past or future. 2431 * @param {Date} time 2432 */ 2433 links.Timeline.prototype.setCustomTime = function(time) { 2434 this.customTime = new Date(time.valueOf()); 2435 this.repaintCustomTime(); 2436 }; 2437 2438 /** 2439 * Retrieve the current custom time. 2440 * @return {Date} customTime 2441 */ 2442 links.Timeline.prototype.getCustomTime = function() { 2443 return new Date(this.customTime.valueOf()); 2444 }; 2445 2446 /** 2447 * Set a custom scale. Autoscaling will be disabled. 2448 * For example setScale(SCALE.MINUTES, 5) will result 2449 * in minor steps of 5 minutes, and major steps of an hour. 2450 * 2451 * @param {links.Timeline.StepDate.SCALE} scale 2452 * A scale. Choose from SCALE.MILLISECOND, 2453 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 2454 * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, 2455 * SCALE.YEAR. 2456 * @param {int} step A step size, by default 1. Choose for 2457 * example 1, 2, 5, or 10. 2458 */ 2459 links.Timeline.prototype.setScale = function(scale, step) { 2460 this.step.setScale(scale, step); 2461 this.render(); // TODO: optimize: only reflow/repaint axis 2462 }; 2463 2464 /** 2465 * Enable or disable autoscaling 2466 * @param {boolean} enable If true or not defined, autoscaling is enabled. 2467 * If false, autoscaling is disabled. 2468 */ 2469 links.Timeline.prototype.setAutoScale = function(enable) { 2470 this.step.setAutoScale(enable); 2471 this.render(); // TODO: optimize: only reflow/repaint axis 2472 }; 2473 2474 /** 2475 * Redraw the timeline 2476 * Reloads the (linked) data table and redraws the timeline when resized. 2477 * See also the method checkResize 2478 */ 2479 links.Timeline.prototype.redraw = function() { 2480 this.setData(this.data); 2481 }; 2482 2483 2484 /** 2485 * Check if the timeline is resized, and if so, redraw the timeline. 2486 * Useful when the webpage is resized. 2487 */ 2488 links.Timeline.prototype.checkResize = function() { 2489 // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter 2490 this.render(); 2491 }; 2492 2493 /** 2494 * Check whether a given item is editable 2495 * @param {links.Timeline.Item} item 2496 * @return {boolean} editable 2497 */ 2498 links.Timeline.prototype.isEditable = function (item) { 2499 if (item) { 2500 if (item.editable != undefined) { 2501 return item.editable; 2502 } 2503 else { 2504 return this.options.editable; 2505 } 2506 } 2507 return false; 2508 }; 2509 2510 /** 2511 * Calculate the factor and offset to convert a position on screen to the 2512 * corresponding date and vice versa. 2513 * After the method calcConversionFactor is executed once, the methods screenToTime and 2514 * timeToScreen can be used. 2515 */ 2516 links.Timeline.prototype.recalcConversion = function() { 2517 this.conversion.offset = this.start.valueOf(); 2518 this.conversion.factor = this.size.contentWidth / 2519 (this.end.valueOf() - this.start.valueOf()); 2520 }; 2521 2522 2523 /** 2524 * Convert a position on screen (pixels) to a datetime 2525 * Before this method can be used, the method calcConversionFactor must be 2526 * executed once. 2527 * @param {int} x Position on the screen in pixels 2528 * @return {Date} time The datetime the corresponds with given position x 2529 */ 2530 links.Timeline.prototype.screenToTime = function(x) { 2531 var conversion = this.conversion; 2532 return new Date(x / conversion.factor + conversion.offset); 2533 }; 2534 2535 /** 2536 * Convert a datetime (Date object) into a position on the screen 2537 * Before this method can be used, the method calcConversionFactor must be 2538 * executed once. 2539 * @param {Date} time A date 2540 * @return {int} x The position on the screen in pixels which corresponds 2541 * with the given date. 2542 */ 2543 links.Timeline.prototype.timeToScreen = function(time) { 2544 var conversion = this.conversion; 2545 return (time.valueOf() - conversion.offset) * conversion.factor; 2546 }; 2547 2548 2549 2550 /** 2551 * Event handler for touchstart event on mobile devices 2552 */ 2553 links.Timeline.prototype.onTouchStart = function(event) { 2554 var params = this.eventParams, 2555 me = this; 2556 2557 if (params.touchDown) { 2558 // if already moving, return 2559 return; 2560 } 2561 2562 params.touchDown = true; 2563 params.zoomed = false; 2564 2565 this.onMouseDown(event); 2566 2567 if (!params.onTouchMove) { 2568 params.onTouchMove = function (event) {me.onTouchMove(event);}; 2569 links.Timeline.addEventListener(document, "touchmove", params.onTouchMove); 2570 } 2571 if (!params.onTouchEnd) { 2572 params.onTouchEnd = function (event) {me.onTouchEnd(event);}; 2573 links.Timeline.addEventListener(document, "touchend", params.onTouchEnd); 2574 } 2575 2576 /* TODO 2577 // check for double tap event 2578 var delta = 500; // ms 2579 var doubleTapStart = (new Date()).valueOf(); 2580 var target = links.Timeline.getTarget(event); 2581 var doubleTapItem = this.getItemIndex(target); 2582 if (params.doubleTapStart && 2583 (doubleTapStart - params.doubleTapStart) < delta && 2584 doubleTapItem == params.doubleTapItem) { 2585 delete params.doubleTapStart; 2586 delete params.doubleTapItem; 2587 me.onDblClick(event); 2588 params.touchDown = false; 2589 } 2590 params.doubleTapStart = doubleTapStart; 2591 params.doubleTapItem = doubleTapItem; 2592 */ 2593 // store timing for double taps 2594 var target = links.Timeline.getTarget(event); 2595 var item = this.getItemIndex(target); 2596 params.doubleTapStartPrev = params.doubleTapStart; 2597 params.doubleTapStart = (new Date()).valueOf(); 2598 params.doubleTapItemPrev = params.doubleTapItem; 2599 params.doubleTapItem = item; 2600 2601 links.Timeline.preventDefault(event); 2602 }; 2603 2604 /** 2605 * Event handler for touchmove event on mobile devices 2606 */ 2607 links.Timeline.prototype.onTouchMove = function(event) { 2608 var params = this.eventParams; 2609 2610 if (event.scale && event.scale !== 1) { 2611 params.zoomed = true; 2612 } 2613 2614 if (!params.zoomed) { 2615 // move 2616 this.onMouseMove(event); 2617 } 2618 else { 2619 if (this.options.zoomable) { 2620 // pinch 2621 // TODO: pinch only supported on iPhone/iPad. Create something manually for Android? 2622 params.zoomed = true; 2623 2624 var scale = event.scale, 2625 oldWidth = (params.end.valueOf() - params.start.valueOf()), 2626 newWidth = oldWidth / scale, 2627 diff = newWidth - oldWidth, 2628 start = new Date(parseInt(params.start.valueOf() - diff/2)), 2629 end = new Date(parseInt(params.end.valueOf() + diff/2)); 2630 2631 // TODO: determine zoom-around-date from touch positions? 2632 2633 this.setVisibleChartRange(start, end); 2634 this.trigger("rangechange"); 2635 } 2636 } 2637 2638 links.Timeline.preventDefault(event); 2639 }; 2640 2641 /** 2642 * Event handler for touchend event on mobile devices 2643 */ 2644 links.Timeline.prototype.onTouchEnd = function(event) { 2645 var params = this.eventParams; 2646 var me = this; 2647 params.touchDown = false; 2648 2649 if (params.zoomed) { 2650 this.trigger("rangechanged"); 2651 } 2652 2653 if (params.onTouchMove) { 2654 links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove); 2655 delete params.onTouchMove; 2656 2657 } 2658 if (params.onTouchEnd) { 2659 links.Timeline.removeEventListener(document, "touchend", params.onTouchEnd); 2660 delete params.onTouchEnd; 2661 } 2662 2663 this.onMouseUp(event); 2664 2665 // check for double tap event 2666 var delta = 500; // ms 2667 var doubleTapEnd = (new Date()).valueOf(); 2668 var target = links.Timeline.getTarget(event); 2669 var doubleTapItem = this.getItemIndex(target); 2670 if (params.doubleTapStartPrev && 2671 (doubleTapEnd - params.doubleTapStartPrev) < delta && 2672 params.doubleTapItem == params.doubleTapItemPrev) { 2673 params.touchDown = true; 2674 me.onDblClick(event); 2675 params.touchDown = false; 2676 } 2677 2678 links.Timeline.preventDefault(event); 2679 }; 2680 2681 2682 /** 2683 * Start a moving operation inside the provided parent element 2684 * @param {Event} event The event that occurred (required for 2685 * retrieving the mouse position) 2686 */ 2687 links.Timeline.prototype.onMouseDown = function(event) { 2688 event = event || window.event; 2689 2690 var params = this.eventParams, 2691 options = this.options, 2692 dom = this.dom; 2693 2694 // only react on left mouse button down 2695 var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); 2696 if (!leftButtonDown && !params.touchDown) { 2697 return; 2698 } 2699 2700 // get mouse position 2701 params.mouseX = links.Timeline.getPageX(event); 2702 params.mouseY = links.Timeline.getPageY(event); 2703 params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content); 2704 params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content); 2705 params.previousLeft = 0; 2706 params.previousOffset = 0; 2707 2708 params.moved = false; 2709 params.start = new Date(this.start.valueOf()); 2710 params.end = new Date(this.end.valueOf()); 2711 2712 params.target = links.Timeline.getTarget(event); 2713 var dragLeft = (dom.items && dom.items.dragLeft) ? dom.items.dragLeft : undefined; 2714 var dragRight = (dom.items && dom.items.dragRight) ? dom.items.dragRight : undefined; 2715 params.itemDragLeft = (params.target === dragLeft); 2716 params.itemDragRight = (params.target === dragRight); 2717 2718 if (params.itemDragLeft || params.itemDragRight) { 2719 params.itemIndex = (this.selection && this.selection.index !== undefined) ? this.selection.index : undefined; 2720 delete params.clusterIndex; 2721 } 2722 else { 2723 params.itemIndex = this.getItemIndex(params.target); 2724 params.clusterIndex = this.getClusterIndex(params.target); 2725 } 2726 2727 params.customTime = (params.target === dom.customTime || 2728 params.target.parentNode === dom.customTime) ? 2729 this.customTime : 2730 undefined; 2731 2732 params.addItem = (options.editable && event.ctrlKey); 2733 if (params.addItem) { 2734 // create a new event at the current mouse position 2735 var x = params.mouseX - params.frameLeft; 2736 var y = params.mouseY - params.frameTop; 2737 2738 var xstart = this.screenToTime(x); 2739 if (options.snapEvents) { 2740 this.step.snap(xstart); 2741 } 2742 var xend = new Date(xstart.valueOf()); 2743 var content = options.NEW; 2744 var group = this.getGroupFromHeight(y); 2745 this.addItem({ 2746 'start': xstart, 2747 'end': xend, 2748 'content': content, 2749 'group': this.getGroupName(group) 2750 }); 2751 params.itemIndex = (this.items.length - 1); 2752 delete params.clusterIndex; 2753 this.selectItem(params.itemIndex); 2754 params.itemDragRight = true; 2755 } 2756 2757 var item = this.items[params.itemIndex]; 2758 var isSelected = this.isSelected(params.itemIndex); 2759 params.editItem = isSelected && this.isEditable(item); 2760 if (params.editItem) { 2761 params.itemStart = item.start; 2762 params.itemEnd = item.end; 2763 params.itemGroup = item.group; 2764 params.itemLeft = item.getLeft(this); // NH Use item.getLeft here 2765 params.itemRight = item.getRight(this); // NH Use item.getRight here 2766 } 2767 else { 2768 this.dom.frame.style.cursor = 'move'; 2769 } 2770 if (!params.touchDown) { 2771 // add event listeners to handle moving the contents 2772 // we store the function onmousemove and onmouseup in the timeline, so we can 2773 // remove the eventlisteners lateron in the function mouseUp() 2774 var me = this; 2775 if (!params.onMouseMove) { 2776 params.onMouseMove = function (event) {me.onMouseMove(event);}; 2777 links.Timeline.addEventListener(document, "mousemove", params.onMouseMove); 2778 } 2779 if (!params.onMouseUp) { 2780 params.onMouseUp = function (event) {me.onMouseUp(event);}; 2781 links.Timeline.addEventListener(document, "mouseup", params.onMouseUp); 2782 } 2783 2784 links.Timeline.preventDefault(event); 2785 } 2786 }; 2787 2788 2789 /** 2790 * Perform moving operating. 2791 * This function activated from within the funcion links.Timeline.onMouseDown(). 2792 * @param {Event} event Well, eehh, the event 2793 */ 2794 links.Timeline.prototype.onMouseMove = function (event) { 2795 event = event || window.event; 2796 2797 var params = this.eventParams, 2798 size = this.size, 2799 dom = this.dom, 2800 options = this.options; 2801 2802 // calculate change in mouse position 2803 var mouseX = links.Timeline.getPageX(event); 2804 var mouseY = links.Timeline.getPageY(event); 2805 2806 if (params.mouseX == undefined) { 2807 params.mouseX = mouseX; 2808 } 2809 if (params.mouseY == undefined) { 2810 params.mouseY = mouseY; 2811 } 2812 2813 var diffX = mouseX - params.mouseX; 2814 var diffY = mouseY - params.mouseY; 2815 2816 // if mouse movement is big enough, register it as a "moved" event 2817 if (Math.abs(diffX) >= 1) { 2818 params.moved = true; 2819 } 2820 2821 if (params.customTime) { 2822 var x = this.timeToScreen(params.customTime); 2823 var xnew = x + diffX; 2824 this.customTime = this.screenToTime(xnew); 2825 this.repaintCustomTime(); 2826 2827 // fire a timechange event 2828 this.trigger('timechange'); 2829 } 2830 else if (params.editItem) { 2831 var item = this.items[params.itemIndex], 2832 left, 2833 right; 2834 2835 if (params.itemDragLeft && options.timeChangeable) { 2836 // move the start of the item 2837 left = params.itemLeft + diffX; 2838 right = params.itemRight; 2839 2840 item.start = this.screenToTime(left); 2841 if (options.snapEvents) { 2842 this.step.snap(item.start); 2843 left = this.timeToScreen(item.start); 2844 } 2845 2846 if (left > right) { 2847 left = right; 2848 item.start = this.screenToTime(left); 2849 } 2850 this.trigger('change'); 2851 } 2852 else if (params.itemDragRight && options.timeChangeable) { 2853 // move the end of the item 2854 left = params.itemLeft; 2855 right = params.itemRight + diffX; 2856 2857 item.end = this.screenToTime(right); 2858 if (options.snapEvents) { 2859 this.step.snap(item.end); 2860 right = this.timeToScreen(item.end); 2861 } 2862 2863 if (right < left) { 2864 right = left; 2865 item.end = this.screenToTime(right); 2866 } 2867 this.trigger('change'); 2868 } 2869 else if (options.timeChangeable) { 2870 // move the item 2871 left = params.itemLeft + diffX; 2872 item.start = this.screenToTime(left); 2873 if (options.snapEvents) { 2874 this.step.snap(item.start); 2875 left = this.timeToScreen(item.start); 2876 } 2877 2878 if (item.end) { 2879 right = left + (params.itemRight - params.itemLeft); 2880 item.end = this.screenToTime(right); 2881 } 2882 this.trigger('change'); 2883 } 2884 2885 item.setPosition(left, right); 2886 2887 var dragging = params.itemDragLeft || params.itemDragRight; 2888 if (this.groups.length && !dragging) { 2889 // move item from one group to another when needed 2890 var y = mouseY - params.frameTop; 2891 var group = this.getGroupFromHeight(y); 2892 if (options.groupsChangeable && item.group !== group) { 2893 // move item to the other group 2894 var index = this.items.indexOf(item); 2895 this.changeItem(index, {'group': this.getGroupName(group)}); 2896 } 2897 else { 2898 this.repaintDeleteButton(); 2899 this.repaintDragAreas(); 2900 } 2901 } 2902 else { 2903 // TODO: does not work well in FF, forces redraw with every mouse move it seems 2904 this.render(); // TODO: optimize, only redraw the items? 2905 // Note: when animate==true, no redraw is needed here, its done by stackItems animation 2906 } 2907 } 2908 else if (options.moveable) { 2909 var interval = (params.end.valueOf() - params.start.valueOf()); 2910 var diffMillisecs = Math.round((-diffX) / size.contentWidth * interval); 2911 var newStart = new Date(params.start.valueOf() + diffMillisecs); 2912 var newEnd = new Date(params.end.valueOf() + diffMillisecs); 2913 this.applyRange(newStart, newEnd); 2914 // if the applied range is moved due to a fixed min or max, 2915 // change the diffMillisecs accordingly 2916 var appliedDiff = (this.start.valueOf() - newStart.valueOf()); 2917 if (appliedDiff) { 2918 diffMillisecs += appliedDiff; 2919 } 2920 2921 this.recalcConversion(); 2922 2923 // move the items by changing the left position of their frame. 2924 // this is much faster than repositioning all elements individually via the 2925 // repaintFrame() function (which is done once at mouseup) 2926 // note that we round diffX to prevent wrong positioning on millisecond scale 2927 var previousLeft = params.previousLeft || 0; 2928 var currentLeft = parseFloat(dom.items.frame.style.left) || 0; 2929 var previousOffset = params.previousOffset || 0; 2930 var frameOffset = previousOffset + (currentLeft - previousLeft); 2931 var frameLeft = -diffMillisecs / interval * size.contentWidth + frameOffset; 2932 2933 dom.items.frame.style.left = (frameLeft) + "px"; 2934 2935 // read the left again from DOM (IE8- rounds the value) 2936 params.previousOffset = frameOffset; 2937 params.previousLeft = parseFloat(dom.items.frame.style.left) || frameLeft; 2938 2939 this.repaintCurrentTime(); 2940 this.repaintCustomTime(); 2941 this.repaintAxis(); 2942 2943 // fire a rangechange event 2944 this.trigger('rangechange'); 2945 } 2946 2947 links.Timeline.preventDefault(event); 2948 }; 2949 2950 2951 /** 2952 * Stop moving operating. 2953 * This function activated from within the funcion links.Timeline.onMouseDown(). 2954 * @param {event} event The event 2955 */ 2956 links.Timeline.prototype.onMouseUp = function (event) { 2957 var params = this.eventParams, 2958 options = this.options; 2959 2960 event = event || window.event; 2961 2962 this.dom.frame.style.cursor = 'auto'; 2963 2964 // remove event listeners here, important for Safari 2965 if (params.onMouseMove) { 2966 links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove); 2967 delete params.onMouseMove; 2968 } 2969 if (params.onMouseUp) { 2970 links.Timeline.removeEventListener(document, "mouseup", params.onMouseUp); 2971 delete params.onMouseUp; 2972 } 2973 //links.Timeline.preventDefault(event); 2974 2975 if (params.customTime) { 2976 // fire a timechanged event 2977 this.trigger('timechanged'); 2978 } 2979 else if (params.editItem) { 2980 var item = this.items[params.itemIndex]; 2981 2982 if (params.moved || params.addItem) { 2983 this.applyChange = true; 2984 this.applyAdd = true; 2985 2986 this.updateData(params.itemIndex, { 2987 'start': item.start, 2988 'end': item.end 2989 }); 2990 2991 // fire an add or changed event. 2992 // Note that the change can be canceled from within an event listener if 2993 // this listener calls the method cancelChange(). 2994 this.trigger(params.addItem ? 'add' : 'changed'); 2995 2996 //retrieve item data again to include changes made to it in the triggered event handlers 2997 item = this.items[params.itemIndex]; 2998 2999 if (params.addItem) { 3000 if (this.applyAdd) { 3001 this.updateData(params.itemIndex, { 3002 'start': item.start, 3003 'end': item.end, 3004 'content': item.content, 3005 'group': this.getGroupName(item.group) 3006 }); 3007 } 3008 else { 3009 // undo an add 3010 this.deleteItem(params.itemIndex); 3011 } 3012 } 3013 else { 3014 if (this.applyChange) { 3015 this.updateData(params.itemIndex, { 3016 'start': item.start, 3017 'end': item.end 3018 }); 3019 } 3020 else { 3021 // undo a change 3022 delete this.applyChange; 3023 delete this.applyAdd; 3024 3025 var item = this.items[params.itemIndex], 3026 domItem = item.dom; 3027 3028 item.start = params.itemStart; 3029 item.end = params.itemEnd; 3030 item.group = params.itemGroup; 3031 // TODO: original group should be restored too 3032 item.setPosition(params.itemLeft, params.itemRight); 3033 3034 this.updateData(params.itemIndex, { 3035 'start': params.itemStart, 3036 'end': params.itemEnd 3037 }); 3038 } 3039 } 3040 3041 // prepare data for clustering, by filtering and sorting by type 3042 if (this.options.cluster) { 3043 this.clusterGenerator.updateData(); 3044 } 3045 3046 this.render(); 3047 } 3048 } 3049 else { 3050 if (!params.moved && !params.zoomed) { 3051 // mouse did not move -> user has selected an item 3052 3053 if (params.target === this.dom.items.deleteButton) { 3054 // delete item 3055 if (this.selection && this.selection.index !== undefined) { 3056 this.confirmDeleteItem(this.selection.index); 3057 } 3058 } 3059 else if (options.selectable) { 3060 // select/unselect item 3061 if (params.itemIndex != undefined) { 3062 if (!this.isSelected(params.itemIndex)) { 3063 this.selectItem(params.itemIndex); 3064 this.trigger('select'); 3065 } 3066 } 3067 else if(params.clusterIndex != undefined) { 3068 this.selectCluster(params.clusterIndex); 3069 this.trigger('select'); 3070 } 3071 else { 3072 if (options.unselectable) { 3073 this.unselectItem(); 3074 this.trigger('select'); 3075 } 3076 } 3077 } 3078 } 3079 else { 3080 // timeline is moved 3081 // TODO: optimize: no need to reflow and cluster again? 3082 this.render(); 3083 3084 if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) { 3085 // fire a rangechanged event 3086 this.trigger('rangechanged'); 3087 } 3088 } 3089 } 3090 }; 3091 3092 /** 3093 * Double click event occurred for an item 3094 * @param {Event} event 3095 */ 3096 links.Timeline.prototype.onDblClick = function (event) { 3097 var params = this.eventParams, 3098 options = this.options, 3099 dom = this.dom, 3100 size = this.size; 3101 event = event || window.event; 3102 3103 if (params.itemIndex != undefined) { 3104 var item = this.items[params.itemIndex]; 3105 if (item && this.isEditable(item)) { 3106 // fire the edit event 3107 this.trigger('edit'); 3108 } 3109 } 3110 else { 3111 if (options.editable) { 3112 // create a new item 3113 3114 // get mouse position 3115 params.mouseX = links.Timeline.getPageX(event); 3116 params.mouseY = links.Timeline.getPageY(event); 3117 var x = params.mouseX - links.Timeline.getAbsoluteLeft(dom.content); 3118 var y = params.mouseY - links.Timeline.getAbsoluteTop(dom.content); 3119 3120 // create a new event at the current mouse position 3121 var xstart = this.screenToTime(x); 3122 if (options.snapEvents) { 3123 this.step.snap(xstart); 3124 } 3125 3126 var content = options.NEW; 3127 var group = this.getGroupFromHeight(y); // (group may be undefined) 3128 var preventRender = true; 3129 this.addItem({ 3130 'start': xstart, 3131 'content': content, 3132 'group': this.getGroupName(group) 3133 }, preventRender); 3134 params.itemIndex = (this.items.length - 1); 3135 this.selectItem(params.itemIndex); 3136 3137 this.applyAdd = true; 3138 3139 // fire an add event. 3140 // Note that the change can be canceled from within an event listener if 3141 // this listener calls the method cancelAdd(). 3142 this.trigger('add'); 3143 3144 if (this.applyAdd) { 3145 // render and select the item 3146 this.render({animate: false}); 3147 this.selectItem(params.itemIndex); 3148 } 3149 else { 3150 // undo an add 3151 this.deleteItem(params.itemIndex); 3152 } 3153 } 3154 } 3155 3156 links.Timeline.preventDefault(event); 3157 }; 3158 3159 3160 /** 3161 * Event handler for mouse wheel event, used to zoom the timeline 3162 * Code from http://adomas.org/javascript-mouse-wheel/ 3163 * @param {Event} event The event 3164 */ 3165 links.Timeline.prototype.onMouseWheel = function(event) { 3166 if (!this.options.zoomable) 3167 return; 3168 3169 if (!event) { /* For IE. */ 3170 event = window.event; 3171 } 3172 3173 // retrieve delta 3174 var delta = 0; 3175 if (event.wheelDelta) { /* IE/Opera. */ 3176 delta = event.wheelDelta/120; 3177 } else if (event.detail) { /* Mozilla case. */ 3178 // In Mozilla, sign of delta is different than in IE. 3179 // Also, delta is multiple of 3. 3180 delta = -event.detail/3; 3181 } 3182 3183 // If delta is nonzero, handle it. 3184 // Basically, delta is now positive if wheel was scrolled up, 3185 // and negative, if wheel was scrolled down. 3186 if (delta) { 3187 // TODO: on FireFox, the window is not redrawn within repeated scroll-events 3188 // -> use a delayed redraw? Make a zoom queue? 3189 3190 var timeline = this; 3191 var zoom = function () { 3192 // perform the zoom action. Delta is normally 1 or -1 3193 var zoomFactor = delta / 5.0; 3194 var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content); 3195 var mouseX = links.Timeline.getPageX(event); 3196 var zoomAroundDate = 3197 (mouseX != undefined && frameLeft != undefined) ? 3198 timeline.screenToTime(mouseX - frameLeft) : 3199 undefined; 3200 3201 timeline.zoom(zoomFactor, zoomAroundDate); 3202 3203 // fire a rangechange and a rangechanged event 3204 timeline.trigger("rangechange"); 3205 timeline.trigger("rangechanged"); 3206 }; 3207 3208 var scroll = function () { 3209 // Scroll the timeline 3210 timeline.move(delta * -0.2); 3211 timeline.trigger("rangechange"); 3212 timeline.trigger("rangechanged"); 3213 }; 3214 3215 if (event.shiftKey) { 3216 scroll(); 3217 } 3218 else { 3219 zoom(); 3220 } 3221 } 3222 3223 // Prevent default actions caused by mouse wheel. 3224 // That might be ugly, but we handle scrolls somehow 3225 // anyway, so don't bother here... 3226 links.Timeline.preventDefault(event); 3227 }; 3228 3229 3230 /** 3231 * Zoom the timeline the given zoomfactor in or out. Start and end date will 3232 * be adjusted, and the timeline will be redrawn. You can optionally give a 3233 * date around which to zoom. 3234 * For example, try zoomfactor = 0.1 or -0.1 3235 * @param {Number} zoomFactor Zooming amount. Positive value will zoom in, 3236 * negative value will zoom out 3237 * @param {Date} zoomAroundDate Date around which will be zoomed. Optional 3238 */ 3239 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) { 3240 // if zoomAroundDate is not provided, take it half between start Date and end Date 3241 if (zoomAroundDate == undefined) { 3242 zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2); 3243 } 3244 3245 // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will 3246 // result in a start>=end ) 3247 if (zoomFactor >= 1) { 3248 zoomFactor = 0.9; 3249 } 3250 if (zoomFactor <= -1) { 3251 zoomFactor = -0.9; 3252 } 3253 3254 // adjust a negative factor such that zooming in with 0.1 equals zooming 3255 // out with a factor -0.1 3256 if (zoomFactor < 0) { 3257 zoomFactor = zoomFactor / (1 + zoomFactor); 3258 } 3259 3260 // zoom start Date and end Date relative to the zoomAroundDate 3261 var startDiff = (this.start.valueOf() - zoomAroundDate); 3262 var endDiff = (this.end.valueOf() - zoomAroundDate); 3263 3264 // calculate new dates 3265 var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor); 3266 var newEnd = new Date(this.end.valueOf() - endDiff * zoomFactor); 3267 3268 // only zoom in when interval is larger than minimum interval (to prevent 3269 // sliding to left/right when having reached the minimum zoom level) 3270 var interval = (newEnd.valueOf() - newStart.valueOf()); 3271 var zoomMin = Number(this.options.zoomMin) || 10; 3272 if (zoomMin < 10) { 3273 zoomMin = 10; 3274 } 3275 if (interval >= zoomMin) { 3276 this.applyRange(newStart, newEnd, zoomAroundDate); 3277 this.render({ 3278 animate: this.options.animate && this.options.animateZoom 3279 }); 3280 } 3281 }; 3282 3283 /** 3284 * Move the timeline the given movefactor to the left or right. Start and end 3285 * date will be adjusted, and the timeline will be redrawn. 3286 * For example, try moveFactor = 0.1 or -0.1 3287 * @param {Number} moveFactor Moving amount. Positive value will move right, 3288 * negative value will move left 3289 */ 3290 links.Timeline.prototype.move = function(moveFactor) { 3291 // zoom start Date and end Date relative to the zoomAroundDate 3292 var diff = (this.end.valueOf() - this.start.valueOf()); 3293 3294 // apply new dates 3295 var newStart = new Date(this.start.valueOf() + diff * moveFactor); 3296 var newEnd = new Date(this.end.valueOf() + diff * moveFactor); 3297 this.applyRange(newStart, newEnd); 3298 3299 this.render(); // TODO: optimize, no need to reflow, only to recalc conversion and repaint 3300 }; 3301 3302 /** 3303 * Apply a visible range. The range is limited to feasible maximum and minimum 3304 * range. 3305 * @param {Date} start 3306 * @param {Date} end 3307 * @param {Date} zoomAroundDate Optional. Date around which will be zoomed. 3308 */ 3309 links.Timeline.prototype.applyRange = function (start, end, zoomAroundDate) { 3310 // calculate new start and end value 3311 var startValue = start.valueOf(); // number 3312 var endValue = end.valueOf(); // number 3313 var interval = (endValue - startValue); 3314 3315 // determine maximum and minimum interval 3316 var options = this.options; 3317 var year = 1000 * 60 * 60 * 24 * 365; 3318 var zoomMin = Number(options.zoomMin) || 10; 3319 if (zoomMin < 10) { 3320 zoomMin = 10; 3321 } 3322 var zoomMax = Number(options.zoomMax) || 10000 * year; 3323 if (zoomMax > 10000 * year) { 3324 zoomMax = 10000 * year; 3325 } 3326 if (zoomMax < zoomMin) { 3327 zoomMax = zoomMin; 3328 } 3329 3330 // determine min and max date value 3331 var min = options.min ? options.min.valueOf() : undefined; // number 3332 var max = options.max ? options.max.valueOf() : undefined; // number 3333 if (min != undefined && max != undefined) { 3334 if (min >= max) { 3335 // empty range 3336 var day = 1000 * 60 * 60 * 24; 3337 max = min + day; 3338 } 3339 if (zoomMax > (max - min)) { 3340 zoomMax = (max - min); 3341 } 3342 if (zoomMin > (max - min)) { 3343 zoomMin = (max - min); 3344 } 3345 } 3346 3347 // prevent empty interval 3348 if (startValue >= endValue) { 3349 endValue += 1000 * 60 * 60 * 24; 3350 } 3351 3352 // prevent too small scale 3353 // TODO: IE has problems with milliseconds 3354 if (interval < zoomMin) { 3355 var diff = (zoomMin - interval); 3356 var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5; 3357 startValue -= Math.round(diff * f); 3358 endValue += Math.round(diff * (1 - f)); 3359 } 3360 3361 // prevent too large scale 3362 if (interval > zoomMax) { 3363 var diff = (interval - zoomMax); 3364 var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5; 3365 startValue += Math.round(diff * f); 3366 endValue -= Math.round(diff * (1 - f)); 3367 } 3368 3369 // prevent to small start date 3370 if (min != undefined) { 3371 var diff = (startValue - min); 3372 if (diff < 0) { 3373 startValue -= diff; 3374 endValue -= diff; 3375 } 3376 } 3377 3378 // prevent to large end date 3379 if (max != undefined) { 3380 var diff = (max - endValue); 3381 if (diff < 0) { 3382 startValue += diff; 3383 endValue += diff; 3384 } 3385 } 3386 3387 // apply new dates 3388 this.start = new Date(startValue); 3389 this.end = new Date(endValue); 3390 }; 3391 3392 /** 3393 * Delete an item after a confirmation. 3394 * The deletion can be cancelled by executing .cancelDelete() during the 3395 * triggered event 'delete'. 3396 * @param {int} index Index of the item to be deleted 3397 */ 3398 links.Timeline.prototype.confirmDeleteItem = function(index) { 3399 this.applyDelete = true; 3400 3401 // select the event to be deleted 3402 if (!this.isSelected(index)) { 3403 this.selectItem(index); 3404 } 3405 3406 // fire a delete event trigger. 3407 // Note that the delete event can be canceled from within an event listener if 3408 // this listener calls the method cancelChange(). 3409 this.trigger('delete'); 3410 3411 if (this.applyDelete) { 3412 this.deleteItem(index); 3413 } 3414 3415 delete this.applyDelete; 3416 }; 3417 3418 /** 3419 * Delete an item 3420 * @param {int} index Index of the item to be deleted 3421 * @param {boolean} [preventRender=false] Do not re-render timeline if true 3422 * (optimization for multiple delete) 3423 */ 3424 links.Timeline.prototype.deleteItem = function(index, preventRender) { 3425 if (index >= this.items.length) { 3426 throw "Cannot delete row, index out of range"; 3427 } 3428 3429 if (this.selection && this.selection.index !== undefined) { 3430 // adjust the selection 3431 if (this.selection.index == index) { 3432 // item to be deleted is selected 3433 this.unselectItem(); 3434 } 3435 else if (this.selection.index > index) { 3436 // update selection index 3437 this.selection.index--; 3438 } 3439 } 3440 3441 // actually delete the item and remove it from the DOM 3442 var item = this.items.splice(index, 1)[0]; 3443 this.renderQueue.hide.push(item); 3444 3445 // delete the row in the original data table 3446 if (this.data) { 3447 if (google && google.visualization && 3448 this.data instanceof google.visualization.DataTable) { 3449 this.data.removeRow(index); 3450 } 3451 else if (links.Timeline.isArray(this.data)) { 3452 this.data.splice(index, 1); 3453 } 3454 else { 3455 throw "Cannot delete row from data, unknown data type"; 3456 } 3457 } 3458 3459 // prepare data for clustering, by filtering and sorting by type 3460 if (this.options.cluster) { 3461 this.clusterGenerator.updateData(); 3462 } 3463 3464 if (!preventRender) { 3465 this.render(); 3466 } 3467 }; 3468 3469 3470 /** 3471 * Delete all items 3472 */ 3473 links.Timeline.prototype.deleteAllItems = function() { 3474 this.unselectItem(); 3475 3476 // delete the loaded items 3477 this.clearItems(); 3478 3479 // delete the groups 3480 this.deleteGroups(); 3481 3482 // empty original data table 3483 if (this.data) { 3484 if (google && google.visualization && 3485 this.data instanceof google.visualization.DataTable) { 3486 this.data.removeRows(0, this.data.getNumberOfRows()); 3487 } 3488 else if (links.Timeline.isArray(this.data)) { 3489 this.data.splice(0, this.data.length); 3490 } 3491 else { 3492 throw "Cannot delete row from data, unknown data type"; 3493 } 3494 } 3495 3496 // prepare data for clustering, by filtering and sorting by type 3497 if (this.options.cluster) { 3498 this.clusterGenerator.updateData(); 3499 } 3500 3501 this.render(); 3502 }; 3503 3504 3505 /** 3506 * Find the group from a given height in the timeline 3507 * @param {Number} height Height in the timeline 3508 * @return {Object | undefined} group The group object, or undefined if out 3509 * of range 3510 */ 3511 links.Timeline.prototype.getGroupFromHeight = function(height) { 3512 var i, 3513 group, 3514 groups = this.groups; 3515 3516 if (groups.length) { 3517 if (this.options.axisOnTop) { 3518 for (i = groups.length - 1; i >= 0; i--) { 3519 group = groups[i]; 3520 if (height > group.top) { 3521 return group; 3522 } 3523 } 3524 } 3525 else { 3526 for (i = 0; i < groups.length; i++) { 3527 group = groups[i]; 3528 if (height > group.top) { 3529 return group; 3530 } 3531 } 3532 } 3533 3534 return group; // return the last group 3535 } 3536 3537 return undefined; 3538 }; 3539 3540 /** 3541 * @constructor links.Timeline.Item 3542 * @param {Object} data Object containing parameters start, end 3543 * content, group, type, editable. 3544 * @param {Object} [options] Options to set initial property values 3545 * {Number} top 3546 * {Number} left 3547 * {Number} width 3548 * {Number} height 3549 */ 3550 links.Timeline.Item = function (data, options) { 3551 if (data) { 3552 /* TODO: use parseJSONDate as soon as it is tested and working (in two directions) 3553 this.start = links.Timeline.parseJSONDate(data.start); 3554 this.end = links.Timeline.parseJSONDate(data.end); 3555 */ 3556 this.start = data.start; 3557 this.end = data.end; 3558 this.content = data.content; 3559 this.className = data.className; 3560 this.editable = data.editable; 3561 this.group = data.group; 3562 this.type = data.type; 3563 } 3564 this.top = 0; 3565 this.left = 0; 3566 this.width = 0; 3567 this.height = 0; 3568 this.lineWidth = 0; 3569 this.dotWidth = 0; 3570 this.dotHeight = 0; 3571 3572 this.rendered = false; // true when the item is draw in the Timeline DOM 3573 3574 if (options) { 3575 // override the default properties 3576 for (var option in options) { 3577 if (options.hasOwnProperty(option)) { 3578 this[option] = options[option]; 3579 } 3580 } 3581 } 3582 3583 }; 3584 3585 3586 3587 /** 3588 * Reflow the Item: retrieve its actual size from the DOM 3589 * @return {boolean} resized returns true if the axis is resized 3590 */ 3591 links.Timeline.Item.prototype.reflow = function () { 3592 // Should be implemented by sub-prototype 3593 return false; 3594 }; 3595 3596 /** 3597 * Append all image urls present in the items DOM to the provided array 3598 * @param {String[]} imageUrls 3599 */ 3600 links.Timeline.Item.prototype.getImageUrls = function (imageUrls) { 3601 if (this.dom) { 3602 links.imageloader.filterImageUrls(this.dom, imageUrls); 3603 } 3604 }; 3605 3606 /** 3607 * Select the item 3608 */ 3609 links.Timeline.Item.prototype.select = function () { 3610 // Should be implemented by sub-prototype 3611 }; 3612 3613 /** 3614 * Unselect the item 3615 */ 3616 links.Timeline.Item.prototype.unselect = function () { 3617 // Should be implemented by sub-prototype 3618 }; 3619 3620 /** 3621 * Creates the DOM for the item, depending on its type 3622 * @return {Element | undefined} 3623 */ 3624 links.Timeline.Item.prototype.createDOM = function () { 3625 // Should be implemented by sub-prototype 3626 }; 3627 3628 /** 3629 * Append the items DOM to the given HTML container. If items DOM does not yet 3630 * exist, it will be created first. 3631 * @param {Element} container 3632 */ 3633 links.Timeline.Item.prototype.showDOM = function (container) { 3634 // Should be implemented by sub-prototype 3635 }; 3636 3637 /** 3638 * Remove the items DOM from the current HTML container 3639 * @param {Element} container 3640 */ 3641 links.Timeline.Item.prototype.hideDOM = function (container) { 3642 // Should be implemented by sub-prototype 3643 }; 3644 3645 /** 3646 * Update the DOM of the item. This will update the content and the classes 3647 * of the item 3648 */ 3649 links.Timeline.Item.prototype.updateDOM = function () { 3650 // Should be implemented by sub-prototype 3651 }; 3652 3653 /** 3654 * Reposition the item, recalculate its left, top, and width, using the current 3655 * range of the timeline and the timeline options. 3656 * @param {links.Timeline} timeline 3657 */ 3658 links.Timeline.Item.prototype.updatePosition = function (timeline) { 3659 // Should be implemented by sub-prototype 3660 }; 3661 3662 /** 3663 * Check if the item is drawn in the timeline (i.e. the DOM of the item is 3664 * attached to the frame. You may also just request the parameter item.rendered 3665 * @return {boolean} rendered 3666 */ 3667 links.Timeline.Item.prototype.isRendered = function () { 3668 return this.rendered; 3669 }; 3670 3671 /** 3672 * Check if the item is located in the visible area of the timeline, and 3673 * not part of a cluster 3674 * @param {Date} start 3675 * @param {Date} end 3676 * @return {boolean} visible 3677 */ 3678 links.Timeline.Item.prototype.isVisible = function (start, end) { 3679 // Should be implemented by sub-prototype 3680 return false; 3681 }; 3682 3683 /** 3684 * Reposition the item 3685 * @param {Number} left 3686 * @param {Number} right 3687 */ 3688 links.Timeline.Item.prototype.setPosition = function (left, right) { 3689 // Should be implemented by sub-prototype 3690 }; 3691 3692 /** 3693 * Calculate the left position of the item 3694 * @param {links.Timeline} timeline 3695 * @return {Number} left 3696 */ 3697 links.Timeline.Item.prototype.getLeft = function (timeline) { 3698 // Should be implemented by sub-prototype 3699 return 0; 3700 }; 3701 3702 /** 3703 * Calculate the right position of the item 3704 * @param {links.Timeline} timeline 3705 * @return {Number} right 3706 */ 3707 links.Timeline.Item.prototype.getRight = function (timeline) { 3708 // Should be implemented by sub-prototype 3709 return 0; 3710 }; 3711 3712 /** 3713 * Calculate the width of the item 3714 * @param {links.Timeline} timeline 3715 * @return {Number} width 3716 */ 3717 links.Timeline.Item.prototype.getWidth = function (timeline) { 3718 // Should be implemented by sub-prototype 3719 return this.width || 0; // last rendered width 3720 }; 3721 3722 3723 /** 3724 * @constructor links.Timeline.ItemBox 3725 * @extends links.Timeline.Item 3726 * @param {Object} data Object containing parameters start, end 3727 * content, group, type, className, editable. 3728 * @param {Object} [options] Options to set initial property values 3729 * {Number} top 3730 * {Number} left 3731 * {Number} width 3732 * {Number} height 3733 */ 3734 links.Timeline.ItemBox = function (data, options) { 3735 links.Timeline.Item.call(this, data, options); 3736 }; 3737 3738 links.Timeline.ItemBox.prototype = new links.Timeline.Item(); 3739 3740 /** 3741 * Reflow the Item: retrieve its actual size from the DOM 3742 * @return {boolean} resized returns true if the axis is resized 3743 * @override 3744 */ 3745 links.Timeline.ItemBox.prototype.reflow = function () { 3746 var dom = this.dom, 3747 dotHeight = dom.dot.offsetHeight, 3748 dotWidth = dom.dot.offsetWidth, 3749 lineWidth = dom.line.offsetWidth, 3750 resized = ( 3751 (this.dotHeight != dotHeight) || 3752 (this.dotWidth != dotWidth) || 3753 (this.lineWidth != lineWidth) 3754 ); 3755 3756 this.dotHeight = dotHeight; 3757 this.dotWidth = dotWidth; 3758 this.lineWidth = lineWidth; 3759 3760 return resized; 3761 }; 3762 3763 /** 3764 * Select the item 3765 * @override 3766 */ 3767 links.Timeline.ItemBox.prototype.select = function () { 3768 var dom = this.dom; 3769 links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active'); 3770 links.Timeline.addClassName(dom.line, 'timeline-event-selected ui-state-active'); 3771 links.Timeline.addClassName(dom.dot, 'timeline-event-selected ui-state-active'); 3772 }; 3773 3774 /** 3775 * Unselect the item 3776 * @override 3777 */ 3778 links.Timeline.ItemBox.prototype.unselect = function () { 3779 var dom = this.dom; 3780 links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active'); 3781 links.Timeline.removeClassName(dom.line, 'timeline-event-selected ui-state-active'); 3782 links.Timeline.removeClassName(dom.dot, 'timeline-event-selected ui-state-active'); 3783 }; 3784 3785 /** 3786 * Creates the DOM for the item, depending on its type 3787 * @return {Element | undefined} 3788 * @override 3789 */ 3790 links.Timeline.ItemBox.prototype.createDOM = function () { 3791 // background box 3792 var divBox = document.createElement("DIV"); 3793 divBox.style.position = "absolute"; 3794 divBox.style.left = this.left + "px"; 3795 divBox.style.top = this.top + "px"; 3796 3797 // contents box (inside the background box). used for making margins 3798 var divContent = document.createElement("DIV"); 3799 divContent.className = "timeline-event-content"; 3800 divContent.innerHTML = this.content; 3801 divBox.appendChild(divContent); 3802 3803 // line to axis 3804 var divLine = document.createElement("DIV"); 3805 divLine.style.position = "absolute"; 3806 divLine.style.width = "0px"; 3807 // important: the vertical line is added at the front of the list of elements, 3808 // so it will be drawn behind all boxes and ranges 3809 divBox.line = divLine; 3810 3811 // dot on axis 3812 var divDot = document.createElement("DIV"); 3813 divDot.style.position = "absolute"; 3814 divDot.style.width = "0px"; 3815 divDot.style.height = "0px"; 3816 divBox.dot = divDot; 3817 3818 this.dom = divBox; 3819 this.updateDOM(); 3820 3821 return divBox; 3822 }; 3823 3824 /** 3825 * Append the items DOM to the given HTML container. If items DOM does not yet 3826 * exist, it will be created first. 3827 * @param {Element} container 3828 * @override 3829 */ 3830 links.Timeline.ItemBox.prototype.showDOM = function (container) { 3831 var dom = this.dom; 3832 if (!dom) { 3833 dom = this.createDOM(); 3834 } 3835 3836 if (dom.parentNode != container) { 3837 if (dom.parentNode) { 3838 // container is changed. remove from old container 3839 this.hideDOM(); 3840 } 3841 3842 // append to this container 3843 container.appendChild(dom); 3844 container.insertBefore(dom.line, container.firstChild); 3845 // Note: line must be added in front of the this, 3846 // such that it stays below all this 3847 container.appendChild(dom.dot); 3848 this.rendered = true; 3849 } 3850 }; 3851 3852 /** 3853 * Remove the items DOM from the current HTML container, but keep the DOM in 3854 * memory 3855 * @override 3856 */ 3857 links.Timeline.ItemBox.prototype.hideDOM = function () { 3858 var dom = this.dom; 3859 if (dom) { 3860 if (dom.parentNode) { 3861 dom.parentNode.removeChild(dom); 3862 } 3863 if (dom.line && dom.line.parentNode) { 3864 dom.line.parentNode.removeChild(dom.line); 3865 } 3866 if (dom.dot && dom.dot.parentNode) { 3867 dom.dot.parentNode.removeChild(dom.dot); 3868 } 3869 this.rendered = false; 3870 } 3871 }; 3872 3873 /** 3874 * Update the DOM of the item. This will update the content and the classes 3875 * of the item 3876 * @override 3877 */ 3878 links.Timeline.ItemBox.prototype.updateDOM = function () { 3879 var divBox = this.dom; 3880 if (divBox) { 3881 var divLine = divBox.line; 3882 var divDot = divBox.dot; 3883 3884 // update contents 3885 divBox.firstChild.innerHTML = this.content; 3886 3887 // update class 3888 divBox.className = "timeline-event timeline-event-box ui-widget ui-state-default"; 3889 divLine.className = "timeline-event timeline-event-line ui-widget ui-state-default"; 3890 divDot.className = "timeline-event timeline-event-dot ui-widget ui-state-default"; 3891 3892 if (this.isCluster) { 3893 links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header'); 3894 links.Timeline.addClassName(divLine, 'timeline-event-cluster ui-widget-header'); 3895 links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header'); 3896 } 3897 3898 // add item specific class name when provided 3899 if (this.className) { 3900 links.Timeline.addClassName(divBox, this.className); 3901 links.Timeline.addClassName(divLine, this.className); 3902 links.Timeline.addClassName(divDot, this.className); 3903 } 3904 3905 // TODO: apply selected className? 3906 } 3907 }; 3908 3909 /** 3910 * Reposition the item, recalculate its left, top, and width, using the current 3911 * range of the timeline and the timeline options. 3912 * @param {links.Timeline} timeline 3913 * @override 3914 */ 3915 links.Timeline.ItemBox.prototype.updatePosition = function (timeline) { 3916 var dom = this.dom; 3917 if (dom) { 3918 var left = timeline.timeToScreen(this.start), 3919 axisOnTop = timeline.options.axisOnTop, 3920 axisTop = timeline.size.axis.top, 3921 axisHeight = timeline.size.axis.height, 3922 boxAlign = (timeline.options.box && timeline.options.box.align) ? 3923 timeline.options.box.align : undefined; 3924 3925 dom.style.top = this.top + "px"; 3926 if (boxAlign == 'right') { 3927 dom.style.left = (left - this.width) + "px"; 3928 } 3929 else if (boxAlign == 'left') { 3930 dom.style.left = (left) + "px"; 3931 } 3932 else { // default or 'center' 3933 dom.style.left = (left - this.width/2) + "px"; 3934 } 3935 3936 var line = dom.line; 3937 var dot = dom.dot; 3938 line.style.left = (left - this.lineWidth/2) + "px"; 3939 dot.style.left = (left - this.dotWidth/2) + "px"; 3940 if (axisOnTop) { 3941 line.style.top = axisHeight + "px"; 3942 line.style.height = Math.max(this.top - axisHeight, 0) + "px"; 3943 dot.style.top = (axisHeight - this.dotHeight/2) + "px"; 3944 } 3945 else { 3946 line.style.top = (this.top + this.height) + "px"; 3947 line.style.height = Math.max(axisTop - this.top - this.height, 0) + "px"; 3948 dot.style.top = (axisTop - this.dotHeight/2) + "px"; 3949 } 3950 } 3951 }; 3952 3953 /** 3954 * Check if the item is visible in the timeline, and not part of a cluster 3955 * @param {Date} start 3956 * @param {Date} end 3957 * @return {Boolean} visible 3958 * @override 3959 */ 3960 links.Timeline.ItemBox.prototype.isVisible = function (start, end) { 3961 if (this.cluster) { 3962 return false; 3963 } 3964 3965 return (this.start > start) && (this.start < end); 3966 }; 3967 3968 /** 3969 * Reposition the item 3970 * @param {Number} left 3971 * @param {Number} right 3972 * @override 3973 */ 3974 links.Timeline.ItemBox.prototype.setPosition = function (left, right) { 3975 var dom = this.dom; 3976 3977 dom.style.left = (left - this.width / 2) + "px"; 3978 dom.line.style.left = (left - this.lineWidth / 2) + "px"; 3979 dom.dot.style.left = (left - this.dotWidth / 2) + "px"; 3980 3981 if (this.group) { 3982 this.top = this.group.top; 3983 dom.style.top = this.top + 'px'; 3984 } 3985 }; 3986 3987 /** 3988 * Calculate the left position of the item 3989 * @param {links.Timeline} timeline 3990 * @return {Number} left 3991 * @override 3992 */ 3993 links.Timeline.ItemBox.prototype.getLeft = function (timeline) { 3994 var boxAlign = (timeline.options.box && timeline.options.box.align) ? 3995 timeline.options.box.align : undefined; 3996 3997 var left = timeline.timeToScreen(this.start); 3998 if (boxAlign == 'right') { 3999 left = left - width; 4000 } 4001 else { // default or 'center' 4002 left = (left - this.width / 2); 4003 } 4004 4005 return left; 4006 }; 4007 4008 /** 4009 * Calculate the right position of the item 4010 * @param {links.Timeline} timeline 4011 * @return {Number} right 4012 * @override 4013 */ 4014 links.Timeline.ItemBox.prototype.getRight = function (timeline) { 4015 var boxAlign = (timeline.options.box && timeline.options.box.align) ? 4016 timeline.options.box.align : undefined; 4017 4018 var left = timeline.timeToScreen(this.start); 4019 var right; 4020 if (boxAlign == 'right') { 4021 right = left; 4022 } 4023 else if (boxAlign == 'left') { 4024 right = (left + this.width); 4025 } 4026 else { // default or 'center' 4027 right = (left + this.width / 2); 4028 } 4029 4030 return right; 4031 }; 4032 4033 /** 4034 * @constructor links.Timeline.ItemRange 4035 * @extends links.Timeline.Item 4036 * @param {Object} data Object containing parameters start, end 4037 * content, group, type, className, editable. 4038 * @param {Object} [options] Options to set initial property values 4039 * {Number} top 4040 * {Number} left 4041 * {Number} width 4042 * {Number} height 4043 */ 4044 links.Timeline.ItemRange = function (data, options) { 4045 links.Timeline.Item.call(this, data, options); 4046 }; 4047 4048 links.Timeline.ItemRange.prototype = new links.Timeline.Item(); 4049 4050 /** 4051 * Select the item 4052 * @override 4053 */ 4054 links.Timeline.ItemRange.prototype.select = function () { 4055 var dom = this.dom; 4056 links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active'); 4057 }; 4058 4059 /** 4060 * Unselect the item 4061 * @override 4062 */ 4063 links.Timeline.ItemRange.prototype.unselect = function () { 4064 var dom = this.dom; 4065 links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active'); 4066 }; 4067 4068 /** 4069 * Creates the DOM for the item, depending on its type 4070 * @return {Element | undefined} 4071 * @override 4072 */ 4073 links.Timeline.ItemRange.prototype.createDOM = function () { 4074 // background box 4075 var divBox = document.createElement("DIV"); 4076 divBox.style.position = "absolute"; 4077 4078 // contents box 4079 var divContent = document.createElement("DIV"); 4080 divContent.className = "timeline-event-content"; 4081 divBox.appendChild(divContent); 4082 4083 this.dom = divBox; 4084 this.updateDOM(); 4085 4086 return divBox; 4087 }; 4088 4089 /** 4090 * Append the items DOM to the given HTML container. If items DOM does not yet 4091 * exist, it will be created first. 4092 * @param {Element} container 4093 * @override 4094 */ 4095 links.Timeline.ItemRange.prototype.showDOM = function (container) { 4096 var dom = this.dom; 4097 if (!dom) { 4098 dom = this.createDOM(); 4099 } 4100 4101 if (dom.parentNode != container) { 4102 if (dom.parentNode) { 4103 // container changed. remove the item from the old container 4104 this.hideDOM(); 4105 } 4106 4107 // append to the new container 4108 container.appendChild(dom); 4109 this.rendered = true; 4110 } 4111 }; 4112 4113 /** 4114 * Remove the items DOM from the current HTML container 4115 * The DOM will be kept in memory 4116 * @override 4117 */ 4118 links.Timeline.ItemRange.prototype.hideDOM = function () { 4119 var dom = this.dom; 4120 if (dom) { 4121 if (dom.parentNode) { 4122 dom.parentNode.removeChild(dom); 4123 } 4124 this.rendered = false; 4125 } 4126 }; 4127 4128 /** 4129 * Update the DOM of the item. This will update the content and the classes 4130 * of the item 4131 * @override 4132 */ 4133 links.Timeline.ItemRange.prototype.updateDOM = function () { 4134 var divBox = this.dom; 4135 if (divBox) { 4136 // update contents 4137 divBox.firstChild.innerHTML = this.content; 4138 4139 // update class 4140 divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default"; 4141 4142 if (this.isCluster) { 4143 links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header'); 4144 } 4145 4146 // add item specific class name when provided 4147 if (this.className) { 4148 links.Timeline.addClassName(divBox, this.className); 4149 } 4150 4151 // TODO: apply selected className? 4152 } 4153 }; 4154 4155 /** 4156 * Reposition the item, recalculate its left, top, and width, using the current 4157 * range of the timeline and the timeline options. * 4158 * @param {links.Timeline} timeline 4159 * @override 4160 */ 4161 links.Timeline.ItemRange.prototype.updatePosition = function (timeline) { 4162 var dom = this.dom; 4163 if (dom) { 4164 var contentWidth = timeline.size.contentWidth, 4165 left = timeline.timeToScreen(this.start), 4166 right = timeline.timeToScreen(this.end); 4167 4168 // limit the width of the this, as browsers cannot draw very wide divs 4169 if (left < -contentWidth) { 4170 left = -contentWidth; 4171 } 4172 if (right > 2 * contentWidth) { 4173 right = 2 * contentWidth; 4174 } 4175 4176 dom.style.top = this.top + "px"; 4177 dom.style.left = left + "px"; 4178 //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth 4179 dom.style.width = Math.max(right - left, 1) + "px"; 4180 } 4181 }; 4182 4183 /** 4184 * Check if the item is visible in the timeline, and not part of a cluster 4185 * @param {Number} start 4186 * @param {Number} end 4187 * @return {boolean} visible 4188 * @override 4189 */ 4190 links.Timeline.ItemRange.prototype.isVisible = function (start, end) { 4191 if (this.cluster) { 4192 return false; 4193 } 4194 4195 return (this.end > start) 4196 && (this.start < end); 4197 }; 4198 4199 /** 4200 * Reposition the item 4201 * @param {Number} left 4202 * @param {Number} right 4203 * @override 4204 */ 4205 links.Timeline.ItemRange.prototype.setPosition = function (left, right) { 4206 var dom = this.dom; 4207 4208 dom.style.left = left + 'px'; 4209 dom.style.width = (right - left) + 'px'; 4210 4211 if (this.group) { 4212 this.top = this.group.top; 4213 dom.style.top = this.top + 'px'; 4214 } 4215 }; 4216 4217 /** 4218 * Calculate the left position of the item 4219 * @param {links.Timeline} timeline 4220 * @return {Number} left 4221 * @override 4222 */ 4223 links.Timeline.ItemRange.prototype.getLeft = function (timeline) { 4224 return timeline.timeToScreen(this.start); 4225 }; 4226 4227 /** 4228 * Calculate the right position of the item 4229 * @param {links.Timeline} timeline 4230 * @return {Number} right 4231 * @override 4232 */ 4233 links.Timeline.ItemRange.prototype.getRight = function (timeline) { 4234 return timeline.timeToScreen(this.end); 4235 }; 4236 4237 /** 4238 * Calculate the width of the item 4239 * @param {links.Timeline} timeline 4240 * @return {Number} width 4241 * @override 4242 */ 4243 links.Timeline.ItemRange.prototype.getWidth = function (timeline) { 4244 return timeline.timeToScreen(this.end) - timeline.timeToScreen(this.start); 4245 }; 4246 4247 /** 4248 * @constructor links.Timeline.ItemFloatingRange 4249 * @extends links.Timeline.Item 4250 * @param {Object} data Object containing parameters start, end 4251 * content, group, type, className, editable. 4252 * @param {Object} [options] Options to set initial property values 4253 * {Number} top 4254 * {Number} left 4255 * {Number} width 4256 * {Number} height 4257 */ 4258 links.Timeline.ItemFloatingRange = function (data, options) { 4259 links.Timeline.Item.call(this, data, options); 4260 }; 4261 4262 links.Timeline.ItemFloatingRange.prototype = new links.Timeline.Item(); 4263 4264 /** 4265 * Select the item 4266 * @override 4267 */ 4268 links.Timeline.ItemFloatingRange.prototype.select = function () { 4269 var dom = this.dom; 4270 links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active'); 4271 }; 4272 4273 /** 4274 * Unselect the item 4275 * @override 4276 */ 4277 links.Timeline.ItemFloatingRange.prototype.unselect = function () { 4278 var dom = this.dom; 4279 links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active'); 4280 }; 4281 4282 /** 4283 * Creates the DOM for the item, depending on its type 4284 * @return {Element | undefined} 4285 * @override 4286 */ 4287 links.Timeline.ItemFloatingRange.prototype.createDOM = function () { 4288 // background box 4289 var divBox = document.createElement("DIV"); 4290 divBox.style.position = "absolute"; 4291 4292 // contents box 4293 var divContent = document.createElement("DIV"); 4294 divContent.className = "timeline-event-content"; 4295 divBox.appendChild(divContent); 4296 4297 this.dom = divBox; 4298 this.updateDOM(); 4299 4300 return divBox; 4301 }; 4302 4303 /** 4304 * Append the items DOM to the given HTML container. If items DOM does not yet 4305 * exist, it will be created first. 4306 * @param {Element} container 4307 * @override 4308 */ 4309 links.Timeline.ItemFloatingRange.prototype.showDOM = function (container) { 4310 var dom = this.dom; 4311 if (!dom) { 4312 dom = this.createDOM(); 4313 } 4314 4315 if (dom.parentNode != container) { 4316 if (dom.parentNode) { 4317 // container changed. remove the item from the old container 4318 this.hideDOM(); 4319 } 4320 4321 // append to the new container 4322 container.appendChild(dom); 4323 this.rendered = true; 4324 } 4325 }; 4326 4327 /** 4328 * Remove the items DOM from the current HTML container 4329 * The DOM will be kept in memory 4330 * @override 4331 */ 4332 links.Timeline.ItemFloatingRange.prototype.hideDOM = function () { 4333 var dom = this.dom; 4334 if (dom) { 4335 if (dom.parentNode) { 4336 dom.parentNode.removeChild(dom); 4337 } 4338 this.rendered = false; 4339 } 4340 }; 4341 4342 /** 4343 * Update the DOM of the item. This will update the content and the classes 4344 * of the item 4345 * @override 4346 */ 4347 links.Timeline.ItemFloatingRange.prototype.updateDOM = function () { 4348 var divBox = this.dom; 4349 if (divBox) { 4350 // update contents 4351 divBox.firstChild.innerHTML = this.content; 4352 4353 // update class 4354 divBox.className = "timeline-event timeline-event-range ui-widget ui-state-default"; 4355 4356 if (this.isCluster) { 4357 links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header'); 4358 } 4359 4360 // add item specific class name when provided 4361 if (this.className) { 4362 links.Timeline.addClassName(divBox, this.className); 4363 } 4364 4365 // TODO: apply selected className? 4366 } 4367 }; 4368 4369 /** 4370 * Reposition the item, recalculate its left, top, and width, using the current 4371 * range of the timeline and the timeline options. * 4372 * @param {links.Timeline} timeline 4373 * @override 4374 */ 4375 links.Timeline.ItemFloatingRange.prototype.updatePosition = function (timeline) { 4376 var dom = this.dom; 4377 if (dom) { 4378 var contentWidth = timeline.size.contentWidth, 4379 left = this.getLeft(timeline), // NH use getLeft 4380 right = this.getRight(timeline); // NH use getRight; 4381 4382 // limit the width of the this, as browsers cannot draw very wide divs 4383 if (left < -contentWidth) { 4384 left = -contentWidth; 4385 } 4386 if (right > 2 * contentWidth) { 4387 right = 2 * contentWidth; 4388 } 4389 4390 dom.style.top = this.top + "px"; 4391 dom.style.left = left + "px"; 4392 //dom.style.width = Math.max(right - left - 2 * this.borderWidth, 1) + "px"; // TODO: borderWidth 4393 dom.style.width = Math.max(right - left, 1) + "px"; 4394 } 4395 }; 4396 4397 /** 4398 * Check if the item is visible in the timeline, and not part of a cluster 4399 * @param {Number} start 4400 * @param {Number} end 4401 * @return {boolean} visible 4402 * @override 4403 */ 4404 links.Timeline.ItemFloatingRange.prototype.isVisible = function (start, end) { 4405 if (this.cluster) { 4406 return false; 4407 } 4408 4409 // NH check for no end value 4410 if (this.end && this.start) { 4411 return (this.end > start) 4412 && (this.start < end); 4413 } else if (this.start) { 4414 return (this.start < end); 4415 } else if (this.end) { 4416 return (this.end > start); 4417 } else {return true;} 4418 }; 4419 4420 /** 4421 * Reposition the item 4422 * @param {Number} left 4423 * @param {Number} right 4424 * @override 4425 */ 4426 links.Timeline.ItemFloatingRange.prototype.setPosition = function (left, right) { 4427 var dom = this.dom; 4428 4429 dom.style.left = left + 'px'; 4430 dom.style.width = (right - left) + 'px'; 4431 4432 if (this.group) { 4433 this.top = this.group.top; 4434 dom.style.top = this.top + 'px'; 4435 } 4436 }; 4437 4438 /** 4439 * Calculate the left position of the item 4440 * @param {links.Timeline} timeline 4441 * @return {Number} left 4442 * @override 4443 */ 4444 links.Timeline.ItemFloatingRange.prototype.getLeft = function (timeline) { 4445 // NH check for no start value 4446 if (this.start) { 4447 return timeline.timeToScreen(this.start); 4448 } else { 4449 return 0; 4450 } 4451 }; 4452 4453 /** 4454 * Calculate the right position of the item 4455 * @param {links.Timeline} timeline 4456 * @return {Number} right 4457 * @override 4458 */ 4459 links.Timeline.ItemFloatingRange.prototype.getRight = function (timeline) { 4460 // NH check for no end value 4461 if (this.end) { 4462 return timeline.timeToScreen(this.end); 4463 } else { 4464 return timeline.size.contentWidth; 4465 } 4466 }; 4467 4468 /** 4469 * Calculate the width of the item 4470 * @param {links.Timeline} timeline 4471 * @return {Number} width 4472 * @override 4473 */ 4474 links.Timeline.ItemFloatingRange.prototype.getWidth = function (timeline) { 4475 return this.getRight(timeline) - this.getLeft(timeline); 4476 }; 4477 4478 /** 4479 * @constructor links.Timeline.ItemDot 4480 * @extends links.Timeline.Item 4481 * @param {Object} data Object containing parameters start, end 4482 * content, group, type, className, editable. 4483 * @param {Object} [options] Options to set initial property values 4484 * {Number} top 4485 * {Number} left 4486 * {Number} width 4487 * {Number} height 4488 */ 4489 links.Timeline.ItemDot = function (data, options) { 4490 links.Timeline.Item.call(this, data, options); 4491 }; 4492 4493 links.Timeline.ItemDot.prototype = new links.Timeline.Item(); 4494 4495 /** 4496 * Reflow the Item: retrieve its actual size from the DOM 4497 * @return {boolean} resized returns true if the axis is resized 4498 * @override 4499 */ 4500 links.Timeline.ItemDot.prototype.reflow = function () { 4501 var dom = this.dom, 4502 dotHeight = dom.dot.offsetHeight, 4503 dotWidth = dom.dot.offsetWidth, 4504 contentHeight = dom.content.offsetHeight, 4505 resized = ( 4506 (this.dotHeight != dotHeight) || 4507 (this.dotWidth != dotWidth) || 4508 (this.contentHeight != contentHeight) 4509 ); 4510 4511 this.dotHeight = dotHeight; 4512 this.dotWidth = dotWidth; 4513 this.contentHeight = contentHeight; 4514 4515 return resized; 4516 }; 4517 4518 /** 4519 * Select the item 4520 * @override 4521 */ 4522 links.Timeline.ItemDot.prototype.select = function () { 4523 var dom = this.dom; 4524 links.Timeline.addClassName(dom, 'timeline-event-selected ui-state-active'); 4525 }; 4526 4527 /** 4528 * Unselect the item 4529 * @override 4530 */ 4531 links.Timeline.ItemDot.prototype.unselect = function () { 4532 var dom = this.dom; 4533 links.Timeline.removeClassName(dom, 'timeline-event-selected ui-state-active'); 4534 }; 4535 4536 /** 4537 * Creates the DOM for the item, depending on its type 4538 * @return {Element | undefined} 4539 * @override 4540 */ 4541 links.Timeline.ItemDot.prototype.createDOM = function () { 4542 // background box 4543 var divBox = document.createElement("DIV"); 4544 divBox.style.position = "absolute"; 4545 4546 // contents box, right from the dot 4547 var divContent = document.createElement("DIV"); 4548 divContent.className = "timeline-event-content"; 4549 divBox.appendChild(divContent); 4550 4551 // dot at start 4552 var divDot = document.createElement("DIV"); 4553 divDot.style.position = "absolute"; 4554 divDot.style.width = "0px"; 4555 divDot.style.height = "0px"; 4556 divBox.appendChild(divDot); 4557 4558 divBox.content = divContent; 4559 divBox.dot = divDot; 4560 4561 this.dom = divBox; 4562 this.updateDOM(); 4563 4564 return divBox; 4565 }; 4566 4567 /** 4568 * Append the items DOM to the given HTML container. If items DOM does not yet 4569 * exist, it will be created first. 4570 * @param {Element} container 4571 * @override 4572 */ 4573 links.Timeline.ItemDot.prototype.showDOM = function (container) { 4574 var dom = this.dom; 4575 if (!dom) { 4576 dom = this.createDOM(); 4577 } 4578 4579 if (dom.parentNode != container) { 4580 if (dom.parentNode) { 4581 // container changed. remove it from old container first 4582 this.hideDOM(); 4583 } 4584 4585 // append to container 4586 container.appendChild(dom); 4587 this.rendered = true; 4588 } 4589 }; 4590 4591 /** 4592 * Remove the items DOM from the current HTML container 4593 * @override 4594 */ 4595 links.Timeline.ItemDot.prototype.hideDOM = function () { 4596 var dom = this.dom; 4597 if (dom) { 4598 if (dom.parentNode) { 4599 dom.parentNode.removeChild(dom); 4600 } 4601 this.rendered = false; 4602 } 4603 }; 4604 4605 /** 4606 * Update the DOM of the item. This will update the content and the classes 4607 * of the item 4608 * @override 4609 */ 4610 links.Timeline.ItemDot.prototype.updateDOM = function () { 4611 if (this.dom) { 4612 var divBox = this.dom; 4613 var divDot = divBox.dot; 4614 4615 // update contents 4616 divBox.firstChild.innerHTML = this.content; 4617 4618 // update classes 4619 divBox.className = "timeline-event-dot-container"; 4620 divDot.className = "timeline-event timeline-event-dot ui-widget ui-state-default"; 4621 4622 if (this.isCluster) { 4623 links.Timeline.addClassName(divBox, 'timeline-event-cluster ui-widget-header'); 4624 links.Timeline.addClassName(divDot, 'timeline-event-cluster ui-widget-header'); 4625 } 4626 4627 // add item specific class name when provided 4628 if (this.className) { 4629 links.Timeline.addClassName(divBox, this.className); 4630 links.Timeline.addClassName(divDot, this.className); 4631 } 4632 4633 // TODO: apply selected className? 4634 } 4635 }; 4636 4637 /** 4638 * Reposition the item, recalculate its left, top, and width, using the current 4639 * range of the timeline and the timeline options. * 4640 * @param {links.Timeline} timeline 4641 * @override 4642 */ 4643 links.Timeline.ItemDot.prototype.updatePosition = function (timeline) { 4644 var dom = this.dom; 4645 if (dom) { 4646 var left = timeline.timeToScreen(this.start); 4647 4648 dom.style.top = this.top + "px"; 4649 dom.style.left = (left - this.dotWidth / 2) + "px"; 4650 4651 dom.content.style.marginLeft = (1.5 * this.dotWidth) + "px"; 4652 //dom.content.style.marginRight = (0.5 * this.dotWidth) + "px"; // TODO 4653 dom.dot.style.top = ((this.height - this.dotHeight) / 2) + "px"; 4654 } 4655 }; 4656 4657 /** 4658 * Check if the item is visible in the timeline, and not part of a cluster. 4659 * @param {Date} start 4660 * @param {Date} end 4661 * @return {boolean} visible 4662 * @override 4663 */ 4664 links.Timeline.ItemDot.prototype.isVisible = function (start, end) { 4665 if (this.cluster) { 4666 return false; 4667 } 4668 4669 return (this.start > start) 4670 && (this.start < end); 4671 }; 4672 4673 /** 4674 * Reposition the item 4675 * @param {Number} left 4676 * @param {Number} right 4677 * @override 4678 */ 4679 links.Timeline.ItemDot.prototype.setPosition = function (left, right) { 4680 var dom = this.dom; 4681 4682 dom.style.left = (left - this.dotWidth / 2) + "px"; 4683 4684 if (this.group) { 4685 this.top = this.group.top; 4686 dom.style.top = this.top + 'px'; 4687 } 4688 }; 4689 4690 /** 4691 * Calculate the left position of the item 4692 * @param {links.Timeline} timeline 4693 * @return {Number} left 4694 * @override 4695 */ 4696 links.Timeline.ItemDot.prototype.getLeft = function (timeline) { 4697 return timeline.timeToScreen(this.start); 4698 }; 4699 4700 /** 4701 * Calculate the right position of the item 4702 * @param {links.Timeline} timeline 4703 * @return {Number} right 4704 * @override 4705 */ 4706 links.Timeline.ItemDot.prototype.getRight = function (timeline) { 4707 return timeline.timeToScreen(this.start) + this.width; 4708 }; 4709 4710 /** 4711 * Retrieve the properties of an item. 4712 * @param {Number} index 4713 * @return {Object} itemData Object containing item properties:<br> 4714 * {Date} start (required), 4715 * {Date} end (optional), 4716 * {String} content (required), 4717 * {String} group (optional), 4718 * {String} className (optional) 4719 * {boolean} editable (optional) 4720 * {String} type (optional) 4721 */ 4722 links.Timeline.prototype.getItem = function (index) { 4723 if (index >= this.items.length) { 4724 throw "Cannot get item, index out of range"; 4725 } 4726 4727 // take the original data as start, includes foreign fields 4728 var data = this.data, 4729 itemData; 4730 if (google && google.visualization && 4731 data instanceof google.visualization.DataTable) { 4732 // map the datatable columns 4733 var cols = links.Timeline.mapColumnIds(data); 4734 4735 itemData = {}; 4736 for (var col in cols) { 4737 if (cols.hasOwnProperty(col)) { 4738 itemData[col] = this.data.getValue(index, cols[col]); 4739 } 4740 } 4741 } 4742 else if (links.Timeline.isArray(this.data)) { 4743 // read JSON array 4744 itemData = links.Timeline.clone(this.data[index]); 4745 } 4746 else { 4747 throw "Unknown data type. DataTable or Array expected."; 4748 } 4749 4750 // override the data with current settings of the item (should be the same) 4751 var item = this.items[index]; 4752 4753 itemData.start = new Date(item.start.valueOf()); 4754 if (item.end) { 4755 itemData.end = new Date(item.end.valueOf()); 4756 } 4757 itemData.content = item.content; 4758 if (item.group) { 4759 itemData.group = this.getGroupName(item.group); 4760 } 4761 if (item.className) { 4762 itemData.className = item.className; 4763 } 4764 if (typeof item.editable !== 'undefined') { 4765 itemData.editable = item.editable; 4766 } 4767 if (item.type) { 4768 itemData.type = item.type; 4769 } 4770 4771 return itemData; 4772 }; 4773 4774 4775 /** 4776 * Retrieve the properties of a cluster. 4777 * @param {Number} index 4778 * @return {Object} clusterdata Object containing cluster properties:<br> 4779 * {Date} start (required), 4780 * {String} type (optional) 4781 * {Array} array with item data as is in getItem() 4782 */ 4783 links.Timeline.prototype.getCluster = function (index) { 4784 if (index >= this.clusters.length) { 4785 throw "Cannot get cluster, index out of range"; 4786 } 4787 4788 var clusterData = {}, 4789 cluster = this.clusters[index], 4790 clusterItems = cluster.items; 4791 4792 clusterData.start = new Date(cluster.start.valueOf()); 4793 if (cluster.type) { 4794 clusterData.type = cluster.type; 4795 } 4796 4797 // push cluster item data 4798 clusterData.items = []; 4799 for(var i = 0; i < clusterItems.length; i++){ 4800 for(var j = 0; j < this.items.length; j++){ 4801 // TODO could be nicer to be able to have the item index into the cluster 4802 if(this.items[j] == clusterItems[i]) 4803 { 4804 clusterData.items.push(this.getItem(j)); 4805 break; 4806 } 4807 4808 } 4809 } 4810 4811 return clusterData; 4812 }; 4813 4814 /** 4815 * Add a new item. 4816 * @param {Object} itemData Object containing item properties:<br> 4817 * {Date} start (required), 4818 * {Date} end (optional), 4819 * {String} content (required), 4820 * {String} group (optional) 4821 * {String} className (optional) 4822 * {Boolean} editable (optional) 4823 * {String} type (optional) 4824 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4825 */ 4826 links.Timeline.prototype.addItem = function (itemData, preventRender) { 4827 var itemsData = [ 4828 itemData 4829 ]; 4830 4831 this.addItems(itemsData, preventRender); 4832 }; 4833 4834 /** 4835 * Add new items. 4836 * @param {Array} itemsData An array containing Objects. 4837 * The objects must have the following parameters: 4838 * {Date} start, 4839 * {Date} end, 4840 * {String} content with text or HTML code, 4841 * {String} group (optional) 4842 * {String} className (optional) 4843 * {String} editable (optional) 4844 * {String} type (optional) 4845 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4846 */ 4847 links.Timeline.prototype.addItems = function (itemsData, preventRender) { 4848 var timeline = this, 4849 items = this.items; 4850 4851 // append the items 4852 itemsData.forEach(function (itemData) { 4853 var index = items.length; 4854 items.push(timeline.createItem(itemData)); 4855 timeline.updateData(index, itemData); 4856 4857 // note: there is no need to add the item to the renderQueue, that 4858 // will be done when this.render() is executed and all items are 4859 // filtered again. 4860 }); 4861 4862 // prepare data for clustering, by filtering and sorting by type 4863 if (this.options.cluster) { 4864 this.clusterGenerator.updateData(); 4865 } 4866 4867 if (!preventRender) { 4868 this.render({ 4869 animate: false 4870 }); 4871 } 4872 }; 4873 4874 /** 4875 * Create an item object, containing all needed parameters 4876 * @param {Object} itemData Object containing parameters start, end 4877 * content, group. 4878 * @return {Object} item 4879 */ 4880 links.Timeline.prototype.createItem = function(itemData) { 4881 var type = itemData.type || (itemData.end ? 'range' : this.options.style); 4882 var data = links.Timeline.clone(itemData); 4883 data.type = type; 4884 data.group = this.getGroup(itemData.group); 4885 // TODO: optimize this, when creating an item, all data is copied twice... 4886 4887 // TODO: is initialTop needed? 4888 var initialTop, 4889 options = this.options; 4890 if (options.axisOnTop) { 4891 initialTop = this.size.axis.height + options.eventMarginAxis + options.eventMargin / 2; 4892 } 4893 else { 4894 initialTop = this.size.contentHeight - options.eventMarginAxis - options.eventMargin / 2; 4895 } 4896 4897 if (type in this.itemTypes) { 4898 return new this.itemTypes[type](data, {'top': initialTop}) 4899 } 4900 4901 console.log('ERROR: Unknown event type "' + type + '"'); 4902 return new links.Timeline.Item(data, { 4903 'top': initialTop 4904 }); 4905 }; 4906 4907 /** 4908 * Edit an item 4909 * @param {Number} index 4910 * @param {Object} itemData Object containing item properties:<br> 4911 * {Date} start (required), 4912 * {Date} end (optional), 4913 * {String} content (required), 4914 * {String} group (optional) 4915 * @param {boolean} [preventRender=false] Do not re-render timeline if true 4916 */ 4917 links.Timeline.prototype.changeItem = function (index, itemData, preventRender) { 4918 var oldItem = this.items[index]; 4919 if (!oldItem) { 4920 throw "Cannot change item, index out of range"; 4921 } 4922 4923 // replace item, merge the changes 4924 var newItem = this.createItem({ 4925 'start': itemData.hasOwnProperty('start') ? itemData.start : oldItem.start, 4926 'end': itemData.hasOwnProperty('end') ? itemData.end : oldItem.end, 4927 'content': itemData.hasOwnProperty('content') ? itemData.content : oldItem.content, 4928 'group': itemData.hasOwnProperty('group') ? itemData.group : this.getGroupName(oldItem.group), 4929 'className': itemData.hasOwnProperty('className') ? itemData.className : oldItem.className, 4930 'editable': itemData.hasOwnProperty('editable') ? itemData.editable : oldItem.editable, 4931 'type': itemData.hasOwnProperty('type') ? itemData.type : oldItem.type 4932 }); 4933 this.items[index] = newItem; 4934 4935 // append the changes to the render queue 4936 this.renderQueue.hide.push(oldItem); 4937 this.renderQueue.show.push(newItem); 4938 4939 // update the original data table 4940 this.updateData(index, itemData); 4941 4942 // prepare data for clustering, by filtering and sorting by type 4943 if (this.options.cluster) { 4944 this.clusterGenerator.updateData(); 4945 } 4946 4947 if (!preventRender) { 4948 // redraw timeline 4949 this.render({ 4950 animate: false 4951 }); 4952 4953 if (this.selection && this.selection.index == index) { 4954 newItem.select(); 4955 } 4956 } 4957 }; 4958 4959 /** 4960 * Delete all groups 4961 */ 4962 links.Timeline.prototype.deleteGroups = function () { 4963 this.groups = []; 4964 this.groupIndexes = {}; 4965 }; 4966 4967 4968 /** 4969 * Get a group by the group name. When the group does not exist, 4970 * it will be created. 4971 * @param {String} groupName the name of the group 4972 * @return {Object} groupObject 4973 */ 4974 links.Timeline.prototype.getGroup = function (groupName) { 4975 var groups = this.groups, 4976 groupIndexes = this.groupIndexes, 4977 groupObj = undefined; 4978 4979 var groupIndex = groupIndexes[groupName]; 4980 if (groupIndex == undefined && groupName != undefined) { // not null or undefined 4981 groupObj = { 4982 'content': groupName, 4983 'labelTop': 0, 4984 'lineTop': 0 4985 // note: this object will lateron get addition information, 4986 // such as height and width of the group 4987 }; 4988 groups.push(groupObj); 4989 // sort the groups 4990 if (this.options.groupsOrder == true) { 4991 groups = groups.sort(function (a, b) { 4992 if (a.content > b.content) { 4993 return 1; 4994 } 4995 if (a.content < b.content) { 4996 return -1; 4997 } 4998 return 0; 4999 }); 5000 } else if (typeof(this.options.groupsOrder) == "function") { 5001 groups = groups.sort(this.options.groupsOrder) 5002 } 5003 5004 // rebuilt the groupIndexes 5005 for (var i = 0, iMax = groups.length; i < iMax; i++) { 5006 groupIndexes[groups[i].content] = i; 5007 } 5008 } 5009 else { 5010 groupObj = groups[groupIndex]; 5011 } 5012 5013 return groupObj; 5014 }; 5015 5016 /** 5017 * Get the group name from a group object. 5018 * @param {Object} groupObj 5019 * @return {String} groupName the name of the group, or undefined when group 5020 * was not provided 5021 */ 5022 links.Timeline.prototype.getGroupName = function (groupObj) { 5023 return groupObj ? groupObj.content : undefined; 5024 }; 5025 5026 /** 5027 * Cancel a change item 5028 * This method can be called insed an event listener which catches the "change" 5029 * event. The changed event position will be undone. 5030 */ 5031 links.Timeline.prototype.cancelChange = function () { 5032 this.applyChange = false; 5033 }; 5034 5035 /** 5036 * Cancel deletion of an item 5037 * This method can be called insed an event listener which catches the "delete" 5038 * event. Deletion of the event will be undone. 5039 */ 5040 links.Timeline.prototype.cancelDelete = function () { 5041 this.applyDelete = false; 5042 }; 5043 5044 5045 /** 5046 * Cancel creation of a new item 5047 * This method can be called insed an event listener which catches the "new" 5048 * event. Creation of the new the event will be undone. 5049 */ 5050 links.Timeline.prototype.cancelAdd = function () { 5051 this.applyAdd = false; 5052 }; 5053 5054 5055 /** 5056 * Select an event. The visible chart range will be moved such that the selected 5057 * event is placed in the middle. 5058 * For example selection = [{row: 5}]; 5059 * @param {Array} selection An array with a column row, containing the row 5060 * number (the id) of the event to be selected. 5061 * @return {boolean} true if selection is succesfully set, else false. 5062 */ 5063 links.Timeline.prototype.setSelection = function(selection) { 5064 if (selection != undefined && selection.length > 0) { 5065 if (selection[0].row != undefined) { 5066 var index = selection[0].row; 5067 if (this.items[index]) { 5068 var item = this.items[index]; 5069 this.selectItem(index); 5070 5071 // move the visible chart range to the selected event. 5072 var start = item.start; 5073 var end = item.end; 5074 var middle; // number 5075 if (end != undefined) { 5076 middle = (end.valueOf() + start.valueOf()) / 2; 5077 } else { 5078 middle = start.valueOf(); 5079 } 5080 var diff = (this.end.valueOf() - this.start.valueOf()), 5081 newStart = new Date(middle - diff/2), 5082 newEnd = new Date(middle + diff/2); 5083 5084 this.setVisibleChartRange(newStart, newEnd); 5085 5086 return true; 5087 } 5088 } 5089 } 5090 else { 5091 // unselect current selection 5092 this.unselectItem(); 5093 } 5094 return false; 5095 }; 5096 5097 /** 5098 * Retrieve the currently selected event 5099 * @return {Array} sel An array with a column row, containing the row number 5100 * of the selected event. If there is no selection, an 5101 * empty array is returned. 5102 */ 5103 links.Timeline.prototype.getSelection = function() { 5104 var sel = []; 5105 if (this.selection) { 5106 if(this.selection.index !== undefined) 5107 { 5108 sel.push({"row": this.selection.index}); 5109 } else { 5110 sel.push({"cluster": this.selection.cluster}); 5111 } 5112 } 5113 return sel; 5114 }; 5115 5116 5117 /** 5118 * Select an item by its index 5119 * @param {Number} index 5120 */ 5121 links.Timeline.prototype.selectItem = function(index) { 5122 this.unselectItem(); 5123 5124 this.selection = undefined; 5125 5126 if (this.items[index] != undefined) { 5127 var item = this.items[index], 5128 domItem = item.dom; 5129 5130 this.selection = { 5131 'index': index 5132 }; 5133 5134 if (item && item.dom) { 5135 // TODO: move adjusting the domItem to the item itself 5136 if (this.isEditable(item)) { 5137 item.dom.style.cursor = 'move'; 5138 } 5139 item.select(); 5140 } 5141 this.repaintDeleteButton(); 5142 this.repaintDragAreas(); 5143 } 5144 }; 5145 5146 /** 5147 * Select an cluster by its index 5148 * @param {Number} index 5149 */ 5150 links.Timeline.prototype.selectCluster = function(index) { 5151 this.unselectItem(); 5152 5153 this.selection = undefined; 5154 5155 if (this.clusters[index] != undefined) { 5156 this.selection = { 5157 'cluster': index 5158 }; 5159 this.repaintDeleteButton(); 5160 this.repaintDragAreas(); 5161 } 5162 }; 5163 5164 /** 5165 * Check if an item is currently selected 5166 * @param {Number} index 5167 * @return {boolean} true if row is selected, else false 5168 */ 5169 links.Timeline.prototype.isSelected = function (index) { 5170 return (this.selection && this.selection.index == index); 5171 }; 5172 5173 /** 5174 * Unselect the currently selected event (if any) 5175 */ 5176 links.Timeline.prototype.unselectItem = function() { 5177 if (this.selection && this.selection.index !== undefined) { 5178 var item = this.items[this.selection.index]; 5179 5180 if (item && item.dom) { 5181 var domItem = item.dom; 5182 domItem.style.cursor = ''; 5183 item.unselect(); 5184 } 5185 5186 this.selection = undefined; 5187 this.repaintDeleteButton(); 5188 this.repaintDragAreas(); 5189 } 5190 }; 5191 5192 5193 /** 5194 * Stack the items such that they don't overlap. The items will have a minimal 5195 * distance equal to options.eventMargin. 5196 * @param {boolean | undefined} animate if animate is true, the items are 5197 * moved to their new position animated 5198 * defaults to false. 5199 */ 5200 links.Timeline.prototype.stackItems = function(animate) { 5201 if (animate == undefined) { 5202 animate = false; 5203 } 5204 5205 // calculate the order and final stack position of the items 5206 var stack = this.stack; 5207 if (!stack) { 5208 stack = {}; 5209 this.stack = stack; 5210 } 5211 stack.sortedItems = this.stackOrder(this.renderedItems); 5212 stack.finalItems = this.stackCalculateFinal(stack.sortedItems); 5213 5214 if (animate || stack.timer) { 5215 // move animated to the final positions 5216 var timeline = this; 5217 var step = function () { 5218 var arrived = timeline.stackMoveOneStep(stack.sortedItems, 5219 stack.finalItems); 5220 5221 timeline.repaint(); 5222 5223 if (!arrived) { 5224 stack.timer = setTimeout(step, 30); 5225 } 5226 else { 5227 delete stack.timer; 5228 } 5229 }; 5230 5231 if (!stack.timer) { 5232 stack.timer = setTimeout(step, 30); 5233 } 5234 } 5235 else { 5236 // move immediately to the final positions 5237 this.stackMoveToFinal(stack.sortedItems, stack.finalItems); 5238 } 5239 }; 5240 5241 /** 5242 * Cancel any running animation 5243 */ 5244 links.Timeline.prototype.stackCancelAnimation = function() { 5245 if (this.stack && this.stack.timer) { 5246 clearTimeout(this.stack.timer); 5247 delete this.stack.timer; 5248 } 5249 }; 5250 5251 links.Timeline.prototype.getItemsByGroup = function(items) { 5252 var itemsByGroup = {}; 5253 for (var i = 0; i < items.length; ++i) { 5254 var item = items[i]; 5255 var group = "undefined"; 5256 5257 if (item.group) { 5258 if (item.group.content) { 5259 group = item.group.content; 5260 } else { 5261 group = item.group; 5262 } 5263 } 5264 5265 if (!itemsByGroup[group]) { 5266 itemsByGroup[group] = []; 5267 } 5268 5269 itemsByGroup[group].push(item); 5270 } 5271 5272 return itemsByGroup; 5273 }; 5274 5275 /** 5276 * Order the items in the array this.items. The default order is determined via: 5277 * - Ranges go before boxes and dots. 5278 * - The item with the oldest start time goes first 5279 * If a custom function has been provided via the stackorder option, then this will be used. 5280 * @param {Array} items Array with items 5281 * @return {Array} sortedItems Array with sorted items 5282 */ 5283 links.Timeline.prototype.stackOrder = function(items) { 5284 // TODO: store the sorted items, to have less work later on 5285 var sortedItems = items.concat([]); 5286 5287 //if a customer stack order function exists, use it. 5288 var f = this.options.customStackOrder && (typeof this.options.customStackOrder === 'function') ? this.options.customStackOrder : function (a, b) 5289 { 5290 if ((a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && 5291 !(b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { 5292 return -1; 5293 } 5294 5295 if (!(a instanceof links.Timeline.ItemRange || a instanceof links.Timeline.ItemFloatingRange) && 5296 (b instanceof links.Timeline.ItemRange || b instanceof links.Timeline.ItemFloatingRange)) { 5297 return 1; 5298 } 5299 5300 return (a.left - b.left); 5301 }; 5302 5303 sortedItems.sort(f); 5304 5305 return sortedItems; 5306 }; 5307 5308 /** 5309 * Adjust vertical positions of the events such that they don't overlap each 5310 * other. 5311 * @param {timeline.Item[]} items 5312 * @return {Object[]} finalItems 5313 */ 5314 links.Timeline.prototype.stackCalculateFinal = function(items) { 5315 var size = this.size, 5316 options = this.options, 5317 axisOnTop = options.axisOnTop, 5318 eventMargin = options.eventMargin, 5319 eventMarginAxis = options.eventMarginAxis, 5320 groupBase = (axisOnTop) 5321 ? size.axis.height + eventMarginAxis + eventMargin/2 5322 : size.contentHeight - eventMarginAxis - eventMargin/2, 5323 groupedItems, groupFinalItems, finalItems = []; 5324 5325 groupedItems = this.getItemsByGroup(items); 5326 5327 // 5328 // groupedItems contains all items by group, plus it may contain an 5329 // additional "undefined" group which contains all items with no group. We 5330 // first process the grouped items, and then the ungrouped 5331 // 5332 for (j = 0; j<this.groups.length; ++j) { 5333 var group = this.groups[j]; 5334 5335 if (!groupedItems[group.content]) { 5336 if (axisOnTop) { 5337 groupBase += options.groupMinHeight + eventMargin; 5338 } else { 5339 groupBase -= (options.groupMinHeight + eventMargin); 5340 } 5341 continue; 5342 } 5343 5344 // initialize final positions and fill finalItems 5345 groupFinalItems = this.finalItemsPosition(groupedItems[group.content], groupBase, group); 5346 groupFinalItems.forEach(function(item) { 5347 finalItems.push(item); 5348 }); 5349 5350 if (axisOnTop) { 5351 groupBase += group.itemsHeight + eventMargin; 5352 } else { 5353 groupBase -= (group.itemsHeight + eventMargin); 5354 } 5355 } 5356 5357 // 5358 // Ungrouped items' turn now! 5359 // 5360 if (groupedItems["undefined"]) { 5361 // initialize final positions and fill finalItems 5362 groupFinalItems = this.finalItemsPosition(groupedItems["undefined"], groupBase); 5363 groupFinalItems.forEach(function(item) { 5364 finalItems.push(item); 5365 }); 5366 } 5367 5368 return finalItems; 5369 }; 5370 5371 links.Timeline.prototype.finalItemsPosition = function(items, groupBase, group) { 5372 var i, 5373 iMax, 5374 options = this.options, 5375 axisOnTop = options.axisOnTop, 5376 eventMargin = options.eventMargin, 5377 groupFinalItems; 5378 5379 // initialize final positions and fill finalItems 5380 groupFinalItems = this.initialItemsPosition(items, groupBase); 5381 5382 // calculate new, non-overlapping positions 5383 for (i = 0, iMax = groupFinalItems.length; i < iMax; i++) { 5384 var finalItem = groupFinalItems[i]; 5385 var collidingItem = null; 5386 5387 if (this.options.stackEvents) { 5388 do { 5389 // TODO: optimize checking for overlap. when there is a gap without items, 5390 // you only need to check for items from the next item on, not from zero 5391 collidingItem = this.stackItemsCheckOverlap(groupFinalItems, i, 0, i-1); 5392 if (collidingItem != null) { 5393 // There is a collision. Reposition the event above the colliding element 5394 if (axisOnTop) { 5395 finalItem.top = collidingItem.top + collidingItem.height + eventMargin; 5396 } 5397 else { 5398 finalItem.top = collidingItem.top - finalItem.height - eventMargin; 5399 } 5400 finalItem.bottom = finalItem.top + finalItem.height; 5401 } 5402 } while (collidingItem); 5403 } 5404 5405 if (group) { 5406 if (axisOnTop) { 5407 group.itemsHeight = (group.itemsHeight) 5408 ? Math.max(group.itemsHeight, finalItem.bottom - groupBase) 5409 : finalItem.height + eventMargin; 5410 } else { 5411 group.itemsHeight = (group.itemsHeight) 5412 ? Math.max(group.itemsHeight, groupBase - finalItem.top) 5413 : finalItem.height + eventMargin; 5414 } 5415 } 5416 } 5417 5418 return groupFinalItems; 5419 }; 5420 5421 links.Timeline.prototype.initialItemsPosition = function(items, groupBase) { 5422 var options = this.options, 5423 axisOnTop = options.axisOnTop, 5424 finalItems = []; 5425 5426 for (var i = 0, iMax = items.length; i < iMax; ++i) { 5427 var item = items[i], 5428 top, 5429 bottom, 5430 height = item.height, 5431 width = item.getWidth(this), 5432 right = item.getRight(this), 5433 left = right - width; 5434 5435 top = (axisOnTop) ? groupBase 5436 : groupBase - height; 5437 5438 bottom = top + height; 5439 5440 finalItems.push({ 5441 'left': left, 5442 'top': top, 5443 'right': right, 5444 'bottom': bottom, 5445 'height': height, 5446 'item': item 5447 }); 5448 } 5449 5450 return finalItems; 5451 }; 5452 5453 /** 5454 * Move the events one step in the direction of their final positions 5455 * @param {Array} currentItems Array with the real items and their current 5456 * positions 5457 * @param {Array} finalItems Array with objects containing the final 5458 * positions of the items 5459 * @return {boolean} arrived True if all items have reached their final 5460 * location, else false 5461 */ 5462 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) { 5463 var arrived = true; 5464 5465 // apply new positions animated 5466 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 5467 var finalItem = finalItems[i], 5468 item = finalItem.item; 5469 5470 var topNow = parseInt(item.top); 5471 var topFinal = parseInt(finalItem.top); 5472 var diff = (topFinal - topNow); 5473 if (diff) { 5474 var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1); 5475 if (Math.abs(diff) > 4) step = diff / 4; 5476 var topNew = parseInt(topNow + step); 5477 5478 if (topNew != topFinal) { 5479 arrived = false; 5480 } 5481 5482 item.top = topNew; 5483 item.bottom = item.top + item.height; 5484 } 5485 else { 5486 item.top = finalItem.top; 5487 item.bottom = finalItem.bottom; 5488 } 5489 5490 item.left = finalItem.left; 5491 item.right = finalItem.right; 5492 } 5493 5494 return arrived; 5495 }; 5496 5497 5498 5499 /** 5500 * Move the events from their current position to the final position 5501 * @param {Array} currentItems Array with the real items and their current 5502 * positions 5503 * @param {Array} finalItems Array with objects containing the final 5504 * positions of the items 5505 */ 5506 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) { 5507 // Put the events directly at there final position 5508 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 5509 var finalItem = finalItems[i], 5510 current = finalItem.item; 5511 5512 current.left = finalItem.left; 5513 current.top = finalItem.top; 5514 current.right = finalItem.right; 5515 current.bottom = finalItem.bottom; 5516 } 5517 }; 5518 5519 5520 5521 /** 5522 * Check if the destiny position of given item overlaps with any 5523 * of the other items from index itemStart to itemEnd. 5524 * @param {Array} items Array with items 5525 * @param {int} itemIndex Number of the item to be checked for overlap 5526 * @param {int} itemStart First item to be checked. 5527 * @param {int} itemEnd Last item to be checked. 5528 * @return {Object} colliding item, or undefined when no collisions 5529 */ 5530 links.Timeline.prototype.stackItemsCheckOverlap = function(items, itemIndex, 5531 itemStart, itemEnd) { 5532 var eventMargin = this.options.eventMargin, 5533 collision = this.collision; 5534 5535 // we loop from end to start, as we suppose that the chance of a 5536 // collision is larger for items at the end, so check these first. 5537 var item1 = items[itemIndex]; 5538 for (var i = itemEnd; i >= itemStart; i--) { 5539 var item2 = items[i]; 5540 if (collision(item1, item2, eventMargin)) { 5541 if (i != itemIndex) { 5542 return item2; 5543 } 5544 } 5545 } 5546 5547 return undefined; 5548 }; 5549 5550 /** 5551 * Test if the two provided items collide 5552 * The items must have parameters left, right, top, and bottom. 5553 * @param {Element} item1 The first item 5554 * @param {Element} item2 The second item 5555 * @param {Number} margin A minimum required margin. Optional. 5556 * If margin is provided, the two items will be 5557 * marked colliding when they overlap or 5558 * when the margin between the two is smaller than 5559 * the requested margin. 5560 * @return {boolean} true if item1 and item2 collide, else false 5561 */ 5562 links.Timeline.prototype.collision = function(item1, item2, margin) { 5563 // set margin if not specified 5564 if (margin == undefined) { 5565 margin = 0; 5566 } 5567 5568 // calculate if there is overlap (collision) 5569 return (item1.left - margin < item2.right && 5570 item1.right + margin > item2.left && 5571 item1.top - margin < item2.bottom && 5572 item1.bottom + margin > item2.top); 5573 }; 5574 5575 5576 /** 5577 * fire an event 5578 * @param {String} event The name of an event, for example "rangechange" or "edit" 5579 */ 5580 links.Timeline.prototype.trigger = function (event) { 5581 // built up properties 5582 var properties = null; 5583 switch (event) { 5584 case 'rangechange': 5585 case 'rangechanged': 5586 properties = { 5587 'start': new Date(this.start.valueOf()), 5588 'end': new Date(this.end.valueOf()) 5589 }; 5590 break; 5591 5592 case 'timechange': 5593 case 'timechanged': 5594 properties = { 5595 'time': new Date(this.customTime.valueOf()) 5596 }; 5597 break; 5598 } 5599 5600 // trigger the links event bus 5601 links.events.trigger(this, event, properties); 5602 5603 // trigger the google event bus 5604 if (google && google.visualization) { 5605 google.visualization.events.trigger(this, event, properties); 5606 } 5607 }; 5608 5609 5610 /** 5611 * Cluster the events 5612 */ 5613 links.Timeline.prototype.clusterItems = function () { 5614 if (!this.options.cluster) { 5615 return; 5616 } 5617 5618 var clusters = this.clusterGenerator.getClusters(this.conversion.factor, this.options.clusterMaxItems); 5619 if (this.clusters != clusters) { 5620 // cluster level changed 5621 var queue = this.renderQueue; 5622 5623 // remove the old clusters from the scene 5624 if (this.clusters) { 5625 this.clusters.forEach(function (cluster) { 5626 queue.hide.push(cluster); 5627 5628 // unlink the items 5629 cluster.items.forEach(function (item) { 5630 item.cluster = undefined; 5631 }); 5632 }); 5633 } 5634 5635 // append the new clusters 5636 clusters.forEach(function (cluster) { 5637 // don't add to the queue.show here, will be done in .filterItems() 5638 5639 // link all items to the cluster 5640 cluster.items.forEach(function (item) { 5641 item.cluster = cluster; 5642 }); 5643 }); 5644 5645 this.clusters = clusters; 5646 } 5647 }; 5648 5649 /** 5650 * Filter the visible events 5651 */ 5652 links.Timeline.prototype.filterItems = function () { 5653 var queue = this.renderQueue, 5654 window = (this.end - this.start), 5655 start = new Date(this.start.valueOf() - window), 5656 end = new Date(this.end.valueOf() + window); 5657 5658 function filter (arr) { 5659 arr.forEach(function (item) { 5660 var rendered = item.rendered; 5661 var visible = item.isVisible(start, end); 5662 if (rendered != visible) { 5663 if (rendered) { 5664 queue.hide.push(item); // item is rendered but no longer visible 5665 } 5666 if (visible && (queue.show.indexOf(item) == -1)) { 5667 queue.show.push(item); // item is visible but neither rendered nor queued up to be rendered 5668 } 5669 } 5670 }); 5671 } 5672 5673 // filter all items and all clusters 5674 filter(this.items); 5675 if (this.clusters) { 5676 filter(this.clusters); 5677 } 5678 }; 5679 5680 /** ------------------------------------------------------------------------ **/ 5681 5682 /** 5683 * @constructor links.Timeline.ClusterGenerator 5684 * Generator which creates clusters of items, based on the visible range in 5685 * the Timeline. There is a set of cluster levels which is cached. 5686 * @param {links.Timeline} timeline 5687 */ 5688 links.Timeline.ClusterGenerator = function (timeline) { 5689 this.timeline = timeline; 5690 this.clear(); 5691 }; 5692 5693 /** 5694 * Clear all cached clusters and data, and initialize all variables 5695 */ 5696 links.Timeline.ClusterGenerator.prototype.clear = function () { 5697 // cache containing created clusters for each cluster level 5698 this.items = []; 5699 this.groups = {}; 5700 this.clearCache(); 5701 }; 5702 5703 /** 5704 * Clear the cached clusters 5705 */ 5706 links.Timeline.ClusterGenerator.prototype.clearCache = function () { 5707 // cache containing created clusters for each cluster level 5708 this.cache = {}; 5709 this.cacheLevel = -1; 5710 this.cache[this.cacheLevel] = []; 5711 }; 5712 5713 /** 5714 * Set the items to be clustered. 5715 * This will clear cached clusters. 5716 * @param {Item[]} items 5717 * @param {Object} [options] Available options: 5718 * {boolean} applyOnChangedLevel 5719 * If true (default), the changed data is applied 5720 * as soon the cluster level changes. If false, 5721 * The changed data is applied immediately 5722 */ 5723 links.Timeline.ClusterGenerator.prototype.setData = function (items, options) { 5724 this.items = items || []; 5725 this.dataChanged = true; 5726 this.applyOnChangedLevel = true; 5727 if (options && options.applyOnChangedLevel) { 5728 this.applyOnChangedLevel = options.applyOnChangedLevel; 5729 } 5730 // console.log('clustergenerator setData applyOnChangedLevel=' + this.applyOnChangedLevel); // TODO: cleanup 5731 }; 5732 5733 /** 5734 * Update the current data set: clear cache, and recalculate the clustering for 5735 * the current level 5736 */ 5737 links.Timeline.ClusterGenerator.prototype.updateData = function () { 5738 this.dataChanged = true; 5739 this.applyOnChangedLevel = false; 5740 }; 5741 5742 /** 5743 * Filter the items per group. 5744 * @private 5745 */ 5746 links.Timeline.ClusterGenerator.prototype.filterData = function () { 5747 // filter per group 5748 var items = this.items || []; 5749 var groups = {}; 5750 this.groups = groups; 5751 5752 // split the items per group 5753 items.forEach(function (item) { 5754 // put the item in the correct group 5755 var groupName = item.group ? item.group.content : ''; 5756 var group = groups[groupName]; 5757 if (!group) { 5758 group = []; 5759 groups[groupName] = group; 5760 } 5761 group.push(item); 5762 5763 // calculate the center of the item 5764 if (item.start) { 5765 if (item.end) { 5766 // range 5767 item.center = (item.start.valueOf() + item.end.valueOf()) / 2; 5768 } 5769 else { 5770 // box, dot 5771 item.center = item.start.valueOf(); 5772 } 5773 } 5774 }); 5775 5776 // sort the items per group 5777 for (var groupName in groups) { 5778 if (groups.hasOwnProperty(groupName)) { 5779 groups[groupName].sort(function (a, b) { 5780 return (a.center - b.center); 5781 }); 5782 } 5783 } 5784 5785 this.dataChanged = false; 5786 }; 5787 5788 /** 5789 * Cluster the events which are too close together 5790 * @param {Number} scale The scale of the current window, 5791 * defined as (windowWidth / (endDate - startDate)) 5792 * @return {Item[]} clusters 5793 */ 5794 links.Timeline.ClusterGenerator.prototype.getClusters = function (scale, maxItems) { 5795 var level = -1, 5796 granularity = 2, // TODO: what granularity is needed for the cluster levels? 5797 timeWindow = 0; // milliseconds 5798 5799 if (scale > 0) { 5800 level = Math.round(Math.log(100 / scale) / Math.log(granularity)); 5801 timeWindow = Math.pow(granularity, level); 5802 } 5803 5804 // clear the cache when and re-filter the data when needed. 5805 if (this.dataChanged) { 5806 var levelChanged = (level != this.cacheLevel); 5807 var applyDataNow = this.applyOnChangedLevel ? levelChanged : true; 5808 if (applyDataNow) { 5809 // TODO: currently drawn clusters should be removed! mark them as invisible? 5810 this.clearCache(); 5811 this.filterData(); 5812 // console.log('clustergenerator: cache cleared...'); // TODO: cleanup 5813 } 5814 } 5815 5816 this.cacheLevel = level; 5817 var clusters = this.cache[level]; 5818 if (!clusters) { 5819 // console.log('clustergenerator: create cluster level ' + level); // TODO: cleanup 5820 clusters = []; 5821 5822 // TODO: spit this method, it is too large 5823 for (var groupName in this.groups) { 5824 if (this.groups.hasOwnProperty(groupName)) { 5825 var items = this.groups[groupName]; 5826 var iMax = items.length; 5827 var i = 0; 5828 while (i < iMax) { 5829 // find all items around current item, within the timeWindow 5830 var item = items[i]; 5831 var neighbors = 1; // start at 1, to include itself) 5832 5833 // loop through items left from the current item 5834 var j = i - 1; 5835 while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) { 5836 if (!items[j].cluster) { 5837 neighbors++; 5838 } 5839 j--; 5840 } 5841 5842 // loop through items right from the current item 5843 var k = i + 1; 5844 while (k < items.length && (items[k].center - item.center) < timeWindow / 2) { 5845 neighbors++; 5846 k++; 5847 } 5848 5849 // loop through the created clusters 5850 var l = clusters.length - 1; 5851 while (l >= 0 && (item.center - clusters[l].center) < timeWindow / 2) { 5852 if (item.group == clusters[l].group) { 5853 neighbors++; 5854 } 5855 l--; 5856 } 5857 5858 // aggregate until the number of items is within maxItems 5859 if (neighbors > maxItems) { 5860 // too busy in this window. 5861 var num = neighbors - maxItems + 1; 5862 var clusterItems = []; 5863 5864 // append the items to the cluster, 5865 // and calculate the average start for the cluster 5866 var avg = undefined; // number. average of all start dates 5867 var min = undefined; // number. minimum of all start dates 5868 var max = undefined; // number. maximum of all start and end dates 5869 var containsRanges = false; 5870 var count = 0; 5871 var m = i; 5872 while (clusterItems.length < num && m < items.length) { 5873 var p = items[m]; 5874 var start = p.start.valueOf(); 5875 var end = p.end ? p.end.valueOf() : p.start.valueOf(); 5876 clusterItems.push(p); 5877 if (count) { 5878 // calculate new average (use fractions to prevent overflow) 5879 avg = (count / (count + 1)) * avg + (1 / (count + 1)) * p.center; 5880 } 5881 else { 5882 avg = p.center; 5883 } 5884 min = (min != undefined) ? Math.min(min, start) : start; 5885 max = (max != undefined) ? Math.max(max, end) : end; 5886 containsRanges = containsRanges || (p instanceof links.Timeline.ItemRange || p instanceof links.Timeline.ItemFloatingRange); 5887 count++; 5888 m++; 5889 } 5890 5891 var cluster; 5892 var title = 'Cluster containing ' + count + 5893 ' events. Zoom in to see the individual events.'; 5894 var content = '<div title="' + title + '">' + count + ' events</div>'; 5895 var group = item.group ? item.group.content : undefined; 5896 if (containsRanges) { 5897 // boxes and/or ranges 5898 cluster = this.timeline.createItem({ 5899 'start': new Date(min), 5900 'end': new Date(max), 5901 'content': content, 5902 'group': group 5903 }); 5904 } 5905 else { 5906 // boxes only 5907 cluster = this.timeline.createItem({ 5908 'start': new Date(avg), 5909 'content': content, 5910 'group': group 5911 }); 5912 } 5913 cluster.isCluster = true; 5914 cluster.items = clusterItems; 5915 cluster.items.forEach(function (item) { 5916 item.cluster = cluster; 5917 }); 5918 5919 clusters.push(cluster); 5920 i += num; 5921 } 5922 else { 5923 delete item.cluster; 5924 i += 1; 5925 } 5926 } 5927 } 5928 } 5929 5930 this.cache[level] = clusters; 5931 } 5932 5933 return clusters; 5934 }; 5935 5936 5937 /** ------------------------------------------------------------------------ **/ 5938 5939 5940 /** 5941 * Event listener (singleton) 5942 */ 5943 links.events = links.events || { 5944 'listeners': [], 5945 5946 /** 5947 * Find a single listener by its object 5948 * @param {Object} object 5949 * @return {Number} index -1 when not found 5950 */ 5951 'indexOf': function (object) { 5952 var listeners = this.listeners; 5953 for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { 5954 var listener = listeners[i]; 5955 if (listener && listener.object == object) { 5956 return i; 5957 } 5958 } 5959 return -1; 5960 }, 5961 5962 /** 5963 * Add an event listener 5964 * @param {Object} object 5965 * @param {String} event The name of an event, for example 'select' 5966 * @param {function} callback The callback method, called when the 5967 * event takes place 5968 */ 5969 'addListener': function (object, event, callback) { 5970 var index = this.indexOf(object); 5971 var listener = this.listeners[index]; 5972 if (!listener) { 5973 listener = { 5974 'object': object, 5975 'events': {} 5976 }; 5977 this.listeners.push(listener); 5978 } 5979 5980 var callbacks = listener.events[event]; 5981 if (!callbacks) { 5982 callbacks = []; 5983 listener.events[event] = callbacks; 5984 } 5985 5986 // add the callback if it does not yet exist 5987 if (callbacks.indexOf(callback) == -1) { 5988 callbacks.push(callback); 5989 } 5990 }, 5991 5992 /** 5993 * Remove an event listener 5994 * @param {Object} object 5995 * @param {String} event The name of an event, for example 'select' 5996 * @param {function} callback The registered callback method 5997 */ 5998 'removeListener': function (object, event, callback) { 5999 var index = this.indexOf(object); 6000 var listener = this.listeners[index]; 6001 if (listener) { 6002 var callbacks = listener.events[event]; 6003 if (callbacks) { 6004 var index = callbacks.indexOf(callback); 6005 if (index != -1) { 6006 callbacks.splice(index, 1); 6007 } 6008 6009 // remove the array when empty 6010 if (callbacks.length == 0) { 6011 delete listener.events[event]; 6012 } 6013 } 6014 6015 // count the number of registered events. remove listener when empty 6016 var count = 0; 6017 var events = listener.events; 6018 for (var e in events) { 6019 if (events.hasOwnProperty(e)) { 6020 count++; 6021 } 6022 } 6023 if (count == 0) { 6024 delete this.listeners[index]; 6025 } 6026 } 6027 }, 6028 6029 /** 6030 * Remove all registered event listeners 6031 */ 6032 'removeAllListeners': function () { 6033 this.listeners = []; 6034 }, 6035 6036 /** 6037 * Trigger an event. All registered event handlers will be called 6038 * @param {Object} object 6039 * @param {String} event 6040 * @param {Object} properties (optional) 6041 */ 6042 'trigger': function (object, event, properties) { 6043 var index = this.indexOf(object); 6044 var listener = this.listeners[index]; 6045 if (listener) { 6046 var callbacks = listener.events[event]; 6047 if (callbacks) { 6048 for (var i = 0, iMax = callbacks.length; i < iMax; i++) { 6049 callbacks[i](properties); 6050 } 6051 } 6052 } 6053 } 6054 }; 6055 6056 6057 /** ------------------------------------------------------------------------ **/ 6058 6059 /** 6060 * @constructor links.Timeline.StepDate 6061 * The class StepDate is an iterator for dates. You provide a start date and an 6062 * end date. The class itself determines the best scale (step size) based on the 6063 * provided start Date, end Date, and minimumStep. 6064 * 6065 * If minimumStep is provided, the step size is chosen as close as possible 6066 * to the minimumStep but larger than minimumStep. If minimumStep is not 6067 * provided, the scale is set to 1 DAY. 6068 * The minimumStep should correspond with the onscreen size of about 6 characters 6069 * 6070 * Alternatively, you can set a scale by hand. 6071 * After creation, you can initialize the class by executing start(). Then you 6072 * can iterate from the start date to the end date via next(). You can check if 6073 * the end date is reached with the function end(). After each step, you can 6074 * retrieve the current date via get(). 6075 * The class step has scales ranging from milliseconds, seconds, minutes, hours, 6076 * days, to years. 6077 * 6078 * Version: 1.2 6079 * 6080 * @param {Date} start The start date, for example new Date(2010, 9, 21) 6081 * or new Date(2010, 9, 21, 23, 45, 00) 6082 * @param {Date} end The end date 6083 * @param {Number} minimumStep Optional. Minimum step size in milliseconds 6084 */ 6085 links.Timeline.StepDate = function(start, end, minimumStep) { 6086 6087 // variables 6088 this.current = new Date(); 6089 this._start = new Date(); 6090 this._end = new Date(); 6091 6092 this.autoScale = true; 6093 this.scale = links.Timeline.StepDate.SCALE.DAY; 6094 this.step = 1; 6095 6096 // initialize the range 6097 this.setRange(start, end, minimumStep); 6098 }; 6099 6100 /// enum scale 6101 links.Timeline.StepDate.SCALE = { 6102 MILLISECOND: 1, 6103 SECOND: 2, 6104 MINUTE: 3, 6105 HOUR: 4, 6106 DAY: 5, 6107 WEEKDAY: 6, 6108 MONTH: 7, 6109 YEAR: 8 6110 }; 6111 6112 6113 /** 6114 * Set a new range 6115 * If minimumStep is provided, the step size is chosen as close as possible 6116 * to the minimumStep but larger than minimumStep. If minimumStep is not 6117 * provided, the scale is set to 1 DAY. 6118 * The minimumStep should correspond with the onscreen size of about 6 characters 6119 * @param {Date} start The start date and time. 6120 * @param {Date} end The end date and time. 6121 * @param {int} minimumStep Optional. Minimum step size in milliseconds 6122 */ 6123 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) { 6124 if (!(start instanceof Date) || !(end instanceof Date)) { 6125 //throw "No legal start or end date in method setRange"; 6126 return; 6127 } 6128 6129 this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); 6130 this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); 6131 6132 if (this.autoScale) { 6133 this.setMinimumStep(minimumStep); 6134 } 6135 }; 6136 6137 /** 6138 * Set the step iterator to the start date. 6139 */ 6140 links.Timeline.StepDate.prototype.start = function() { 6141 this.current = new Date(this._start.valueOf()); 6142 this.roundToMinor(); 6143 }; 6144 6145 /** 6146 * Round the current date to the first minor date value 6147 * This must be executed once when the current date is set to start Date 6148 */ 6149 links.Timeline.StepDate.prototype.roundToMinor = function() { 6150 // round to floor 6151 // IMPORTANT: we have no breaks in this switch! (this is no bug) 6152 //noinspection FallthroughInSwitchStatementJS 6153 switch (this.scale) { 6154 case links.Timeline.StepDate.SCALE.YEAR: 6155 this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); 6156 this.current.setMonth(0); 6157 case links.Timeline.StepDate.SCALE.MONTH: this.current.setDate(1); 6158 case links.Timeline.StepDate.SCALE.DAY: // intentional fall through 6159 case links.Timeline.StepDate.SCALE.WEEKDAY: this.current.setHours(0); 6160 case links.Timeline.StepDate.SCALE.HOUR: this.current.setMinutes(0); 6161 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setSeconds(0); 6162 case links.Timeline.StepDate.SCALE.SECOND: this.current.setMilliseconds(0); 6163 //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds 6164 } 6165 6166 if (this.step != 1) { 6167 // round down to the first minor value that is a multiple of the current step size 6168 switch (this.scale) { 6169 case links.Timeline.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; 6170 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; 6171 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; 6172 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; 6173 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6174 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; 6175 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; 6176 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; 6177 default: break; 6178 } 6179 } 6180 }; 6181 6182 /** 6183 * Check if the end date is reached 6184 * @return {boolean} true if the current date has passed the end date 6185 */ 6186 links.Timeline.StepDate.prototype.end = function () { 6187 return (this.current.valueOf() > this._end.valueOf()); 6188 }; 6189 6190 /** 6191 * Do the next step 6192 */ 6193 links.Timeline.StepDate.prototype.next = function() { 6194 var prev = this.current.valueOf(); 6195 6196 // Two cases, needed to prevent issues with switching daylight savings 6197 // (end of March and end of October) 6198 if (this.current.getMonth() < 6) { 6199 switch (this.scale) { 6200 case links.Timeline.StepDate.SCALE.MILLISECOND: 6201 6202 this.current = new Date(this.current.valueOf() + this.step); break; 6203 case links.Timeline.StepDate.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; 6204 case links.Timeline.StepDate.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; 6205 case links.Timeline.StepDate.SCALE.HOUR: 6206 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); 6207 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) 6208 var h = this.current.getHours(); 6209 this.current.setHours(h - (h % this.step)); 6210 break; 6211 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6212 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 6213 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 6214 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 6215 default: break; 6216 } 6217 } 6218 else { 6219 switch (this.scale) { 6220 case links.Timeline.StepDate.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; 6221 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; 6222 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; 6223 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; 6224 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6225 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 6226 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 6227 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 6228 default: break; 6229 } 6230 } 6231 6232 if (this.step != 1) { 6233 // round down to the correct major value 6234 switch (this.scale) { 6235 case links.Timeline.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; 6236 case links.Timeline.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; 6237 case links.Timeline.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; 6238 case links.Timeline.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; 6239 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6240 case links.Timeline.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; 6241 case links.Timeline.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; 6242 case links.Timeline.StepDate.SCALE.YEAR: break; // nothing to do for year 6243 default: break; 6244 } 6245 } 6246 6247 // safety mechanism: if current time is still unchanged, move to the end 6248 if (this.current.valueOf() == prev) { 6249 this.current = new Date(this._end.valueOf()); 6250 } 6251 }; 6252 6253 6254 /** 6255 * Get the current datetime 6256 * @return {Date} current The current date 6257 */ 6258 links.Timeline.StepDate.prototype.getCurrent = function() { 6259 return this.current; 6260 }; 6261 6262 /** 6263 * Set a custom scale. Autoscaling will be disabled. 6264 * For example setScale(SCALE.MINUTES, 5) will result 6265 * in minor steps of 5 minutes, and major steps of an hour. 6266 * 6267 * @param {links.Timeline.StepDate.SCALE} newScale 6268 * A scale. Choose from SCALE.MILLISECOND, 6269 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 6270 * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, 6271 * SCALE.YEAR. 6272 * @param {Number} newStep A step size, by default 1. Choose for 6273 * example 1, 2, 5, or 10. 6274 */ 6275 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) { 6276 this.scale = newScale; 6277 6278 if (newStep > 0) { 6279 this.step = newStep; 6280 } 6281 6282 this.autoScale = false; 6283 }; 6284 6285 /** 6286 * Enable or disable autoscaling 6287 * @param {boolean} enable If true, autoascaling is set true 6288 */ 6289 links.Timeline.StepDate.prototype.setAutoScale = function (enable) { 6290 this.autoScale = enable; 6291 }; 6292 6293 6294 /** 6295 * Automatically determine the scale that bests fits the provided minimum step 6296 * @param {Number} minimumStep The minimum step size in milliseconds 6297 */ 6298 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) { 6299 if (minimumStep == undefined) { 6300 return; 6301 } 6302 6303 var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); 6304 var stepMonth = (1000 * 60 * 60 * 24 * 30); 6305 var stepDay = (1000 * 60 * 60 * 24); 6306 var stepHour = (1000 * 60 * 60); 6307 var stepMinute = (1000 * 60); 6308 var stepSecond = (1000); 6309 var stepMillisecond= (1); 6310 6311 // find the smallest step that is larger than the provided minimumStep 6312 if (stepYear*1000 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;} 6313 if (stepYear*500 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 500;} 6314 if (stepYear*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 100;} 6315 if (stepYear*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 50;} 6316 if (stepYear*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 10;} 6317 if (stepYear*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 5;} 6318 if (stepYear > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1;} 6319 if (stepMonth*3 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 3;} 6320 if (stepMonth > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 1;} 6321 if (stepDay*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 5;} 6322 if (stepDay*2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 2;} 6323 if (stepDay > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1;} 6324 if (stepDay/2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.WEEKDAY; this.step = 1;} 6325 if (stepHour*4 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 4;} 6326 if (stepHour > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 1;} 6327 if (stepMinute*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;} 6328 if (stepMinute*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;} 6329 if (stepMinute*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;} 6330 if (stepMinute > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;} 6331 if (stepSecond*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 15;} 6332 if (stepSecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 10;} 6333 if (stepSecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 5;} 6334 if (stepSecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 1;} 6335 if (stepMillisecond*200 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;} 6336 if (stepMillisecond*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;} 6337 if (stepMillisecond*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;} 6338 if (stepMillisecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;} 6339 if (stepMillisecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;} 6340 if (stepMillisecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;} 6341 }; 6342 6343 /** 6344 * Snap a date to a rounded value. The snap intervals are dependent on the 6345 * current scale and step. 6346 * @param {Date} date the date to be snapped 6347 */ 6348 links.Timeline.StepDate.prototype.snap = function(date) { 6349 if (this.scale == links.Timeline.StepDate.SCALE.YEAR) { 6350 var year = date.getFullYear() + Math.round(date.getMonth() / 12); 6351 date.setFullYear(Math.round(year / this.step) * this.step); 6352 date.setMonth(0); 6353 date.setDate(0); 6354 date.setHours(0); 6355 date.setMinutes(0); 6356 date.setSeconds(0); 6357 date.setMilliseconds(0); 6358 } 6359 else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) { 6360 if (date.getDate() > 15) { 6361 date.setDate(1); 6362 date.setMonth(date.getMonth() + 1); 6363 // important: first set Date to 1, after that change the month. 6364 } 6365 else { 6366 date.setDate(1); 6367 } 6368 6369 date.setHours(0); 6370 date.setMinutes(0); 6371 date.setSeconds(0); 6372 date.setMilliseconds(0); 6373 } 6374 else if (this.scale == links.Timeline.StepDate.SCALE.DAY || 6375 this.scale == links.Timeline.StepDate.SCALE.WEEKDAY) { 6376 switch (this.step) { 6377 case 5: 6378 case 2: 6379 date.setHours(Math.round(date.getHours() / 24) * 24); break; 6380 default: 6381 date.setHours(Math.round(date.getHours() / 12) * 12); break; 6382 } 6383 date.setMinutes(0); 6384 date.setSeconds(0); 6385 date.setMilliseconds(0); 6386 } 6387 else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) { 6388 switch (this.step) { 6389 case 4: 6390 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; 6391 default: 6392 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; 6393 } 6394 date.setSeconds(0); 6395 date.setMilliseconds(0); 6396 } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) { 6397 switch (this.step) { 6398 case 15: 6399 case 10: 6400 date.setMinutes(Math.round(date.getMinutes() / 5) * 5); 6401 date.setSeconds(0); 6402 break; 6403 case 5: 6404 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; 6405 default: 6406 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; 6407 } 6408 date.setMilliseconds(0); 6409 } 6410 else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) { 6411 switch (this.step) { 6412 case 15: 6413 case 10: 6414 date.setSeconds(Math.round(date.getSeconds() / 5) * 5); 6415 date.setMilliseconds(0); 6416 break; 6417 case 5: 6418 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; 6419 default: 6420 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; 6421 } 6422 } 6423 else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) { 6424 var step = this.step > 5 ? this.step / 2 : 1; 6425 date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); 6426 } 6427 }; 6428 6429 /** 6430 * Check if the current step is a major step (for example when the step 6431 * is DAY, a major step is each first day of the MONTH) 6432 * @return {boolean} true if current date is major, else false. 6433 */ 6434 links.Timeline.StepDate.prototype.isMajor = function() { 6435 switch (this.scale) { 6436 case links.Timeline.StepDate.SCALE.MILLISECOND: 6437 return (this.current.getMilliseconds() == 0); 6438 case links.Timeline.StepDate.SCALE.SECOND: 6439 return (this.current.getSeconds() == 0); 6440 case links.Timeline.StepDate.SCALE.MINUTE: 6441 return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); 6442 // Note: this is no bug. Major label is equal for both minute and hour scale 6443 case links.Timeline.StepDate.SCALE.HOUR: 6444 return (this.current.getHours() == 0); 6445 case links.Timeline.StepDate.SCALE.WEEKDAY: // intentional fall through 6446 case links.Timeline.StepDate.SCALE.DAY: 6447 return (this.current.getDate() == 1); 6448 case links.Timeline.StepDate.SCALE.MONTH: 6449 return (this.current.getMonth() == 0); 6450 case links.Timeline.StepDate.SCALE.YEAR: 6451 return false; 6452 default: 6453 return false; 6454 } 6455 }; 6456 6457 6458 /** 6459 * Returns formatted text for the minor axislabel, depending on the current 6460 * date and the scale. For example when scale is MINUTE, the current time is 6461 * formatted as "hh:mm". 6462 * @param {Object} options 6463 * @param {Date} [date] custom date. if not provided, current date is taken 6464 */ 6465 links.Timeline.StepDate.prototype.getLabelMinor = function(options, date) { 6466 if (date == undefined) { 6467 date = this.current; 6468 } 6469 6470 switch (this.scale) { 6471 case links.Timeline.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds()); 6472 case links.Timeline.StepDate.SCALE.SECOND: return String(date.getSeconds()); 6473 case links.Timeline.StepDate.SCALE.MINUTE: 6474 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 6475 case links.Timeline.StepDate.SCALE.HOUR: 6476 return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2); 6477 case links.Timeline.StepDate.SCALE.WEEKDAY: return options.DAYS_SHORT[date.getDay()] + ' ' + date.getDate(); 6478 case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate()); 6479 case links.Timeline.StepDate.SCALE.MONTH: return options.MONTHS_SHORT[date.getMonth()]; // month is zero based 6480 case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear()); 6481 default: return ""; 6482 } 6483 }; 6484 6485 6486 /** 6487 * Returns formatted text for the major axislabel, depending on the current 6488 * date and the scale. For example when scale is MINUTE, the major scale is 6489 * hours, and the hour will be formatted as "hh". 6490 * @param {Object} options 6491 * @param {Date} [date] custom date. if not provided, current date is taken 6492 */ 6493 links.Timeline.StepDate.prototype.getLabelMajor = function(options, date) { 6494 if (date == undefined) { 6495 date = this.current; 6496 } 6497 6498 switch (this.scale) { 6499 case links.Timeline.StepDate.SCALE.MILLISECOND: 6500 return this.addZeros(date.getHours(), 2) + ":" + 6501 this.addZeros(date.getMinutes(), 2) + ":" + 6502 this.addZeros(date.getSeconds(), 2); 6503 case links.Timeline.StepDate.SCALE.SECOND: 6504 return date.getDate() + " " + 6505 options.MONTHS[date.getMonth()] + " " + 6506 this.addZeros(date.getHours(), 2) + ":" + 6507 this.addZeros(date.getMinutes(), 2); 6508 case links.Timeline.StepDate.SCALE.MINUTE: 6509 return options.DAYS[date.getDay()] + " " + 6510 date.getDate() + " " + 6511 options.MONTHS[date.getMonth()] + " " + 6512 date.getFullYear(); 6513 case links.Timeline.StepDate.SCALE.HOUR: 6514 return options.DAYS[date.getDay()] + " " + 6515 date.getDate() + " " + 6516 options.MONTHS[date.getMonth()] + " " + 6517 date.getFullYear(); 6518 case links.Timeline.StepDate.SCALE.WEEKDAY: 6519 case links.Timeline.StepDate.SCALE.DAY: 6520 return options.MONTHS[date.getMonth()] + " " + 6521 date.getFullYear(); 6522 case links.Timeline.StepDate.SCALE.MONTH: 6523 return String(date.getFullYear()); 6524 default: 6525 return ""; 6526 } 6527 }; 6528 6529 /** 6530 * Add leading zeros to the given value to match the desired length. 6531 * For example addZeros(123, 5) returns "00123" 6532 * @param {int} value A value 6533 * @param {int} len Desired final length 6534 * @return {string} value with leading zeros 6535 */ 6536 links.Timeline.StepDate.prototype.addZeros = function(value, len) { 6537 var str = "" + value; 6538 while (str.length < len) { 6539 str = "0" + str; 6540 } 6541 return str; 6542 }; 6543 6544 6545 6546 /** ------------------------------------------------------------------------ **/ 6547 6548 /** 6549 * Image Loader service. 6550 * can be used to get a callback when a certain image is loaded 6551 * 6552 */ 6553 links.imageloader = (function () { 6554 var urls = {}; // the loaded urls 6555 var callbacks = {}; // the urls currently being loaded. Each key contains 6556 // an array with callbacks 6557 6558 /** 6559 * Check if an image url is loaded 6560 * @param {String} url 6561 * @return {boolean} loaded True when loaded, false when not loaded 6562 * or when being loaded 6563 */ 6564 function isLoaded (url) { 6565 if (urls[url] == true) { 6566 return true; 6567 } 6568 6569 var image = new Image(); 6570 image.src = url; 6571 if (image.complete) { 6572 return true; 6573 } 6574 6575 return false; 6576 } 6577 6578 /** 6579 * Check if an image url is being loaded 6580 * @param {String} url 6581 * @return {boolean} loading True when being loaded, false when not loading 6582 * or when already loaded 6583 */ 6584 function isLoading (url) { 6585 return (callbacks[url] != undefined); 6586 } 6587 6588 /** 6589 * Load given image url 6590 * @param {String} url 6591 * @param {function} callback 6592 * @param {boolean} sendCallbackWhenAlreadyLoaded optional 6593 */ 6594 function load (url, callback, sendCallbackWhenAlreadyLoaded) { 6595 if (sendCallbackWhenAlreadyLoaded == undefined) { 6596 sendCallbackWhenAlreadyLoaded = true; 6597 } 6598 6599 if (isLoaded(url)) { 6600 if (sendCallbackWhenAlreadyLoaded) { 6601 callback(url); 6602 } 6603 return; 6604 } 6605 6606 if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) { 6607 return; 6608 } 6609 6610 var c = callbacks[url]; 6611 if (!c) { 6612 var image = new Image(); 6613 image.src = url; 6614 6615 c = []; 6616 callbacks[url] = c; 6617 6618 image.onload = function (event) { 6619 urls[url] = true; 6620 delete callbacks[url]; 6621 6622 for (var i = 0; i < c.length; i++) { 6623 c[i](url); 6624 } 6625 } 6626 } 6627 6628 if (c.indexOf(callback) == -1) { 6629 c.push(callback); 6630 } 6631 } 6632 6633 /** 6634 * Load a set of images, and send a callback as soon as all images are 6635 * loaded 6636 * @param {String[]} urls 6637 * @param {function } callback 6638 * @param {boolean} sendCallbackWhenAlreadyLoaded 6639 */ 6640 function loadAll (urls, callback, sendCallbackWhenAlreadyLoaded) { 6641 // list all urls which are not yet loaded 6642 var urlsLeft = []; 6643 urls.forEach(function (url) { 6644 if (!isLoaded(url)) { 6645 urlsLeft.push(url); 6646 } 6647 }); 6648 6649 if (urlsLeft.length) { 6650 // there are unloaded images 6651 var countLeft = urlsLeft.length; 6652 urlsLeft.forEach(function (url) { 6653 load(url, function () { 6654 countLeft--; 6655 if (countLeft == 0) { 6656 // done! 6657 callback(); 6658 } 6659 }, sendCallbackWhenAlreadyLoaded); 6660 }); 6661 } 6662 else { 6663 // we are already done! 6664 if (sendCallbackWhenAlreadyLoaded) { 6665 callback(); 6666 } 6667 } 6668 } 6669 6670 /** 6671 * Recursively retrieve all image urls from the images located inside a given 6672 * HTML element 6673 * @param {Node} elem 6674 * @param {String[]} urls Urls will be added here (no duplicates) 6675 */ 6676 function filterImageUrls (elem, urls) { 6677 var child = elem.firstChild; 6678 while (child) { 6679 if (child.tagName == 'IMG') { 6680 var url = child.src; 6681 if (urls.indexOf(url) == -1) { 6682 urls.push(url); 6683 } 6684 } 6685 6686 filterImageUrls(child, urls); 6687 6688 child = child.nextSibling; 6689 } 6690 } 6691 6692 return { 6693 'isLoaded': isLoaded, 6694 'isLoading': isLoading, 6695 'load': load, 6696 'loadAll': loadAll, 6697 'filterImageUrls': filterImageUrls 6698 }; 6699 })(); 6700 6701 6702 /** ------------------------------------------------------------------------ **/ 6703 6704 6705 /** 6706 * Add and event listener. Works for all browsers 6707 * @param {Element} element An html element 6708 * @param {string} action The action, for example "click", 6709 * without the prefix "on" 6710 * @param {function} listener The callback function to be executed 6711 * @param {boolean} useCapture 6712 */ 6713 links.Timeline.addEventListener = function (element, action, listener, useCapture) { 6714 if (element.addEventListener) { 6715 if (useCapture === undefined) 6716 useCapture = false; 6717 6718 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 6719 action = "DOMMouseScroll"; // For Firefox 6720 } 6721 6722 element.addEventListener(action, listener, useCapture); 6723 } else { 6724 element.attachEvent("on" + action, listener); // IE browsers 6725 } 6726 }; 6727 6728 /** 6729 * Remove an event listener from an element 6730 * @param {Element} element An html dom element 6731 * @param {string} action The name of the event, for example "mousedown" 6732 * @param {function} listener The listener function 6733 * @param {boolean} useCapture 6734 */ 6735 links.Timeline.removeEventListener = function(element, action, listener, useCapture) { 6736 if (element.removeEventListener) { 6737 // non-IE browsers 6738 if (useCapture === undefined) 6739 useCapture = false; 6740 6741 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 6742 action = "DOMMouseScroll"; // For Firefox 6743 } 6744 6745 element.removeEventListener(action, listener, useCapture); 6746 } else { 6747 // IE browsers 6748 element.detachEvent("on" + action, listener); 6749 } 6750 }; 6751 6752 6753 /** 6754 * Get HTML element which is the target of the event 6755 * @param {Event} event 6756 * @return {Element} target element 6757 */ 6758 links.Timeline.getTarget = function (event) { 6759 // code from http://www.quirksmode.org/js/events_properties.html 6760 if (!event) { 6761 event = window.event; 6762 } 6763 6764 var target; 6765 6766 if (event.target) { 6767 target = event.target; 6768 } 6769 else if (event.srcElement) { 6770 target = event.srcElement; 6771 } 6772 6773 if (target.nodeType != undefined && target.nodeType == 3) { 6774 // defeat Safari bug 6775 target = target.parentNode; 6776 } 6777 6778 return target; 6779 }; 6780 6781 /** 6782 * Stop event propagation 6783 */ 6784 links.Timeline.stopPropagation = function (event) { 6785 if (!event) 6786 event = window.event; 6787 6788 if (event.stopPropagation) { 6789 event.stopPropagation(); // non-IE browsers 6790 } 6791 else { 6792 event.cancelBubble = true; // IE browsers 6793 } 6794 }; 6795 6796 6797 /** 6798 * Cancels the event if it is cancelable, without stopping further propagation of the event. 6799 */ 6800 links.Timeline.preventDefault = function (event) { 6801 if (!event) 6802 event = window.event; 6803 6804 if (event.preventDefault) { 6805 event.preventDefault(); // non-IE browsers 6806 } 6807 else { 6808 event.returnValue = false; // IE browsers 6809 } 6810 }; 6811 6812 6813 /** 6814 * Retrieve the absolute left value of a DOM element 6815 * @param {Element} elem A dom element, for example a div 6816 * @return {number} left The absolute left position of this element 6817 * in the browser page. 6818 */ 6819 links.Timeline.getAbsoluteLeft = function(elem) { 6820 var doc = document.documentElement; 6821 var body = document.body; 6822 6823 var left = elem.offsetLeft; 6824 var e = elem.offsetParent; 6825 while (e != null && e != body && e != doc) { 6826 left += e.offsetLeft; 6827 left -= e.scrollLeft; 6828 e = e.offsetParent; 6829 } 6830 return left; 6831 }; 6832 6833 /** 6834 * Retrieve the absolute top value of a DOM element 6835 * @param {Element} elem A dom element, for example a div 6836 * @return {number} top The absolute top position of this element 6837 * in the browser page. 6838 */ 6839 links.Timeline.getAbsoluteTop = function(elem) { 6840 var doc = document.documentElement; 6841 var body = document.body; 6842 6843 var top = elem.offsetTop; 6844 var e = elem.offsetParent; 6845 while (e != null && e != body && e != doc) { 6846 top += e.offsetTop; 6847 top -= e.scrollTop; 6848 e = e.offsetParent; 6849 } 6850 return top; 6851 }; 6852 6853 /** 6854 * Get the absolute, vertical mouse position from an event. 6855 * @param {Event} event 6856 * @return {Number} pageY 6857 */ 6858 links.Timeline.getPageY = function (event) { 6859 if (('targetTouches' in event) && event.targetTouches.length) { 6860 event = event.targetTouches[0]; 6861 } 6862 6863 if ('pageY' in event) { 6864 return event.pageY; 6865 } 6866 6867 // calculate pageY from clientY 6868 var clientY = event.clientY; 6869 var doc = document.documentElement; 6870 var body = document.body; 6871 return clientY + 6872 ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - 6873 ( doc && doc.clientTop || body && body.clientTop || 0 ); 6874 }; 6875 6876 /** 6877 * Get the absolute, horizontal mouse position from an event. 6878 * @param {Event} event 6879 * @return {Number} pageX 6880 */ 6881 links.Timeline.getPageX = function (event) { 6882 if (('targetTouches' in event) && event.targetTouches.length) { 6883 event = event.targetTouches[0]; 6884 } 6885 6886 if ('pageX' in event) { 6887 return event.pageX; 6888 } 6889 6890 // calculate pageX from clientX 6891 var clientX = event.clientX; 6892 var doc = document.documentElement; 6893 var body = document.body; 6894 return clientX + 6895 ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 6896 ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 6897 }; 6898 6899 /** 6900 * Adds one or more className's to the given elements style 6901 * @param {Element} elem 6902 * @param {String} className 6903 */ 6904 links.Timeline.addClassName = function(elem, className) { 6905 var classes = elem.className.split(' '); 6906 var classesToAdd = className.split(' '); 6907 6908 var added = false; 6909 for (var i=0; i<classesToAdd.length; i++) { 6910 if (classes.indexOf(classesToAdd[i]) == -1) { 6911 classes.push(classesToAdd[i]); // add the class to the array 6912 added = true; 6913 } 6914 } 6915 6916 if (added) { 6917 elem.className = classes.join(' '); 6918 } 6919 }; 6920 6921 /** 6922 * Removes one or more className's from the given elements style 6923 * @param {Element} elem 6924 * @param {String} className 6925 */ 6926 links.Timeline.removeClassName = function(elem, className) { 6927 var classes = elem.className.split(' '); 6928 var classesToRemove = className.split(' '); 6929 6930 var removed = false; 6931 for (var i=0; i<classesToRemove.length; i++) { 6932 var index = classes.indexOf(classesToRemove[i]); 6933 if (index != -1) { 6934 classes.splice(index, 1); // remove the class from the array 6935 removed = true; 6936 } 6937 } 6938 6939 if (removed) { 6940 elem.className = classes.join(' '); 6941 } 6942 }; 6943 6944 /** 6945 * Check if given object is a Javascript Array 6946 * @param {*} obj 6947 * @return {Boolean} isArray true if the given object is an array 6948 */ 6949 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni 6950 links.Timeline.isArray = function (obj) { 6951 if (obj instanceof Array) { 6952 return true; 6953 } 6954 return (Object.prototype.toString.call(obj) === '[object Array]'); 6955 }; 6956 6957 /** 6958 * Shallow clone an object 6959 * @param {Object} object 6960 * @return {Object} clone 6961 */ 6962 links.Timeline.clone = function (object) { 6963 var clone = {}; 6964 for (var prop in object) { 6965 if (object.hasOwnProperty(prop)) { 6966 clone[prop] = object[prop]; 6967 } 6968 } 6969 return clone; 6970 }; 6971 6972 /** 6973 * parse a JSON date 6974 * @param {Date | String | Number} date Date object to be parsed. Can be: 6975 * - a Date object like new Date(), 6976 * - a long like 1356970529389, 6977 * an ISO String like "2012-12-31T16:16:07.213Z", 6978 * or a .Net Date string like 6979 * "\/Date(1356970529389)\/" 6980 * @return {Date} parsedDate 6981 */ 6982 links.Timeline.parseJSONDate = function (date) { 6983 if (date == undefined) { 6984 return undefined; 6985 } 6986 6987 //test for date 6988 if (date instanceof Date) { 6989 return date; 6990 } 6991 6992 // test for MS format. 6993 // FIXME: will fail on a Number 6994 var m = date.match(/\/Date\((-?\d+)([-\+]?\d{2})?(\d{2})?\)\//i); 6995 if (m) { 6996 var offset = m[2] 6997 ? (3600000 * m[2]) // hrs offset 6998 + (60000 * m[3] * (m[2] / Math.abs(m[2]))) // mins offset 6999 : 0; 7000 7001 return new Date( 7002 (1 * m[1]) // ticks 7003 + offset 7004 ); 7005 } 7006 7007 // failing that, try to parse whatever we've got. 7008 return Date.parse(date); 7009 }; 7010