1 /** 2 * @file treegrid.js 3 * 4 * @brief 5 * TreeGrid is a visualization which represents data in a hierarchical grid 6 * view. It is designed to handle large amouts of data, and has options for lazy 7 * loading. Items in the TreeGrid can contain custom HTML code. Information in 8 * one item can be spread over multiple columns, and can have action buttons on 9 * the right. 10 * TreeGrid offers built in functionality to sort, arrange, and filter items. 11 * 12 * TreeGrid is part of the CHAP Links library. 13 * 14 * TreeGrid is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and 15 * Internet Explorer 9+. 16 * 17 * @license 18 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 19 * use this file except in compliance with the License. You may obtain a copy 20 * of the License at 21 * 22 * http://www.apache.org/licenses/LICENSE-2.0 23 * 24 * Unless required by applicable law or agreed to in writing, software 25 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 26 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 27 * License for the specific language governing permissions and limitations under 28 * the License. 29 * 30 * Copyright (c) 2011-2015 Almende B.V. 31 * 32 * @author Jos de Jong, <jos@almende.org> 33 * @date 2015-11-20 34 * @version 1.8.0 35 */ 36 37 /* 38 * TODO 39 * - send the changed items along with the change event? 40 * - the columns of an item should be reset after a drop 41 * - when the columns change (by reading a new item), the already drawn items and header are not updated 42 * - with multiple subgrids in one item, the header does not overlap correctly when scrolling down 43 * - get the TreeGrid working on IE8 again 44 * - dataconnector: be able to define how to generate childs based on the 45 * definition of a field name and dataconnector type 46 * 47 * - couple events from dataconnectors to the TreeGrid? 48 * - drag and drop: 49 * - enable dropping in between items 50 * - multiselect: 51 * - make shift+click working over different levels of grids 52 * - remove the addEventListener and removeEventListener methods from dataconnector? 53 * -> use the eventbus instead? 54 * - test if there is indeed no memory leakage from created event listeners on 55 * the dataconnectors 56 * - implement horizontal scrollbar when data does not fit 57 */ 58 59 60 /** 61 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, 62 * "links" 63 */ 64 if (typeof links === 'undefined') { 65 links = {}; 66 // important: do not use var, as "var links = {};" will overwrite 67 // the existing links variable value with undefined in IE8, IE7. 68 } 69 70 71 /** 72 * Ensure the variable google exists 73 */ 74 if (typeof google === 'undefined') { 75 google = undefined; 76 // important: do not use var, as "var google = undefined;" will overwrite 77 // the existing google variable value with undefined in IE8, IE7. 78 } 79 80 81 /** 82 * @constructor links.TreeGrid 83 * The TreeGrid is a visualization to represent data in a hierarchical list view. 84 * 85 * TreeGrid is developed in javascript as a Google Visualization Chart. 86 * 87 * @param {Element} container The DOM element in which the TreeGrid will 88 * be created. Normally a div element. 89 * @param {Object} options 90 */ 91 links.TreeGrid = function(container, options) { 92 // Internet Explorer does not support Array.indexOf, 93 // so we define it here in that case 94 // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ 95 if(!Array.prototype.indexOf) { 96 Array.prototype.indexOf = function(obj){ 97 for(var i = 0; i < this.length; i++){ 98 if(this[i] == obj){ 99 return i; 100 } 101 } 102 return -1; 103 } 104 } 105 106 this.options = { 107 'width': '100%', 108 'height': '100%', 109 'padding': 4, // px // TODO: padding is not yet used 110 'indentationWidth': 20, // px 111 'items': { 112 'defaultHeight': 24, // px 113 'minHeight': 24 // px 114 }, 115 //'dropAreaHeight': 10 // px // TODO: dropAreas 116 'dropAreaHeight': 0 // px 117 }; 118 119 // temporary warning 120 try { 121 if (options.itemHeight != undefined) { 122 console.log('WARNING: property option.itemHeight is no longer supported. ' + 123 'It is changed to options.items.defaultHeight'); 124 } 125 } 126 catch (e) {} 127 128 if (options) { 129 links.TreeGrid.extend(this.options, options); 130 } 131 132 // remove all elements from the container element. 133 while (container.hasChildNodes()) { 134 container.removeChild(container.firstChild); 135 } 136 137 this.frame = new links.TreeGrid.Frame(container, this.options); 138 this.frame.setTreeGrid(this); 139 this.frame.repaint(); 140 this.frame.reflow(); 141 142 // fire the ready event 143 this.trigger('ready'); 144 }; 145 146 /** 147 * Recursively extend obj with all properties in newProps. 148 * @param {Object} obj 149 * @param {Object} newProps 150 */ 151 links.TreeGrid.extend = function(obj, newProps) { 152 for (var i in newProps) { 153 if (newProps.hasOwnProperty(i)) { 154 if (obj[i] instanceof Object) { 155 if (newProps[i] instanceof Object) { 156 links.TreeGrid.extend(obj[i], newProps[i]); 157 } 158 else { 159 // value from newProps will be neglected 160 } 161 } 162 else { 163 obj[i] = newProps[i]; 164 } 165 } 166 } 167 }; 168 169 /** 170 * Main drawing logic. This is the function that needs to be called 171 * in the html page, to draw the TreeGrid. 172 * 173 * A data table with the events must be provided, and an options table. 174 * 175 * @param {links.DataConnector} data A DataConnector 176 * @param {Object} options A name/value map containing settings 177 * for the TreeGrid. Optional. 178 */ 179 links.TreeGrid.prototype.draw = function(data, options) { 180 // TODO: support multiple input types: DataConnector, Google DataTable, JSON table, ... 181 182 if (options) { 183 // merge options 184 links.TreeGrid.extend(this.options, options); 185 } 186 187 var grid = new links.TreeGrid.Grid(data, this.options); 188 this.frame.setGrid(grid); 189 }; 190 191 /** 192 * Redraw the TreeGrid. 193 * This method can be used after the size of the TreeGrid changed, for example 194 * due to resizing of the page 195 */ 196 links.TreeGrid.prototype.redraw = function() { 197 if (this.frame) { 198 this.frame.onRangeChange(); 199 } 200 }; 201 202 /** 203 * Expand one or multiple items 204 * @param {Object | Object[]} items A single object or an array with 205 * multiple objects 206 */ 207 links.TreeGrid.prototype.expand = function(items) { 208 if (!links.TreeGrid.isArray(items)) { 209 items = [items]; 210 } 211 212 for (var i = 0; i < items.length; i++) { 213 var itemData = items[i]; 214 var item = this.frame.findItem(itemData); 215 item && item.expand(); 216 } 217 }; 218 219 /** 220 * Collapse one or multiple items 221 * @param {Object | Object[]} items A single object or an array with 222 * multiple objects 223 */ 224 links.TreeGrid.prototype.collapse = function(items) { 225 if (!links.TreeGrid.isArray(items)) { 226 items = [items]; 227 } 228 229 for (var i = 0; i < items.length; i++) { 230 var itemData = items[i]; 231 var item = this.frame.findItem(itemData); 232 item && item.collapse(); 233 } 234 }; 235 236 /** 237 * Get the selected items 238 * @return {Array[]} selected items 239 */ 240 links.TreeGrid.prototype.getSelection = function() { 241 return this.frame.getSelection(); 242 }; 243 244 /** 245 * Set the selected items 246 * @param {Array[] | Object} items a single item or array with items 247 */ 248 links.TreeGrid.prototype.setSelection = function(items) { 249 this.frame.setSelection(items); 250 }; 251 252 /** 253 * Base prototype for Frame, Grid, and Item 254 */ 255 links.TreeGrid.Node = function () { 256 this.top = 0; 257 this.width = 0; 258 this.left = 0; 259 this.height = 0; 260 this.visible = true; 261 this.selected = false; 262 }; 263 264 /** 265 * Set a parent node for this node 266 * @param {links.TreeGrid.Node} parent 267 */ 268 links.TreeGrid.Node.prototype.setParent = function(parent) { 269 this.parent = parent; 270 }; 271 272 /** 273 * get the absolute top position in pixels 274 * @return {Number} absTop 275 */ 276 links.TreeGrid.Node.prototype.getAbsTop = function() { 277 return (this.parent ? this.parent.getAbsTop() : 0) + this.top; 278 }; 279 280 /** 281 * get the absolute left position in pixels 282 * @return {Number} absLeft 283 */ 284 links.TreeGrid.Node.prototype.getAbsLeft = function() { 285 return (this.parent ? this.parent.getAbsLeft() : 0) + this.left; 286 }; 287 288 /** 289 * get the height in pixels 290 * @return {Number} height 291 */ 292 links.TreeGrid.Node.prototype.getHeight = function() { 293 return this.height; 294 }; 295 296 /** 297 * get the width in pixels 298 * @return {Number} width 299 */ 300 links.TreeGrid.Node.prototype.getWidth = function() { 301 return this.width; 302 }; 303 304 /** 305 * get the relative left position in pixels, relative to its parent node 306 * @return {Number} left 307 */ 308 links.TreeGrid.Node.prototype.getLeft = function() { 309 return this.left; 310 }; 311 312 /** 313 * get the relative top position in pixels, relative to its parent node 314 * @return {Number} top 315 */ 316 links.TreeGrid.Node.prototype.getTop = function() { 317 return this.top; 318 }; 319 320 /** 321 * set the relative left position in pixels, relative to its parent node 322 * @param {Number} left 323 */ 324 links.TreeGrid.Node.prototype.setLeft = function(left) { 325 this.left = left || 0; 326 }; 327 328 /** 329 * set the relative top position in pixels, relative to its parent node 330 * @param {Number} top 331 */ 332 links.TreeGrid.Node.prototype.setTop = function(top) { 333 this.top = top || 0; 334 }; 335 336 /** 337 * Retrieve the main TreeGrid, the base object. 338 */ 339 links.TreeGrid.Node.prototype.getTreeGrid = function() { 340 return this.parent ? this.parent.getTreeGrid() : undefined; 341 }; 342 343 /** 344 * Get all (globally) selected items 345 * @return {Object[]} selectedItems 346 */ 347 links.TreeGrid.Node.prototype.getSelection = function() { 348 return this.parent ? this.parent.getSelection() : []; 349 }; 350 351 /** 352 * Get the HTML container where the HTML elements can be added 353 * @return {Element} container 354 */ 355 links.TreeGrid.Node.prototype.getContainer = function() { 356 return this.parent ? this.parent.getContainer() : undefined; 357 }; 358 359 /** 360 * Retrieve the currently visible window of the main frame 361 * @return {Object} window Object containing parameters top, left, width, height 362 */ 363 links.TreeGrid.Node.prototype.getVisibleWindow = function() { 364 if (this.parent) { 365 return this.parent.getVisibleWindow(); 366 } 367 368 return { 369 'top': 0, 370 'left': 0, 371 'width': 0, 372 'height': 0 373 }; 374 }; 375 376 /** 377 * Change the visibility of this node 378 * @param {Boolean} visible if true, node will be visible, else node will be 379 * hidden. 380 */ 381 links.TreeGrid.Node.prototype.setVisible = function(visible) { 382 this.visible = (visible == true); 383 }; 384 385 /** 386 * Check if the node is currently visible 387 * @return {Boolean} visible 388 */ 389 links.TreeGrid.Node.prototype.isVisible = function() { 390 return this.visible && (this.parent ? this.parent.isVisible() : true); 391 }; 392 393 /** 394 * Update the height of this node when one of its childs is resized. 395 * This will not cause a reflow or repaint, but just updates the height. 396 * 397 * @param {links.TreeGrid.Node} child Child node which has been resized 398 * @param {Number} diffHeight Change in height 399 */ 400 links.TreeGrid.Node.prototype.updateHeight = function(child, diffHeight) { 401 // method must be implemented by all inherited prototypes 402 }; 403 404 /** 405 * Let the parent of this node know that the height of this node has changed 406 * This will not cause a repaint, but just updates the height of the parent 407 * accordingly. 408 * @param {Number} diffHeight difference in height 409 */ 410 links.TreeGrid.Node.prototype.onUpdateHeight = function (diffHeight) { 411 if (this.parent) { 412 this.parent.updateHeight(this, diffHeight); 413 } 414 }; 415 416 /** 417 * Repaint. Will create/position/repaint all DOM elements of this node 418 * @return {Boolean} resized True if some elements are resized 419 * In that case, a redraw is required 420 */ 421 links.TreeGrid.Node.prototype.repaint = function() { 422 // method must be implemented by all inherited prototypes 423 return false; 424 }; 425 426 /** 427 * Reflow. Calculate and position/resize the elements of this node 428 */ 429 links.TreeGrid.Node.prototype.reflow = function() { 430 // method must be implemented by all inherited prototypes 431 }; 432 433 /** 434 * Update. Will recalculate the visible area, and start loading missing data 435 */ 436 links.TreeGrid.Node.prototype.update = function() { 437 // TODO: this must be implemented by all inherited prototypes 438 }; 439 440 /** 441 * Remove the DOM of the node and set the node invisible 442 * This will cause a repaint and reflow 443 */ 444 links.TreeGrid.Node.prototype.hide = function () { 445 this.setVisible(false); 446 this.repaint(); 447 this.reflow(); 448 }; 449 450 451 /** 452 * Make the node visible and repaint it 453 */ 454 links.TreeGrid.Node.prototype.show = function () { 455 this.setVisible(true); 456 this.repaint(); 457 }; 458 459 460 /** 461 * Select this node 462 */ 463 links.TreeGrid.Node.prototype.select = function() { 464 this.selected = true; 465 }; 466 467 /** 468 * Unselect this node 469 */ 470 links.TreeGrid.Node.prototype.unselect = function() { 471 this.selected = false; 472 }; 473 474 /** 475 * onResize will execute a reflow and a repaint. 476 */ 477 links.TreeGrid.Node.prototype.onResize = function() { 478 if (this.parent && this.parent.onResize) { 479 this.parent.onResize(); 480 } 481 }; 482 483 /** 484 * Generate HTML Dom with action icons 485 * @param {links.TreeGrid.Node} node 486 * @param {Array} actions 487 * @returns {HTMLElement} 488 */ 489 links.TreeGrid.Node.createActionIcons = function (node, actions) { 490 var domActions = document.createElement('DIV'); 491 var domAction; 492 domActions.style.position = 'absolute'; 493 domActions.className = 'treegrid-actions'; 494 domActions.style.top = 0 + 'px'; 495 domActions.style.right = 24 + 'px'; // reckon with width of the scrollbar 496 for (var i = 0, iMax = actions.length; i < iMax; i++) { 497 var action = actions[i]; 498 if (action.event) { 499 if (action.image) { 500 // create an image button 501 domAction = document.createElement('INPUT'); 502 domAction.treeGridType = 'action'; 503 domAction.type = 'image'; 504 domAction.className = 'treegrid-action-image'; 505 domAction.title = action.title || ''; 506 domAction.src = action.image; 507 domAction.event = action.event; 508 domAction.id = action.id; 509 domAction.item = node; 510 domAction.style.width = action.width || ''; 511 domAction.style.height = action.height || ''; 512 domActions.appendChild(domAction); 513 } 514 else { 515 // create a text link 516 domAction = document.createElement('A'); 517 domAction.treeGridType = 'action'; 518 domAction.className = 'treegrid-action-link'; 519 domAction.href = '#'; 520 domAction.title = action.title || ''; 521 domAction.innerHTML = action.text ? action.text : action.event; 522 domAction.event = action.event; 523 domAction.id = action.id; 524 domAction.item = node; 525 domAction.style.width = action.width || ''; 526 domAction.style.height = action.height || ''; 527 domActions.appendChild(domAction); 528 } 529 } 530 else { 531 // TODO: throw warning? 532 } 533 } 534 return domActions; 535 }; 536 537 /** 538 * The Frame is the base for a TreeGrid, it creates a DOM container and creates 539 * scrollbars etc. 540 */ 541 links.TreeGrid.Frame = function (container, options) { 542 this.options = options; 543 this.container = container; 544 545 this.dom = {}; 546 547 this.frameWidth = 0; 548 this.frameHeight = 0; 549 550 this.grid = undefined; 551 this.eventParams = {}; 552 553 this.selection = []; // selected items 554 555 this.top = 0; 556 this.left = 0; 557 this.window = { 558 'left': 0, 559 'top': 0, 560 'height': 0, 561 'width': 0 562 }; 563 564 this.hoveredItem = null; 565 566 // create the HTML DOM 567 this.repaint(); 568 }; 569 570 links.TreeGrid.Frame.prototype = new links.TreeGrid.Node(); 571 572 /** 573 * Find an Item by its data 574 * @param {Object} itemData 575 * @return {links.TreeGrid.Item | null} 576 */ 577 links.TreeGrid.Frame.prototype.findItem = function (itemData) { 578 if (this.grid) { 579 return this.grid.findItem(itemData); 580 } 581 return null; 582 }; 583 584 /** 585 * Trigger an event, but do this via the treegrid object 586 * @param {String} event 587 * @param {Object} params optional parameters 588 */ 589 links.TreeGrid.Frame.prototype.trigger = function(event, params) { 590 if (this.treegrid) { 591 this.treegrid.trigger(event, params); 592 } 593 else { 594 throw 'Error: cannot trigger an event because treegrid is missing'; 595 } 596 }; 597 598 /** 599 * Find the root node, a Frame, from a Grid or Item node. 600 * @param {links.TreeGrid.Item} node 601 * @returns {links.TreeGrid.Frame | null} 602 */ 603 links.TreeGrid.Frame.findFrame = function (node) { 604 while (node) { 605 if (node instanceof links.TreeGrid.Frame) { 606 return node; 607 } 608 node = node.parent; 609 } 610 return null; 611 } 612 613 /** 614 * Get the HTML DOM container of the Frame 615 * @return {Element} container 616 */ 617 links.TreeGrid.Frame.prototype.getContainer = function () { 618 return this.dom.itemFrame; 619 }; 620 621 /** 622 * Retrieve the main TreeGrid, the base object. 623 */ 624 links.TreeGrid.Frame.prototype.getTreeGrid = function() { 625 return this.treegrid; 626 }; 627 628 /** 629 * set the main TreeGrid, the base object. 630 * @param {links.TreeGrid} treegrid 631 */ 632 links.TreeGrid.Frame.prototype.setTreeGrid = function(treegrid) { 633 this.treegrid = treegrid; 634 }; 635 636 /** 637 * Set a grid 638 * @param {links.TreeGrid.Grid} grid 639 */ 640 links.TreeGrid.Frame.prototype.setGrid = function (grid) { 641 if (this.grid) { 642 // remove old grid 643 this.grid.hide(); 644 delete this.grid; 645 } 646 647 this.grid = grid; 648 this.grid.setParent(this); 649 this.gridHeight = this.grid.getHeight(); 650 651 this.update(); 652 this.repaint(); 653 }; 654 655 /** 656 * Get the absolute top position of the frame 657 * @return {Number} absTop 658 */ 659 links.TreeGrid.Frame.prototype.getAbsTop = function() { 660 return (this.verticalScroll ? -this.verticalScroll.get() : 0); 661 }; 662 663 /** 664 * onResize event. overwritten from Node 665 */ 666 links.TreeGrid.Frame.prototype.onResize = function() { 667 //console.log('Frame.onResize'); // TODO: cleanup 668 669 this.repaint(); 670 671 var loopCount = 0; // for safety 672 var maxLoopCount = 10; 673 while (this.reflow() && (loopCount < maxLoopCount)) { 674 this.update(); 675 this.repaint(); 676 loopCount++; 677 } 678 679 if (loopCount >= maxLoopCount) { 680 try { 681 console.log('Warning: maximum number of loops exceeded'); 682 } catch (err) {} 683 } 684 }; 685 686 /** 687 * The visible window changed, due 688 */ 689 links.TreeGrid.Frame.prototype.onRangeChange = function() { 690 this._updateVisibleWindow(); 691 this.update(); 692 this.repaint(); 693 694 var loopCount = 0; // for safety 695 var maxLoopCount = 10; 696 var resized = this.reflow(); 697 while (this.reflow()) { 698 this.repaint(); 699 } 700 701 if (loopCount >= maxLoopCount) { 702 try { 703 console.log('Warning: maximum number of loops exceeded'); 704 } catch (err) {} 705 } 706 }; 707 708 /** 709 * Get the currently visible part of the contents 710 * @return {Object} window Object containing parameters left, top, width, height 711 */ 712 links.TreeGrid.Frame.prototype.getVisibleWindow = function() { 713 return this.window; 714 }; 715 716 /** 717 * Update the data of the frame 718 */ 719 links.TreeGrid.Frame.prototype.update = function() { 720 if (this.grid) { 721 this.grid.update(); 722 } 723 }; 724 725 /** 726 * Update the currently visible window. 727 */ 728 links.TreeGrid.Frame.prototype._updateVisibleWindow = function () { 729 var grid = this.grid, 730 frameHeight = this.frameHeight, 731 frameWidth = this.frameWidth, 732 gridTop = (grid ? grid.getTop() : 0), 733 gridHeight = (grid ? grid.getHeight() : 0), 734 scrollTop = this.verticalScroll ? this.verticalScroll.get() : 0; 735 736 // update top position, relative on total height and scrollTop 737 if (gridHeight > 0 && frameHeight > 0 && scrollTop > 0) { 738 this.top = (gridTop + gridHeight - frameHeight) / (gridHeight - frameHeight) * scrollTop; 739 } 740 else { 741 this.top = 0; 742 } 743 744 // update the visible window object 745 this.window = { 746 'left': this.left, 747 'top': this.top, 748 'height': frameHeight, 749 'width': frameWidth 750 }; 751 }; 752 753 /** 754 * Recalculate sizes of DOM elements 755 * @return {Boolean} resized true if the contents are resized, else false 756 */ 757 links.TreeGrid.Frame.prototype.reflow = function () { 758 //console.log('Frame.reflow'); 759 var resized = false; 760 var dom = this.dom, 761 options = this.options, 762 grid = this.grid; 763 764 if (grid) { 765 var gridResized = grid.reflow(); 766 resized = resized || gridResized; 767 } 768 769 var frameHeight = dom.mainFrame ? dom.mainFrame.clientHeight : 0; 770 var frameWidth = dom.mainFrame ? dom.mainFrame.clientWidth : 0; 771 resized = resized || (this.frameHeight != frameHeight); 772 resized = resized || (this.frameWidth != frameWidth); 773 this.frameHeight = frameHeight; 774 this.frameWidth = frameWidth; 775 776 if (resized) { 777 this.verticalScroll.setInterval(0, this.gridHeight - frameHeight); 778 this._updateVisibleWindow(); 779 } 780 this.verticalScroll.reflow(); 781 782 return resized; 783 }; 784 785 /** 786 * Update the height of this node, because a child's height has been changed. 787 * This will not cause any repaints, but just updates the height of this node. 788 * updateHeight() is called via an onUpdateHeight() from a child node. 789 * @param {links.TreeGrid.Node} child 790 * @param {Number} diffHeight change in height 791 */ 792 links.TreeGrid.Frame.prototype.updateHeight = function (child, diffHeight) { 793 if (child == this.grid) { 794 this.gridHeight += diffHeight; 795 } 796 }; 797 798 799 /** 800 * Redraw the TreeGrid 801 * (child grids are not redrawn) 802 */ 803 links.TreeGrid.Frame.prototype.repaint = function() { 804 //console.log('Frame.repaint'); 805 this._repaintFrame(); 806 this._repaintScrollbars(); 807 this._repaintGrid(); 808 }; 809 810 /** 811 * Redraw the frame 812 */ 813 links.TreeGrid.Frame.prototype._repaintFrame = function() { 814 var frame = this, 815 dom = this.dom, 816 options = this.options; 817 818 // create the main frame 819 var mainFrame = dom.mainFrame; 820 if (!mainFrame) { 821 // the surrounding main frame 822 mainFrame = document.createElement('DIV'); 823 mainFrame.className = 'treegrid-frame'; 824 mainFrame.style.position = 'relative'; 825 mainFrame.style.overflow = 'hidden'; 826 //mainFrame.style.overflow = 'visible'; // TODO: cleanup 827 mainFrame.style.left = '0px'; 828 mainFrame.style.top = '0px'; 829 mainFrame.frame = this; 830 831 this.container.appendChild(mainFrame); 832 dom.mainFrame = mainFrame; 833 834 links.TreeGrid.addEventListener(mainFrame, 'mousedown', function (event) { 835 frame.onMouseDown(event); 836 }); 837 links.TreeGrid.addEventListener(mainFrame, 'mouseover', function (event) { 838 frame.onMouseOver(event); 839 }); 840 links.TreeGrid.addEventListener(mainFrame, 'mouseleave', function (event) { 841 // this is mouseleave on purpose, must not be mouseout 842 frame.onMouseLeave(event); 843 }); 844 links.TreeGrid.addEventListener(mainFrame, 'mousewheel', function (event) { 845 frame.onMouseWheel(event); 846 }); 847 links.TreeGrid.addEventListener(mainFrame, 'touchstart', function (event) { 848 frame.onTouchStart(event); 849 }); 850 851 var dragImage = document.createElement('div'); 852 dragImage.innerHTML = '1 item'; 853 dragImage.className = 'treegrid-drag-image'; 854 this.dom.dragImage = dragImage; 855 856 links.dnd.makeDraggable(mainFrame, { 857 'dragImage': dragImage, 858 'dragImageOffsetX': 10, 859 'dragImageOffsetY': -10, 860 'dragStart': function (event) {return frame.onDragStart(event);} 861 //'dragEnd': function (event) {return frame.onDragEnd(event);} // TODO: cleanup 862 }); 863 } 864 865 // resize frame 866 mainFrame.style.width = options.width || '100%'; 867 mainFrame.style.height = options.height || '100%'; 868 869 // create the frame for holding the items 870 var itemFrame = dom.itemFrame; 871 if (!itemFrame) { 872 // the surrounding main frame 873 itemFrame = document.createElement('DIV'); 874 itemFrame.style.position = 'absolute'; 875 itemFrame.style.left = '0px'; 876 itemFrame.style.top = '0px'; 877 itemFrame.style.width = '100%'; 878 itemFrame.style.height = '0px'; 879 dom.mainFrame.appendChild(itemFrame); 880 dom.itemFrame = itemFrame; 881 } 882 }; 883 884 /** 885 * Repaint the grid 886 */ 887 links.TreeGrid.Frame.prototype._repaintGrid = function() { 888 if (this.grid) { 889 this.grid.repaint(); 890 } 891 }; 892 893 /** 894 * Redraw the scrollbar 895 */ 896 links.TreeGrid.Frame.prototype._repaintScrollbars = function() { 897 var dom = this.dom; 898 var scrollContainer = dom.scrollContainer; 899 if (!scrollContainer) { 900 scrollContainer = document.createElement('div'); 901 scrollContainer.style.position = 'absolute'; 902 scrollContainer.style.zIndex = 9999; // TODO: not so nice solution 903 scrollContainer.style.right = '0px'; // TODO: test on old IE 904 scrollContainer.style.top = '0px'; 905 scrollContainer.style.height = '100%'; 906 scrollContainer.style.width = '16px'; 907 dom.mainFrame.appendChild(scrollContainer); 908 dom.scrollContainer = scrollContainer; 909 910 var frame = this; 911 verticalScroll = new links.TreeGrid.VerticalScroll(scrollContainer); 912 verticalScroll.addOnChangeHandler(function (value) { 913 frame.onRangeChange(); 914 }); 915 this.verticalScroll = verticalScroll; 916 } 917 this.verticalScroll.redraw(); 918 }; 919 920 /** 921 * Check if given object is a Javascript Array 922 * @param {*} obj 923 * @return {Boolean} isArray true if the given object is an array 924 */ 925 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni 926 links.TreeGrid.isArray = function (obj) { 927 if (obj instanceof Array) { 928 return true; 929 } 930 return (Object.prototype.toString.call(obj) === '[object Array]'); 931 }; 932 933 /** 934 * @constructor links.TreeGrid.Grid 935 * Grid can display one Grid, with data from one DataConnector 936 * 937 * @param {links.DataConnector} data 938 * @param {Object} options A key-value map with options 939 */ 940 links.TreeGrid.Grid = function (data, options) { 941 // set dataconnector 942 // TODO: add support for a google DataTable 943 944 this.setData(data); 945 946 this.dom = { 947 }; 948 949 // initialize options 950 this.options = { 951 'items': { 952 'defaultHeight': 24, // px 953 'minHeight': 24 // px 954 } 955 }; 956 if (options) { 957 // merge options 958 links.TreeGrid.extend(this.options, options); 959 } 960 961 this.columns = []; 962 this.itemsHeight = 0; // Total height of all items 963 this.items = []; 964 this.itemCount = undefined; // total number of items 965 this.visibleItems = []; 966 this.expandedItems = []; 967 this.iconsWidth = 0; 968 969 this.headerHeight = 0; 970 this.header = new links.TreeGrid.Header({'options': this.options}); 971 this.header.setParent(this); 972 973 this.loadingHeight = 0; 974 this.emptyHeight = 0; 975 this.errorHeight = 0; 976 this.height = this.options.items.defaultHeight; 977 978 this.dropAreas = []; 979 this.dropAreaHeight = this.options.dropAreaHeight; 980 981 // offset and limit gives the currently visible items 982 this.offset = 0; 983 this.limit = 0; 984 }; 985 986 links.TreeGrid.Grid.prototype = new links.TreeGrid.Node(); 987 988 /** 989 * update the data: update items in the visible range, and update the item count 990 */ 991 links.TreeGrid.Grid.prototype.update = function() { 992 // determine the limit and offset 993 var currentRange = { 994 'offset': this.offset, 995 'limit': this.limit 996 }; 997 var window = this.getVisibleWindow(); 998 var newRange = this._getRangeFromWindow(window, currentRange); 999 this.offset = newRange.offset; 1000 this.limit = newRange.limit; 1001 1002 var grid = this, 1003 items = this.items, 1004 offset = this.offset, 1005 limit = this.limit; 1006 1007 //console.log('update', this.left, offset, offset + limit) 1008 1009 // update childs of the items 1010 var updateItems = links.TreeGrid.mergeArray(this.visibleItems, this.expandedItems); 1011 for (var i = 0, iMax = updateItems.length; i < iMax; i++) { 1012 var item = updateItems[i]; 1013 if (item) { 1014 item.update(); 1015 } 1016 } 1017 1018 var changeCallback = function (changedItems) { 1019 //console.log('changesCallback', changedItems, newOffset, newLimit); 1020 grid.error = undefined; 1021 grid._updateItems(offset, limit); 1022 }; 1023 1024 var changeErrback = function (err) { 1025 grid.error = err; 1026 }; 1027 1028 // update the items. on callback, load all uninitialized and changed items 1029 this._getChanges(offset, limit, changeCallback, changeErrback); 1030 }; 1031 1032 1033 /** 1034 * Update the height of this grid, because a child's height has been changed. 1035 * This will not cause any repaints, but just updates the height of this node. 1036 * updateHeight() is called via an onUpdateHeight() from a child node. 1037 * @param {links.TreeGrid.Node} child 1038 * @param {Number} diffHeight change in height 1039 */ 1040 links.TreeGrid.Grid.prototype.updateHeight = function (child, diffHeight) { 1041 var index = -1; 1042 1043 if (child instanceof links.TreeGrid.Header) { 1044 this.headerHeight += diffHeight; 1045 index = -1; 1046 } 1047 else if (child instanceof links.TreeGrid.Item) { 1048 this.itemsHeight += diffHeight; 1049 index = this.items.indexOf(child); 1050 } 1051 1052 // move all lower down items 1053 var items = this.items; 1054 for (var i = index + 1, iMax = items.length; i < iMax; i++) { 1055 var item = items[i]; 1056 if (item) { 1057 item.top += diffHeight; 1058 } 1059 } 1060 }; 1061 1062 /** 1063 * Event handler for drop event 1064 */ 1065 links.TreeGrid.Grid.prototype.onDrop = function(event) { 1066 // TODO: trigger event? 1067 var items = event.dataTransfer.getData('items'); 1068 1069 if (this.dataConnector) { 1070 var me = this; 1071 var callback = function (resp) { 1072 /* TODO 1073 if (me.expanded) { 1074 me.onResize(); 1075 } 1076 else { 1077 me.expand(); 1078 }*/ 1079 1080 // set the returned items as accepted items 1081 if (resp && resp.items) { 1082 accepted = items.filter(function (item) { 1083 return resp.items.indexOf(item.data) !== -1; 1084 }); 1085 event.dataTransfer.setData('items', accepted); 1086 } 1087 else { 1088 accepted = items; 1089 } 1090 1091 // update the selection 1092 var frame = links.TreeGrid.Frame.findFrame(me); 1093 if (frame && accepted.length > 0) { 1094 // select the moved items 1095 var first = me.items[startIndex]; 1096 frame.select(first); 1097 if (accepted.length > 1) { 1098 var last = me.items[startIndex + accepted.length - 1]; 1099 if (last) { 1100 frame.select(last, false, true); 1101 } 1102 } 1103 } 1104 1105 // fire the dragEnd event on the source frame 1106 var srcFrame = event.dataTransfer.getData('srcFrame'); 1107 srcFrame.onDragEnd(event); 1108 }; 1109 var errback = callback; 1110 1111 // prevent a circular loop, when an item is dropped on one of its own 1112 // childs. So, remove items from which this item is a child 1113 var i = 0; 1114 while (i < items.length) { 1115 var checkItem = this; 1116 while (checkItem && checkItem != items[i]) { 1117 checkItem = checkItem.parent; 1118 } 1119 if (checkItem == items[i]) { 1120 items.splice(i, 1); 1121 } 1122 else { 1123 i++; 1124 } 1125 } 1126 1127 var itemsData = []; 1128 for (var i = 0; i < items.length; i++) { 1129 itemsData.push(items[i].data); 1130 } 1131 if (event.dataTransfer.dropEffect == 'move' || event.dataTransfer.dropEffect == 'copy') { 1132 var sameDataConnector = event.dropTarget && 1133 event.dragSource === event.dropTarget.parent || 1134 event.dragSource === event.dropTarget.grid; 1135 event.dataTransfer.sameDataConnector = sameDataConnector; 1136 1137 if (this.dataConnector.insertItemsBefore !== links.DataConnector.prototype.insertItemsBefore) { 1138 var item; 1139 var beforeItem = null; 1140 if (event.dropTarget instanceof links.TreeGrid.Item) { 1141 item = event.dropTarget; 1142 beforeItem = item.parent.items[item.index + 1]; 1143 } 1144 else if (event.dropTarget instanceof links.TreeGrid.Header) { 1145 item = event.dropTarget; 1146 beforeItem = item.parent.items[0]; 1147 } 1148 var beforeData = beforeItem && beforeItem.data; 1149 var startIndex = beforeItem ? beforeItem.index : this.itemCount; 1150 1151 if (sameDataConnector) { 1152 this.dataConnector.moveItems(itemsData, beforeData, callback, errback); 1153 } 1154 else { 1155 this.dataConnector.insertItemsBefore(itemsData, beforeData, callback, errback); 1156 } 1157 } 1158 else { 1159 this.dataConnector.appendItems(itemsData, callback, errback); 1160 } 1161 } 1162 /* TODO 1163 else if (event.dataTransfer.dropEffect == 'link') { 1164 // TODO: should be used to link one item to another item... 1165 } 1166 else { 1167 // TODO 1168 }*/ 1169 } 1170 else { 1171 console.log('dropped but do nothing', event.dataTransfer.dropEffect); 1172 } 1173 1174 links.TreeGrid.preventDefault(event); 1175 }; 1176 1177 /** 1178 * merge two arrays 1179 * returns a copy of the merged arrays, containing only distinct elements 1180 */ 1181 links.TreeGrid.mergeArray = function(array1, array2) { 1182 var merged = array1.slice(0); // copy first array 1183 for (var i = 0, iMax = array2.length; i < iMax; i++) { 1184 var elem = array2[i]; 1185 if (array1.indexOf(elem) == -1) { 1186 merged.push(elem); 1187 } 1188 } 1189 return merged; 1190 }; 1191 1192 1193 /** 1194 * Recalculate the size of all elements in the Grid (the width and 1195 * height of header, items, fields) 1196 * @return {Boolean} resized True if some elements are resized 1197 * In that case, a redraw is required 1198 */ 1199 links.TreeGrid.Grid.prototype.reflow = function() { 1200 var resized = false, 1201 visibleItems = this.visibleItems; 1202 1203 // preform a reflow on all childs (the header, visible items, expanded items) 1204 if (this.header) { 1205 var headerResized = this.header.reflow(); 1206 resized = resized || headerResized; 1207 } 1208 1209 var reflowItems = links.TreeGrid.mergeArray(this.visibleItems, this.expandedItems); 1210 for (var i = 0, iMax = reflowItems.length; i < iMax; i++) { 1211 var item = reflowItems[i]; 1212 if (item) { 1213 var itemResized = item.reflow(); 1214 resized = resized || itemResized; 1215 } 1216 } 1217 1218 // reflow for all drop areas 1219 var dropAreas = this.dropAreas; 1220 for (var i = 0, iMax = dropAreas.length; i < iMax; i++) { 1221 dropAreas[i].reflow(); 1222 } 1223 1224 // calculate the width of the fields of the header 1225 var widths = this.header.getFieldWidths(); 1226 var columns = this.columns, 1227 indentationWidth = this.options.indentationWidth; 1228 for (var i = 0, iMax = widths.length; i < iMax; i++) { 1229 var column = columns[i]; 1230 if (column && !column.fixedWidth) { 1231 var width = widths[i] + indentationWidth; 1232 if (width > column.width) { 1233 column.width = width; 1234 resized = true; 1235 } 1236 } 1237 } 1238 1239 // calculate the width of the fields 1240 for (var i = 0, iMax = visibleItems.length; i < iMax; i++) { 1241 var item = visibleItems[i]; 1242 var offset = 0; 1243 var widths = item.getFieldWidths(); 1244 for (var j = 0, jMax = columns.length; j < jMax; j++) { 1245 var column = columns[j]; 1246 1247 if (!column.fixedWidth) { 1248 var width = widths[j] + indentationWidth; 1249 if (width > column.width) { 1250 column.width = width; 1251 resized = true; 1252 } 1253 } 1254 } 1255 } 1256 1257 // calculate the width of the icons 1258 if (this.isVisible()) { 1259 var iconsWidth = 0; 1260 for (var i = 0, iMax = visibleItems.length; i < iMax; i++) { 1261 var item = visibleItems[i]; 1262 var width = item.getIconsWidth(); 1263 iconsWidth = Math.max(width, iconsWidth); 1264 } 1265 if (this.iconsWidth != iconsWidth) { 1266 resized = true; 1267 } 1268 this.iconsWidth = iconsWidth; 1269 } 1270 1271 // calculate the left postions of the columns 1272 var left = indentationWidth + this.iconsWidth; 1273 for (var i = 0, iMax = columns.length; i < iMax; i++) { 1274 var column = columns[i]; 1275 if (left != column.left) { 1276 column.left = left; 1277 resized = true; 1278 } 1279 left += column.width; 1280 } 1281 1282 // calculate the width of the grid in total 1283 var width = 0; 1284 if (columns && columns.length) { 1285 var lastColumn = columns[columns.length - 1]; 1286 var width = lastColumn.left + lastColumn.width; 1287 } 1288 resized = resized || (width != this.width); 1289 this.width = width; 1290 1291 // update the height of loading message 1292 if (this.loading) { 1293 if (this.dom.loading) { 1294 this.loadingHeight = this.dom.loading.clientHeight; 1295 } 1296 else { 1297 // leave the height as it is 1298 } 1299 } 1300 else { 1301 this.loadingHeight = 0; 1302 } 1303 1304 // update the height of error message 1305 if (this.error) { 1306 if (this.dom.error) { 1307 this.errorHeight = this.dom.error.clientHeight; 1308 } 1309 else { 1310 // leave the height as it is 1311 } 1312 } 1313 else { 1314 this.errorHeight = 0; 1315 } 1316 1317 // update the height of empty message 1318 if (this.itemCount == 0) { 1319 if (this.dom.empty) { 1320 this.emptyHeight = this.dom.empty.clientHeight; 1321 } 1322 else { 1323 // leave the height as it is 1324 } 1325 } 1326 else { 1327 this.emptyHeight = 0; 1328 } 1329 1330 // calculate the total height 1331 var height = 0; 1332 height += this.headerHeight; 1333 height += this.itemsHeight; 1334 height += this.loadingHeight; 1335 height += this.emptyHeight; 1336 //height += this.errorHeight; // We do not append the height of the error to the total height. 1337 if (height == 0) { 1338 // grid can never have zero height, should always contain some message 1339 height = this.options.items.defaultHeight; 1340 } 1341 1342 var diffHeight = (height - this.height); 1343 if (diffHeight) { 1344 resized = true; 1345 this.height = height; 1346 this.onUpdateHeight(diffHeight); 1347 } 1348 1349 return resized; 1350 }; 1351 1352 1353 /** 1354 * Redraw the grid 1355 * this will recursively the header, the items, and the childs of items 1356 */ 1357 links.TreeGrid.Grid.prototype.repaint = function() { 1358 //console.log('repaint'); 1359 1360 var grid = this, 1361 dom = grid.dom; 1362 1363 this._repaintLoading(); 1364 this._repaintEmpty(); 1365 this._repaintError(); 1366 this._repaintHeader(); 1367 this._repaintItems(); 1368 1369 /* TODO: dropareas 1370 this._repaintDropAreas(); 1371 */ 1372 1373 window.count = window.count ? window.count + 1 : 1; // TODO: cleanup 1374 }; 1375 1376 /** 1377 * Redraw a "loading" text when the grid is uninitialized 1378 */ 1379 links.TreeGrid.Grid.prototype._repaintLoading = function() { 1380 //console.log('repaintLoading', this.left, this.isVisible(), this.loading); // TODO: cleanup 1381 1382 // redraw loading icon 1383 if (this.isVisible() && this.itemCount == undefined && !this.error) { 1384 var domLoadingIcon = this.dom.loadingIcon; 1385 if (!domLoadingIcon) { 1386 // create loading icon 1387 domLoadingIcon = document.createElement('DIV'); 1388 domLoadingIcon.className = 'treegrid-loading-icon'; 1389 domLoadingIcon.style.position = 'absolute'; 1390 //domLoadingIcon.style.zIndex = 9999; // TODO: loading icon of the Grid always on top? 1391 domLoadingIcon.title = 'refreshing...'; 1392 1393 this.getContainer().appendChild(domLoadingIcon); 1394 this.dom.loadingIcon = domLoadingIcon; 1395 } 1396 1397 // position the loading icon 1398 domLoadingIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 1399 domLoadingIcon.style.left = this.getAbsLeft() + 'px'; 1400 } 1401 else { 1402 if (this.dom.loadingIcon) { 1403 this.dom.loadingIcon.parentNode.removeChild(this.dom.loadingIcon); 1404 this.dom.loadingIcon = undefined; 1405 } 1406 } 1407 1408 // redraw loading text 1409 if (this.isVisible() && !this.error && this.itemCount == undefined) { 1410 var domLoadingText = this.dom.loadingText; 1411 if (!domLoadingText) { 1412 // create a "loading..." text 1413 domLoadingText = document.createElement('div'); 1414 domLoadingText.className = 'treegrid-loading'; 1415 domLoadingText.style.position = 'absolute'; 1416 domLoadingText.appendChild(document.createTextNode('loading...')); 1417 this.getContainer().appendChild(domLoadingText); 1418 this.dom.loadingText = domLoadingText; 1419 } 1420 1421 // position the loading text 1422 domLoadingText.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 1423 domLoadingText.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px'; 1424 } 1425 else { 1426 if (this.dom.loadingText) { 1427 delete this.loadingHeight; 1428 1429 this.dom.loadingText.parentNode.removeChild(this.dom.loadingText); 1430 this.dom.loadingText = undefined; 1431 } 1432 } 1433 }; 1434 1435 /** 1436 * Redraw a "(empty)" text when the grid container zero items 1437 */ 1438 links.TreeGrid.Grid.prototype._repaintEmpty = function() { 1439 var dom = this.dom; 1440 1441 if (this.itemCount == 0 && this.isVisible()) { 1442 var domEmpty = dom.empty; 1443 if (!domEmpty) { 1444 // draw a "empty" text 1445 domEmpty = document.createElement('div'); 1446 domEmpty.className = 'treegrid-loading'; 1447 domEmpty.style.position = 'absolute'; 1448 domEmpty.appendChild(document.createTextNode('(empty)')); 1449 this.getContainer().appendChild(domEmpty); 1450 dom.empty = domEmpty; 1451 1452 var item = this.parent; 1453 var dataTransfer = item.dataConnector ? item.dataConnector.getOptions().dataTransfer : undefined; 1454 if (dataTransfer) { 1455 links.dnd.makeDroppable(domEmpty, { 1456 'dropEffect': dataTransfer.dropEffect, 1457 'drop': function (event) {item.onDrop(event);}, 1458 'dragEnter': function (event) {item.onDragEnter(event);}, 1459 'dragOver': function (event) {item.onDragOver(event);}, 1460 'dragLeave': function (event) {item.onDragLeave(event);} 1461 }); 1462 } 1463 } 1464 1465 // position the empty text 1466 domEmpty.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 1467 domEmpty.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px'; 1468 domEmpty.style.width = (this.parent.width - 2 * this.options.indentationWidth) + 'px'; // TODO: not so nice... use real width 1469 } 1470 else { 1471 if (dom.empty) { 1472 links.dnd.removeDroppable(domEmpty); 1473 1474 dom.empty.parentNode.removeChild(dom.empty); 1475 delete this.dom.empty; 1476 } 1477 } 1478 }; 1479 1480 /** 1481 * Redraw an error message (if any) 1482 */ 1483 links.TreeGrid.Grid.prototype._repaintError = function() { 1484 var dom = this.dom; 1485 1486 if (this.isVisible() && this.error) { 1487 var domError = dom.error; 1488 if (!domError) { 1489 // draw a "loading..." text 1490 domError = document.createElement('div'); 1491 domError.className = 'treegrid-error'; 1492 domError.style.position = 'absolute'; 1493 domError.appendChild(document.createTextNode('Error: ' + 1494 links.TreeGrid.Grid._errorToString(this.error))); 1495 this.getContainer().appendChild(domError); 1496 dom.error = domError; 1497 } 1498 1499 // position the error message 1500 domError.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 1501 domError.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px'; 1502 } 1503 else { 1504 if (dom.error) { 1505 dom.error.parentNode.removeChild(dom.error); 1506 dom.error = undefined; 1507 } 1508 } 1509 1510 // redraw error icon 1511 if (this.isVisible() && this.error && !this.loading) { 1512 var domErrorIcon = this.dom.errorIcon; 1513 if (!domErrorIcon) { 1514 // create error icon 1515 domErrorIcon = document.createElement('DIV'); 1516 domErrorIcon.className = 'treegrid-error-icon'; 1517 domErrorIcon.style.position = 'absolute'; 1518 domErrorIcon.title = this.error; 1519 1520 this.getContainer().appendChild(domErrorIcon); 1521 this.dom.errorIcon = domErrorIcon; 1522 } 1523 1524 // position the error icon 1525 var absLeft = this.getAbsLeft(); 1526 domErrorIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 1527 domErrorIcon.style.left = absLeft + 'px'; 1528 domErrorIcon.style.zIndex = absLeft + 1; 1529 } 1530 else { 1531 if (this.dom.errorIcon) { 1532 this.dom.errorIcon.parentNode.removeChild(this.dom.errorIcon); 1533 this.dom.errorIcon = undefined; 1534 } 1535 } 1536 }; 1537 1538 1539 /** 1540 * Retrieve the available columns from the given fields 1541 * @param {Object} fields example object containing field names/values 1542 */ 1543 links.TreeGrid.Grid.prototype.getColumnsFromFields = function (fields) { 1544 var columns = []; 1545 if (fields) { 1546 var i = 0; 1547 for (var fieldName in fields) { 1548 if (fields.hasOwnProperty(fieldName)) { 1549 var field = fields[fieldName]; 1550 if (fieldName.charAt(0) != '_' && !links.TreeGrid.isArray(field) && 1551 !(field instanceof links.DataConnector)) { 1552 columns[i] = { 1553 'name': fieldName 1554 }; 1555 i++; 1556 } 1557 } 1558 } 1559 } 1560 return columns; 1561 }; 1562 1563 1564 /** 1565 * Update the columns of the header 1566 * @param {Object[]} columns Column names and additional information 1567 * The object contains parameters 'name', 1568 * and optionally 'text', 'title' 1569 */ 1570 links.TreeGrid.Grid.prototype.setColumns = function (columns) { 1571 var indentationWidth = this.options.indentationWidth; 1572 var newColumns = []; 1573 var changed = false; 1574 1575 // console.log('setColumns start', columns, this.columns, indentationWidth); 1576 1577 for (var i = 0, iMax = columns.length; i < iMax; i++) { 1578 var curColumn = this.columns[i]; 1579 var column = columns[i]; 1580 1581 // check for changes in the fields 1582 if (!curColumn) { 1583 changed = true; 1584 } 1585 if (!changed) { 1586 for (var field in column) { 1587 if (curColumn[field] != column[field]) { 1588 changed = true; 1589 break; 1590 } 1591 } 1592 } 1593 if (!changed) { 1594 for (var field in curColumn) { 1595 if (field != 'width' && 1596 field != 'left' && 1597 curColumn[field] != column[field]) { 1598 changed = true; 1599 break; 1600 } 1601 } 1602 } 1603 1604 // create a new column object 1605 var newColumn = { 1606 'name': '', 1607 'width': 0, 1608 'left': 0 1609 }; 1610 1611 // copy width from current column 1612 if (!changed && curColumn) { 1613 if (curColumn.width != undefined) { 1614 newColumn.width = curColumn.width; 1615 } 1616 if (curColumn.left != undefined) { 1617 newColumn.left = curColumn.left; 1618 } 1619 } 1620 1621 // set a fixed width if applicable 1622 newColumn.fixedWidth = (column.width != undefined); 1623 1624 // copy values from new column data 1625 for (field in column) { 1626 newColumn[field] = column[field]; 1627 } 1628 1629 // store the new colum fields 1630 this.columns[i] = newColumn; 1631 newColumns[i] = newColumn; 1632 } 1633 1634 if (this.columns.length != columns.length) { 1635 changed = true; 1636 } 1637 1638 if (changed) { 1639 // replace the contents of columns array. 1640 // Important: keep the same array object, all items link to this object! 1641 this.columns.splice(0, this.columns.length); 1642 for (var i = 0; i < newColumns.length; i++) { 1643 this.columns.push(newColumns[i]); 1644 } 1645 //console.log('columns changed!'); 1646 } 1647 //console.log('setColumns end', this.columns); 1648 }; 1649 1650 /** 1651 * Set or change the data or dataconnector for the grid 1652 * @param {Array | links.DataConnector} data 1653 */ 1654 links.TreeGrid.Grid.prototype.setData = function (data) { 1655 var changed = (data != this.data); 1656 1657 if (changed) { 1658 // create new data connector 1659 var dataConnector; 1660 if (links.TreeGrid.isArray(data)) { 1661 dataConnector = new links.DataTable(data); 1662 } 1663 else if (data instanceof links.DataConnector) { 1664 dataConnector = data; 1665 } 1666 else { 1667 throw 'Error: no valid data. JSON Array or DataConnector expected.'; 1668 } 1669 1670 // clean up old data connector 1671 if (this.dataConnector && this.eventListener) { 1672 this.dataConnector.removeEventListener(this.eventListener); 1673 this.eventListener = undefined; 1674 this.dataConnector = undefined; 1675 1676 // cleanup data 1677 var items = this.items; 1678 for (var i = 0, iMax = this.items.length; i < iMax; i++) { 1679 var item = items[i]; 1680 if (item) { 1681 item.data = undefined; 1682 } 1683 } 1684 } 1685 1686 var grid = this; 1687 this.eventListener = function (event, params) { 1688 if (event == 'change') { 1689 grid.update(); 1690 } 1691 }; 1692 1693 // store and link the new dataconnector 1694 // TODO: use the eventbus instead of the addEventListener structure? 1695 this.dataConnector = dataConnector; 1696 this.dataConnector.addEventListener(this.eventListener); 1697 1698 if (this.dataConnector && this.dataConnector.options.showHeader != undefined) { 1699 this.showHeader = this.dataConnector.options.showHeader; 1700 } 1701 else { 1702 this.showHeader = true; 1703 } 1704 } 1705 }; 1706 1707 /** 1708 * Remove an item 1709 * all lower down items will be shifted one up. These changes take only 1710 * place in the display of the treegrid, and are not refected to a dataconnector 1711 * @param {links.TreeGrid.Item} item 1712 */ 1713 links.TreeGrid.Grid.prototype._removeItem = function (item) { 1714 var items = this.items; 1715 var index = items.indexOf(item); 1716 if (index != -1) { 1717 if (item.expanded) { 1718 item.collapse(); 1719 } 1720 1721 var visIndex = this.visibleItems.indexOf(item); 1722 if (visIndex != -1) { 1723 this.visibleItems.splice(visIndex, 1); 1724 } 1725 1726 var height = item.getHeight(); 1727 this.updateHeight(item, -height); 1728 item.hide(); 1729 items.splice(index, 1); 1730 this.itemCount--; 1731 1732 // update index of all lower down items 1733 for (var i = index, iMax = items.length; i < iMax; i++) { 1734 items[i].index = i; 1735 } 1736 } 1737 }; 1738 1739 /** 1740 * Update the columns of the header 1741 * @param {Object[]} columns 1742 */ 1743 links.TreeGrid.Grid.prototype._updateHeader = function (columns) { 1744 this.header.setFields(columns); 1745 }; 1746 1747 /** 1748 * Get the items which are changed, and give them a status dirty=true. 1749 * After that, the changed items may be retrieved 1750 * @param {Number} offset 1751 * @param {Number} limit 1752 * @param {function} callback. Called with the array containing the changed 1753 * items as first parameter. Note that the items 1754 * themselves are not yet updated! 1755 * @param {function} errback 1756 */ 1757 links.TreeGrid.Grid.prototype._getChanges = function (offset, limit, callback, errback) { 1758 var grid = this; 1759 //console.log('_getChanges', offset, limit) 1760 1761 // create a list with items to be checked for changes 1762 // only check items when they are not already checked for changes 1763 var checkData = []; 1764 var checkItemIds = []; 1765 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1766 var item = this.getItem(i); 1767 checkData.push(item.data); 1768 checkItemIds.push(i); 1769 } 1770 1771 var changesCallback = function (resp) { 1772 var changedItems = resp.items || []; 1773 var itemsChanged = (checkData.length < limit || changedItems.length > 0 ); 1774 1775 //console.log('changesCallback', resp.totalItems, resp.items); 1776 1777 // update the item count 1778 var countChanged = (resp.totalItems !== grid.itemCount); 1779 if (countChanged) { 1780 /* TODO 1781 if (grid.totalItems !== undefined || resp.totalItems !== 0) { 1782 // On the first run, grid.totalItems will be undefined. When getChanges 1783 // in that case returns resp.totalItems==0, we do not set the totalItems 1784 // here but leave it undefined, forcing a call of getItems. 1785 grid.setItemCount(resp.totalItems); 1786 }*/ 1787 grid.setItemCount(resp.totalItems); 1788 } 1789 1790 // give changed items a 'dirty' status, and unmark the items from their updating status 1791 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1792 var item = grid.getItem(i); 1793 item.updating = false; 1794 1795 if (!item.data || changedItems.indexOf(item.data) != -1) { 1796 item.dirty = true; 1797 } 1798 1799 //console.log('changesCallback', i, item.dirty, changedItems.indexOf(item.data), item.data); 1800 } 1801 1802 //console.log('changesCallback item[0].updating=', grid.getItem(0).updating); 1803 1804 if (countChanged || itemsChanged) { 1805 //console.log('there are changes or dirty items'); 1806 grid.onResize(); 1807 } 1808 1809 if (callback) { 1810 callback(changedItems); 1811 } 1812 }; 1813 1814 var changesErrback = function (err) { 1815 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1816 var item = grid.getItem(i); 1817 item.error = err; 1818 item.updating = false; 1819 item.dirty = true; 1820 } 1821 1822 grid.onResize(); 1823 1824 if (errback) { 1825 errback(err); 1826 } 1827 }; 1828 1829 //console.log('_getChanges', offset, limit, checkData, checkItemIds) 1830 1831 // mark the items as updating 1832 for (var i = 0, iMax = checkItemIds.length; i < iMax; i++) { 1833 var id = checkItemIds[i]; 1834 this.items[id].updating = true; 1835 } 1836 1837 // check for changes in the items 1838 // Note: we always check for changes, also if checkData.length==0, 1839 // because we want to retrieve the item count too 1840 this.dataConnector.getChanges(offset, limit, checkData, changesCallback, changesErrback); 1841 }; 1842 1843 /** 1844 * Retrieve the items in the range of current window 1845 * @param {Number} offset 1846 * @param {Number} limit 1847 * @param {function} callback 1848 * @param {function} errback 1849 */ 1850 links.TreeGrid.Grid.prototype._updateItems = function (offset, limit, callback, errback) { 1851 var grid = this, 1852 items = this.items; 1853 1854 // first minimize the range of items to be retrieved: 1855 // limit to: 1856 // - dirty items 1857 // - not loaded items 1858 // - items not being loaded 1859 // TODO: optimize this, do not search twice for the same item (by calling .getItem()) 1860 var item = this.getItem(offset); 1861 while (limit > 0 && (item.loading || (!item.dirty && item.data))) { 1862 offset++; 1863 limit--; 1864 item = this.getItem(offset); 1865 } 1866 item = this.getItem(offset + limit - 1); 1867 //while (limit > 0 && item.data && !item.dirty) { 1868 while (limit > 0 && (item.loading || (!item.dirty && item.data))) { 1869 limit--; 1870 item = this.getItem(offset + limit - 1); 1871 } 1872 1873 // mark all items which are going to be loaded as "loading" and "dirty" 1874 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1875 var item = this.getItem(i); 1876 if (item.error || item.dirty || !item.data) { 1877 item.loading = true; 1878 item.dirty = true; 1879 } 1880 } 1881 1882 var getItemsCallback = function (resp) { 1883 //console.log('items retrieved', offset, limit, resp); 1884 var newItems = resp.items; 1885 1886 // set the loaded items to not-loading 1887 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1888 var item = grid.getItem(i); 1889 item.loading = false; 1890 item.dirty = false; 1891 } 1892 1893 // store the new ites 1894 var columns_final = []; 1895 for (var i = 0, iMax = newItems.length; i < iMax; i++) { 1896 var data = newItems[i]; 1897 var columns = grid.dataConnector.getOptions().columns || grid.getColumnsFromFields(newItems[i]); 1898 if(columns.length > columns_final.length){ 1899 columns_final = columns; 1900 } 1901 grid.setColumns(columns_final); 1902 if(i == 0){ 1903 grid._updateHeader(grid.columns); 1904 } 1905 if (data) { 1906 var index = offset + i; 1907 var item = grid.getItem(index); 1908 item.data = data; 1909 item.setFields(data, grid.columns); 1910 item.error = undefined; 1911 } 1912 } 1913 1914 grid.onResize(); 1915 1916 if (callback) { 1917 callback(); 1918 } 1919 } 1920 1921 var getItemsErrback = function (err) { 1922 // set all items to error 1923 for (var i = offset, iMax = offset + limit; i < iMax; i++) { 1924 var item = grid.getItem(i); 1925 item.loading = false; 1926 item.dirty = true; 1927 item.error = err; 1928 } 1929 1930 grid.onResize(); 1931 1932 if (errback) { 1933 errback(err); 1934 } 1935 }; 1936 1937 if (limit > 0 || this.totalItems === undefined) { 1938 //console.log('_updateItems offset=' + offset + ', limit=' + limit ); // TODO: cleanup 1939 1940 this.repaint(); 1941 this.dataConnector.getItems(offset, limit, getItemsCallback, getItemsErrback); 1942 } 1943 else { 1944 if (callback) { 1945 callback(); 1946 } 1947 } 1948 }; 1949 1950 1951 /** 1952 * Redraw the header 1953 */ 1954 links.TreeGrid.Grid.prototype._repaintHeader = function () { 1955 var visible = (this.showHeader && this.itemCount != undefined && this.itemCount > 0); 1956 this.header.setVisible(visible); 1957 this.header.repaint(); 1958 } 1959 1960 /** 1961 * Redraw the items in the currently visible window 1962 */ 1963 links.TreeGrid.Grid.prototype._repaintItems = function () { 1964 // remove items which are outside the visible window 1965 var visible = this.isVisible(), 1966 visibleItems = this.visibleItems, 1967 i = 0; 1968 while (i < visibleItems.length) { 1969 var item = visibleItems[i]; 1970 if (item.index < this.offset || item.index >= this.offset + this.limit || !visible) { 1971 item.hide(); 1972 visibleItems.splice(i, 1); 1973 i--; 1974 } 1975 i++; 1976 } 1977 1978 // add items inside the visible window 1979 var itemCount = this.itemCount || 0, 1980 iStart = this.offset, 1981 iEnd = Math.min(this.offset + this.limit, itemCount); 1982 if (this.isVisible()) { 1983 for (var i = iStart; i < iEnd; i++) { 1984 var item = this.getItem(i); 1985 item.setVisible(true); 1986 if (visibleItems.indexOf(item) == -1) { 1987 visibleItems.push(item); 1988 } 1989 } 1990 } 1991 1992 // repaint the visible items 1993 for (var i = 0; i < visibleItems.length; i++) { 1994 var item = visibleItems[i]; 1995 item.repaint(); 1996 } 1997 }; 1998 1999 /** 2000 * Redraw the dropareas between the items 2001 */ 2002 links.TreeGrid.Grid.prototype._repaintDropAreas = function () { 2003 var dropEffect = 'none'; 2004 if (this.dataConnector && 2005 this.dataConnector.options && 2006 this.dataConnector.options.dataTransfer && 2007 this.dataConnector.options.dataTransfer.dropEffect) { 2008 dropEffect = this.dataConnector.options.dataTransfer.dropEffect; 2009 } 2010 2011 if (dropEffect != 'none' && this.isVisible()) { 2012 var itemCount = this.itemCount || 0, 2013 iStart = this.offset, 2014 iEnd = Math.min(this.offset + this.limit, itemCount), 2015 dropAreas = this.dropAreas, 2016 dropAreaHeight = this.dropAreaHeight, 2017 container = this.getContainer(); 2018 2019 // create one droparea for each of the currently visible items 2020 var missingCount = this.limit - dropAreas.length; 2021 var redundantCount = -missingCount; 2022 for (var i = 0; i < missingCount; i++) { 2023 var dropArea = new links.TreeGrid.DropArea({ 2024 'dataConnector': this.dataConnector, 2025 'item': this.getItem(this.offset + i), 2026 'height': dropAreaHeight 2027 }); 2028 dropArea.setParent(this); 2029 dropAreas.push(dropArea); 2030 } 2031 for (var i = 0; i < redundantCount; i++) { 2032 var dropArea = dropAreas.shift(); 2033 dropArea.hide(); 2034 } 2035 2036 // position the dropareas right above the items 2037 for (var i = iStart; i < iEnd; i++) { 2038 var item = this.getItem(i); 2039 //var itemTop = item.getAbsTop(); 2040 var dropArea = dropAreas[i - this.offset]; 2041 dropArea.setTop(item.top - dropAreaHeight); 2042 dropArea.item = item; 2043 dropArea.repaint(); 2044 } 2045 } 2046 else { 2047 var dropAreas = this.dropAreas; 2048 while (dropAreas.length > 0) { 2049 var dropArea = dropAreas.shift(); 2050 dropArea.hide(); 2051 } 2052 } 2053 }; 2054 2055 links.TreeGrid.Grid.prototype.expand = function (items) { 2056 if (!links.TreeGrid.isArray(items)) { 2057 items = [items]; 2058 } 2059 2060 for (var i = 0; i < items.length; i++) { 2061 var itemsData = items[i]; 2062 var item = this.findItem(itemsData); 2063 item && item.expand(); 2064 } 2065 }; 2066 2067 links.TreeGrid.Grid.prototype.collapse = function (items) { 2068 if (!links.TreeGrid.isArray(items)) { 2069 items = [items]; 2070 } 2071 2072 for (var i = 0; i < items.length; i++) { 2073 var itemsData = items[i]; 2074 var item = this.findItem(itemsData); 2075 item && item.collapse(); 2076 } 2077 }; 2078 2079 /** 2080 * Find an Item by its data 2081 * @param {Object} itemData 2082 * @return {links.TreeGrid.Item | null} 2083 */ 2084 links.TreeGrid.Grid.prototype.findItem = function (itemData) { 2085 for (var i = 0; i < this.items.length; i++) { 2086 var found = this.items[i].findItem(itemData); 2087 if (found) { 2088 return found; 2089 } 2090 } 2091 return null; 2092 }; 2093 2094 /** 2095 * @constructor links.TreeGrid.Header 2096 * @param {Object} params. A key-value map containing parameters: 2097 * height, options 2098 */ 2099 links.TreeGrid.Header = function (params) { 2100 if (params) { 2101 this.height = params.height || 0; 2102 this.options = params.options; 2103 } 2104 2105 this.fieldsHeight = 0; 2106 2107 // data 2108 this.dom = {}; 2109 this.columns = undefined; 2110 }; 2111 2112 links.TreeGrid.Header.prototype = new links.TreeGrid.Node(); 2113 2114 2115 /** 2116 * Clear the header of the grid 2117 */ 2118 links.TreeGrid.Header.prototype.clearFields = function () { 2119 this.columns = undefined; 2120 this.fieldsHeight = 0; 2121 }; 2122 2123 2124 /** 2125 * Redraw the header of the grid 2126 */ 2127 links.TreeGrid.Header.prototype.repaint = function () { 2128 if (this.isVisible() && this.columns) { 2129 // check if the columns are changed 2130 var columns = this.columns; 2131 var prevColumns = this.prevColumns; 2132 if (columns != prevColumns) { 2133 // columns are changed. remove old dom 2134 this.hide(); 2135 this.prevColumns = columns; 2136 } 2137 2138 var domHeader = this.dom.header; 2139 if (!domHeader) { 2140 // create the DOM 2141 domHeader = document.createElement('DIV'); 2142 domHeader.header = this; 2143 domHeader.treeGridType = 'header'; 2144 domHeader.className = 'treegrid-header'; 2145 domHeader.style.position = 'absolute'; 2146 domHeader.style.zIndex = 1 + this.getAbsLeft(); // TODO: not so nice to use zIndex and the abs left. use a subgrid level? 2147 this.getContainer().appendChild(domHeader); 2148 this.dom.header = domHeader; 2149 this.dom.fields = []; 2150 2151 if (this.columns) { 2152 // create fields 2153 var padding = this.options.padding; 2154 for (var i = 0, iMax = columns.length; i < iMax; i++) { 2155 if (!this.dom.fields[i]) { 2156 var column = this.columns[i]; 2157 var domField = document.createElement('DIV'); 2158 domField.className = 'treegrid-header-field' + (column.sortable ? ' treegrid-sortable' : ''); 2159 domField.style.position = 'absolute'; 2160 domField.style.top = '0px'; 2161 domField.innerHTML = column.text || column.name || ''; 2162 domField.title = column.title || ''; 2163 domHeader.appendChild(domField); 2164 2165 this.dom.fields[i] = domField; 2166 } 2167 } 2168 } 2169 } 2170 2171 // update actions 2172 var actions = this.parent && this.parent.dataConnector && this.parent.dataConnector.actions; 2173 if (JSON.stringify(actions) !== JSON.stringify(this.actions)) { 2174 this.actions = actions; 2175 2176 if (this.dom.actions) { 2177 var parent = this.dom.actions.parentNode; 2178 parent && parent.removeChild(this.dom.actions); 2179 delete this.dom.actions; 2180 } 2181 2182 if (actions) { 2183 var domActions = links.TreeGrid.Node.createActionIcons(this, actions); 2184 this.dom.actions = domActions; 2185 domHeader.appendChild(domActions); 2186 } 2187 } 2188 2189 // update filters (create sorting buttons 2190 var filters = this.parent && this.parent.dataConnector && this.parent.dataConnector.filters; 2191 if (!this.filters || JSON.stringify(filters) !== JSON.stringify(this.filters)) { 2192 this.filters = filters; 2193 2194 var orderIcons = { 2195 asc: '▾', 2196 desc: '▴', 2197 'null': '▴▾' 2198 } 2199 2200 if (this.columns) { 2201 for (var i = 0, iMax = columns.length; i < iMax; i++) { 2202 var column = this.columns[i]; 2203 var domField = this.dom.fields[i]; 2204 2205 // remove old dom field 2206 if (domField.domSort) { 2207 domField.removeChild(domField.domSort); 2208 delete domField.domSort; 2209 } 2210 2211 if (column.sortable) { 2212 // create new DOM field 2213 var dataConnector = this.parent.dataConnector; 2214 var entries = dataConnector.getSorting(); 2215 var entry = null; 2216 if (entries) { 2217 for (var j = 0; j < entries.length; j++) { 2218 if (entries[j].field == column.name) { 2219 entry = entries[j]; 2220 break; 2221 } 2222 } 2223 } 2224 2225 var domSort = document.createElement('SPAN'); 2226 var order = entry && entry.order; 2227 domSort.innerHTML = ' ' + orderIcons[entry && entry.order]; 2228 domSort.title = 'Sort this column'; 2229 domSort.className = 'treegrid-order'; 2230 2231 domField.appendChild(domSort); 2232 domField.domSort = domSort; 2233 (function (field, order) { 2234 domField.onclick = function () { 2235 dataConnector.setSorting([{ 2236 field: field, 2237 order: (order === 'asc') ? 'desc' : (order === 'desc') ? null : 'asc' 2238 }]); 2239 } 2240 })(column.name, order) 2241 } 2242 } 2243 } 2244 } 2245 2246 // reposition the header 2247 var absTop = Math.max(this.getAbsTop(), 0); 2248 domHeader.style.top = absTop + 'px'; 2249 domHeader.style.left = this.getAbsLeft() + 'px'; 2250 domHeader.style.height = this.height + 'px'; 2251 domHeader.style.width = this.width + 'px'; 2252 2253 /* TODO: width of the header? 2254 if (this.left) { 2255 var lastColumn = this.columns[this.columns.length-1]; 2256 header.dom.style.width = lastColumn.left+ lastColumn.width + 'px'; 2257 } 2258 else { 2259 header.dom.style.width = '100%'; 2260 }*/ 2261 2262 // position the columns 2263 var domFields = this.dom.fields; 2264 for (var i = 0, iMax = Math.min(domFields.length, columns.length); i < iMax; i++) { 2265 domFields[i].style.left = columns[i].left + 'px'; 2266 } 2267 } 2268 else { 2269 // not visible. 2270 // remove the header DOM 2271 if (this.dom.header) { 2272 this.dom.header.parentNode.removeChild(this.dom.header); 2273 this.dom.header = undefined; 2274 this.dom.fields = undefined; 2275 } 2276 } 2277 }; 2278 2279 /** 2280 * Recalculate the size of the DOM elements of the header 2281 * @return {Boolean} resized 2282 */ 2283 links.TreeGrid.Header.prototype.reflow = function () { 2284 var resized = false; 2285 2286 // calculate maximum height of the fields 2287 var domFields = this.dom ? this.dom.fields : undefined, 2288 fieldCount = domFields ? domFields.length : 0, 2289 fieldsHeight = this.options.items.minHeight; 2290 if (domFields) { 2291 for (var i = 0; i < fieldCount; i++) { 2292 if (domFields[i]) { 2293 fieldsHeight = Math.max(fieldsHeight, domFields[i].clientHeight); 2294 } 2295 } 2296 this.fieldsHeight = fieldsHeight; 2297 } 2298 else if (!this.columns) { 2299 // zero fields available, reset the fieldsHeight 2300 this.fieldsHeight = 0; 2301 } 2302 else { 2303 // leave fieldsHeight as it is... 2304 } 2305 2306 // calculate the height of action icons (if any) 2307 var domActions = this.dom && this.dom.actions; 2308 var actionsHeight = domActions ? domActions.clientHeight : 0; 2309 2310 /* TODO: needed for auto sizing with 2311 // calculate the width of the header 2312 var contentWidth = 0; 2313 var lastColumn = this.columns ? this.columns[this.columns.length - 1] : undefined; 2314 if (lastColumn) { 2315 contentWidth = lastColumn.left + lastColumn.width; 2316 } 2317 resized = resized || (this.contentWidth != contentWidth); 2318 this.contentWidth = contentWidth; 2319 */ 2320 this.width = this.getVisibleWindow().width - this.getAbsLeft(); 2321 2322 // calculate total height 2323 var height = Math.max(this.fieldsHeight, actionsHeight); 2324 2325 var diffHeight = (height - this.height); 2326 if (diffHeight) { 2327 resized = true; 2328 this.height = height; 2329 this.onUpdateHeight(diffHeight); 2330 } 2331 2332 return resized; 2333 }; 2334 2335 /** 2336 * Handle a click on an action icon in a header 2337 * @param {string} event 2338 */ 2339 links.TreeGrid.Header.prototype.onEvent = function (event) { 2340 var dataConnector = this.parent.dataConnector; // TODO: not so nice accessing dataconnector like this 2341 var params = { 2342 dataConnector: dataConnector || null 2343 }; 2344 2345 // send the event to the treegrid 2346 links.events.trigger(this.getTreeGrid(), event, params); 2347 2348 // send the event to the dataconnector 2349 if (dataConnector) { 2350 dataConnector._onEvent(event, params); 2351 } 2352 }; 2353 2354 /** 2355 * store a link to the columns 2356 * TODO: comment 2357 * @param {Array} columns 2358 */ 2359 links.TreeGrid.Header.prototype.setFields = function (columns) { 2360 if (columns) { 2361 this.columns = columns; 2362 } 2363 }; 2364 2365 /** 2366 * Calculate the width of the fields from the HTML DOM 2367 * @return {Number[]} widths 2368 */ 2369 links.TreeGrid.Header.prototype.getFieldWidths = function () { 2370 var widths = []; 2371 2372 if (this.dom.fields) { 2373 var fields = this.dom.fields; 2374 for (var i = 0, iMax = fields.length; i < iMax; i++) { 2375 widths[i] = fields[i].clientWidth; 2376 } 2377 } 2378 2379 return widths; 2380 }; 2381 2382 2383 /** 2384 * Set the number of items 2385 * @param {Number} itemCount 2386 */ 2387 links.TreeGrid.Grid.prototype.setItemCount = function (itemCount) { 2388 var defaultHeight = this.options.items.defaultHeight; 2389 var diff = (itemCount - (this.itemCount || 0)); 2390 2391 //console.log('setItemCount', this.itemCount, itemCount); 2392 2393 if (diff > 0) { 2394 // items added 2395 var diffHeight = (defaultHeight + this.dropAreaHeight) * diff; 2396 this.itemsHeight += diffHeight; 2397 } 2398 2399 if (diff < 0) { 2400 // there are items removed 2401 var oldItemCount = this.itemCount; 2402 2403 // adjust the itemsHeight 2404 for (var i = itemCount; i < oldItemCount; i++) { 2405 var item = this.items[i]; 2406 var itemHeight = item ? item.getHeight() : defaultHeight; 2407 this.itemsHeight -= (itemHeight + this.dropAreaHeight); 2408 } 2409 2410 // remove all items at the tail 2411 // important: loop until this.items.length, not oldItemCount 2412 for (var i = oldItemCount; i < this.items.length; i++) { 2413 var item = this.items[i]; 2414 if (item) { 2415 item.hide(); 2416 delete this.items[i]; 2417 } 2418 } 2419 2420 if (itemCount == 0) { 2421 // TODO: not so nice to reset the header this way 2422 this.header.clearFields(); 2423 } 2424 } 2425 2426 this.itemCount = itemCount || 0; 2427 }; 2428 2429 /** 2430 * Add an item to the list with expanded grids. 2431 * This list is used to update all grids. 2432 */ 2433 links.TreeGrid.Grid.prototype.registerExpandedItem = function (item) { 2434 var index = this.expandedItems.indexOf(item); 2435 if (index == -1) { 2436 this.expandedItems.push(item); 2437 } 2438 }; 2439 2440 /** 2441 * Add an item to the list with expanded items 2442 * This list is used to update all grids. 2443 */ 2444 links.TreeGrid.Grid.prototype.unregisterExpandedItem = function (item) { 2445 var index = this.expandedItems.indexOf(item); 2446 if (index != -1) { 2447 this.expandedItems.splice(index, 1); 2448 } 2449 }; 2450 2451 /** 2452 * Get the number of items 2453 * @return {Number} itemCount 2454 */ 2455 links.TreeGrid.Grid.prototype.getItemCount = function () { 2456 return this.itemCount; 2457 }; 2458 2459 2460 /** 2461 * retrieve item at given index. If the node doesn't exist, it will be created 2462 * The node will also be created when the index is out of range 2463 * @param {Number} index 2464 * @return {links.TreeGrid.Item} item 2465 */ 2466 links.TreeGrid.Grid.prototype.getItem = function (index) { 2467 var item = this.items[index]; 2468 2469 if (!item ) { 2470 // create node when not existing 2471 item = new links.TreeGrid.Item({ 2472 'options': this.options, 2473 'index': index, // TODO: remove this index 2474 'top': this._calculateItemTop(index), 2475 'height': this.options.items.defaultHeight 2476 }); 2477 item.setParent(this); 2478 this.items[index] = item; 2479 } 2480 2481 return item; 2482 }; 2483 2484 2485 /** 2486 * Calculate the top of an item, by calculating the bottom of the 2487 * previous item . 2488 * This method is used when an items top and height are still undefined 2489 * @param {Number} index 2490 * @return {Number} top 2491 */ 2492 links.TreeGrid.Grid.prototype._calculateItemTop = function(index) { 2493 var items = this.items, 2494 defaultHeight = this.options.items.defaultHeight, 2495 prevBottom = 0, 2496 prev = undefined; 2497 2498 // find the last defined item before this item 2499 for (var i = index - 1; i >= 0; i--) { 2500 prev = items[i]; 2501 if (prev && prev.top) { 2502 prevBottom += prev.top + prev.height; 2503 break; 2504 } 2505 else { 2506 prevBottom += defaultHeight; 2507 } 2508 } 2509 2510 // use the bottom of the previous item as top, or, if none of the 2511 // previous items is defined, just calculate based on the default height 2512 // of an item 2513 var top = (prev != undefined) ? 2514 (prevBottom + this.dropAreaHeight) : 2515 (this.headerHeight + defaultHeight * index + this.dropAreaHeight * (index + 1)); 2516 2517 return top; 2518 }; 2519 2520 2521 /** 2522 * @constructor links.TreeGrid.Item 2523 * @param {Object} params. A key-value map containing parameters: 2524 * index, top, options 2525 */ 2526 links.TreeGrid.Item = function (params) { 2527 if (params) { 2528 this.options = params.options; 2529 this.index = params.index || 0; // TODO: remove this index 2530 this.top = params.top || 0; 2531 } 2532 2533 // objects 2534 this.height = this.options.items.defaultHeight; 2535 this.data = undefined; // link to the original data of this item 2536 this.fields = undefined; // array with the fields 2537 this.fieldsHeight = 0; 2538 this.grid = undefined; 2539 this.gridHeight = 0; 2540 2541 // status 2542 this.dirty = false; 2543 this.loading = false; 2544 this.loadingHeight = 0; 2545 this.error = undefined; 2546 this.errorheight = 0; 2547 this.dataTransfer = {}; // for drag and drop properties 2548 2549 // html dom 2550 this.dom = {}; 2551 }; 2552 2553 links.TreeGrid.Item.prototype = new links.TreeGrid.Node(); 2554 2555 /** 2556 * Find an Item by its data 2557 * @param {Object} itemData 2558 * @return {links.TreeGrid.Item | null} 2559 */ 2560 links.TreeGrid.Item.prototype.findItem = function (itemData) { 2561 if (this.data === itemData) { 2562 return this; 2563 } 2564 2565 if (this.grid) { 2566 return this.grid.findItem(itemData); 2567 } 2568 2569 return null; 2570 }; 2571 2572 /** 2573 * Evaluate given function with a custom current object 2574 * When the given fn is a string, it will be evaluated 2575 * WARNING: evaluating fn when it is a string is unsafe. It is safer to provide 2576 * fn as a javascript function. 2577 * @param {function or String} fn 2578 * @param {Object} obj 2579 */ 2580 links.TreeGrid.eval = function (fn, obj) { 2581 var t = typeof(fn); 2582 if (t == 'function') { 2583 return fn.call(obj); 2584 } 2585 else if (t == 'string') { 2586 var evalHistory = links.TreeGrid.evalHistory; 2587 if (!evalHistory) { 2588 evalHistory = {}; 2589 links.TreeGrid.evalHistory = evalHistory; 2590 } 2591 2592 var f = evalHistory[fn]; 2593 if (!f) { 2594 f = eval('f=(' + fn + ');'); 2595 evalHistory[fn] = f; 2596 } 2597 return f.call(obj); 2598 } 2599 else { 2600 throw new Error('Function must be of type function or string'); 2601 } 2602 }; 2603 2604 2605 /** 2606 * read the field values from the item data 2607 * @param {Object} data Item data 2608 * @param {Array} columns Array with column objects, the column objects 2609 * contain a name, left, and width of the column 2610 */ 2611 links.TreeGrid.Item.prototype.setFields = function (data, columns) { 2612 if (data && columns) { 2613 // read the field values from the columns 2614 var fields = []; 2615 for (var i = 0, iMax = columns.length; i < iMax; i++) { 2616 var col = columns[i]; 2617 if (col.format) { 2618 fields[i] = links.TreeGrid.eval(col.format, data) || ''; 2619 } 2620 else { 2621 fields[i] = (data[col.name] || ''); 2622 } 2623 } 2624 this.fields = fields; 2625 this.columns = columns; 2626 2627 // link to the icons 2628 this.icons = data._icons; 2629 2630 // link to the actions 2631 this.actions = data._actions; 2632 2633 // find dataconnectors 2634 var dataconnectors = []; 2635 for (var name in data) { 2636 if (data.hasOwnProperty(name) && name.charAt(0) != '_') { 2637 var value = data[name]; 2638 if (links.TreeGrid.isArray(value)) { 2639 dataconnectors.push({ 2640 'name': name, 2641 'data': new links.DataTable(value) 2642 }); 2643 } 2644 else if (value instanceof links.DataConnector) { 2645 dataconnectors.push({ 2646 'name': name, 2647 'data': value 2648 }); 2649 } 2650 } 2651 2652 // TODO: remove warning in the future 2653 if (name == '_childs') { 2654 try { 2655 console.log('WARNING: special field _childs encountered. ' + 2656 'This field is no longer in use for subgrids, and is now a regular hidden field. ' + 2657 'Use a fieldname without underscore instead for subgrids.'); 2658 } 2659 catch (err) {} 2660 } 2661 } 2662 2663 // create dataconnector 2664 var dataconnector = undefined; 2665 if (dataconnectors.length == 1) { 2666 // a single dataconnector 2667 dataconnector = dataconnectors[0].data; 2668 } 2669 else if (dataconnectors.length > 1) { 2670 // create a new dataconnector containing multipe dataconnectors 2671 var options = {'showHeader': false}; 2672 dataconnector = new links.DataTable(dataconnectors, options); 2673 } 2674 2675 if (dataconnector) { 2676 // TODO: is it needed to store childs as a dataConnector here in Item? 2677 this.dataConnector = dataconnector; 2678 if (this.grid) { 2679 this.grid.setData(this.dataConnector); 2680 this.grid.update(); 2681 } 2682 } 2683 else { 2684 // no data connector 2685 if (this.dataConnector) { 2686 delete this.dataConnector; 2687 } 2688 if (this.grid) { 2689 this.grid.hide(); 2690 this.gridHeight = 0; // TODO: not so nice to set the height and expanded to zero like this 2691 delete this.grid; 2692 } 2693 if (this.expanded) { 2694 this.expanded = false; 2695 this.parent.unregisterExpandedItem(this); 2696 } 2697 } 2698 } 2699 }; 2700 2701 2702 /** 2703 * Calculate the width of the fields from the HTML DOM 2704 * @return {Number[]} widths 2705 */ 2706 links.TreeGrid.Item.prototype.getFieldWidths = function () { 2707 var widths = []; 2708 2709 if (this.dom.fields) { 2710 var fields = this.dom.fields; 2711 for (var i = 0, iMax = this.columns.length; i < iMax; i++) { 2712 widths[i] = fields[i] ? fields[i].clientWidth : 0; 2713 } 2714 } 2715 2716 return widths; 2717 }; 2718 2719 /** 2720 * Calculate the total width of the icons (if any) 2721 * @return {Number} width 2722 */ 2723 links.TreeGrid.Item.prototype.getIconsWidth = function () { 2724 if (this.dom.icons) { 2725 return this.dom.icons.clientWidth; 2726 } 2727 return 0; 2728 }; 2729 2730 2731 /** 2732 * Update the height of this item, because a child's height has been changed. 2733 * This will not cause any repaints, but just updates the height of this node. 2734 * updateHeight() is called via an onUpdateHeight() from a child node. 2735 * @param {links.TreeGrid.Node} child 2736 * @param {Number} diffHeight change in height 2737 */ 2738 links.TreeGrid.Item.prototype.updateHeight = function (child, diffHeight) { 2739 if (child == this.grid) { 2740 this.gridHeight += diffHeight; 2741 } 2742 }; 2743 2744 2745 /** 2746 * trigger an event 2747 * @param {String} event Event name. For example 'expand' or 'collapse' 2748 */ 2749 links.TreeGrid.Item.prototype.onEvent = function (event) { 2750 var params = { 2751 //'index': this.index, // TODO: dangerous, invalid when items are deleted/inserted... 2752 'items': [this.data] 2753 }; 2754 2755 // send the event to the treegrid 2756 links.events.trigger(this.getTreeGrid(), event, params); 2757 2758 // send the event to the dataconnector 2759 var dataConnector = this.parent.dataConnector; // TODO: not so nice accessing dataconnector like this 2760 if (dataConnector) { 2761 dataConnector._onEvent(event, params); 2762 } 2763 }; 2764 2765 /** 2766 * Create grid if not yet instantiated 2767 * @return {links.TreeGrid.Grid} Returns the created (or already existing) grid 2768 * @private 2769 */ 2770 links.TreeGrid.Item.prototype._createGrid = function () { 2771 if (!this.grid) { 2772 // create a grid for the child data 2773 this.grid = new links.TreeGrid.Grid(this.dataConnector, this.options); 2774 this.grid.setParent(this); 2775 this.grid.setLeft(this.left + this.options.indentationWidth); 2776 this.grid.setTop(this.height); 2777 } 2778 return this.grid; 2779 }; 2780 2781 /** 2782 * Expand the item 2783 */ 2784 links.TreeGrid.Item.prototype.expand = function () { 2785 if (this.expanded) return; 2786 2787 if (this.dataConnector) { 2788 this.expanded = true; 2789 this.parent.registerExpandedItem(this); 2790 2791 if (this.dom.buttonExpand) { 2792 this.dom.buttonExpand.className = 'treegrid-unfold'; 2793 } 2794 2795 // create grid if not yet instantiated 2796 this._createGrid(); 2797 2798 // if grid was already loaded before, make it visible 2799 this.setVisible(true); 2800 this.grid.setVisible(true); 2801 this.gridHeight += (this.grid.getHeight() + this.options.padding); 2802 2803 this.onEvent('expand'); 2804 this.onResize(); 2805 } 2806 }; 2807 2808 /** 2809 * Collapse the item 2810 */ 2811 links.TreeGrid.Item.prototype.collapse = function () { 2812 if (!this.expanded) return; 2813 2814 if (this.dataConnector) { 2815 this.expanded = false; 2816 this.parent.unregisterExpandedItem(this); 2817 2818 if (this.dom.buttonExpand) { 2819 this.dom.buttonExpand.className = 'treegrid-fold'; 2820 } 2821 2822 if (this.grid) { 2823 this.grid.setVisible(false); 2824 2825 this.gridHeight -= (this.grid.getHeight() + this.options.padding); 2826 } 2827 2828 this.onEvent('collapse'); 2829 this.onResize(); 2830 } 2831 }; 2832 2833 /** 2834 * Toggle expand/collapse of the item 2835 */ 2836 links.TreeGrid.Item.prototype.toggleExpand = function () { 2837 if (this.expanded) { 2838 this.collapse(); 2839 } 2840 else { 2841 this.expand(); 2842 } 2843 }; 2844 2845 /** 2846 * Event handler for drag over event 2847 */ 2848 links.TreeGrid.Item.prototype.onDragOver = function(event) { 2849 if (this.dataTransfer.dragging) { 2850 return; // we cannot drop the item onto itself 2851 } 2852 2853 // we need to repaint on every dragover event. 2854 // because the item consists of various elements, the dragenter and dragleave 2855 // events are fired every time we enter/leave one of the elements. 2856 // this causes a dragleave executed last wrongly 2857 var threshold = this.fieldsHeight / 2; 2858 var dragbefore = (((event.offsetY || event.layerY) < threshold) || !this.dataConnector); 2859 dragbefore = false; // TODO: cleanup the dragbefore thing, and create a separate drop area for dropping inbetween 2860 this.dataTransfer.dragover = !dragbefore; 2861 this.dataTransfer.dragbefore = dragbefore; 2862 // TODO: get the correct vertical offset, independent of the child 2863 2864 this.repaint(); 2865 2866 links.TreeGrid.preventDefault(event); 2867 return false; 2868 }; 2869 2870 2871 /** 2872 * Event handler for drag enter event 2873 * this will highlight the current item 2874 */ 2875 links.TreeGrid.Item.prototype.onDragEnter = function(event) { 2876 if (this.dataTransfer.dragging) { 2877 return; // we cannot drop the item onto itself 2878 } 2879 2880 /* TODO 2881 event.dataTransfer.allowedEffect = this.dataConnector ? 'move' : 'none'; 2882 event.dataTransfer.dropEffect = this.dataConnector ? 'move' : 'none'; 2883 */ 2884 2885 //console.log('onDragEnter', this.dragcount, event.target); 2886 //this.dataTransfer.dragover = true; 2887 //this.repaint(); 2888 2889 return false; 2890 }; 2891 2892 /** 2893 * Event handler for drag leave event 2894 */ 2895 links.TreeGrid.Item.prototype.onDragLeave = function(event) { 2896 if (this.dataTransfer.dragging) { 2897 return; // we cannot drop the item onto itself 2898 } 2899 2900 //console.log('onDragLeave', this.dragcount, event.target); 2901 2902 this.dataTransfer.dragover = false; 2903 this.dataTransfer.dragbefore = false; 2904 this.repaint(); 2905 2906 return false; 2907 }; 2908 2909 /** 2910 * Event handler for drop event 2911 */ 2912 links.TreeGrid.Item.prototype.onDrop = function(event) { 2913 var items = event.dataTransfer.getData('items'); 2914 this.dataTransfer.dragover = false; 2915 this.dataTransfer.dragbefore = false; 2916 this.repaint(); 2917 2918 if (this.dataConnector) { 2919 var me = this; 2920 var callback = function (resp) { 2921 //* TODO 2922 if (me.expanded) { 2923 me.onResize(); 2924 } 2925 else { 2926 me.expand(); 2927 } 2928 //*/ 2929 2930 // set the returned items as accepted items 2931 if (resp && resp.items) { 2932 accepted = event.dataTransfer.getData('items').filter(function (item) { 2933 return resp.items.indexOf(item.data) !== -1; 2934 }); 2935 event.dataTransfer.setData('items', accepted); 2936 } 2937 2938 // TODO: select the just dropped items 2939 2940 // fire the dragEnd event on the source frame 2941 var srcFrame = event.dataTransfer.getData('srcFrame'); 2942 srcFrame.onDragEnd(event); 2943 }; 2944 var errback = callback; 2945 2946 // console.log('drop', items); 2947 2948 // prevent a circular loop, when an item is dropped on one of its own 2949 // childs. So, remove items from which this item is a child 2950 var i = 0; 2951 while (i < items.length) { 2952 var checkItem = this; 2953 while (checkItem && checkItem != items[i]) { 2954 checkItem = checkItem.parent; 2955 } 2956 if (checkItem == items[i]) { 2957 items.splice(i, 1); 2958 } 2959 else { 2960 i++; 2961 } 2962 } 2963 2964 var itemsData = []; 2965 for (var i = 0; i < items.length; i++) { 2966 itemsData.push(items[i].data); 2967 } 2968 this.dataConnector.appendItems(itemsData, callback, errback); 2969 } 2970 else if (this.parent && this.parent.dataConnector && 2971 event.dataTransfer.dropEffect == 'link') { 2972 var targetItemData = this.data; 2973 var callback = function (resp) { 2974 // TODO: redraw on callback? 2975 }; 2976 var errback = function (err) { 2977 console.log(err); 2978 }; 2979 2980 var sourceItemsData = []; 2981 for (var i = 0; i < items.length; i++) { 2982 sourceItemsData.push(items[i].data); 2983 } 2984 this.parent.dataConnector.linkItems(sourceItemsData, targetItemData, 2985 callback, errback); 2986 } 2987 else { 2988 console.log('dropped but do nothing', event.dataTransfer.dropEffect); // TODO 2989 } 2990 2991 links.TreeGrid.preventDefault(event); 2992 }; 2993 2994 2995 /** 2996 * Redraw the node 2997 */ 2998 links.TreeGrid.Item.prototype.repaint = function () { 2999 this._repaintError(); 3000 this._repaintLoading(); 3001 this._repaintFields(); 3002 this._repaintGrid(); 3003 }; 3004 3005 /** 3006 * Update the data of the child grid (if there is a child grid) 3007 */ 3008 links.TreeGrid.Item.prototype.update = function() { 3009 if (this.grid && this.expanded) { 3010 this.grid.update(); 3011 } 3012 }; 3013 3014 /** 3015 * Recalculate the size of the DOM elements of the header 3016 * @return {Boolean} resized 3017 */ 3018 links.TreeGrid.Item.prototype.reflow = function () { 3019 var resized = false; 3020 3021 // update and reflow the grid 3022 if (this.grid && this.expanded) { 3023 var gridResized = this.grid.reflow(); 3024 resized = resized || gridResized; 3025 } 3026 3027 /* TODO: needed for auto width 3028 // calculate the width of the item 3029 var width = 0; 3030 var lastColumn = this.columns ? this.columns[this.columns.length - 1] : undefined; 3031 if (lastColumn) { 3032 width = lastColumn.left + lastColumn.width; 3033 } 3034 resized = resized || (this.width != width); 3035 this.width = width; 3036 */ 3037 this.width = this.getVisibleWindow().width - this.getAbsLeft(); 3038 3039 if (this.isVisible()) { 3040 var fieldsHeight = this.options.items.minHeight, 3041 fields = this.dom.fields, 3042 actions = this.dom.actions, 3043 icons = this.dom.icons, 3044 expandButton = this.dom.expandButton, 3045 fieldCount = fields ? fields.length : 0; 3046 3047 // calculate width of the icons 3048 if (icons) { 3049 var iconsWidth = icons.clientWidth; 3050 if (iconsWidth != this.iconsWidth) { 3051 resized = true; 3052 } 3053 this.iconsWidth = iconsWidth; 3054 } 3055 else { 3056 // leave iconsWidth as it is 3057 } 3058 3059 // calculate maximum height of the fields 3060 if (fields || actions || icons || expandButton || actions) { 3061 for (var i = 0; i < fieldCount; i++) { 3062 if (fields[i]) { 3063 fieldsHeight = Math.max(fieldsHeight, fields[i].clientHeight); 3064 } 3065 } 3066 if (actions) { 3067 fieldsHeight = Math.max(fieldsHeight, actions.clientHeight); 3068 } 3069 if (icons) { 3070 fieldsHeight = Math.max(fieldsHeight, icons.clientHeight); 3071 } 3072 if (expandButton) { 3073 fieldsHeight = Math.max(fieldsHeight, expandButton.clientHeight); 3074 } 3075 this.fieldsHeight = fieldsHeight; 3076 } 3077 else { 3078 // leave the fieldsheight as it is 3079 } 3080 } 3081 else { 3082 // leave the fieldsHeight as it is 3083 } 3084 3085 // update the height of loading message 3086 if (this.loading) { 3087 if (this.dom.loading) { 3088 this.loadingHeight = this.dom.loading.clientHeight; 3089 } 3090 else { 3091 // leave the height as it is 3092 } 3093 } 3094 else { 3095 this.loadingHeight = 0; 3096 } 3097 3098 // update the height of error message 3099 if (this.error) { 3100 if (this.dom.error) { 3101 this.errorHeight = this.dom.error.clientHeight; 3102 } 3103 else { 3104 // leave the height as it is 3105 } 3106 } 3107 else { 3108 this.errorHeight = 0; 3109 } 3110 3111 // update the height of the fields empty, error, and loading 3112 var height = 0; 3113 height += this.fieldsHeight; 3114 height += this.loadingHeight; 3115 height += this.errorHeight; 3116 height += this.gridHeight; 3117 if (height == 0) { 3118 height = this.options.items.defaultHeight; 3119 } 3120 3121 var diffHeight = (height - this.height); 3122 if (diffHeight) { 3123 resized = true; 3124 this.height = height; 3125 this.onUpdateHeight(diffHeight); 3126 } 3127 3128 return resized; 3129 }; 3130 3131 3132 /** 3133 * Get the visible range from the given window 3134 * @param {Object} window An object with parameters top, left, width, 3135 * height. 3136 * @param {Object} currentRange optional, current range. makes getting the range 3137 * faster. Object containing a parameter offset and 3138 * limit 3139 * @return {Object} range An object with parameters offset and limit 3140 */ 3141 links.TreeGrid.Grid.prototype._getRangeFromWindow = function(window, currentRange) { 3142 // use the current range as start 3143 var defaultHeight = this.options.items.defaultHeight, 3144 itemCount = (this.itemCount != undefined) ? this.itemCount : Math.ceil(window.height / defaultHeight), 3145 windowTop = -this.getAbsTop() + this.header.getHeight(), // normalize the top 3146 windowBottom = windowTop + window.height - this.header.getHeight(), 3147 newOffset = currentRange ? currentRange.offset : 0, 3148 newLimit = 0; 3149 3150 var item, top, height, bottom; 3151 3152 //console.log('_getRangeFromWindow', window.top, window.top + window.height, this.top, this.top + this.height) 3153 3154 // find the first visible item 3155 item = this.items[newOffset]; 3156 top = item ? item.top : this._calculateItemTop(newOffset); 3157 height = item ? item.getHeight() : defaultHeight; 3158 bottom = top + height; 3159 while ((newOffset < itemCount - 1) && (bottom < windowTop)) { 3160 newOffset++; 3161 item = this.items[newOffset]; 3162 top = bottom + this.dropAreaHeight; 3163 height = item ? item.getHeight() : defaultHeight; 3164 bottom = top + height; 3165 } 3166 while ((newOffset > 0) && top > windowTop) { 3167 newOffset--; 3168 item = this.items[newOffset]; 3169 height = item ? item.getHeight() : defaultHeight; 3170 bottom = top; 3171 top = top - height - this.dropAreaHeight; 3172 } 3173 3174 // find the last visible item 3175 while ((newOffset + newLimit < itemCount - 1) && (top < windowBottom)) { 3176 newLimit++; 3177 item = this.items[newOffset + newLimit]; 3178 top = bottom + this.dropAreaHeight; 3179 height = item ? item.getHeight() : defaultHeight; 3180 bottom = top + height; 3181 //console.log('item', newOffset + newLimit, top, height, bottom, windowBottom) // TODO: cleanup 3182 } 3183 if (top < windowBottom && bottom > windowTop && newOffset + newLimit < itemCount) { 3184 newLimit++; 3185 } 3186 3187 // console.log('range', this.left, newOffset, newLimit, newLimit ? newOffset + newLimit-1 : undefined); // TODO: cleanup 3188 3189 return { 3190 'offset': newOffset, 3191 'limit': newLimit 3192 }; 3193 }; 3194 3195 3196 /** 3197 * Repaint the HTML DOM fields of this item 3198 */ 3199 links.TreeGrid.Item.prototype._repaintFields = function() { 3200 var field; 3201 if (this.isVisible() && this.fields) { 3202 // check if the fields are changed 3203 var fields = this.fields; 3204 var prevFields = this.prevFields; 3205 if (fields != prevFields) { 3206 // fields are changed. remove old dom 3207 if (this.dom.frame) { 3208 this.dom.frame.parentNode.removeChild(this.dom.frame); 3209 delete this.dom.frame; 3210 } 3211 this.prevFields = fields; 3212 } 3213 3214 var domFrame = this.dom.frame; 3215 if (!domFrame) { 3216 // create the dom frame 3217 var domFrame = document.createElement('DIV'); 3218 domFrame.className = 'treegrid-item'; 3219 domFrame.style.position = 'absolute'; // TODO 3220 //domFrame.style.position = 'relative'; 3221 domFrame.item = this; 3222 domFrame.treeGridType = 'item'; 3223 this.dom.frame = domFrame; 3224 //this.getContainer().appendChild(domFrame); // TODO 3225 3226 // create expand button 3227 if (this.dataConnector) { 3228 var buttonExpand = document.createElement('button'); 3229 buttonExpand.treeGridType = 'expand'; 3230 buttonExpand.className = this.expanded ? 'treegrid-unfold' : 'treegrid-fold'; 3231 buttonExpand.style.position = 'absolute'; 3232 buttonExpand.grid = this; // TODO: is this used and needed? 3233 buttonExpand.node = this; 3234 buttonExpand.index = this.index; // TODO: remove this index, use the node instead 3235 3236 domFrame.appendChild(buttonExpand); 3237 this.dom.buttonExpand = buttonExpand; 3238 } 3239 3240 // create icons 3241 var icons = this.icons; 3242 if (icons) { 3243 var domIcons = document.createElement('DIV'); 3244 domIcons.className = 'treegrid-icons'; 3245 domIcons.style.position = 'absolute'; 3246 domIcons.style.top = '0px'; 3247 for (var i = 0, iMax = icons.length; i < iMax; i++) { 3248 var icon = icons[i]; 3249 if (icon && icon.image) { 3250 var domIcon = document.createElement('img'); 3251 domIcon.className = 'treegrid-icon'; 3252 domIcon.src = icon.image; 3253 domIcon.title = icon.title ? icon.title : ''; 3254 domIcon.style.width = icon.width ? icon.width : ''; 3255 domIcon.style.height = icon.height ? icon.height : ''; 3256 domIcons.appendChild(domIcon); 3257 } 3258 } 3259 domFrame.appendChild(domIcons); 3260 this.dom.icons = domIcons; 3261 } 3262 3263 // create the fields 3264 var domFields = []; 3265 this.dom.fields = domFields; 3266 var fields = this.fields; 3267 for (var i = 0, iMax = fields.length; i < iMax; i++) { 3268 var field = fields[i]; 3269 3270 var domField = document.createElement('DIV'); 3271 domField.className = 'treegrid-item-field'; 3272 domField.style.position = 'absolute'; 3273 //domField.style.position = 'relative'; 3274 domField.style.top = '0px'; 3275 3276 var col = this.columns[i]; 3277 if (col && col.fixedWidth) { 3278 domField.style.width = col.width + 'px'; 3279 } 3280 3281 domField.innerHTML = field; 3282 domFrame.appendChild(domField); 3283 domFields.push(domField); 3284 } 3285 3286 // create the actions 3287 if (this.actions) { 3288 var domActions = links.TreeGrid.Node.createActionIcons(this, this.actions); 3289 this.dom.actions = domActions; 3290 domFrame.appendChild(domActions); 3291 } 3292 3293 // create event handlers for drag and drop 3294 var item = this; 3295 // TODO: not so nice accessing the parent grid like this 3296 3297 var dataTransfer = this.dataConnector ? this.dataConnector.getOptions().dataTransfer : undefined; 3298 if (dataTransfer) { 3299 if (dataTransfer.dropEffect != undefined && dataTransfer.dropEffect != 'none') { 3300 this.dataTransfer.dropEffect = dataTransfer.dropEffect; 3301 3302 links.dnd.makeDroppable(domFrame, { 3303 'dropEffect':dataTransfer.dropEffect, 3304 'drop':function (event) { 3305 item.onDrop(event); 3306 }, 3307 'dragEnter':function (event) { 3308 item.onDragEnter(event); 3309 }, 3310 'dragOver':function (event) { 3311 item.onDragOver(event); 3312 }, 3313 'dragLeave':function (event) { 3314 item.onDragLeave(event); 3315 } 3316 }); 3317 } 3318 } 3319 else if (this.parent && this.parent.dataConnector) { 3320 // Check if the items parent has a dataconnector with dropEffect 'link' 3321 var dataTransfer = this.parent.dataConnector.getOptions().dataTransfer; 3322 if (dataTransfer && dataTransfer.dropEffect == 'link') { 3323 this.dataTransfer.dropEffect = dataTransfer.dropEffect; 3324 3325 links.dnd.makeDroppable(domFrame, { 3326 'dropEffect':dataTransfer.dropEffect, 3327 'drop':function (event) { 3328 item.onDrop(event); 3329 }, 3330 'dragEnter':function (event) { 3331 item.onDragEnter(event); 3332 }, 3333 'dragOver':function (event) { 3334 item.onDragOver(event); 3335 }, 3336 'dragLeave':function (event) { 3337 item.onDragLeave(event); 3338 } 3339 }); 3340 } 3341 } 3342 } 3343 3344 if (!domFrame.parentNode) { 3345 this.getContainer().appendChild(domFrame); 3346 } 3347 3348 // position the frame 3349 var left = this.getAbsLeft(); 3350 domFrame.style.top = this.getAbsTop() + 'px'; 3351 domFrame.style.left = left + 'px'; 3352 // TODO 3353 //domFrame.style.top = 0 + 'px'; 3354 //domFrame.style.left = 0 + 'px'; 3355 domFrame.style.height = this.fieldsHeight + 'px'; 3356 domFrame.style.width = this.width - 2 + 'px'; 3357 3358 // position the icons 3359 var domIcons = this.dom.icons; 3360 if (domIcons) { 3361 domIcons.style.left = this.options.indentationWidth + 'px'; 3362 } 3363 3364 // position the fields 3365 var domFields = this.dom.fields; 3366 if (domFields) { 3367 for (var i = 0, iMax = this.columns.length; i < iMax; i++) { 3368 var col = this.columns[i]; 3369 var domField = domFields[i]; 3370 if (domField) { 3371 domField.style.left = col.left + 'px'; 3372 } 3373 } 3374 } 3375 3376 // show/hide the expand button (hide in case of error, to make place for an error icon) 3377 if (this.dom.buttonExpand) { 3378 this.dom.buttonExpand.style.visibility = (this.error == undefined) ? 'visible' : 'hidden'; 3379 } 3380 3381 // check the class name depending on the status 3382 var className = 'treegrid-item'; 3383 className += ((this.index % 2) ? ' treegrid-item-odd' : ' treegrid-item-even'); 3384 className += ' treegrid-level-' + (left / this.options.indentationWidth); 3385 if (this.selected || this.dataTransfer.dragging) { 3386 className += ' treegrid-item-selected'; 3387 } 3388 else if (this.dataTransfer.dragover) { 3389 className += ' treegrid-item-dragover'; 3390 } 3391 else if (this.dataTransfer.dragbefore) { 3392 className += ' treegrid-item-dragbefore'; 3393 } 3394 if (this.dirty) { 3395 className += ' treegrid-item-dirty'; 3396 } 3397 domFrame.className = className; 3398 } 3399 else { 3400 links.dnd.removeDraggable(this.dom.frame); 3401 links.dnd.removeDroppable(this.dom.frame); 3402 3403 if (this.dom.frame && this.dom.frame.parentNode) { 3404 this.dom.frame.parentNode.removeChild(this.dom.frame); 3405 } 3406 if (this.dom.frame) { 3407 this.dom = {}; 3408 } 3409 } 3410 }; 3411 3412 /** 3413 * Repaint the subgrid of this item, if available. 3414 */ 3415 links.TreeGrid.Item.prototype._repaintGrid = function() { 3416 if (this.grid) { 3417 this.grid.repaint(); 3418 } 3419 }; 3420 3421 /** 3422 * Repaint the loading text and icon (when the item is being loaded). 3423 */ 3424 links.TreeGrid.Item.prototype._repaintLoading = function() { 3425 // loading icon 3426 if (this.isVisible() && this.loading && (!this.fields || this.error || this.dirty)) { 3427 var domLoadingIcon = this.dom.loadingIcon; 3428 if (!domLoadingIcon) { 3429 // create loading icon 3430 domLoadingIcon = document.createElement('DIV'); 3431 domLoadingIcon.className = 'treegrid-loading-icon'; 3432 domLoadingIcon.style.position = 'absolute'; 3433 //domLoadingIcon.style.top = '0px'; 3434 //domLoadingIcon.style.left = '0px'; 3435 domLoadingIcon.title = 'loading...'; 3436 3437 this.getContainer().appendChild(domLoadingIcon); 3438 this.dom.loadingIcon = domLoadingIcon; 3439 } 3440 3441 // position loading icon 3442 domLoadingIcon.style.top = this.getAbsTop() + 'px'; 3443 domLoadingIcon.style.left = this.getAbsLeft() + 'px'; 3444 } 3445 else { 3446 // delete loading icon 3447 if (this.dom.loadingIcon) { 3448 this.dom.loadingIcon.parentNode.removeChild(this.dom.loadingIcon); 3449 delete this.dom.loadingIcon; 3450 } 3451 } 3452 3453 // loading text 3454 if (this.isVisible() && this.loading && !this.fields && !this.error) { 3455 var domLoadingText = this.dom.loadingText; 3456 if (!domLoadingText) { 3457 // create loading text 3458 domLoadingText = document.createElement('DIV'); 3459 domLoadingText.style.position = 'absolute'; 3460 domLoadingText.appendChild(document.createTextNode('loading...')); 3461 domLoadingText.className = 'treegrid-loading'; 3462 3463 this.getContainer().appendChild(domLoadingText); 3464 this.dom.loadingText = domLoadingText; 3465 } 3466 3467 // position loading text 3468 domLoadingText.style.top = this.getAbsTop() + 'px'; 3469 domLoadingText.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px'; 3470 domLoadingText.style.height = this.height + 'px'; 3471 } 3472 else { 3473 // delete loading text 3474 if (this.dom.loadingText) { 3475 this.dom.loadingText.parentNode.removeChild(this.dom.loadingText); 3476 delete this.dom.loadingText; 3477 } 3478 } 3479 }; 3480 3481 3482 /** 3483 * Repaint error text and icon when the item contains an error 3484 */ 3485 links.TreeGrid.Item.prototype._repaintError = function() { 3486 if (this.isVisible() && this.error && !this.fields) { 3487 // create item error 3488 var domError = this.dom.error; 3489 if (!domError) { 3490 // create the dom 3491 domError = document.createElement('DIV'); 3492 domError.style.position = 'absolute'; 3493 domError.appendChild(document.createTextNode('Error: ' + 3494 links.TreeGrid.Grid._errorToString(this.error))); 3495 domError.className = 'treegrid-error'; 3496 3497 this.getContainer().appendChild(domError); 3498 this.dom.error = domError; 3499 } 3500 3501 // position item error 3502 domError.style.top = this.getAbsTop() + 'px'; 3503 domError.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px'; 3504 } 3505 else { 3506 // delete item error 3507 if (this.dom.error) { 3508 this.dom.error.parentNode.removeChild(this.dom.error); 3509 delete this.dom.error; 3510 } 3511 } 3512 3513 // redraw error icon 3514 if (this.isVisible() && this.error && !this.loading) { 3515 var domErrorIcon = this.dom.errorIcon; 3516 if (!domErrorIcon) { 3517 // create error icon 3518 domErrorIcon = document.createElement('DIV'); 3519 domErrorIcon.className = 'treegrid-error-icon'; 3520 domErrorIcon.style.position = 'absolute'; 3521 domErrorIcon.title = this.error; 3522 3523 this.getContainer().appendChild(domErrorIcon); 3524 this.dom.errorIcon = domErrorIcon; 3525 } 3526 3527 // position the error icon 3528 var absLeft = this.getAbsLeft(); 3529 domErrorIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px'; 3530 domErrorIcon.style.left = absLeft + 'px'; 3531 } 3532 else { 3533 if (this.dom.errorIcon) { 3534 this.dom.errorIcon.parentNode.removeChild(this.dom.errorIcon); 3535 this.dom.errorIcon = undefined; 3536 } 3537 } 3538 }; 3539 3540 3541 /** 3542 * @constructor links.TreeGrid.DropArea 3543 * @param {Object} params. A key-value map containing parameters: 3544 * grid, item, top, height 3545 */ 3546 links.TreeGrid.DropArea = function (params) { 3547 if (params) { 3548 this.dataConnector = params.dataConnector || undefined; 3549 this.item = params.item || undefined; 3550 this.top = params.top || 0; 3551 this.height = params.height || 6; 3552 } 3553 3554 this.dragover = false; 3555 3556 // data 3557 this.dom = {}; 3558 }; 3559 3560 3561 links.TreeGrid.DropArea.prototype = new links.TreeGrid.Node(); 3562 3563 /** 3564 * reflow the DropArea 3565 */ 3566 links.TreeGrid.DropArea.prototype.reflow = function () { 3567 this.width = this.getVisibleWindow().width - this.getAbsLeft(); 3568 }; 3569 3570 /** 3571 * repaint the droparea 3572 */ 3573 links.TreeGrid.DropArea.prototype.repaint = function () { 3574 if (this.isVisible()) { 3575 var dropArea = this.dom.dropArea; 3576 3577 // create the droparea 3578 if (!dropArea) { 3579 var container = this.getContainer(); 3580 dropArea = document.createElement('DIV'); 3581 dropArea.style.position = 'absolute'; 3582 dropArea.style.left = '0px'; 3583 dropArea.style.top = '0px'; 3584 dropArea.style.height = this.height + 'px'; 3585 container.appendChild(dropArea); 3586 this.dom.dropArea = dropArea; 3587 3588 // drop events 3589 var dataTransfer = this.dataConnector ? this.dataConnector.getOptions().dataTransfer : undefined; 3590 if (dataTransfer) { 3591 var me = this; 3592 this.dropEffect = dataTransfer.dropEffect; 3593 links.TreeGrid.addEventListener(dropArea, 'dragover', 3594 function (event) { 3595 return me.onDragOver(event); 3596 }); 3597 links.TreeGrid.addEventListener(dropArea, 'dragenter', 3598 function (event) { 3599 return me.onDragEnter(event); 3600 }); 3601 links.TreeGrid.addEventListener(dropArea, 'dragleave', 3602 function (event) { 3603 return me.onDragLeave(event); 3604 }); 3605 links.TreeGrid.addEventListener(dropArea, 'drop', 3606 function (event) { 3607 return me.onDrop(event); 3608 }); 3609 } 3610 } 3611 3612 // position the droparea 3613 dropArea.className = this.dragover ? 'treegrid-droparea' : ''; 3614 dropArea.style.top = this.getAbsTop() + 'px'; 3615 dropArea.style.left = this.getAbsLeft() + 'px'; 3616 dropArea.style.width = (this.width - 2) + 'px'; 3617 } 3618 else { 3619 if (this.dom.dropArea) { 3620 this.dom.dropArea.parentNode.removeChild(this.dom.dropArea); 3621 delete this.dom.dropArea; 3622 } 3623 } 3624 }; 3625 3626 3627 /** 3628 * Event handler for drag over event 3629 */ 3630 links.TreeGrid.DropArea.prototype.onDragOver = function(event) { 3631 this.dragover = true; 3632 3633 event.dataTransfer.allowedEffect = 'none'; 3634 event.dataTransfer.dropEffect = this.dropEffect || 'none'; 3635 this.repaint(); 3636 3637 links.TreeGrid.preventDefault(event); 3638 return false; 3639 }; 3640 3641 3642 /** 3643 * Event handler for drag enter event 3644 * this will highlight the current item 3645 */ 3646 links.TreeGrid.DropArea.prototype.onDragEnter = function(event) { 3647 this.dragover = true; 3648 3649 this.repaint(); 3650 3651 event.dataTransfer.allowedEffect = 'none'; 3652 event.dataTransfer.dropEffect = this.dataConnector ? 'move' : 'none'; 3653 3654 return false; 3655 }; 3656 3657 /** 3658 * Event handler for drag leave event 3659 */ 3660 links.TreeGrid.DropArea.prototype.onDragLeave = function(event) { 3661 //console.log('onDragLeave', event); 3662 3663 this.dragover = false; 3664 3665 this.repaint(); 3666 3667 return false; 3668 }; 3669 3670 /** 3671 * Event handler for drop event 3672 */ 3673 links.TreeGrid.DropArea.prototype.onDrop = function(event) { 3674 var data = JSON.parse(event.dataTransfer.getData('items')); 3675 this.dragover = false; 3676 this.repaint(); 3677 3678 if (this.dataConnector) { 3679 var me = this; 3680 var callback = function () { 3681 me.parent.onResize(); 3682 }; 3683 var errback = callback; 3684 3685 var items = [data.item]; 3686 var itemBefore = this.item.data; 3687 this.dataConnector.insertItemsBefore(items, itemBefore, callback, errback); 3688 3689 /* TODO: trigger event? 3690 // send drop event 3691 this.dataConnector._onEvent('drop', { 3692 'dataConnector': this.dataConnector, 3693 'dropEffect': event.dataTransfer.dropEffect, 3694 'items': items 3695 }); 3696 */ 3697 } 3698 else { 3699 console.log('dropped but do nothing', event.dataTransfer.dropEffect); 3700 } 3701 3702 links.TreeGrid.preventDefault(event); 3703 }; 3704 3705 3706 /** 3707 * Convert an error to string 3708 * @param {*} err 3709 * @return {String} err 3710 */ 3711 links.TreeGrid.Grid._errorToString = function(err) { 3712 if (typeof(err) == 'string') { 3713 return err; 3714 } 3715 else if (err instanceof Object) { 3716 if (err.message && typeof(err.message) == 'string') { 3717 return err.message; 3718 } 3719 if (err.error && typeof(err.error) == 'string') { 3720 return err.error; 3721 } 3722 if (JSON) { 3723 return JSON.stringify(err); 3724 } 3725 } 3726 3727 return String(err); 3728 }; 3729 3730 3731 /** 3732 * @prototype VerticalScroll 3733 * creates a vertical scrollbar in given HTML DOM element 3734 * @param {Element} container Scroll bar will be created inside this 3735 * container 3736 * @param {Number} min Minimum value for the scrollbar 3737 * @param {Number} max Maximum value for the scrollbar 3738 * @param {Number} value Current value of the scrollbar 3739 */ 3740 links.TreeGrid.VerticalScroll = function (container, min, max, value) { 3741 this.container = container; 3742 this.dom = {}; 3743 this.height = 0; 3744 3745 this.min = 0; 3746 this.max = 0; 3747 this.value = 0; 3748 3749 // eventParams can contain event data for example on mouse down. 3750 this.eventParams = {}; 3751 this.onChangeHandlers = []; 3752 3753 this.setInterval(min, max); 3754 this.set(value); 3755 }; 3756 3757 /** 3758 * Redraw the scrollbar 3759 */ 3760 links.TreeGrid.VerticalScroll.prototype.redraw = function () { 3761 var background = this.dom.background; 3762 if (!background) { 3763 background = document.createElement('div'); 3764 background.className = 'treegrid-verticalscroll-background'; 3765 background.style.width = '100%'; 3766 background.style.height = '100%'; 3767 this.container.appendChild(background); 3768 3769 this.dom.background = background; 3770 } 3771 3772 var bar = this.dom.bar; 3773 if (!bar) { 3774 bar = document.createElement('div'); 3775 bar.className = 'treegrid-verticalscroll-bar'; 3776 bar.style.position = 'absolute'; 3777 bar.style.left = '20%'; 3778 bar.style.width = '60%'; 3779 bar.style.right = '20%'; 3780 bar.style.top = '0px'; 3781 bar.style.height = '0px'; 3782 this.container.appendChild(bar); 3783 3784 var me = this; 3785 var params = this.eventParams; 3786 params._onMouseDown = function (event) { 3787 me._onMouseDown(event); 3788 }; 3789 links.TreeGrid.addEventListener(bar, 'mousedown', params._onMouseDown); 3790 3791 this.dom.bar = bar; 3792 } 3793 3794 // position the bar 3795 var interval = (this.max - this.min); 3796 if (interval > 0) { 3797 var height = this.height; 3798 var borderWidth = 2; // TODO: retrieve borderWidth from css? 3799 var barHeight = Math.max(height * height / (interval + height), 20); 3800 var barTop = this.value * (height - barHeight - 2 * borderWidth) / interval; 3801 bar.style.height = barHeight + 'px'; 3802 bar.style.top = barTop + 'px'; 3803 bar.style.display = ''; 3804 } 3805 else { 3806 bar.style.display = 'none'; 3807 } 3808 }; 3809 3810 3811 /** 3812 * Check if the scrollbar is resized and if so, redraw the scrollbar 3813 * @return {Boolean} resized 3814 */ 3815 links.TreeGrid.VerticalScroll.prototype.checkResize = function () { 3816 var resized = this.reflow(); 3817 if (resized) { 3818 this.redraw(); 3819 } 3820 return resized; 3821 }; 3822 3823 /** 3824 * Recalculate the size of the elements of the scrollbar 3825 */ 3826 links.TreeGrid.VerticalScroll.prototype.reflow = function () { 3827 var resized = false; 3828 3829 this.height = this.dom.background.clientHeight; 3830 3831 return resized; 3832 }; 3833 3834 /** 3835 * Set the interval for the vertical scroll bar 3836 * @param {Number} min Minimum value, start of the interval 3837 * @param {Number} max Maximum value, end of the interval 3838 */ 3839 links.TreeGrid.VerticalScroll.prototype.setInterval = function (min, max) { 3840 this.min = min || 0; 3841 this.max = max || 0; 3842 if (this.max < this.min) { 3843 this.max = this.min; 3844 } 3845 3846 // value may be out of range now, so set it again 3847 this.set(this.value); 3848 }; 3849 3850 3851 /** 3852 * Set the current value of the scrollbar 3853 * The value must be within the range of the scrollbar 3854 * @param {Number} value 3855 */ 3856 links.TreeGrid.VerticalScroll.prototype.set = function (value) { 3857 this.value = value || this.min; 3858 if (this.value < this.min) { 3859 this.value = this.min; 3860 } 3861 if (this.value > this.max) { 3862 this.value = this.max; 3863 } 3864 3865 this.redraw(); 3866 }; 3867 3868 /** 3869 * Increase or decrease the value of the scrollbar by a delta 3870 * @param {Number} delta A positive or negative value 3871 */ 3872 links.TreeGrid.VerticalScroll.prototype.increase = function (delta) { 3873 var value = this.get(); 3874 value += delta; 3875 this.set(value); 3876 }; 3877 3878 /** 3879 * Retrieve the current value of the scrollbar 3880 * @return {Number} value 3881 */ 3882 links.TreeGrid.VerticalScroll.prototype.get = function () { 3883 return this.value; 3884 }; 3885 3886 3887 /** 3888 * Handler for mouse down event for scrollbar 3889 * @param {Event} event 3890 */ 3891 links.TreeGrid.VerticalScroll.prototype._onMouseDown = function(event) { 3892 var params = this.eventParams; 3893 3894 event = event || window.event; 3895 params.startMouseX = event.clientX; 3896 params.startMouseY = event.clientY; 3897 params.startValue = this.value; 3898 3899 var me = this; 3900 if (!params._onMouseMove) { 3901 params._onMouseMove = function (event) {me._onMouseMove(event);}; 3902 links.TreeGrid.addEventListener(document, "mousemove", params._onMouseMove); 3903 } 3904 if (!params._onMouseUp) { 3905 params._onMouseUp = function (event) {me._onMouseUp(event);}; 3906 links.TreeGrid.addEventListener(document, "mouseup", params._onMouseUp); 3907 } 3908 3909 links.TreeGrid.preventDefault(event); 3910 links.TreeGrid.stopPropagation(event); 3911 }; 3912 3913 /** 3914 * Handler for mouse move event for scrollbar 3915 * @param {Event} event 3916 */ 3917 links.TreeGrid.VerticalScroll.prototype._onMouseMove = function(event) { 3918 var params = this.eventParams; 3919 3920 event = event || window.event; 3921 var mouseX = event.clientX; 3922 var mouseY = event.clientY; 3923 var diffX = mouseX - params.startMouseX; 3924 var diffY = mouseY - params.startMouseY; 3925 3926 var interval = (this.max - this.min); 3927 var diff = (diffY / this.height) * (interval + this.height); 3928 3929 this.set(params.startValue + diff); 3930 3931 this._callbackOnChangeHandlers(); 3932 3933 links.TreeGrid.preventDefault(event); 3934 links.TreeGrid.stopPropagation(event); 3935 }; 3936 3937 3938 /** 3939 * Handler for mouse up event for scrollbar 3940 * @param {Event} event 3941 */ 3942 links.TreeGrid.VerticalScroll.prototype._onMouseUp = function(event) { 3943 var params = this.eventParams; 3944 var me = this; 3945 3946 // remove event listeners 3947 if (params._onMouseMove) { 3948 links.TreeGrid.removeEventListener(document, "mousemove", params._onMouseMove); 3949 params._onMouseMove = undefined; 3950 } 3951 if (!params.onMouseUp) { 3952 links.TreeGrid.removeEventListener(document, "mouseup", params._onMouseUp); 3953 params._onMouseUp = undefined; 3954 } 3955 3956 links.TreeGrid.preventDefault(event); 3957 links.TreeGrid.stopPropagation(event); 3958 }; 3959 3960 /** 3961 * Add a callback hander which is executed when the value of the scroll 3962 * bar is changed by the user (not after the method set() is executed) 3963 * The callback is executed with the new value as parameter 3964 * @param {Function} callback 3965 */ 3966 links.TreeGrid.VerticalScroll.prototype.addOnChangeHandler = function(callback) { 3967 this.removeOnChangeHandler(callback); 3968 this.onChangeHandlers.push(callback); 3969 }; 3970 3971 /** 3972 * Remove an onchange callback hander 3973 * @param {Function} callback Handler to be removed 3974 */ 3975 links.TreeGrid.VerticalScroll.prototype.removeOnChangeHandler = function(callback) { 3976 var index = this.onChangeHandlers.indexOf(callback); 3977 this.onChangeHandlers.splice(index, 1); 3978 }; 3979 3980 3981 /** 3982 * Call all onchange callback handlers 3983 */ 3984 links.TreeGrid.VerticalScroll.prototype._callbackOnChangeHandlers = function() { 3985 var handlers = this.onChangeHandlers; 3986 var value = this.value; 3987 for (var i = 0, iMax = handlers.length; i < iMax; i++) { 3988 handlers[i](value); 3989 } 3990 }; 3991 3992 3993 /** 3994 * @constructor links.DataConnector 3995 * this prototype should be inherited and its methods must be overwritten 3996 * @param {Object} options 3997 */ 3998 links.DataConnector = function (options) { 3999 this.options = options || {}; 4000 this.eventListeners = []; // registered event handlers 4001 this.expanded = false; 4002 }; 4003 4004 /** 4005 * Trigger an event 4006 * @param {String} event 4007 * @param {Object} params 4008 */ 4009 links.DataConnector.prototype.trigger = function (event, params) { 4010 // send the event to the treegrid 4011 links.events.trigger(this, event, params); 4012 4013 // trigger the google event bus 4014 if (google && google.visualization && google.visualization.events) { 4015 google.visualization.events.trigger(this, event, params); 4016 } 4017 4018 // TODO: remove this code? 4019 // send the event to all event listeners 4020 var eventListeners = this.eventListeners; 4021 for (var i = 0, iMax = eventListeners.length; i < iMax; i++) { 4022 var callback = eventListeners[i]; 4023 callback (event, params); 4024 } 4025 }; 4026 4027 /** 4028 * Asynchronously check for changes for a number of items. 4029 * The method will return the items which are changed. 4030 * The changed items can be updated via the method getItems. 4031 * @param {Number} index Index of the first item to be checked 4032 * @param {Number} num Number of items to be checked 4033 * @param {Object[]} items A list with the current versions of these items 4034 * @param {function} callback Callback method called on success. Called with one 4035 * object as parameter, containing fields: 4036 * {Number} totalItems 4037 * {Array with Objects} items The changed items 4038 * @param {function} errback Callback method called on failure. Called with 4039 * an error message as parameter. 4040 */ 4041 links.DataConnector.prototype.getChanges = function (index, num, items, callback, errback) { 4042 throw 'Error: method getChanges is not implemented'; 4043 }; 4044 4045 /** 4046 * Asynchronously get a number of items by index 4047 * @param {Number} index Index of the first item to be retrieved 4048 * @param {Number} num Number of items to be retrieved 4049 * @param {function} callback Callback method called on success. Called with one 4050 * object as parameter, containing fields: 4051 * {Number} totalItems 4052 * {Array with Objects} items 4053 * @param {function} errback Callback method called on failure. Called with 4054 * an error message as parameter. 4055 */ 4056 links.DataConnector.prototype.getItems = function (index, num, callback, errback) { 4057 errback('Error: method getItems is not implemented'); 4058 }; 4059 4060 /** 4061 * Asynchronously update a number of items. 4062 * The callback returns the updated items, which may be newly instantiated objects . 4063 * @param {Object[]} items A list with items to be updated 4064 * @param {function} callback Callback method called on success. Called with one 4065 * object as parameter, containing fields: 4066 * {Number} totalItems 4067 * {Array with Objects} items The updated items 4068 * @param {function} errback Callback method called on failure. Called with 4069 * an error message as parameter. 4070 */ 4071 links.DataConnector.prototype.updateItems = function (items, callback, errback) { 4072 errback('Error: method updateItems is not implemented'); 4073 }; 4074 4075 /** 4076 * Asynchronously append a number of items. 4077 * The callback returns the appended items, which may be newly instantiated objects . 4078 * @param {Object[]} items A list with items to be added 4079 * @param {function} callback Callback method called on success. Called with one 4080 * object as parameter, containing fields: 4081 * {Number} totalItems 4082 * {Array with Objects} items The appended items 4083 * @param {function} errback Callback method called on failure. Called with 4084 * an error message as parameter. 4085 */ 4086 links.DataConnector.prototype.appendItems = function (items, callback, errback) { 4087 errback('Error: method appendItems is not implemented'); 4088 }; 4089 4090 /** 4091 * Asynchronously insert a number of items. 4092 * The callback returns the inserted items, which may be newly instantiated objects . 4093 * @param {Object[]} items A list with items to be inserted 4094 * @param {Object} [beforeItem] The items will be inserted before this item. 4095 * When beforeItem is undefined, the items will be 4096 * moved to the end of the data. 4097 * @param {function} callback Callback method called on success. Called with one 4098 * object as parameter, containing fields: 4099 * {Number} totalItems 4100 * {Array with Objects} items The inserted items 4101 * @param {function} errback Callback method called on failure. Called with 4102 * an error message as parameter. 4103 */ 4104 links.DataConnector.prototype.insertItemsBefore = function (items, beforeItem, callback, errback) { 4105 errback('Error: method insertItemsBefore is not implemented'); 4106 }; 4107 4108 /** 4109 * Asynchronously move a number of items. 4110 * The callback returns the moved items, which may be newly instantiated objects . 4111 * @param {Object[]} items A list with items to be moved 4112 * @param {Object} [beforeItem] The items will be inserted before this item. 4113 * When beforeItem is undefined, the items will be 4114 * moved to the end of the data. 4115 * @param {function} callback Callback method called on success. Called with one 4116 * object as parameter, containing fields: 4117 * {Number} totalItems 4118 * {Array with Objects} items The moved items 4119 * @param {function} errback Callback method called on failure. Called with 4120 * an error message as parameter. 4121 */ 4122 links.DataConnector.prototype.moveItems = function (items, beforeItem, callback, errback) { 4123 errback('Error: method moveItems is not implemented'); 4124 }; 4125 4126 /** 4127 * Asynchronously remove a number of items. 4128 * The callback returns the removed items. 4129 * @param {Object[]} items A list with items to be removed 4130 * @param {function} callback Callback method called on success. Called with one 4131 * object as parameter, containing fields: 4132 * {Number} totalItems 4133 * {Array with Objects} items The removed items 4134 * @param {function} errback Callback method called on failure. Called with 4135 * an error message as parameter. 4136 */ 4137 links.DataConnector.prototype.removeItems = function (items, callback, errback) { 4138 errback('Error: method removeItems is not implemented'); 4139 }; 4140 4141 /** 4142 * Asynchronously link a source item to a target item. 4143 * The callback returns the linked items. 4144 * @param {Object[]} sourceItems 4145 * @param {Object} targetItem 4146 * @param {function} callback Callback method called on success. Called with 4147 * one object as parameter, containing fields: 4148 * {Number} totalItems 4149 * {Array with Objects} items The removed items 4150 * @param {function} errback Callback method called on failure. Called with 4151 * an error message as parameter. 4152 */ 4153 links.DataConnector.prototype.linkItems = function (sourceItems, targetItem, callback, errback) { 4154 errback('Error: method linkItems is not implemented'); 4155 }; 4156 4157 /** 4158 * internal onEvent handler 4159 * @param {String} event 4160 * @param {Object} params. Object containing index (Number), 4161 * and item (Object). 4162 */ 4163 links.DataConnector.prototype._onEvent = function (event, params) { 4164 this.trigger(event, params); 4165 this.onEvent(event, params); 4166 }; 4167 4168 /** 4169 * onEvent handler 4170 * @param {String} event 4171 * @param {Object} params. Object containing index (Number), 4172 * and item (Object). 4173 */ 4174 links.DataConnector.prototype.onEvent = function (event, params) { 4175 // this method can be overwritten 4176 }; 4177 4178 // TODO: comment 4179 links.DataConnector.prototype.setFiltering = function (filters) { 4180 console.log('Error: method setFiltering is not implemented'); 4181 }; 4182 4183 // TODO: comment 4184 links.DataConnector.prototype.setSorting = function (sorting) { 4185 console.log('Error: method setSorting is not implemented'); 4186 }; 4187 4188 // TODO: comment 4189 links.DataConnector.prototype.getFiltering = function (filters) { 4190 console.log('Error: method getFiltering is not implemented'); 4191 }; 4192 4193 // TODO: comment 4194 links.DataConnector.prototype.getSorting = function (sorting) { 4195 console.log('Error: method getSorting is not implemented'); 4196 }; 4197 4198 /** 4199 * Add an event listener to the DataConnector 4200 * @param {function} callback The callback method will be called with two 4201 * parameters: 4202 * {String} event 4203 * {Object} params 4204 */ 4205 links.DataConnector.prototype.addEventListener = function (callback) { 4206 var index = this.eventListeners.indexOf(callback); 4207 if (index == -1) { 4208 this.eventListeners.push(callback); 4209 } 4210 }; 4211 4212 /** 4213 * Remove an event listener from the DataConnector 4214 * @param {function} callback The registered callback method 4215 */ 4216 links.DataConnector.prototype.removeEventListener = function (callback) { 4217 var index = this.eventListeners.indexOf(callback); 4218 if (index != -1) { 4219 this.eventListeners.splice(index, 1); 4220 } 4221 }; 4222 4223 /** 4224 * Set options for the dataconnector 4225 * @param {Object} options Available options: 4226 * 'columns': 4227 * An array containing objects, each object 4228 * contains parameters 'name', and optionally 4229 * 'text' and 'title'. The provided fields will 4230 * be displayed in the given order. 4231 * 'dataTransfer': 4232 * An object containing the parameters: 4233 * 'allowedEffect': 4234 * A string value 'none', 'link', 'move', or 'copy' 4235 * 'dropEffect': 4236 * A string value 'none', 'link', 'move', or 'copy 4237 */ 4238 links.DataConnector.prototype.setOptions = function (options) { 4239 this.options = options || {}; 4240 }; 4241 4242 /** 4243 * Get the currently set options 4244 */ 4245 links.DataConnector.prototype.getOptions = function () { 4246 return this.options; 4247 }; 4248 4249 /** 4250 * Set action icons 4251 * @param {Array} actions 4252 */ 4253 links.DataConnector.prototype.setActions = function (actions) { 4254 this.actions = actions; 4255 this.trigger('change', undefined); 4256 }; 4257 4258 /** 4259 * @constructor links.DataTable 4260 * Asynchronous link to a data table 4261 * @param {Array} data A javascript array containing objects 4262 * @param {Object} options 4263 */ 4264 links.DataTable = function (data, options) { 4265 this.data = data || []; 4266 this.setOptions(options); 4267 4268 this.filteredData = this.data; 4269 }; 4270 4271 links.DataTable.prototype = new links.DataConnector(); 4272 4273 /** 4274 * Asynchronously get a number of items by index 4275 * @param {Number} index Index of the first item to be retrieved 4276 * @param {Number} num Number of items to be retrieved 4277 * @param {function} callback Callback method called on success. Called with one 4278 * object as parameter, containing fields: 4279 * {Number} totalItems 4280 * {Array with Objects} items 4281 * @param {function} errback Callback method called on failure. Called with 4282 * an error message as parameter. 4283 */ 4284 links.DataTable.prototype.getItems = function (index, num, callback, errback) { 4285 var items = [], 4286 filteredData = this.filteredData, 4287 count = filteredData.length; 4288 for (var i = index, iMax = Math.min(index + num, count) ; i < iMax; i++) { 4289 items.push(filteredData[i]); 4290 } 4291 callback && callback({ 4292 'totalItems': filteredData.length, 4293 'items': items 4294 }); 4295 }; 4296 4297 4298 /** 4299 * Asynchronously update a number of items. 4300 * The callback returns the updated items, which may be newly instantiated objects . 4301 * @param {Object[]} items A list with items to be updated 4302 * @param {function} callback Callback method called on success. Called with one 4303 * object as parameter, containing fields: 4304 * {Number} totalItems 4305 * {Array with Objects} items The updated items 4306 * @param {function} errback Callback method called on failure. Called with 4307 * an error message as parameter. 4308 */ 4309 links.DataTable.prototype.updateItems = function (items, callback, errback) { 4310 var num = items.length; 4311 var data = this.data; 4312 for (var i = 0; i < num; i++) { 4313 var item = items[i]; 4314 var index = data.indexOf(item); 4315 if (index != -1) { 4316 // clone the item, so we can distinguish changed items by their pointer 4317 data[index] = {}; 4318 for (var prop in item) { 4319 if (item.hasOwnProperty(prop)) { 4320 data[index][prop] = item[prop]; 4321 } 4322 } 4323 } 4324 else { 4325 errback && errback("Cannot find item"); // TODO: better error 4326 return; 4327 } 4328 } 4329 4330 // perform filtering and sorting again 4331 this._applySortingAndFilters(); 4332 4333 callback && callback({ 4334 'totalItems': this.filteredData.length, 4335 'items': items 4336 }); 4337 4338 this.trigger('change', undefined); 4339 }; 4340 4341 /** 4342 * Asynchronously append a number of items. 4343 * The callback returns the appended items, which may be newly instantiated objects . 4344 * @param {Object[]} items A list with items to be added 4345 * @param {function} callback Callback method called on success. Called with one 4346 * object as parameter, containing fields: 4347 * {Number} totalItems 4348 * {Array with Objects} items The appended items 4349 * @param {function} errback Callback method called on failure. Called with 4350 * an error message as parameter. 4351 */ 4352 links.DataTable.prototype.appendItems = function (items, callback, errback) { 4353 var num = items.length; 4354 for (var i = 0; i < num; i++) { 4355 this.data.push(items[i]); 4356 } 4357 4358 // perform filtering and sorting again 4359 this._applySortingAndFilters(); 4360 4361 callback && callback({ 4362 'totalItems': this.filteredData.length, 4363 'items': items 4364 }); 4365 4366 this.trigger('change', undefined); 4367 }; 4368 4369 /** 4370 * Asynchronously insert a number of items. 4371 * The callback returns the inserted items, which may be newly instantiated objects . 4372 * @param {Object[]} items A list with items to be inserted 4373 * @param {Object} [beforeItem] The items will be inserted before this item. 4374 * When beforeItem is undefined, the items will be 4375 * moved to the end of the data. 4376 * @param {function} callback Callback method called on success. Called with one 4377 * object as parameter, containing fields: 4378 * {Number} totalItems 4379 * {Array with Objects} items The inserted items 4380 * @param {function} errback Callback method called on failure. Called with 4381 * an error message as parameter. 4382 */ 4383 links.DataTable.prototype.insertItemsBefore = function (items, beforeItem, callback, errback) { 4384 // find the item before which the new items will be inserted 4385 var data = this.data; 4386 var beforeIndex = beforeItem ? data.indexOf(beforeItem) : data.length; 4387 if (beforeIndex == -1) { 4388 errback && errback("Cannot find item"); // TODO: better error 4389 return; 4390 } 4391 4392 // insert the new data 4393 data.splice.apply(data, [beforeIndex, 0].concat(items)); 4394 4395 // perform filtering and sorting again 4396 this._applySortingAndFilters(); 4397 4398 callback && callback({ 4399 'totalItems': this.filteredData.length, 4400 'items': items 4401 }); 4402 4403 this.trigger('change', undefined); 4404 }; 4405 4406 4407 /** 4408 * Asynchronously move a number of items. 4409 * The callback returns the moved items, which may be newly instantiated objects . 4410 * @param {Object[]} items A list with items to be moved 4411 * @param {Object} [beforeItem] The items will be inserted before this item. 4412 * When beforeItem is undefined, the items will be 4413 * moved to the end of the data. 4414 * @param {function} callback Callback method called on success. Called with one 4415 * object as parameter, containing fields: 4416 * {Number} totalItems 4417 * {Array with Objects} items The moved items 4418 * @param {function} errback Callback method called on failure. Called with 4419 * an error message as parameter. 4420 */ 4421 links.DataTable.prototype.moveItems = function (items, beforeItem, callback, errback) { 4422 // find the index of the before item 4423 var beforeIndex = beforeItem ? this.data.indexOf(beforeItem) : this.data.length; 4424 if (beforeIndex == -1) { 4425 errback && errback("Cannot find item"); // TODO: better error 4426 return; 4427 } 4428 4429 // find the indexes of all items 4430 var num = items.length; 4431 var indexes = []; 4432 for (var i = 0; i < num; i++) { 4433 var index = this.data.indexOf(items[i]); 4434 if (index != -1) { 4435 indexes[i] = index; 4436 } 4437 else { 4438 errback && errback("Cannot find item"); // TODO: better error 4439 return; 4440 } 4441 } 4442 4443 // order the indexes in ascending order 4444 indexes.sort(function (a, b) { 4445 return a > b ? 1 : a < b ? -1 : 0; 4446 }); 4447 4448 // if all items are found, move them from the last to the first (else we alter the indexes) 4449 var offset = 0; 4450 for (var i = num - 1; i >= 0; i--) { 4451 var index = indexes[i]; 4452 if (index < beforeIndex) { 4453 offset++; 4454 } 4455 this.data.splice(index, 1); 4456 } 4457 this.data.splice.apply(this.data, [beforeIndex - offset, 0].concat(items)); 4458 4459 // perform filtering and sorting again 4460 this._applySortingAndFilters(); 4461 4462 callback && callback({ 4463 'totalItems': this.filteredData.length, 4464 'items': items 4465 }); 4466 4467 this.trigger('change', undefined); 4468 }; 4469 4470 4471 /** 4472 * Asynchronously remove a number of items. 4473 * The callback returns the removed items. 4474 * @param {Object[]} items A list with items to be removed 4475 * @param {function} callback Callback method called on success. Called with one 4476 * object as parameter, containing fields: 4477 * {Number} totalItems 4478 * {Array with Objects} items The removed items 4479 * @param {function} errback Callback method called on failure. Called with 4480 * an error message as parameter. 4481 */ 4482 links.DataTable.prototype.removeItems = function (items, callback, errback) { 4483 var num = items.length; 4484 for (var i = 0; i < num; i++) { 4485 var index = this.data.indexOf(items[i]); 4486 if (index != -1) { 4487 this.data.splice(index, 1); 4488 } 4489 else { 4490 errback && errback("Cannot find item"); // TODO: better error 4491 return; 4492 } 4493 } 4494 4495 // perform filtering and sorting again 4496 this._applySortingAndFilters(); 4497 4498 callback && callback({ 4499 'totalItems': this.filteredData.length, 4500 'items': items 4501 }); 4502 4503 this.trigger('change', undefined); 4504 }; 4505 4506 /** 4507 * Asynchronously check for changes for a number of items. 4508 * The method will return the items which are changed. 4509 * The changed items can be updated via the method getItems. 4510 * @param {Number} index Index of the first item to be checked 4511 * @param {Number} num Number of items to be checked 4512 * @param {Object[]} items A list with items to be checked for changes. 4513 * @param {function} callback Callback method called on success. Called with one 4514 * object as parameter, containing fields: 4515 * {Number} totalItems 4516 * {Array with Objects} items The changed items 4517 * @param {function} errback Callback method called on failure. Called with 4518 * an error message as parameter. 4519 */ 4520 links.DataTable.prototype.getChanges = function (index, num, items, callback, errback) { 4521 var changedItems = [], 4522 filteredData = this.filteredData, 4523 count = filteredData.length; 4524 4525 for (var i = 0; i < num; i++) { 4526 var item = items[i]; 4527 if (item != filteredData[index + i]) { 4528 changedItems.push(item); 4529 } 4530 } 4531 4532 callback && callback({ 4533 'totalItems': this.filteredData.length, 4534 'items': changedItems 4535 }); 4536 }; 4537 4538 /** 4539 * Force the DataTable to be changed by incrementing the update sequence 4540 */ 4541 links.DataTable.prototype.update = function () { 4542 this._applySortingAndFilters(); 4543 4544 this.trigger('change', undefined); 4545 }; 4546 4547 /** 4548 * onEvent handler. Can be overwritten by an implementation 4549 * @param {String} event 4550 * @param {Object} params 4551 */ 4552 // TODO: remove the onEvent handler? 4553 links.DataTable.prototype.onEvent = function (event, params) { 4554 }; 4555 4556 /** 4557 * Set a filter for this DataTable 4558 * @param {Object[]} filters An array containing filter objects. 4559 * a filter object can contain parameters 4560 * `field`, `value`, `startValue`, `endValue`, 4561 * `values`. 4562 */ 4563 links.DataTable.prototype.setFiltering = function (filters) { 4564 this.filters = filters; 4565 this._applySortingAndFilters(); 4566 this.trigger('change', undefined); 4567 } 4568 4569 /** 4570 * Returns the current filtering array, returns undefined if there is no sorting defined. 4571 * @return {Object[] | undefined} 4572 */ 4573 links.DataTable.prototype.getFiltering = function () { 4574 return this.sorting; 4575 } 4576 4577 /** 4578 * Set sorting for this DataTable. Can sort one or multiple columns 4579 * @param {Array.<{field: string, order: string}>} filters 4580 * An array containing sorting objects. 4581 * a sorting object contains parameters 4582 * `field`, `order`. Order can be `asc`, `desc`, or null. 4583 */ 4584 links.DataTable.prototype.setSorting = function (sorting) { 4585 this.sorting = sorting; 4586 this._applySortingAndFilters(); 4587 this.trigger('change', undefined); 4588 } 4589 4590 /** 4591 * Returns the current sorting array, returns undefined if there is no sorting defined. 4592 * @return {Array.<{field: string, order: string}> | undefined} 4593 */ 4594 links.DataTable.prototype.getSorting = function () { 4595 return this.sorting; 4596 } 4597 4598 /** 4599 * Apply sorting and filtering (if set) 4600 * This method is executed after the data has been changed. 4601 * See also methods `setSorting` and `setFiltering` 4602 */ 4603 links.DataTable.prototype._applySortingAndFilters = function () { 4604 // filter the data 4605 if (this.filters) { 4606 this.filteredData = []; 4607 for (var i = 0, iMax = this.data.length; i < iMax; i++) { 4608 var item = this.data[i]; 4609 var emit = true; 4610 for (var f = 0, fMax = this.filters.length; f < fMax; f++) { 4611 var filter = this.filters[f]; 4612 if (filter.field) { 4613 var value = item[filter.field]; 4614 if (filter.value && (value != filter.value)) { 4615 emit = false; 4616 } 4617 if (filter.startValue && value < filter.startValue) { 4618 emit = false; 4619 } 4620 if (filter.endValue && value > filter.endValue) { 4621 emit = false; 4622 } 4623 if (filter.values && (filter.values.indexOf(value) == -1)) { 4624 emit = false; 4625 } 4626 } 4627 } 4628 4629 if (emit) { 4630 this.filteredData.push(item); 4631 } 4632 } 4633 } 4634 else { 4635 this.filteredData = this.data.slice(0); 4636 } 4637 4638 // order the filtered data 4639 if (this.sorting) { 4640 // create a list with fields that need to be ordered 4641 var orders = []; 4642 for (var f = 0, fMax = this.sorting.length; f < fMax; f++) { 4643 var entry = this.sorting[f]; 4644 if (entry.field && entry.order) { 4645 var order = entry.order.toLowerCase(); 4646 if (order == 'asc' || order == 'desc') { 4647 orders.push({ 4648 'field': entry.field, 4649 'direction': ((order == 'asc') ? 1 : -1) 4650 }); 4651 } 4652 else { 4653 throw 'Unknown order "' + order + '". ' + 4654 'Available values: "asc", "desc".'; 4655 } 4656 } 4657 } 4658 4659 var len = orders.length; 4660 if (len > 0) { 4661 this.filteredData.sort(function (a, b) { 4662 for (var i = 0; i < len; i++) { 4663 var order = orders[i]; 4664 var field = order.field; 4665 var direction = order.direction; 4666 4667 if (a[field] == b[field]) { 4668 if (i == len - 1) { 4669 return 0; 4670 } 4671 else { 4672 // compare with the next filter 4673 } 4674 } 4675 else { 4676 return (a[field] > b[field]) ? direction : -direction; 4677 } 4678 } 4679 }); 4680 } 4681 } 4682 }; 4683 4684 4685 /** 4686 * @constructor links.CouchConnector 4687 * @param {String} url Url can point to a database or to a view 4688 */ 4689 // TODO: update the couchconnector 4690 links.CouchConnector = function (url) { 4691 this.url = url; 4692 this.data = []; 4693 this.filter = undefined; 4694 4695 this.updateSeq = undefined; 4696 this.blockSize = 16; // data will be retrieved in blocks of this blockSize 4697 // TODO: make blockSize customizable 4698 4699 this.totalItems = this.blockSize; 4700 }; 4701 4702 links.CouchConnector.prototype = new links.DataConnector(); 4703 4704 links.CouchConnector.prototype.getItems = function (index, num, callback, errback) { 4705 // first check if the requested data is already loaded 4706 var me = this; 4707 var data = this.data; 4708 var dataComplete = true; 4709 for (var i = index, iMax = index + num; i < iMax; i++) { 4710 if (data[i] == undefined) { 4711 dataComplete = false; 4712 break; 4713 } 4714 } 4715 4716 // TODO: smarter retrieve only the missing parts of the data, not the whole interval again. 4717 4718 function getSubset (index, num) { 4719 var dataSubset = []; 4720 for (var i = index, iMax = index + num; i < iMax; i++) { 4721 var d = data[i]; 4722 dataSubset.push(d ? d.value : undefined); 4723 } 4724 return dataSubset; 4725 } 4726 4727 if (dataComplete) { 4728 var dataChanged = false; 4729 // if all data is available, check if data has changed on the server 4730 // TODO: check change of update sequence 4731 4732 if (dataChanged) { 4733 // clear all data 4734 this.data = []; 4735 data = this.data; 4736 dataComplete = false; 4737 } 4738 } 4739 4740 if (!dataComplete) { 4741 // choose skip and limit to match block size 4742 var skipRem = index % this.blockSize, 4743 skip = index - skipRem, 4744 limitRem = (num + skipRem) % this.blockSize, 4745 limit = (num + skipRem - limitRem) + (limitRem != 0 ? this.blockSize : 0); 4746 4747 // cut off the part of items which are already loaded 4748 while (data[skip] && limit > 0) { 4749 skip++; 4750 limit--; 4751 } 4752 while (data[skip + limit - 1] && limit > 0) { 4753 limit--; 4754 } 4755 4756 // find a startkey, to spead up the request 4757 var startkey = undefined; 4758 var startKeyIndex = skip - 1; 4759 while (startKeyIndex > 0 && data[startKeyIndex] == undefined) { 4760 startKeyIndex--; 4761 } 4762 if (data[startKeyIndex]) { 4763 startkey = data[startKeyIndex].key; 4764 } 4765 4766 var separator = (this.url.indexOf('?') == -1) ? '?' : '&'; 4767 var url; 4768 if (startkey) { 4769 url = this.url + separator + 4770 'skip=' + (skip - startKeyIndex) + 4771 '&limit=' + limit + 4772 '&startkey=' + escape(JSON.stringify(startkey)); 4773 4774 // TODO: reckon with filter? 4775 } 4776 else { 4777 url = this.url + separator + 'skip=' + skip + '&limit=' + limit; 4778 4779 if (this.filter) { 4780 if (this.filter.value != undefined) { 4781 url += '&key=' + escape(JSON.stringify(this.filter.value)); 4782 } 4783 if (this.filter.startValue != undefined) { 4784 url += '&startkey=' + escape(JSON.stringify(this.filter.startValue)); 4785 } 4786 if (this.filter.endValue != undefined) { 4787 url += '&endkey=' + escape(JSON.stringify(this.filter.endValue)); 4788 } 4789 } 4790 } 4791 4792 /* TODO: descending order 4793 if (this.filter && this.filter.order) { 4794 if (this.filter.order == 'DESC') { 4795 url += '&descending=true'; 4796 // TODO: startkey and endkey must be interchanged 4797 } 4798 } 4799 */ 4800 4801 // TODO: reckon with filter values 4802 // TODO: reckon with filter order: asc/desc 4803 4804 // TODO: using skip for paginating results is a very bad solution, very slow 4805 // create a smarter solution to retrieve results with an as small as 4806 // possible skip, starting at the closest retrieved document key 4807 4808 //console.log('Retrieving data from server url=' + url); 4809 4810 links.getJSONP(url, function (response) { 4811 if (response.error) { 4812 errback(response); 4813 return; 4814 } 4815 4816 var rows = response.rows; 4817 var dataSubset = []; 4818 for (var i = 0, iMax = rows.length; i < iMax; i++) { 4819 data[i + skip] = rows[i]; 4820 } 4821 4822 // set the number of total items 4823 me.totalItems = Math.min(me.data.length + me.blockSize, response.total_rows); 4824 4825 var dataSubset = getSubset(index, num); 4826 callback({ 4827 'totalItems': me.totalItems, 4828 'items': dataSubset 4829 }); 4830 }, errback); 4831 } 4832 else { 4833 // all data is already loaded 4834 var dataSubset = getSubset(index, num); 4835 callback({ 4836 'totalItems': me.totalItems, 4837 'items': dataSubset 4838 }); 4839 } 4840 }; 4841 4842 links.CouchConnector.prototype._getUpdateSeq = function (callback, errback) { 4843 var viewIndex = this.url.indexOf('_view'); 4844 if (viewIndex == -1) { 4845 errback('Error: cannot get information on this view, url is no view'); 4846 return; 4847 } 4848 4849 // TODO: check _change?since=3 4850 // http://guide.couchdb.org/draft/notifications.html 4851 4852 var url = this.url.substring(0, viewIndex) + '_info'; 4853 4854 links.getJSONP(url, function (info) { 4855 if (data.error) { 4856 errback(data); 4857 return; 4858 } 4859 4860 var update_seq = info.view_index.update_seq; 4861 callback(update_seq); 4862 }, errback); 4863 }; 4864 4865 links.CouchConnector.prototype.getChanges = function (index, num, items, 4866 callback, errback) { 4867 // TODO: implement CouchConnector.getChanges, use real update_seq from couch 4868 var changedItems = []; 4869 4870 // TODO: check for changes in the items and in the total count 4871 4872 callback({ 4873 'totalItems': (this.totalItems || 10), 4874 'items': changedItems 4875 }); 4876 return changedItems; 4877 }; 4878 4879 4880 /** 4881 * Set a filter for this DataTable 4882 * @param {Object[]} filters An array containing filter objects. 4883 * a filter object contains parameters 4884 * field, value, startValue, endValue 4885 */ 4886 links.CouchConnector.prototype.setFiltering = function (filters) { 4887 if (filters.length > 1) { 4888 throw "CouchConnector can currently only handle one filter"; 4889 } 4890 else if (filters.length > 0) { 4891 this.filter = filters[0]; 4892 } 4893 4894 // TODO: invalidate currently retrieved data 4895 }; 4896 4897 /** 4898 * Set a filter for this DataTable 4899 * @param {Object[]} sorting An array containing one or multipel objects. 4900 * contains parameters `field`, `order` 4901 */ 4902 links.CouchConnector.prototype.setSorting = function (sorting) { 4903 if (filters.length > 1) { 4904 throw "CouchConnector can currently only handle one order"; 4905 } 4906 else if (sorting.length > 0) { 4907 this.sorting = sorting[0]; 4908 } 4909 4910 // TODO: invalidate currently retrieved data 4911 }; 4912 4913 /** 4914 * Retrieve a JSON response via javascript injection. 4915 * Note1: it is not possible to know when the injection failed, but you can 4916 * create a timeout which checks if the callback has been called succesfully 4917 * and if not, throw an error. 4918 * Note2: jsonp must be enabled on the server side. (For example in the 4919 * couchdb configuration the option 'allow_jsonp' must be set true) 4920 * @author Jos de Jong 4921 * @param {String} url The url to be retrieved 4922 * @param {function} callback. On response, the callback function will be called 4923 * with the retrieved data as parameter (JSON object) 4924 * @param {function} errback On error, the errback function will be called 4925 * without parameters 4926 */ 4927 links.getJSONP = function (url, callback, errback) { 4928 //console.log('getJSONP ' + url) // TODO: cleanup 4929 4930 // create a random function name to use as temporary callback function 4931 var callbackName = 'callback' + Math.round(Math.random() * 1e10); 4932 4933 // create a script to be injected in the document 4934 var script = document.createElement('script'); 4935 var separator = (url.indexOf('?') == -1) ? '?' : '&'; 4936 script.src = url + separator + 'callback=' + callbackName; 4937 script.onerror = function (event) { 4938 // clean up created function and script 4939 document.body.removeChild(script); 4940 delete window[callbackName]; 4941 4942 if (errback) { 4943 errback(); 4944 } 4945 }; 4946 script.type = 'text/javascript'; 4947 4948 // create the temporary callback function 4949 window[callbackName] = function (data) { 4950 // clean up created function and script 4951 document.body.removeChild(script); 4952 delete window[callbackName]; 4953 4954 // call callback function with retrieved data 4955 if (callback) { 4956 callback(data); 4957 } 4958 }; 4959 4960 // inject the script in the document 4961 document.body.appendChild(script); 4962 4963 // TODO: built something to check for an error. only possible with a timeout? 4964 }; 4965 4966 4967 /** 4968 * Event handler for drag start event 4969 */ 4970 links.TreeGrid.Frame.prototype.onDragStart = function(event) { 4971 // create a copy of the selection array 4972 /* TODO: cleanup 4973 var items = []; 4974 for (var i = 0; i < this.selection.length; i++) { 4975 var sel = this.selection[i]; 4976 items.push(sel); 4977 } 4978 4979 var dragImage = this.dom.dragImage; 4980 if (dragImage) { 4981 var count = items.length; 4982 dragImage.innerHTML = count + ' item' + ((count != 1) ? 's' : ''); 4983 } 4984 event.dataTransfer.setData('items', items); 4985 */ 4986 4987 // check if there are selected items that can be dragged 4988 var items = []; 4989 for (var i = 0; i < this.selection.length; i++) { 4990 var sel = this.selection[i]; 4991 4992 var parent = sel.parent; 4993 var dataConnector = parent ? parent.dataConnector : undefined; 4994 var options = dataConnector ? dataConnector.options : undefined; 4995 var dataTransfer = options ? options.dataTransfer : undefined; 4996 var allowedEffect = dataTransfer ? dataTransfer.allowedEffect : undefined; 4997 4998 // validate whether at least one of the items can be moved or copied 4999 if (allowedEffect != undefined && allowedEffect.toLowerCase() != 'none') { 5000 items.push(sel); 5001 } 5002 } 5003 5004 event.dragSource = parent; // TODO: this does not work when there are multiple parents in a multi selection 5005 5006 if (items.length > 0) { 5007 var dragImage = this.dom.dragImage; 5008 if (dragImage) { 5009 var count = items.length; 5010 dragImage.innerHTML = count + ' item' + ((count != 1) ? 's' : ''); 5011 } 5012 5013 event.dataTransfer.setData('items', items); 5014 event.dataTransfer.setData('srcFrame', this); 5015 return true; 5016 } 5017 else { 5018 return false; 5019 } 5020 }; 5021 5022 /** 5023 * Event handler for drag start event 5024 */ 5025 links.TreeGrid.Frame.prototype.onDragEnd = function(event) { 5026 var dropEffect = event.dataTransfer.dropEffect; 5027 if (dropEffect == 'move' && !event.dataTransfer.sameDataConnector) { 5028 // note: in case of sameDataConnector, the event is already handled by onDrop() as a moveItems event. 5029 var frame = this; 5030 var items = event.dataTransfer.getData('items'); 5031 var callbacksInProgress = items.length; 5032 5033 var callback = function () { 5034 callbacksInProgress--; 5035 if (callbacksInProgress == 0) { 5036 frame.unselect(); 5037 frame.onResize(); 5038 } 5039 }; 5040 var errback = callback; 5041 5042 for (var i = 0; i < items.length; i++) { 5043 var item = items[i]; 5044 // FIXME: removing the item is a temporary hack. This only works 5045 // in case of a single user, and prevents the state of the items 5046 // (expanded or not) from being shifted. 5047 // The real solution is to be able to really store the state of an 5048 // not on an index basis, but on an item basis. 5049 // Use a linked list instead of an array? 5050 item.parent._removeItem(item); 5051 5052 // TODO: not so nice accessing the parent grid this way... 5053 item.parent.dataConnector.removeItems([item.data], callback, errback); 5054 } 5055 } 5056 /* TODO 5057 else if (dropEffect == 'link') { 5058 // TODO: linkedItems 5059 } 5060 else if (dropEffect == 'copy') { 5061 // TODO: copiedItems 5062 } 5063 */ 5064 5065 // TODO: trigger event? 5066 5067 this.repaint(); 5068 }; 5069 5070 5071 /** 5072 * A click event 5073 * @param {event} event The event that occurred 5074 */ 5075 links.TreeGrid.Frame.prototype.onMouseDown = function(event) { 5076 event = event || window.event; 5077 5078 // only react on left mouse button down 5079 var params = this.eventParams; 5080 var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); 5081 if (!leftButtonDown && !params.touchDown) { 5082 return; 5083 } 5084 5085 var target = links.TreeGrid.getTarget(event); 5086 5087 if (target.treeGridType && target.treeGridType == 'expand') { 5088 target.grid.toggleExpand(); 5089 links.TreeGrid.stopPropagation(event); // TODO: does not work 5090 } 5091 else if (target.treeGridType && target.treeGridType == 'action') { 5092 target.item.onEvent(target.event); 5093 links.TreeGrid.stopPropagation(event); // TODO: does not work 5094 } 5095 else { 5096 var item = links.TreeGrid.getItemFromTarget(target); 5097 if (item) { 5098 // select the item 5099 var keepSelection = event.ctrlKey; 5100 var selectRange = event.shiftKey; 5101 this.select(item, keepSelection, selectRange); 5102 } 5103 else { 5104 this.unselect(); 5105 } 5106 } 5107 5108 links.TreeGrid.preventDefault(event); 5109 }; 5110 5111 /** 5112 * Hover over an item 5113 * @param {event} event 5114 */ 5115 links.TreeGrid.Frame.prototype.onMouseOver = function (event) { 5116 event = event || window.event; 5117 5118 // check whether hovering an item 5119 var item = links.TreeGrid.getItemFromTarget(event.target); 5120 //console.log('enter', event, item) 5121 5122 if (this.hoveredItem !== item) { 5123 if (this.hoveredItem) { 5124 this.trigger('leave', {item: this.hoveredItem.data}); 5125 } 5126 5127 if (item) { 5128 this.trigger('enter', {item: item.data}); 5129 } 5130 5131 this.hoveredItem = item || null; 5132 } 5133 }; 5134 5135 /** 5136 * Leave the frame 5137 * @param {event} event 5138 */ 5139 links.TreeGrid.Frame.prototype.onMouseLeave = function (event) { 5140 if (this.hoveredItem) { 5141 this.trigger('leave', {item: this.hoveredItem.data}); 5142 } 5143 this.hoveredItem = null; 5144 }; 5145 5146 /** 5147 * Set given node selected 5148 * @param {links.TreeGrid.Node} node 5149 * @param {Boolean} keepSelection If true, the current node is added to the 5150 * selection. append is false by default 5151 * @param {Boolean} selectRange If true, a range of nodes is selected from 5152 * the last selected node to this node 5153 */ 5154 links.TreeGrid.Frame.prototype.select = function(node, keepSelection, selectRange) { 5155 var triggerEvent = false; 5156 5157 if (selectRange) { 5158 var startNode = this.selection.shift(); 5159 var endNode = node; 5160 5161 // ensure having nodes in the same grid 5162 if (startNode && startNode.parent != endNode.parent) { 5163 startNode.unselect(); 5164 startNode.repaint(); 5165 startNode = undefined; 5166 } 5167 if (!startNode) { 5168 startNode = endNode; 5169 } 5170 5171 // remove selection 5172 while (this.selection.length) { 5173 var selectedNode = this.selection.pop(); 5174 selectedNode.unselect(); 5175 selectedNode.repaint(); 5176 } 5177 5178 var parent = startNode.parent; 5179 var startIndex = parent.items.indexOf(startNode); 5180 var endIndex = (startNode == endNode) ? startIndex : parent.items.indexOf(endNode); 5181 if (endIndex >= startIndex) { 5182 var index = startIndex; 5183 while (index <= endIndex) { 5184 var node = parent.items[index]; 5185 node.select(); 5186 node.repaint(); 5187 this.selection.push(node); 5188 index++; 5189 } 5190 } 5191 else { 5192 var index = startIndex; 5193 while (index >= endIndex) { 5194 var node = parent.items[index]; 5195 node.select(); 5196 node.repaint(); 5197 5198 // important to add to the end of the array, we want to keep 5199 // our 'start' node at the start of the selection array, needed when 5200 // we adjust this range. 5201 this.selection.push(node); 5202 index--; 5203 } 5204 } 5205 } 5206 else if (keepSelection) { 5207 // append this node to the selection 5208 var index = this.selection.indexOf(node); 5209 if (index == -1) { 5210 node.select(); 5211 node.repaint(); 5212 this.selection.push(node); 5213 } 5214 else { 5215 node.unselect(); 5216 node.repaint(); 5217 this.selection.splice(index, 1); 5218 } 5219 } 5220 else { 5221 if (!node.selected) { 5222 // remove selection 5223 while (this.selection.length) { 5224 var selectedNode = this.selection.pop(); 5225 selectedNode.unselect(); 5226 selectedNode.repaint(); 5227 } 5228 5229 // append this node to the selection 5230 node.select(); 5231 node.repaint(); 5232 this.selection.push(node); 5233 } 5234 } 5235 5236 // trigger selection event 5237 this.trigger('select', { 5238 //'index': node.index, // TODO: cleanup 5239 'items': this.getSelection() 5240 }); 5241 }; 5242 5243 5244 /** 5245 * Unselect all selected nodes 5246 * @param {Boolean} triggerEvent Optional. True by default 5247 */ 5248 links.TreeGrid.Frame.prototype.unselect = function(triggerEvent) { 5249 var selection = this.selection; 5250 for (var i = 0, iMax = selection.length; i < iMax; i++) { 5251 selection[i].unselect(); 5252 selection[i].repaint(); 5253 } 5254 this.selection = []; 5255 5256 if (triggerEvent == undefined) { 5257 triggerEvent = true; 5258 } 5259 5260 // trigger selection event 5261 if (triggerEvent) { 5262 this.trigger('select', { 5263 'items': [] 5264 }); 5265 } 5266 }; 5267 5268 /** 5269 * Get the selected items 5270 * @return {Array[]} selected items 5271 */ 5272 links.TreeGrid.Frame.prototype.getSelection = function() { 5273 // create an array with the data of the selected items (instead of the items 5274 // themselves) 5275 var selection = this.selection; 5276 var selectedData = []; 5277 for (var i = 0, iMax = selection.length; i < iMax; i++) { 5278 selectedData.push(selection[i].data); 5279 } 5280 5281 return selectedData; 5282 }; 5283 5284 /** 5285 * Set the selected items 5286 * @param {Array[] | Object} items a single item or array with items 5287 */ 5288 links.TreeGrid.Frame.prototype.setSelection = function(items) { 5289 this.unselect(); 5290 5291 if (!items) { 5292 items = []; 5293 } 5294 else if (!links.TreeGrid.isArray(items)) { 5295 items = [items]; 5296 } 5297 5298 var keepSelection = true; 5299 for (var i = 0; i < items.length; i++) { 5300 var itemData = items[i]; 5301 var item = this.findItem(itemData); 5302 item && this.select(item, keepSelection); 5303 } 5304 }; 5305 5306 /** 5307 * Event handler for touchstart event on mobile devices 5308 */ 5309 links.TreeGrid.Frame.prototype.onTouchStart = function(event) { 5310 var params = this.eventParams, 5311 me = this; 5312 5313 if (params.touchDown) { 5314 // if already moving, return 5315 return; 5316 } 5317 5318 params.startClientY = event.targetTouches[0].clientY; 5319 params.currentClientY = params.startClientY; 5320 params.previousClientY = params.startClientY; 5321 params.startScrollValue = this.verticalScroll.get(); 5322 params.touchDown = true; 5323 5324 if (!params.onTouchMove) { 5325 params.onTouchMove = function (event) {me.onTouchMove(event);}; 5326 links.TreeGrid.addEventListener(document, "touchmove", params.onTouchMove); 5327 } 5328 if (!params.onTouchEnd) { 5329 params.onTouchEnd = function (event) {me.onTouchEnd(event);}; 5330 links.TreeGrid.addEventListener(document, "touchend", params.onTouchEnd); 5331 } 5332 5333 // don't do preventDefault here, it will block onclick events... 5334 //links.TreeGrid.preventDefault(event); 5335 }; 5336 5337 /** 5338 * Event handler for touchmove event on mobile devices 5339 */ 5340 links.TreeGrid.Frame.prototype.onTouchMove = function(event) { 5341 var params = this.eventParams; 5342 5343 var clientY = event.targetTouches[0].clientY; 5344 var diff = (clientY - params.startClientY); 5345 this.verticalScroll.set(params.startScrollValue - diff); 5346 5347 this.onResize(); 5348 5349 params.previousClientY = params.currentClientY; 5350 params.currentClientY = clientY; 5351 5352 this.trigger('rangechange', undefined); 5353 5354 links.TreeGrid.preventDefault(event); 5355 }; 5356 5357 5358 /** 5359 * Event handler for touchend event on mobile devices 5360 */ 5361 links.TreeGrid.Frame.prototype.onTouchEnd = function(event) { 5362 var params = this.eventParams, 5363 me = this; 5364 params.touchDown = false; 5365 5366 var diff = (params.currentClientY - params.startClientY); 5367 var speed = (params.currentClientY - params.previousClientY); 5368 5369 var decellerate = function () { 5370 if (!params.touchDown) { 5371 me.verticalScroll.set(params.startScrollValue - diff); 5372 5373 me.onRangeChange(); 5374 5375 diff += speed; 5376 speed *= 0.8; 5377 5378 if (Math.abs(speed) > 1) { 5379 setTimeout(decellerate, 50); 5380 } 5381 } 5382 }; 5383 decellerate(); 5384 5385 this.trigger("rangechanged", undefined); 5386 5387 if (params.onTouchMove) { 5388 links.TreeGrid.removeEventListener(document, "touchmove", params.onTouchMove); 5389 delete params.onTouchMove; 5390 5391 } 5392 if (params.onTouchEnd) { 5393 links.TreeGrid.removeEventListener(document, "touchend", params.onTouchEnd); 5394 delete params.onTouchEnd; 5395 } 5396 5397 links.TreeGrid.preventDefault(event); 5398 }; 5399 5400 /** 5401 * Event handler for mouse wheel event, 5402 * Code from http://adomas.org/javascript-mouse-wheel/ 5403 * @param {event} event 5404 */ 5405 links.TreeGrid.Frame.prototype.onMouseWheel = function(event) { 5406 if (!event) { /* For IE. */ 5407 event = window.event; 5408 } 5409 5410 // retrieve delta 5411 var delta = 0; 5412 if (event.wheelDelta) { /* IE/Opera. */ 5413 delta = event.wheelDelta/120; 5414 } else if (event.detail) { /* Mozilla case. */ 5415 // In Mozilla, sign of delta is different than in IE. 5416 // Also, delta is multiple of 3. 5417 delta = -event.detail/3; 5418 } 5419 5420 // If delta is nonzero, handle it. 5421 // Basically, delta is now positive if wheel was scrolled up, 5422 // and negative, if wheel was scrolled down. 5423 if (delta) { 5424 // TODO: on FireFox, the window is not redrawn within repeated scroll-events 5425 // -> use a delayed redraw? Make a zoom queue? 5426 5427 this.verticalScroll.increase(-delta * 50); 5428 5429 this.onRangeChange(); 5430 5431 // fire a rangechanged event 5432 this.trigger('rangechanged', undefined); 5433 } 5434 5435 // Prevent default actions caused by mouse wheel. 5436 // That might be ugly, but we handle scrolls somehow 5437 // anyway, so don't bother here... 5438 links.TreeGrid.preventDefault(event); 5439 }; 5440 5441 5442 /** 5443 * fire an event 5444 * @param {String} event The name of an event, for example 'rangechange' or 'edit' 5445 * @param {Object} params Optional object with parameters 5446 */ 5447 links.TreeGrid.prototype.trigger = function (event, params) { 5448 // trigger the links event bus 5449 links.events.trigger(this, event, params); 5450 5451 // trigger the google event bus 5452 if (google && google.visualization && google.visualization.events) { 5453 google.visualization.events.trigger(this, event, params); 5454 } 5455 }; 5456 5457 5458 5459 /** ------------------------------------------------------------------------ **/ 5460 5461 5462 5463 /** 5464 * Drag and Drop library 5465 * 5466 * This module allows to create draggable and droppable elements in a webpage, 5467 * and easy transfer of data of any type between drag and drop areas. 5468 * 5469 * The interface of the library is equal to the 'real' drag and drop API. 5470 * However, this library works on all browsers without issues (in contrast to 5471 * the official drag and drop API). 5472 * https://developer.mozilla.org/En/DragDrop/Drag_and_Drop 5473 * 5474 * The library is tested on: Chrome, Firefox, Opera, Safari, 5475 * Internet Explorer 5.5+ 5476 * 5477 * DOCUMENTATION 5478 * 5479 * To create a draggable area, use the method makeDraggable: 5480 * dnd.makeDraggable(element, options); 5481 * 5482 * with parameters: 5483 * {HTMLElement} element The element to become draggable. 5484 * {Object} options An object with options. 5485 * 5486 * available options: 5487 * {String} effectAllowed The allowed drag effect. Available values: 5488 * 'copy', 'move', 'link', 'copyLink', 5489 * 'copyMove', 'linkMove', 'all', 'none'. 5490 * Default value is 'all'. 5491 * {String or HTMLElement} dragImage 5492 * Image to be used as drag image. If no 5493 * drag image is provided, an opague clone 5494 * of the drag area is used. 5495 * {Number} dragImageOffsetX 5496 * Horizontal offset for the drag image 5497 * {Number} dragImageOffsetY 5498 * Vertical offset for the drag image 5499 * {function} dragStart Method called once on start of a drag. 5500 * The method is called with an event object 5501 * as parameter. The event object contains a 5502 * parameter 'data' to pass data to a drop 5503 * event. This data can be any type. 5504 * {function} drag Method called repeatedly while dragging. 5505 * The method is called with an event object 5506 * as parameter. 5507 * {function} dragEnd Method called after the drag event is 5508 * finished. The method is called with an 5509 * event object as parameter. This event 5510 * object contains a parameter 'dropEffect' 5511 * with the applied drop effect, which is 5512 * undefined when no drop occurred. 5513 * 5514 * To make a droppable area, use the method makeDroppable: 5515 * dnd.makeDroppable(element, options); 5516 * 5517 * with parameters: 5518 * {HTMLElement} element The element to become droppable. 5519 * {Object} options An object with options. 5520 * 5521 * available options: 5522 * {String} dropEffect The drop effect. Available 5523 * values: 'copy', 'move', 'link', 'none'. 5524 * Default value is 'link' 5525 * {function} dragEnter Method called once when the dragged image 5526 * enters the drop area. Can be used to 5527 * apply visual effects to the drop area. 5528 * {function} dragLeave Method called once when the dragged image 5529 * leaves the drop area. Can be used to 5530 * remove visual effects from the drop area. 5531 * {function} dragOver Method called repeatedly when moving 5532 * over the drop area. 5533 * {function} drop Method called when the drag image is 5534 * dropped on this drop area. 5535 * The method is called with an event object 5536 * as parameter. The event object contains a 5537 * parameter 'data' which can contain data 5538 * provided by the drag area. 5539 * 5540 * Created draggable or doppable areas are registed in the drag and 5541 * drop module. To remove a draggable or droppable area, the 5542 * following methods can be used respectively: 5543 * dnd.removeDraggable(element); 5544 * dnd.removeDroppable(element); 5545 * 5546 * which removes all drag and drop functionality from the concerning 5547 * element. 5548 * 5549 * 5550 * EXAMPLE 5551 * 5552 * var drag = document.getElementById('drag'); 5553 * dnd.makeDraggable(drag, { 5554 * 'dragStart': function (event) { 5555 * event.data = 'Hello World!'; // data can be any type 5556 * } 5557 * }); 5558 * 5559 * var drop = document.getElementById('drop'); 5560 * dnd.makeDroppable(drop, { 5561 * 'drop': function (event) { 5562 * alert(event.data); // will alert 'Hello World!' 5563 * }, 5564 * 'dragEnter': function (event) { 5565 * drop.style.backgroundColor = 'yellow'; // set visual effect 5566 * }, 5567 * 'dragLeave': function (event) { 5568 * drop.style.backgroundColor = ''; // remove visual effect 5569 * } 5570 * }); 5571 * 5572 */ 5573 links.dnd = function () { 5574 var dragAreas = []; // all registered drag areas 5575 var dropAreas = []; // all registered drop areas 5576 var _currentDropArea = null; // holds the currently hovered dropArea 5577 5578 var dragArea = undefined; // currently dragged area 5579 var dragImage = undefined; 5580 var dragImageOffsetX = 0; 5581 var dragImageOffsetY = 0; 5582 var dragEvent = {}; // object with event properties, passed to each event 5583 var mouseMove = undefined; 5584 var mouseUp = undefined; 5585 var originalCursor = undefined; 5586 5587 function isDragging() { 5588 return (dragArea != undefined); 5589 } 5590 5591 /** 5592 * Make an HTML element draggable 5593 * @param {Element} element 5594 * @param {Object} options. available parameters: 5595 * {String} effectAllowed 5596 * {String or HTML DOM} dragImage 5597 * {function} dragStart 5598 * {function} dragEnd 5599 */ 5600 function makeDraggable (element, options) { 5601 // create an object holding the dragarea and options 5602 var newDragArea = { 5603 'element': element 5604 }; 5605 if (options) { 5606 links.TreeGrid.extend(newDragArea, options); 5607 } 5608 dragAreas.push(newDragArea); 5609 5610 var mouseDown = function (event) { 5611 event = event || window.event; 5612 var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); 5613 if (!leftButtonDown) { 5614 return; 5615 } 5616 5617 // create mousemove listener 5618 mouseMove = function (event) { 5619 if (!isDragging()) { 5620 dragStart(event, newDragArea); 5621 } 5622 dragOver(event); 5623 5624 preventDefault(event); 5625 }; 5626 addEventListener(document, 'mousemove', mouseMove); 5627 5628 // create mouseup listener 5629 mouseUp = function (event) { 5630 if (isDragging()) { 5631 dragEnd(event); 5632 } 5633 5634 // remove event listeners 5635 if (mouseMove) { 5636 removeEventListener(document, 'mousemove', mouseMove); 5637 mouseMove = undefined; 5638 } 5639 if (mouseUp) { 5640 removeEventListener(document, 'mouseup', mouseUp); 5641 mouseUp = undefined; 5642 } 5643 5644 preventDefault(event); 5645 }; 5646 addEventListener(document, 'mouseup', mouseUp); 5647 5648 preventDefault(event); 5649 }; 5650 addEventListener(element, 'mousedown', mouseDown); 5651 5652 newDragArea.mouseDown = mouseDown; 5653 } 5654 5655 5656 /** 5657 * Make an HTML element droppable 5658 * @param {Element} element 5659 * @param {Object} options. available parameters: 5660 * {String} dropEffect 5661 * {function} dragEnter 5662 * {function} dragLeave 5663 * {function} drop 5664 */ 5665 function makeDroppable (element, options) { 5666 var newDropArea = { 5667 'element': element, 5668 'mouseOver': false 5669 }; 5670 if (options) { 5671 links.TreeGrid.extend(newDropArea, options); 5672 } 5673 if (!newDropArea.dropEffect) { 5674 newDropArea.dropEffect = 'link'; 5675 } 5676 5677 dropAreas.push(newDropArea); 5678 } 5679 5680 /** 5681 * Remove draggable functionality from element 5682 * @param {Element} element 5683 */ 5684 function removeDraggable (element) { 5685 var i = 0; 5686 while (i < dragAreas.length) { 5687 var d = dragAreas[i]; 5688 if (d.element == element) { 5689 removeEventListener(d.element, 'mousedown', d.mouseDown); 5690 dragAreas.splice(i, 1); 5691 } 5692 else { 5693 i++; 5694 } 5695 } 5696 } 5697 5698 /** 5699 * Remove droppabe functionality from element 5700 * @param {Element} element 5701 */ 5702 function removeDroppable (element) { 5703 var i = 0; 5704 while (i < dropAreas.length) { 5705 if (dropAreas[i].element == element) { 5706 dropAreas.splice(i, 1); 5707 } 5708 else { 5709 i++; 5710 } 5711 } 5712 } 5713 5714 function dragStart(event, newDragArea) { 5715 // register the dragarea 5716 if (dragArea) { 5717 return; 5718 } 5719 dragArea = newDragArea; 5720 5721 // trigger event 5722 var proceed = true; 5723 if (dragArea.dragStart) { 5724 var data = {}; 5725 dragEvent = { 5726 'dataTransfer' : { 5727 'dragArea': dragArea.element, 5728 'dropArea': undefined, 5729 'data': data, 5730 'getData': function (key) { 5731 return data[key]; 5732 }, 5733 'setData': function (key, value) { 5734 data[key] = value; 5735 }, 5736 'clearData': function (key) { 5737 delete data[key]; 5738 } 5739 }, 5740 'clientX': event.clientX, 5741 'clientY': event.clientY 5742 }; 5743 5744 var ret = dragArea.dragStart(dragEvent); 5745 proceed = (ret !== false); 5746 } 5747 5748 if (!proceed) { 5749 // cancel dragevent 5750 dragArea = undefined; 5751 return; 5752 } 5753 5754 // create dragImage 5755 var clone = undefined; 5756 dragImage = document.createElement('div'); 5757 dragImage.style.position = 'absolute'; 5758 if (typeof(dragArea.dragImage) == 'string') { 5759 // create dragImage from HTML string 5760 dragImage.innerHTML = dragArea.dragImage; 5761 dragImageOffsetX = -dragArea.dragImageOffsetX || 0; 5762 dragImageOffsetY = -dragArea.dragImageOffsetY || 0; 5763 } 5764 else if (dragArea.dragImage) { 5765 // create dragImage from HTML DOM element 5766 dragImage.appendChild(dragArea.dragImage); 5767 dragImageOffsetX = -dragArea.dragImageOffsetX || 0; 5768 dragImageOffsetY = -dragArea.dragImageOffsetY || 0; 5769 } 5770 else { 5771 // clone the drag area 5772 clone = dragArea.element.cloneNode(true); 5773 dragImageOffsetX = (event.clientX || 0) - getAbsoluteLeft(dragArea.element); 5774 dragImageOffsetY = (event.clientY || 0) - getAbsoluteTop(dragArea.element); 5775 clone.style.left = '0px'; 5776 clone.style.top = '0px'; 5777 clone.style.opacity = '0.7'; 5778 clone.style.filter = 'alpha(opacity=70)'; 5779 5780 dragImage.appendChild(clone); 5781 } 5782 document.body.appendChild(dragImage); 5783 5784 // adjust the cursor 5785 if (originalCursor == undefined) { 5786 originalCursor = document.body.style.cursor; 5787 } 5788 document.body.style.cursor = 'move'; 5789 } 5790 5791 function dragOver (event) { 5792 if (!dragImage) { 5793 return; 5794 } 5795 5796 // adjust position of the dragImage 5797 if (dragImage) { 5798 dragImage.style.left = (event.clientX - dragImageOffsetX) + 'px'; 5799 dragImage.style.top = (event.clientY - dragImageOffsetY) + 'px'; 5800 } 5801 5802 // adjust event properties 5803 dragEvent.clientX = event.clientX; 5804 dragEvent.clientY = event.clientY; 5805 5806 // find the current dropArea 5807 var currentDropArea = findDropArea(event); 5808 if (currentDropArea) { 5809 // adjust event properties 5810 dragEvent.dataTransfer.dropArea = currentDropArea.element; 5811 dragEvent.dataTransfer.dropEffect = getAllowedDropEffect(dragArea.effectAllowed, currentDropArea.dropEffect); 5812 5813 if (currentDropArea.dragOver) { 5814 currentDropArea.dragOver(dragEvent); 5815 5816 // TODO 5817 // // dropEffect may be changed during dragOver 5818 //currentDropArea.dropEffect = dragEvent.dataTransfer.dropEffect; 5819 } 5820 } 5821 else { 5822 dragEvent.dataTransfer.dropArea = undefined; 5823 dragEvent.dataTransfer.dropEffect = undefined; 5824 } 5825 5826 if (dragArea.drag) { 5827 // dragEvent.dataTransfer.effectAllowed = dragArea.effectAllowed; 5828 5829 dragArea.drag(dragEvent); 5830 5831 // TODO 5832 // // effectAllowed may be changed during drag 5833 // dragArea.effectAllowed = dragEvent.dataTransfer.effectAllowed; 5834 } 5835 } 5836 5837 function dragEnd (event) { 5838 // remove the dragImage 5839 if (dragImage && dragImage.parentNode) { 5840 dragImage.parentNode.removeChild(dragImage); 5841 } 5842 dragImage = undefined; 5843 5844 // restore cursor 5845 document.body.style.cursor = originalCursor || ''; 5846 originalCursor = undefined; 5847 5848 // find the current dropArea 5849 var currentDropArea = findDropArea(event); 5850 if (currentDropArea) { 5851 // adjust event properties 5852 dragEvent.dataTransfer.dropArea = currentDropArea.element; 5853 dragEvent.dataTransfer.dropEffect = getAllowedDropEffect(dragArea.effectAllowed, currentDropArea.dropEffect); 5854 5855 // trigger drop event 5856 if (dragEvent.dataTransfer.dropEffect) { 5857 if (currentDropArea.drop) { 5858 currentDropArea.drop(dragEvent); 5859 } 5860 } 5861 } 5862 else { 5863 dragEvent.dataTransfer.dropArea = undefined; 5864 dragEvent.dataTransfer.dropEffect = undefined; 5865 } 5866 5867 // trigger dragEnd event 5868 if (dragArea.dragEnd) { 5869 dragArea.dragEnd(dragEvent); 5870 } 5871 5872 // remove the dragArea 5873 dragArea = undefined; 5874 5875 // clear event data 5876 dragEvent = {}; 5877 } 5878 5879 /** 5880 * Return the current dropEffect, taking into account the allowed drop effects 5881 * @param {String} effectAllowed 5882 * @param {String} dropEffect 5883 * @return allowedDropEffect the allowed dropEffect, or undefined when 5884 * not allowed 5885 */ 5886 function getAllowedDropEffect (effectAllowed, dropEffect) { 5887 if (!dropEffect || dropEffect == 'none') { 5888 // none 5889 return undefined; 5890 } 5891 5892 if (!effectAllowed || effectAllowed.toLowerCase() == 'all') { 5893 // all 5894 return dropEffect; 5895 } 5896 5897 if (effectAllowed.toLowerCase().indexOf(dropEffect.toLowerCase()) != -1 ) { 5898 return dropEffect; 5899 } 5900 5901 return undefined; 5902 } 5903 5904 /** 5905 * Find the current droparea, and call dragEnter() and dragLeave() on change of droparea. 5906 * The found droparea is returned and also stored in the variable _currentDropArea 5907 * @param {Event} event 5908 * @return {Object| null} Returns the dropArea if found, or else null 5909 */ 5910 function findDropArea (event) { 5911 // TODO: dnd prototype should not have knowledge about TreeGrid.Item and dataConnectors, this is a hack 5912 var newDropArea = null; 5913 5914 // get the hovered Item (if any) 5915 var item = null; 5916 var elem = event.target || event.srcElement; 5917 while (elem) { 5918 if (elem.item instanceof links.TreeGrid.Item) { 5919 item = elem.item; 5920 break; 5921 } 5922 elem = elem.parentNode; 5923 } 5924 5925 // check if there is a droparea overlapping with current dragarea 5926 if (item) { 5927 for (var i = 0; i < dropAreas.length; i++) { 5928 var dropArea = dropAreas[i]; 5929 5930 if ((item.dom.frame == dropArea.element) && !newDropArea && 5931 getAllowedDropEffect(dragArea.effectAllowed, dropArea.dropEffect)) { 5932 // on droparea 5933 newDropArea = dropArea; 5934 } 5935 } 5936 } 5937 5938 // see if there is a parent with droparea 5939 if (!newDropArea) { 5940 var parent = item && item.parent; 5941 if (!parent) { 5942 // header 5943 var header = findAttribute(event.target || event.srcElement, 'header'); 5944 parent = header && header.parent; 5945 } 5946 if (!parent) { 5947 // root 5948 var frame = findAttribute(event.target || event.srcElement, 'frame'); 5949 parent = frame && frame.grid; 5950 } 5951 5952 if (parent && parent.dataConnector && 5953 !newDropArea && 5954 getAllowedDropEffect(parent.dataConnector.options.dataTransfer.effectAllowed, 5955 parent.dataConnector.options.dataTransfer.dropEffect)) { 5956 5957 // Fake a dropArea (yes, this is a terrible hack) 5958 var element = event.target || event.srcElement; 5959 if (_currentDropArea && _currentDropArea.element === element) { 5960 // fake dropArea already exists 5961 newDropArea = _currentDropArea; 5962 } 5963 else { 5964 // create a new fake dropArea 5965 var dashedLine = document.createElement('div'); 5966 dashedLine.className = 'treegrid-droparea after'; 5967 5968 newDropArea = { 5969 element: element, 5970 dropEffect: parent.dataConnector.options.dataTransfer.dropEffect, 5971 dragEnter: function (event) { 5972 if (item) { 5973 item.dom.frame.appendChild(dashedLine); 5974 } 5975 else if (header) { 5976 dashedLine.style.bottom = '-5px'; 5977 header.dom.header.appendChild(dashedLine); 5978 } 5979 else if (frame) { 5980 dashedLine.style.top = frame.gridHeight + 'px'; 5981 dashedLine.style.bottom = ''; 5982 frame.dom.mainFrame.appendChild(dashedLine); 5983 } 5984 }, 5985 dragLeave: function (event) { 5986 dashedLine.parentNode && dashedLine.parentNode.removeChild(dashedLine); 5987 }, 5988 dragOver: function (event) { 5989 // nothing to do 5990 }, 5991 drop: function (event) { 5992 newDropArea.dragLeave(event); 5993 event.dropTarget = item || header || frame; 5994 parent.onDrop(event); 5995 } 5996 }; 5997 } 5998 } 5999 } 6000 6001 // leave current dropArea 6002 if (_currentDropArea !== newDropArea) { 6003 if (_currentDropArea) { 6004 if (_currentDropArea.dragLeave) { 6005 dragEvent.dataTransfer.dropArea = _currentDropArea; 6006 dragEvent.dataTransfer.dropEffect = undefined; 6007 _currentDropArea.dragLeave(dragEvent); 6008 } 6009 _currentDropArea.mouseOver = false; 6010 } 6011 6012 if (newDropArea) { 6013 if (newDropArea.dragEnter) { 6014 dragEvent.dataTransfer.dropArea = newDropArea; 6015 dragEvent.dataTransfer.dropEffect = undefined; 6016 newDropArea.dragEnter(dragEvent); 6017 } 6018 newDropArea.mouseOver = true; 6019 } 6020 6021 _currentDropArea = newDropArea; 6022 } 6023 6024 return _currentDropArea; 6025 } 6026 6027 /** 6028 * Find an attribute in the parent tree of given element 6029 * @param {EventTarget} elem 6030 * @param {string} attribute 6031 * @returns {* | null} 6032 */ 6033 function findAttribute(elem, attribute) { 6034 while (elem) { 6035 if (elem[attribute]) { 6036 return elem[attribute]; 6037 } 6038 6039 elem = elem.parentNode; 6040 } 6041 return null; 6042 } 6043 6044 /** 6045 * Add and event listener. Works for all browsers 6046 * @param {Element} element An html element 6047 * @param {string} action The action, for example 'click', 6048 * without the prefix 'on' 6049 * @param {function} listener The callback function to be executed 6050 * @param {boolean} useCapture 6051 */ 6052 function addEventListener (element, action, listener, useCapture) { 6053 if (element.addEventListener) { 6054 if (useCapture === undefined) { 6055 useCapture = false; 6056 } 6057 6058 if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { 6059 action = 'DOMMouseScroll'; // For Firefox 6060 } 6061 6062 element.addEventListener(action, listener, useCapture); 6063 } else { 6064 element.attachEvent('on' + action, listener); // IE browsers 6065 } 6066 } 6067 6068 /** 6069 * Remove an event listener from an element 6070 * @param {Element} element An html dom element 6071 * @param {string} action The name of the event, for example 'mousedown' 6072 * @param {function} listener The listener function 6073 * @param {boolean} useCapture 6074 */ 6075 function removeEventListener (element, action, listener, useCapture) { 6076 if (element.removeEventListener) { 6077 // non-IE browsers 6078 if (useCapture === undefined) { 6079 useCapture = false; 6080 } 6081 6082 if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { 6083 action = 'DOMMouseScroll'; // For Firefox 6084 } 6085 6086 element.removeEventListener(action, listener, useCapture); 6087 } else { 6088 // IE browsers 6089 element.detachEvent('on' + action, listener); 6090 } 6091 } 6092 6093 /** 6094 * Stop event propagation 6095 */ 6096 function stopPropagation (event) { 6097 if (!event) 6098 event = window.event; 6099 6100 if (event.stopPropagation) { 6101 event.stopPropagation(); // non-IE browsers 6102 } 6103 else { 6104 event.cancelBubble = true; // IE browsers 6105 } 6106 } 6107 6108 /** 6109 * Cancels the event if it is cancelable, without stopping further propagation of the event. 6110 */ 6111 function preventDefault (event) { 6112 if (!event) 6113 event = window.event; 6114 6115 if (event.preventDefault) { 6116 event.preventDefault(); // non-IE browsers 6117 } 6118 else { 6119 event.returnValue = false; // IE browsers 6120 } 6121 } 6122 6123 6124 /** 6125 * Retrieve the absolute left value of a DOM element 6126 * @param {Element} elem A dom element, for example a div 6127 * @return {number} left The absolute left position of this element 6128 * in the browser page. 6129 */ 6130 function getAbsoluteLeft (elem) { 6131 var left = 0; 6132 while( elem != null ) { 6133 left += elem.offsetLeft; 6134 //left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too 6135 elem = elem.offsetParent; 6136 } 6137 return left; 6138 } 6139 6140 /** 6141 * Retrieve the absolute top value of a DOM element 6142 * @param {Element} elem A dom element, for example a div 6143 * @return {number} top The absolute top position of this element 6144 * in the browser page. 6145 */ 6146 function getAbsoluteTop (elem) { 6147 var top = 0; 6148 while( elem != null ) { 6149 top += elem.offsetTop; 6150 //left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too 6151 elem = elem.offsetParent; 6152 } 6153 return top; 6154 } 6155 6156 // return public methods 6157 return { 6158 'makeDraggable': makeDraggable, 6159 'makeDroppable': makeDroppable, 6160 'removeDraggable': removeDraggable, 6161 'removeDroppable': removeDroppable 6162 }; 6163 }(); 6164 6165 6166 6167 /** ------------------------------------------------------------------------ **/ 6168 6169 6170 /** 6171 * Event bus for adding and removing event listeners and for triggering events. 6172 * This is a singleton. 6173 */ 6174 links.events = links.events || { 6175 'listeners': [], 6176 6177 /** 6178 * Find a single listener by its object 6179 * @param {Object} object 6180 * @return {Number} index -1 when not found 6181 */ 6182 'indexOf': function (object) { 6183 var listeners = this.listeners; 6184 for (var i = 0, iMax = this.listeners.length; i < iMax; i++) { 6185 var listener = listeners[i]; 6186 if (listener && listener.object == object) { 6187 return i; 6188 } 6189 } 6190 return -1; 6191 }, 6192 6193 /** 6194 * Add an event listener 6195 * @param {Object} object 6196 * @param {String} event The name of an event, for example 'select' 6197 * @param {function} callback The callback method, called when the 6198 * event takes place 6199 */ 6200 'addListener': function (object, event, callback) { 6201 var index = this.indexOf(object); 6202 var listener = this.listeners[index]; 6203 if (!listener) { 6204 listener = { 6205 'object': object, 6206 'events': {} 6207 }; 6208 this.listeners.push(listener); 6209 } 6210 6211 var callbacks = listener.events[event]; 6212 if (!callbacks) { 6213 callbacks = []; 6214 listener.events[event] = callbacks; 6215 } 6216 6217 // add the callback if it does not yet exist 6218 if (callbacks.indexOf(callback) == -1) { 6219 callbacks.push(callback); 6220 } 6221 }, 6222 6223 /** 6224 * Remove an event listener 6225 * @param {Object} object 6226 * @param {String} event The name of an event, for example 'select' 6227 * @param {function} callback The registered callback method 6228 */ 6229 'removeListener': function (object, event, callback) { 6230 var index = this.indexOf(object); 6231 var listener = this.listeners[index]; 6232 if (listener) { 6233 var callbacks = listener.events[event]; 6234 if (callbacks) { 6235 var callbackIndex = callbacks.indexOf(callback); 6236 if (callbackIndex != -1) { 6237 callbacks.splice(callbackIndex, 1); 6238 } 6239 6240 // remove the array when empty 6241 if (callbacks.length == 0) { 6242 delete listener.events[event]; 6243 } 6244 } 6245 6246 // count the number of registered events. remove listener when empty 6247 var count = 0; 6248 var events = listener.events; 6249 for (var e in events) { 6250 if (events.hasOwnProperty(e)) { 6251 count++; 6252 } 6253 } 6254 if (count == 0) { 6255 delete this.listeners[index]; 6256 } 6257 } 6258 }, 6259 6260 /** 6261 * Remove all registered event listeners 6262 */ 6263 'removeAllListeners': function () { 6264 this.listeners = []; 6265 }, 6266 6267 /** 6268 * Trigger an event. All registered event handlers will be called 6269 * @param {Object} object 6270 * @param {String} event 6271 * @param {Object} params (optional) 6272 */ 6273 'trigger': function (object, event, params) { 6274 var index = this.indexOf(object); 6275 var listener = this.listeners[index]; 6276 if (listener) { 6277 var callbacks = listener.events[event]; 6278 if (callbacks) { 6279 for (var i = 0, iMax = callbacks.length; i < iMax; i++) { 6280 callbacks[i](params); 6281 } 6282 } 6283 } 6284 } 6285 }; 6286 6287 6288 6289 6290 /** ------------------------------------------------------------------------ **/ 6291 6292 6293 /** 6294 * Add and event listener. Works for all browsers 6295 * @param {Element} element An html element 6296 * @param {string} action The action, for example 'click', 6297 * without the prefix 'on' 6298 * @param {function} listener The callback function to be executed 6299 * @param {boolean} useCapture 6300 */ 6301 links.TreeGrid.addEventListener = function (element, action, listener, useCapture) { 6302 if (element.addEventListener) { 6303 if (useCapture === undefined) { 6304 useCapture = false; 6305 } 6306 6307 if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { 6308 action = 'DOMMouseScroll'; // For Firefox 6309 } 6310 6311 element.addEventListener(action, listener, useCapture); 6312 } else { 6313 element.attachEvent('on' + action, listener); // IE browsers 6314 } 6315 }; 6316 6317 /** 6318 * Remove an event listener from an element 6319 * @param {Element} element An html dom element 6320 * @param {string} action The name of the event, for example 'mousedown' 6321 * @param {function} listener The listener function 6322 * @param {boolean} useCapture 6323 */ 6324 links.TreeGrid.removeEventListener = function(element, action, listener, useCapture) { 6325 if (element.removeEventListener) { 6326 // non-IE browsers 6327 if (useCapture === undefined) { 6328 useCapture = false; 6329 } 6330 6331 if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { 6332 action = 'DOMMouseScroll'; // For Firefox 6333 } 6334 6335 element.removeEventListener(action, listener, useCapture); 6336 } else { 6337 // IE browsers 6338 element.detachEvent('on' + action, listener); 6339 } 6340 }; 6341 6342 6343 /** 6344 * Get HTML element which is the target of the event 6345 * @param {MouseEvent} event 6346 * @return {Element} target element 6347 */ 6348 links.TreeGrid.getTarget = function (event) { 6349 // code from http://www.quirksmode.org/js/events_properties.html 6350 if (!event) { 6351 event = window.event; 6352 } 6353 6354 var target; 6355 6356 if (event.target) { 6357 target = event.target; 6358 } 6359 else if (event.srcElement) { 6360 target = event.srcElement; 6361 } 6362 6363 if (target.nodeType !== undefined && target.nodeType == 3) { 6364 // defeat Safari bug 6365 target = target.parentNode; 6366 } 6367 6368 return target; 6369 }; 6370 6371 /** 6372 * Recursively find the treegrid item of which this target element is a part of. 6373 * @param {Element} target 6374 * @return {links.TreeGrid.Item} item Item or undefined when not found 6375 */ 6376 links.TreeGrid.getItemFromTarget = function (target) { 6377 var elem = target; 6378 while (elem) { 6379 if (elem.treeGridType == 'item' && elem.item) { 6380 return elem.item; 6381 } 6382 elem = elem.parentElement; 6383 } 6384 6385 return undefined; 6386 }; 6387 6388 /** 6389 * Stop event propagation 6390 */ 6391 links.TreeGrid.stopPropagation = function (event) { 6392 if (!event) { 6393 event = window.event; 6394 } 6395 6396 if (event.stopPropagation) { 6397 event.stopPropagation(); // non-IE browsers 6398 } 6399 else { 6400 event.cancelBubble = true; // IE browsers 6401 } 6402 }; 6403 6404 6405 /** 6406 * Cancels the event if it is cancelable, without stopping further propagation of the event. 6407 */ 6408 links.TreeGrid.preventDefault = function (event) { 6409 if (!event) { 6410 event = window.event; 6411 } 6412 6413 if (event.preventDefault) { 6414 event.preventDefault(); // non-IE browsers 6415 } 6416 else { 6417 event.returnValue = false; // IE browsers 6418 } 6419 }; 6420 6421