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