| Current File : /home/jvzmxxx/wiki1/extensions/InteractiveTimeline/chap-links-library/js/src/treegrid/treegrid.js |
/**
* @file treegrid.js
*
* @brief
* TreeGrid is a visualization which represents data in a hierarchical grid
* view. It is designed to handle large amouts of data, and has options for lazy
* loading. Items in the TreeGrid can contain custom HTML code. Information in
* one item can be spread over multiple columns, and can have action buttons on
* the right.
* TreeGrid offers built in functionality to sort, arrange, and filter items.
*
* TreeGrid is part of the CHAP Links library.
*
* TreeGrid is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
* Internet Explorer 9+.
*
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Copyright (c) 2011-2012 Almende B.V.
*
* @author Jos de Jong, <jos@almende.org>
* @date 2012-07-03
* @version 1.2.0
*/
/*
* TODO
* - send the changed items along with the change event?
* - the columns of an item should be reset after a drop
* - when the columns change (by reading a new item), the already drawn items and header are not updated
* - with multiple subgrids in one item, the header does not overlap correctly when scrolling down
* - get the TreeGrid working on IE8 again
* - dataconnector: be able to define how to generate childs based on the
* definition of a field name and dataconnector type
*
* - couple events from dataconnectors to the TreeGrid?
* - drag and drop:
* - enable dropping in between items
* - multiselect:
* - make shift+click working over different levels of grids
* - remove the addEventListener and removeEventListener methods from dataconnector?
* -> use the eventbus instead?
* - test if there is indeed no memory leakage from created event listeners on
* the dataconnectors
* - implement horizontal scrollbar when data does not fit
*/
/**
* Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
* "links"
*/
if (typeof links === 'undefined') {
links = {};
// important: do not use var, as "var links = {};" will overwrite
// the existing links variable value with undefined in IE8, IE7.
}
/**
* Ensure the variable google exists
*/
if (typeof google === 'undefined') {
google = undefined;
// important: do not use var, as "var google = undefined;" will overwrite
// the existing google variable value with undefined in IE8, IE7.
}
/**
* @constructor links.TreeGrid
* The TreeGrid is a visualization to represent data in a hierarchical list view.
*
* TreeGrid is developed in javascript as a Google Visualization Chart.
*
* @param {Element} container The DOM element in which the TreeGrid will
* be created. Normally a div element.
* @param {Object} options
*/
links.TreeGrid = function(container, options) {
// Internet Explorer does not support Array.indexOf,
// so we define it here in that case
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj){
for(var i = 0; i < this.length; i++){
if(this[i] == obj){
return i;
}
}
return -1;
}
}
this.options = {
'width': '100%',
'height': '100%',
'padding': 4, // px // TODO: padding is not yet used
'indentationWidth': 20, // px
'items': {
'defaultHeight': 24, // px
'minHeight': 24 // px
},
//'dropAreaHeight': 10 // px // TODO: dropAreas
'dropAreaHeight': 0 // px
};
// temporary warning
try {
if (options.itemHeight != undefined) {
console.log('WARNING: property option.itemHeight is no longer supported. ' +
'It is changed to options.items.defaultHeight');
}
}
catch (e) {}
if (options) {
links.TreeGrid.extend(this.options, options);
}
// remove all elements from the container element.
while (container.hasChildNodes()) {
container.removeChild(container.firstChild);
}
this.frame = new links.TreeGrid.Frame(container, this.options);
this.frame.setTreeGrid(this);
this.frame.repaint();
this.frame.reflow();
// fire the ready event
this.trigger('ready');
};
/**
* Recursively extend obj with all properties in newProps.
* @param {Object} obj
* @param {Object} newProps
*/
links.TreeGrid.extend = function(obj, newProps) {
for (var i in newProps) {
if (newProps.hasOwnProperty(i)) {
if (obj[i] instanceof Object) {
if (newProps[i] instanceof Object) {
links.TreeGrid.extend(obj[i], newProps[i]);
}
else {
// value from newProps will be neglected
}
}
else {
obj[i] = newProps[i];
}
}
}
};
/**
* Main drawing logic. This is the function that needs to be called
* in the html page, to draw the TreeGrid.
*
* A data table with the events must be provided, and an options table.
*
* @param {links.DataConnector} data A DataConnector
* @param {Object} options A name/value map containing settings
* for the TreeGrid. Optional.
*/
links.TreeGrid.prototype.draw = function(data, options) {
// TODO: support multiple input types: DataConnector, Google DataTable, JSON table, ...
if (options) {
// merge options
links.TreeGrid.extend(this.options, options);
}
var grid = new links.TreeGrid.Grid(data, this.options);
this.frame.setGrid(grid);
};
/**
* Redraw the TreeGrid.
* This method can be used after the size of the TreeGrid changed, for example
* due to resizing of the page
*/
links.TreeGrid.prototype.redraw = function() {
if (this.frame) {
this.frame.onRangeChange();
}
};
/**
* Base prototype for Frame, Grid, and Item
*/
links.TreeGrid.Node = function () {
this.top = 0;
this.width = 0;
this.left = 0;
this.height = 0;
this.visible = true;
this.selected = false;
};
/**
* Set a parent node for this node
* @param {links.TreeGrid.Node} parent
*/
links.TreeGrid.Node.prototype.setParent = function(parent) {
this.parent = parent;
};
/**
* get the absolute top position in pixels
* @return {Number} absTop
*/
links.TreeGrid.Node.prototype.getAbsTop = function() {
return (this.parent ? this.parent.getAbsTop() : 0) + this.top;
};
/**
* get the absolute left position in pixels
* @return {Number} absLeft
*/
links.TreeGrid.Node.prototype.getAbsLeft = function() {
return (this.parent ? this.parent.getAbsLeft() : 0) + this.left;
};
/**
* get the height in pixels
* @return {Number} height
*/
links.TreeGrid.Node.prototype.getHeight = function() {
return this.height;
};
/**
* get the width in pixels
* @return {Number} width
*/
links.TreeGrid.Node.prototype.getWidth = function() {
return this.width;
};
/**
* get the relative left position in pixels, relative to its parent node
* @return {Number} left
*/
links.TreeGrid.Node.prototype.getLeft = function() {
return this.left;
};
/**
* get the relative top position in pixels, relative to its parent node
* @return {Number} top
*/
links.TreeGrid.Node.prototype.getTop = function() {
return this.top;
};
/**
* set the relative left position in pixels, relative to its parent node
* @param {Number} left
*/
links.TreeGrid.Node.prototype.setLeft = function(left) {
this.left = left || 0;
};
/**
* set the relative top position in pixels, relative to its parent node
* @param {Number} top
*/
links.TreeGrid.Node.prototype.setTop = function(top) {
this.top = top || 0;
};
/**
* Retrieve the main TreeGrid, the base object.
*/
links.TreeGrid.Node.prototype.getTreeGrid = function() {
return this.parent ? this.parent.getTreeGrid() : undefined;
};
/**
* Get all (globally) selected items
* @return {Object[]} selectedItems
*/
links.TreeGrid.Node.prototype.getSelection = function() {
return this.parent ? this.parent.getSelection() : [];
};
/**
* Get the HTML container where the HTML elements can be added
* @return {Element} container
*/
links.TreeGrid.Node.prototype.getContainer = function() {
return this.parent ? this.parent.getContainer() : undefined;
};
/**
* Retrieve the currently visible window of the main frame
* @return {Object} window Object containing parameters top, left, width, height
*/
links.TreeGrid.Node.prototype.getVisibleWindow = function() {
if (this.parent) {
return this.parent.getVisibleWindow();
}
return {
'top': 0,
'left': 0,
'width': 0,
'height': 0
};
};
/**
* Change the visibility of this node
* @param {Boolean} visible if true, node will be visible, else node will be
* hidden.
*/
links.TreeGrid.Node.prototype.setVisible = function(visible) {
this.visible = (visible == true);
};
/**
* Check if the node is currently visible
* @return {Boolean} visible
*/
links.TreeGrid.Node.prototype.isVisible = function() {
return this.visible && (this.parent ? this.parent.isVisible() : true);
};
/**
* Update the height of this node when one of its childs is resized.
* This will not cause a reflow or repaint, but just updates the height.
*
* @param {links.TreeGrid.Node} child Child node which has been resized
* @param {Number} diffHeight Change in height
*/
links.TreeGrid.Node.prototype.updateHeight = function(child, diffHeight) {
// method must be implemented by all inherited prototypes
};
/**
* Let the parent of this node know that the height of this node has changed
* This will not cause a repaint, but just updates the height of the parent
* accordingly.
* @param {Number} diffHeight difference in height
*/
links.TreeGrid.Node.prototype.onUpdateHeight = function (diffHeight) {
if (this.parent) {
this.parent.updateHeight(this, diffHeight);
}
};
/**
* Repaint. Will create/position/repaint all DOM elements of this node
* @return {Boolean} resized True if some elements are resized
* In that case, a redraw is required
*/
links.TreeGrid.Node.prototype.repaint = function() {
// method must be implemented by all inherited prototypes
return false;
};
/**
* Reflow. Calculate and position/resize the elements of this node
*/
links.TreeGrid.Node.prototype.reflow = function() {
// method must be implemented by all inherited prototypes
};
/**
* Update. Will recalculate the visible area, and start loading missing data
*/
links.TreeGrid.Node.prototype.update = function() {
// TODO: this must be implemented by all inherited prototypes
};
/**
* Remove the DOM of the node and set the node invisible
* This will cause a repaint and reflow
*/
links.TreeGrid.Node.prototype.hide = function () {
this.setVisible(false);
this.repaint();
this.reflow();
};
/**
* Make the node visible and repaint it
*/
links.TreeGrid.Node.prototype.show = function () {
this.setVisible(true);
this.repaint();
};
/**
* Select this node
*/
links.TreeGrid.Node.prototype.select = function() {
this.selected = true;
};
/**
* Unselect this node
*/
links.TreeGrid.Node.prototype.unselect = function() {
this.selected = false;
};
/**
* onResize will execute a reflow and a repaint.
*/
links.TreeGrid.Node.prototype.onResize = function() {
if (this.parent && this.parent.onResize) {
this.parent.onResize();
}
};
/**
* The Frame is the base for a TreeGrid, it creates a DOM container and creates
* scrollbars etc.
*/
links.TreeGrid.Frame = function (container, options) {
this.options = options;
this.container = container;
this.dom = {};
this.frameWidth = 0;
this.frameHeight = 0;
this.grid = undefined;
this.eventParams = {};
this.selection = []; // selected items
this.top = 0;
this.left = 0;
this.window = {
'left': 0,
'top': 0,
'height': 0,
'width': 0
};
// create the HTML DOM
this.repaint();
};
links.TreeGrid.Frame.prototype = new links.TreeGrid.Node();
/**
* Trigger an event, but do this via the treegrid object
* @param {String} event
* @param {Object} params optional parameters
*/
links.TreeGrid.Frame.prototype.trigger = function(event, params) {
if (this.treegrid) {
this.treegrid.trigger(event, params);
}
else {
throw 'Error: cannot trigger an event because treegrid is missing';
}
};
/**
* Get the HTML DOM container of the Frame
* @return {Element} container
*/
links.TreeGrid.Frame.prototype.getContainer = function () {
return this.dom.itemFrame;
};
/**
* Retrieve the main TreeGrid, the base object.
*/
links.TreeGrid.Frame.prototype.getTreeGrid = function() {
return this.treegrid;
};
/**
* set the main TreeGrid, the base object.
* @param {links.TreeGrid} treegrid
*/
links.TreeGrid.Frame.prototype.setTreeGrid = function(treegrid) {
this.treegrid = treegrid;
};
/**
* Set a grid
* @param {links.TreeGrid.Grid} grid
*/
links.TreeGrid.Frame.prototype.setGrid = function (grid) {
if (this.grid) {
// remove old grid
this.grid.hide();
delete this.grid;
}
this.grid = grid;
this.grid.setParent(this);
this.gridHeight = this.grid.getHeight();
this.update();
this.repaint();
};
/**
* Get the absolute top position of the frame
* @return {Number} absTop
*/
links.TreeGrid.Frame.prototype.getAbsTop = function() {
return (this.verticalScroll ? -this.verticalScroll.get() : 0);
};
/**
* onResize event. overwritten from Node
*/
links.TreeGrid.Frame.prototype.onResize = function() {
//console.log('Frame.onResize'); // TODO: cleanup
this.repaint();
var loopCount = 0; // for safety
var maxLoopCount = 10;
while (this.reflow() && (loopCount < maxLoopCount)) {
this.update();
this.repaint();
loopCount++;
}
if (loopCount >= maxLoopCount) {
try {
console.log('Warning: maximum number of loops exceeded');
} catch (err) {}
}
};
/**
* The visible window changed, due
*/
links.TreeGrid.Frame.prototype.onRangeChange = function() {
this._updateVisibleWindow();
this.update();
this.repaint();
var loopCount = 0; // for safety
var maxLoopCount = 10;
var resized = this.reflow();
while (this.reflow()) {
this.repaint();
}
if (loopCount >= maxLoopCount) {
try {
console.log('Warning: maximum number of loops exceeded');
} catch (err) {}
}
};
/**
* Get the currently visible part of the contents
* @return {Object} window Object containing parameters left, top, width, height
*/
links.TreeGrid.Frame.prototype.getVisibleWindow = function() {
return this.window;
};
/**
* Update the data of the frame
*/
links.TreeGrid.Frame.prototype.update = function() {
if (this.grid) {
this.grid.update();
}
};
/**
* Update the currently visible window.
*/
links.TreeGrid.Frame.prototype._updateVisibleWindow = function () {
var grid = this.grid,
frameHeight = this.frameHeight,
frameWidth = this.frameWidth,
gridTop = (grid ? grid.getTop() : 0),
gridHeight = (grid ? grid.getHeight() : 0),
scrollTop = this.verticalScroll ? this.verticalScroll.get() : 0;
// update top position, relative on total height and scrollTop
if (gridHeight > 0 && frameHeight > 0 && scrollTop > 0) {
this.top = (gridTop + gridHeight - frameHeight) / (gridHeight - frameHeight) * scrollTop;
}
else {
this.top = 0;
}
// update the visible window object
this.window = {
'left': this.left,
'top': this.top,
'height': frameHeight,
'width': frameWidth
};
};
/**
* Recalculate sizes of DOM elements
* @return {Boolean} resized true if the contents are resized, else false
*/
links.TreeGrid.Frame.prototype.reflow = function () {
//console.log('Frame.reflow');
var resized = false;
var dom = this.dom,
options = this.options,
grid = this.grid;
if (grid) {
var gridResized = grid.reflow();
resized = resized || gridResized;
}
var frameHeight = dom.mainFrame ? dom.mainFrame.clientHeight : 0;
var frameWidth = dom.mainFrame ? dom.mainFrame.clientWidth : 0;
resized = resized || (this.frameHeight != frameHeight);
resized = resized || (this.frameWidth != frameWidth);
this.frameHeight = frameHeight;
this.frameWidth = frameWidth;
if (resized) {
this.verticalScroll.setInterval(0, this.gridHeight - frameHeight);
this._updateVisibleWindow();
}
this.verticalScroll.reflow();
return resized;
};
/**
* Update the height of this node, because a child's height has been changed.
* This will not cause any repaints, but just updates the height of this node.
* updateHeight() is called via an onUpdateHeight() from a child node.
* @param {links.TreeGrid.Node} child
* @param {Number} diffHeight change in height
*/
links.TreeGrid.Frame.prototype.updateHeight = function (child, diffHeight) {
if (child == this.grid) {
this.gridHeight += diffHeight;
}
};
/**
* Redraw the TreeGrid
* (child grids are not redrawn)
*/
links.TreeGrid.Frame.prototype.repaint = function() {
//console.log('Frame.repaint');
this._repaintFrame();
this._repaintScrollbars();
this._repaintGrid();
};
/**
* Redraw the frame
*/
links.TreeGrid.Frame.prototype._repaintFrame = function() {
var frame = this,
dom = this.dom,
options = this.options;
// create the main frame
var mainFrame = dom.mainFrame;
if (!mainFrame) {
// the surrounding main frame
mainFrame = document.createElement('DIV');
mainFrame.className = 'treegrid-frame';
mainFrame.style.position = 'relative';
mainFrame.style.overflow = 'hidden';
//mainFrame.style.overflow = 'visible'; // TODO: cleanup
mainFrame.style.left = '0px';
mainFrame.style.top = '0px';
this.container.appendChild(mainFrame);
dom.mainFrame = mainFrame;
links.TreeGrid.addEventListener(mainFrame, 'mousedown',
function (event) {
frame.onMouseDown(event);
});
links.TreeGrid.addEventListener(mainFrame, 'mousewheel',
function (event) {
frame.onMouseWheel(event);
});
links.TreeGrid.addEventListener(mainFrame, 'touchstart',
function (event) {
frame.onTouchStart(event);
});
var dragImage = document.createElement('div');
dragImage.innerHTML = '1 item';
dragImage.className = 'treegrid-drag-image';
this.dom.dragImage = dragImage;
links.dnd.makeDraggable(mainFrame, {
'dragImage': dragImage,
'dragImageOffsetX': 10,
'dragImageOffsetY': -10,
'dragStart': function (event) {return frame.onDragStart(event);},
'dragEnd': function (event) {return frame.onDragEnd(event);}
});
}
// resize frame
mainFrame.style.width = options.width || '100%';
mainFrame.style.height = options.height || '100%';
// create the frame for holding the items
var itemFrame = dom.itemFrame;
if (!itemFrame) {
// the surrounding main frame
itemFrame = document.createElement('DIV');
itemFrame.style.position = 'absolute';
itemFrame.style.left = '0px';
itemFrame.style.top = '0px';
itemFrame.style.width = '100%';
itemFrame.style.height = '0px';
dom.mainFrame.appendChild(itemFrame);
dom.itemFrame = itemFrame;
}
};
/**
* Repaint the grid
*/
links.TreeGrid.Frame.prototype._repaintGrid = function() {
if (this.grid) {
this.grid.repaint();
}
};
/**
* Redraw the scrollbar
*/
links.TreeGrid.Frame.prototype._repaintScrollbars = function() {
var dom = this.dom;
var scrollContainer = dom.scrollContainer;
if (!scrollContainer) {
scrollContainer = document.createElement('div');
scrollContainer.style.position = 'absolute';
scrollContainer.style.zIndex = 9999; // TODO: not so nice solution
scrollContainer.style.right = '0px'; // TODO: test on old IE
scrollContainer.style.top = '0px';
scrollContainer.style.height = '100%';
scrollContainer.style.width = '16px';
dom.mainFrame.appendChild(scrollContainer);
dom.scrollContainer = scrollContainer;
var frame = this;
verticalScroll = new links.TreeGrid.VerticalScroll(scrollContainer);
verticalScroll.addOnChangeHandler(function (value) {
frame.onRangeChange();
});
this.verticalScroll = verticalScroll;
}
this.verticalScroll.redraw();
};
/**
* Check if given object is a Javascript Array
* @param {*} obj
* @return {Boolean} isArray true if the given object is an array
*/
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.TreeGrid.isArray = function (obj) {
if (obj instanceof Array) {
return true;
}
return (Object.prototype.toString.call(obj) === '[object Array]');
};
/**
* @constructor links.TreeGrid.Grid
* Grid can display one Grid, with data from one DataConnector
*
* @param {links.DataConnector} data
* @param {Object} options A key-value map with options
*/
links.TreeGrid.Grid = function (data, options) {
// set dataconnector
// TODO: add support for a google DataTable
this.setData(data);
this.dom = {
};
// initialize options
this.options = {
'items': {
'defaultHeight': 24, // px
'minHeight': 24 // px
}
};
if (options) {
// merge options
links.TreeGrid.extend(this.options, options);
}
this.columns = [];
this.itemsHeight = 0; // Total height of all items
this.items = [];
this.itemCount = undefined; // total number of items
this.visibleItems = [];
this.expandedItems = [];
this.iconsWidth = 0;
this.headerHeight = 0;
this.header = new links.TreeGrid.Header({'options': this.options});
this.header.setParent(this);
this.loadingHeight = 0;
this.emptyHeight = 0;
this.errorHeight = 0;
this.height = this.options.items.defaultHeight;
this.dropAreas = [];
this.dropAreaHeight = this.options.dropAreaHeight;
// offset and limit gives the currently visible items
this.offset = 0;
this.limit = 0;
};
links.TreeGrid.Grid.prototype = new links.TreeGrid.Node();
/**
* update the data: update items in the visible range, and update the item count
*/
links.TreeGrid.Grid.prototype.update = function() {
// determine the limit and offset
var currentRange = {
'offset': this.offset,
'limit': this.limit
};
var window = this.getVisibleWindow();
var newRange = this._getRangeFromWindow(window, currentRange);
this.offset = newRange.offset;
this.limit = newRange.limit;
var grid = this,
items = this.items,
offset = this.offset,
limit = this.limit;
//console.log('update', this.left, offset, offset + limit)
// update childs of the items
var updateItems = links.TreeGrid.mergeArray(this.visibleItems, this.expandedItems);
for (var i = 0, iMax = updateItems.length; i < iMax; i++) {
var item = updateItems[i];
if (item) {
item.update();
}
}
var changeCallback = function (changedItems) {
//console.log('changesCallback', changedItems, newOffset, newLimit);
grid.error = undefined;
grid._updateItems(offset, limit);
};
var changeErrback = function (err) {
grid.error = err;
};
// update the items. on callback, load all uninitialized and changed items
this._getChanges(offset, limit, changeCallback, changeErrback);
};
/**
* Update the height of this grid, because a child's height has been changed.
* This will not cause any repaints, but just updates the height of this node.
* updateHeight() is called via an onUpdateHeight() from a child node.
* @param {links.TreeGrid.Node} child
* @param {Number} diffHeight change in height
*/
links.TreeGrid.Grid.prototype.updateHeight = function (child, diffHeight) {
var index = -1;
if (child instanceof links.TreeGrid.Header) {
this.headerHeight += diffHeight;
index = -1;
}
else if (child instanceof links.TreeGrid.Item) {
this.itemsHeight += diffHeight;
index = this.items.indexOf(child);
}
// move all lower down items
var items = this.items;
for (var i = index + 1, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item) {
item.top += diffHeight;
}
}
};
/**
* Event handler for drop event
*/
links.TreeGrid.Grid.prototype.onDrop = function(event) {
var data = JSON.parse(event.dataTransfer.getData('text'));
// TODO: trigger event?
if (this.dataConnector) {
var me = this;
var callback = function () {
/* TODO
if (me.expanded) {
me.onResize();
}
else {
me.onExpand();
}*/
};
var errback = callback;
var items = [data.item];
if (event.dataTransfer.dropEffect == 'move') {
this.dataConnector.appendItems(items, callback, errback);
}
else if (event.dataTransfer.dropEffect == 'copy') {
this.dataConnector.appendItems(items, callback, errback);
}
/* TODO
else if (event.dataTransfer.dropEffect == 'link') {
// TODO: should be used to link one item to another item...
}
else {
// TODO
}*/
}
else {
console.log('dropped but do nothing', event.dataTransfer.dropEffect);
}
links.TreeGrid.preventDefault(event);
};
/**
* merge two arrays
* returns a copy of the merged arrays, containing only distinct elements
*/
links.TreeGrid.mergeArray = function(array1, array2) {
var merged = array1.slice(0); // copy first array
for (var i = 0, iMax = array2.length; i < iMax; i++) {
var elem = array2[i];
if (array1.indexOf(elem) == -1) {
merged.push(elem);
}
}
return merged;
};
/**
* Recalculate the size of all elements in the Grid (the width and
* height of header, items, fields)
* @return {Boolean} resized True if some elements are resized
* In that case, a redraw is required
*/
links.TreeGrid.Grid.prototype.reflow = function() {
var resized = false,
visibleItems = this.visibleItems;
// preform a reflow on all childs (the header, visible items, expanded items)
if (this.header) {
var headerResized = this.header.reflow();
resized = resized || headerResized;
}
var reflowItems = links.TreeGrid.mergeArray(this.visibleItems, this.expandedItems);
for (var i = 0, iMax = reflowItems.length; i < iMax; i++) {
var item = reflowItems[i];
if (item) {
var itemResized = item.reflow();
resized = resized || itemResized;
}
}
// reflow for all drop areas
var dropAreas = this.dropAreas;
for (var i = 0, iMax = dropAreas.length; i < iMax; i++) {
dropAreas[i].reflow();
}
// calculate the width of the fields of the header
var widths = this.header.getFieldWidths();
var columns = this.columns,
indentationWidth = this.options.indentationWidth;
for (var i = 0, iMax = widths.length; i < iMax; i++) {
var column = columns[i];
if (!column.fixedWidth) {
var width = widths[i] + indentationWidth;
if (width > column.width) {
column.width = width;
resized = true;
}
}
}
// calculate the width of the fields
for (var i = 0, iMax = visibleItems.length; i < iMax; i++) {
var item = visibleItems[i];
var offset = 0;
var widths = item.getFieldWidths();
for (var j = 0, jMax = columns.length; j < jMax; j++) {
var column = columns[j];
if (!column.fixedWidth) {
var width = widths[j] + indentationWidth;
if (width > column.width) {
column.width = width;
resized = true;
}
}
}
}
// calculate the width of the icons
if (this.isVisible()) {
var iconsWidth = 0;
for (var i = 0, iMax = visibleItems.length; i < iMax; i++) {
var item = visibleItems[i];
var width = item.getIconsWidth();
iconsWidth = Math.max(width, iconsWidth);
}
if (this.iconsWidth != iconsWidth) {
resized = true;
}
this.iconsWidth = iconsWidth;
}
// calculate the left postions of the columns
var left = indentationWidth + this.iconsWidth;
for (var i = 0, iMax = columns.length; i < iMax; i++) {
var column = columns[i];
column.left = left;
left += column.width;
}
// calculate the width of the grid in total
var width = 0;
if (columns && columns.length) {
var lastColumn = columns[columns.length - 1];
var width = lastColumn.left + lastColumn.width;
}
resized = resized || (width != this.width);
this.width = width;
// update the height of loading message
if (this.loading) {
if (this.dom.loading) {
this.loadingHeight = this.dom.loading.clientHeight;
}
else {
// leave the height as it is
}
}
else {
this.loadingHeight = 0;
}
// update the height of error message
if (this.error) {
if (this.dom.error) {
this.errorHeight = this.dom.error.clientHeight;
}
else {
// leave the height as it is
}
}
else {
this.errorHeight = 0;
}
// update the height of empty message
if (this.itemCount == 0) {
if (this.dom.empty) {
this.emptyHeight = this.dom.empty.clientHeight;
}
else {
// leave the height as it is
}
}
else {
this.emptyHeight = 0;
}
// calculate the total height
var height = 0;
height += this.headerHeight;
height += this.itemsHeight;
height += this.loadingHeight;
height += this.emptyHeight;
//height += this.errorHeight; // We do not append the height of the error to the total height.
if (height == 0) {
// grid can never have zero height, should always contain some message
height = this.options.items.defaultHeight;
}
var diffHeight = (height - this.height);
if (diffHeight) {
resized = true;
this.height = height;
this.onUpdateHeight(diffHeight);
}
return resized;
};
/**
* Redraw the grid
* this will recursively the header, the items, and the childs of items
*/
links.TreeGrid.Grid.prototype.repaint = function() {
//console.log('repaint');
var grid = this,
dom = grid.dom;
this._repaintLoading();
this._repaintEmpty();
this._repaintError();
this._repaintHeader();
this._repaintItems();
/* TODO: dropareas
this._repaintDropAreas();
*/
window.count = window.count ? window.count + 1 : 1; // TODO: cleanup
};
/**
* Redraw a "loading" text when the grid is uninitialized
*/
links.TreeGrid.Grid.prototype._repaintLoading = function() {
//console.log('repaintLoading', this.left, this.isVisible(), this.loading); // TODO: cleanup
// redraw loading icon
if (this.isVisible() && this.itemCount == undefined && !this.error) {
var domLoadingIcon = this.dom.loadingIcon;
if (!domLoadingIcon) {
// create loading icon
domLoadingIcon = document.createElement('DIV');
domLoadingIcon.className = 'treegrid-loading-icon';
domLoadingIcon.style.position = 'absolute';
//domLoadingIcon.style.zIndex = 9999; // TODO: loading icon of the Grid always on top?
domLoadingIcon.title = 'refreshing...';
this.getContainer().appendChild(domLoadingIcon);
this.dom.loadingIcon = domLoadingIcon;
}
// position the loading icon
domLoadingIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domLoadingIcon.style.left = this.getAbsLeft() + 'px';
}
else {
if (this.dom.loadingIcon) {
this.dom.loadingIcon.parentNode.removeChild(this.dom.loadingIcon);
this.dom.loadingIcon = undefined;
}
}
// redraw loading text
if (this.isVisible() && !this.error && this.itemCount == undefined) {
var domLoadingText = this.dom.loadingText;
if (!domLoadingText) {
// create a "loading..." text
domLoadingText = document.createElement('div');
domLoadingText.className = 'treegrid-loading';
domLoadingText.style.position = 'absolute';
domLoadingText.appendChild(document.createTextNode('loading...'));
this.getContainer().appendChild(domLoadingText);
this.dom.loadingText = domLoadingText;
}
// position the loading text
domLoadingText.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domLoadingText.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px';
}
else {
if (this.dom.loadingText) {
delete this.loadingHeight;
this.dom.loadingText.parentNode.removeChild(this.dom.loadingText);
this.dom.loadingText = undefined;
}
}
};
/**
* Redraw a "(empty)" text when the grid container zero items
*/
links.TreeGrid.Grid.prototype._repaintEmpty = function() {
var dom = this.dom;
if (this.itemCount == 0 && this.isVisible()) {
var domEmpty = dom.empty;
if (!domEmpty) {
// draw a "empty" text
domEmpty = document.createElement('div');
domEmpty.className = 'treegrid-loading';
domEmpty.style.position = 'absolute';
domEmpty.appendChild(document.createTextNode('(empty)'));
this.getContainer().appendChild(domEmpty);
dom.empty = domEmpty;
var item = this.parent;
var dataTransfer = item.dataConnector ? item.dataConnector.getOptions().dataTransfer : undefined;
if (dataTransfer) {
links.dnd.makeDroppable(domEmpty, {
'dropEffect': dataTransfer.dropEffect,
'drop': function (event) {item.onDrop(event);},
'dragEnter': function (event) {item.onDragEnter(event);},
'dragOver': function (event) {item.onDragOver(event);},
'dragLeave': function (event) {item.onDragLeave(event);}
});
}
}
// position the empty text
domEmpty.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domEmpty.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px';
domEmpty.style.width = (this.parent.width - 2 * this.options.indentationWidth) + 'px'; // TODO: not so nice... use real width
}
else {
if (dom.empty) {
links.dnd.removeDroppable(domEmpty);
dom.empty.parentNode.removeChild(dom.empty);
delete this.dom.empty;
}
}
};
/**
* Redraw an error message (if any)
*/
links.TreeGrid.Grid.prototype._repaintError = function() {
var dom = this.dom;
if (this.isVisible() && this.error) {
var domError = dom.error;
if (!domError) {
// draw a "loading..." text
domError = document.createElement('div');
domError.className = 'treegrid-error';
domError.style.position = 'absolute';
domError.appendChild(document.createTextNode('Error: ' +
links.TreeGrid.Grid._errorToString(this.error)));
this.getContainer().appendChild(domError);
dom.error = domError;
}
// position the error message
domError.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domError.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px';
}
else {
if (dom.error) {
dom.error.parentNode.removeChild(dom.error);
dom.error = undefined;
}
}
// redraw error icon
if (this.isVisible() && this.error && !this.loading) {
var domErrorIcon = this.dom.errorIcon;
if (!domErrorIcon) {
// create error icon
domErrorIcon = document.createElement('DIV');
domErrorIcon.className = 'treegrid-error-icon';
domErrorIcon.style.position = 'absolute';
domErrorIcon.title = this.error;
this.getContainer().appendChild(domErrorIcon);
this.dom.errorIcon = domErrorIcon;
}
// position the error icon
var absLeft = this.getAbsLeft();
domErrorIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domErrorIcon.style.left = absLeft + 'px';
domErrorIcon.style.zIndex = absLeft + 1;
}
else {
if (this.dom.errorIcon) {
this.dom.errorIcon.parentNode.removeChild(this.dom.errorIcon);
this.dom.errorIcon = undefined;
}
}
};
/**
* Retrieve the available columns from the given fields
* @param {Object} fields example object containing field names/values
*/
links.TreeGrid.Grid.prototype.getColumnsFromFields = function (fields) {
var columns = [];
if (fields) {
var i = 0;
for (var fieldName in fields) {
if (fields.hasOwnProperty(fieldName)) {
var field = fields[fieldName];
if (fieldName.charAt(0) != '_' && !links.TreeGrid.isArray(field) &&
!(field instanceof links.DataConnector)) {
columns[i] = {
'name': fieldName
};
i++;
}
}
}
}
return columns;
};
/**
* Update the columns of the header
* @param {Object[]} columns Column names and additional information
* The object contains parameters 'name',
* and optionally 'text', 'title'
*/
links.TreeGrid.Grid.prototype.setColumns = function (columns) {
var indentationWidth = this.options.indentationWidth;
var newColumns = [];
var changed = false;
// console.log('setColumns start', columns, this.columns, indentationWidth);
for (var i = 0, iMax = columns.length; i < iMax; i++) {
var curColumn = this.columns[i];
var column = columns[i];
// check for changes in the fields
if (!curColumn) {
changed = true;
}
if (!changed) {
for (var field in column) {
if (curColumn[field] != column[field]) {
changed = true;
break;
}
}
}
if (!changed) {
for (var field in curColumn) {
if (field != 'width' &&
field != 'left' &&
curColumn[field] != column[field]) {
changed = true;
break;
}
}
}
// create a new column object
var newColumn = {
'name': '',
'width': 0,
'left': 0
};
// copy width from current column
if (!changed && curColumn) {
if (curColumn.width != undefined) {
newColumn.width = curColumn.width;
}
if (curColumn.left != undefined) {
newColumn.left = curColumn.left;
}
}
// set a fixed width if applicable
newColumn.fixedWidth = (column.width != undefined);
// copy values from new column data
for (field in column) {
newColumn[field] = column[field];
}
// store the new colum fields
this.columns[i] = newColumn;
newColumns[i] = newColumn;
}
if (this.columns.length != columns.length) {
changed = true;
}
if (changed) {
// replace the contents of columns array.
// Important: keep the same array object, all items link to this object!
this.columns.splice(0, this.columns.length);
for (var i = 0; i < newColumns.length; i++) {
this.columns.push(newColumns[i]);
}
//console.log('columns changed!');
}
//console.log('setColumns end', this.columns);
};
/**
* Set or change the data or dataconnector for the grid
* @param {Array | links.DataConnector} data
*/
links.TreeGrid.Grid.prototype.setData = function (data) {
var changed = (data != this.data);
if (changed) {
// create new data connector
var dataConnector;
if (links.TreeGrid.isArray(data)) {
dataConnector = new links.DataTable(data);
}
else if (data instanceof links.DataConnector) {
dataConnector = data;
}
else {
throw 'Error: no valid data. JSON Array or DataConnector expected.';
}
// clean up old data connector
if (this.dataConnector && this.eventListener) {
this.dataConnector.removeEventListener(this.eventListener);
this.eventListener = undefined;
this.dataConnector = undefined;
// cleanup data
var items = this.items;
for (var i = 0, iMax = this.items.length; i < iMax; i++) {
var item = items[i];
if (item) {
item.data = undefined;
}
}
}
var grid = this;
this.eventListener = function (event, params) {
if (event == 'change') {
grid.update();
}
};
// store and link the new dataconnector
// TODO: use the eventbus instead of the addEventListener structure?
this.dataConnector = dataConnector;
this.dataConnector.addEventListener(this.eventListener);
if (this.dataConnector && this.dataConnector.options.showHeader != undefined) {
this.showHeader = this.dataConnector.options.showHeader;
}
else {
this.showHeader = true;
}
}
};
/**
* Remove an item
* all lower down items will be shifted one up. These changes take only
* place in the display of the treegrid, and are not refected to a dataconnector
* @param {links.TreeGrid.Item} item
*/
links.TreeGrid.Grid.prototype._removeItem = function (item) {
var items = this.items;
var index = items.indexOf(item);
if (index != -1) {
if (item.expanded) {
item.onExpand();
}
var visIndex = this.visibleItems.indexOf(item);
if (visIndex != -1) {
this.visibleItems.splice(visIndex, 1);
}
var height = item.getHeight();
this.updateHeight(item, -height);
item.hide();
items.splice(index, 1);
this.itemCount--;
}
};
/**
* Update the columns of the header
* @param {Object[]} columns
*/
links.TreeGrid.Grid.prototype._updateHeader = function (columns) {
this.header.setFields(columns);
};
/**
* Get the items which are changed, and give them a status dirty=true.
* After that, the changed items may be retrieved
* @param {Number} offset
* @param {Number} limit
* @param {function} callback. Called with the array containing the changed
* items as first parameter. Note that the items
* themselves are not yet updated!
* @param {function} errback
*/
links.TreeGrid.Grid.prototype._getChanges = function (offset, limit, callback, errback) {
var grid = this;
//console.log('_getChanges', offset, limit)
// create a list with items to be checked for changes
// only check items when they are not already checked for changes
var checkData = [];
var checkItemIds = [];
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = this.getItem(i);
checkData.push(item.data);
checkItemIds.push(i);
}
var changesCallback = function (resp) {
var changedItems = resp.items || [];
var itemsChanged = (checkData.length < limit || changedItems.length > 0 );
//console.log('changesCallback', resp.totalItems, resp.items);
// update the item count
var countChanged = (resp.totalItems !== grid.itemCount);
if (countChanged) {
/* TODO
if (grid.totalItems !== undefined || resp.totalItems !== 0) {
// On the first run, grid.totalItems will be undefined. When getChanges
// in that case returns resp.totalItems==0, we do not set the totalItems
// here but leave it undefined, forcing a call of getItems.
grid.setItemCount(resp.totalItems);
}*/
grid.setItemCount(resp.totalItems);
}
// give changed items a 'dirty' status, and unmark the items from their updating status
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = grid.getItem(i);
item.updating = false;
if (!item.data || changedItems.indexOf(item.data) != -1) {
item.dirty = true;
}
//console.log('changesCallback', i, item.dirty, changedItems.indexOf(item.data), item.data);
}
//console.log('changesCallback item[0].updating=', grid.getItem(0).updating);
if (countChanged || itemsChanged) {
//console.log('there are changes or dirty items');
grid.onResize();
}
if (callback) {
callback(changedItems);
}
};
var changesErrback = function (err) {
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = grid.getItem(i);
item.error = err;
item.updating = false;
item.dirty = true;
}
grid.onResize();
if (errback) {
errback(err);
}
};
//console.log('_getChanges', offset, limit, checkData, checkItemIds)
// mark the items as updating
for (var i = 0, iMax = checkItemIds.length; i < iMax; i++) {
var id = checkItemIds[i];
this.items[id].updating = true;
}
// check for changes in the items
// Note: we always check for changes, also if checkData.length==0,
// because we want to retrieve the item count too
this.dataConnector.getChanges(offset, limit, checkData, changesCallback, changesErrback);
};
/**
* Retrieve the items in the range of current window
* @param {Number} offset
* @param {Number} limit
* @param {function} callback
* @param {function} errback
*/
links.TreeGrid.Grid.prototype._updateItems = function (offset, limit, callback, errback) {
var grid = this,
items = this.items;
// first minimize the range of items to be retrieved:
// limit to:
// - dirty items
// - not loaded items
// - items not being loaded
// TODO: optimize this, do not search twice for the same item (by calling .getItem())
var item = this.getItem(offset);
while (limit > 0 && (item.loading || (!item.dirty && item.data))) {
offset++;
limit--;
item = this.getItem(offset);
}
item = this.getItem(offset + limit - 1);
//while (limit > 0 && item.data && !item.dirty) {
while (limit > 0 && (item.loading || (!item.dirty && item.data))) {
limit--;
item = this.getItem(offset + limit - 1);
}
// mark all items which are going to be loaded as "loading" and "dirty"
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = this.getItem(i);
if (item.error || item.dirty || !item.data) {
item.loading = true;
item.dirty = true;
}
}
var getItemsCallback = function (resp) {
//console.log('items retrieved', offset, limit, resp);
var newItems = resp.items;
// set the loaded items to not-loading
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = grid.getItem(i);
item.loading = false;
item.dirty = false;
}
// store the new ites
var columns_final = [];
for (var i = 0, iMax = newItems.length; i < iMax; i++) {
var data = newItems[i];
var columns = grid.dataConnector.getOptions().columns || grid.getColumnsFromFields(newItems[i]);
if(columns.length > columns_final.length){
columns_final = columns;
}
grid.setColumns(columns_final);
if(i == 0){
grid._updateHeader(grid.columns);
}
if (data) {
var index = offset + i;
var item = grid.getItem(index);
item.data = data;
item.setFields(data, grid.columns);
item.error = undefined;
}
}
grid.onResize();
if (callback) {
callback();
}
}
var getItemsErrback = function (err) {
// set all items to error
for (var i = offset, iMax = offset + limit; i < iMax; i++) {
var item = grid.getItem(i);
item.loading = false;
item.dirty = true;
item.error = err;
}
grid.onResize();
if (errback) {
errback(err);
}
}
if (limit > 0 || this.totalItems === undefined) {
//console.log('_updateItems offset=' + offset + ', limit=' + limit ); // TODO: cleanup
this.repaint();
this.dataConnector.getItems(offset, limit, getItemsCallback, getItemsErrback);
}
else {
if (callback) {
callback();
}
}
};
/**
* Redraw the header
*/
links.TreeGrid.Grid.prototype._repaintHeader = function () {
var visible = (this.showHeader && this.itemCount != undefined && this.itemCount > 0);
this.header.setVisible(visible);
this.header.repaint();
}
/**
* Redraw the items in the currently visible window
*/
links.TreeGrid.Grid.prototype._repaintItems = function () {
var columns = this.columns,
windowTop = (this.window && this.window.top) ? this.window.top : 0,
visibleItems = this.visibleItems;
// remove items which are outside the visible window
var visible = this.isVisible(),
visibleItems = this.visibleItems,
i = 0;
while (i < visibleItems.length) {
var item = visibleItems[i];
if (item.index < this.offset || item.index >= this.offset + this.limit || !visible) {
item.hide();
var index = visibleItems.indexOf(item);
if (index != -1) {
visibleItems.splice(index, 1);
i--;
}
}
i++;
}
// add items inside the visible window
var itemCount = this.itemCount || 0,
iStart = this.offset,
iEnd = Math.min(this.offset + this.limit, itemCount);
if (this.isVisible()) {
for (var i = iStart; i < iEnd; i++) {
var item = this.getItem(i);
item.setVisible(true);
if (visibleItems.indexOf(item) == -1) {
visibleItems.push(item);
}
}
}
// repaint the visible items
for (var i = 0; i < visibleItems.length; i++) {
var item = visibleItems[i];
item.repaint();
}
};
/**
* Redraw the dropareas between the items
*/
links.TreeGrid.Grid.prototype._repaintDropAreas = function () {
var dropEffect = 'none';
if (this.dataConnector &&
this.dataConnector.options &&
this.dataConnector.options.dataTransfer &&
this.dataConnector.options.dataTransfer.dropEffect) {
dropEffect = this.dataConnector.options.dataTransfer.dropEffect;
}
if (dropEffect != 'none' && this.isVisible()) {
var itemCount = this.itemCount || 0,
iStart = this.offset,
iEnd = Math.min(this.offset + this.limit, itemCount),
dropAreas = this.dropAreas,
dropAreaHeight = this.dropAreaHeight,
container = this.getContainer();
// create one droparea for each of the currently visible items
var missingCount = this.limit - dropAreas.length;
var redundantCount = -missingCount;
for (var i = 0; i < missingCount; i++) {
var dropArea = new links.TreeGrid.DropArea({
'dataConnector': this.dataConnector,
'item': this.getItem(this.offset + i),
'height': dropAreaHeight
});
dropArea.setParent(this);
dropAreas.push(dropArea);
}
for (var i = 0; i < redundantCount; i++) {
var dropArea = dropAreas.shift();
dropArea.hide();
}
// position the dropareas right above the items
for (var i = iStart; i < iEnd; i++) {
var item = this.getItem(i);
//var itemTop = item.getAbsTop();
var dropArea = dropAreas[i - this.offset];
dropArea.setTop(item.top - dropAreaHeight);
dropArea.item = item;
dropArea.repaint();
}
}
else {
var dropAreas = this.dropAreas;
while (dropAreas.length > 0) {
var dropArea = dropAreas.shift();
dropArea.hide();
}
}
};
/**
* @constructor links.TreeGrid.Header
* @param {Object} params. A key-value map containing parameters:
* height, options
*/
links.TreeGrid.Header = function (params) {
if (params) {
this.height = params.height || 0;
this.options = params.options;
}
this.fieldsHeight = 0;
// data
this.dom = {};
this.columns = undefined;
};
links.TreeGrid.Header.prototype = new links.TreeGrid.Node();
/**
* Clear the header of the grid
*/
links.TreeGrid.Header.prototype.clearFields = function () {
this.columns = undefined;
this.fieldsHeight = 0;
};
/**
* Redraw the header of the grid
*/
links.TreeGrid.Header.prototype.repaint = function () {
if (this.isVisible() && this.columns) {
// check if the columns are changed
var columns = this.columns;
var prevColumns = this.prevColumns;
if (columns != prevColumns) {
// columns are changed. remove old dom
this.hide();
this.prevColumns = columns;
}
var domHeader = this.dom.header;
if (!domHeader) {
// create the DOM
domHeader = document.createElement('DIV');
domHeader.treeGridType = 'header';
domHeader.className = 'treegrid-header';
domHeader.style.position = 'absolute';
domHeader.style.zIndex = 1 + this.getAbsLeft(); // TODO: not so nice to use zIndex and the abs left. use a subgrid level?
this.getContainer().appendChild(domHeader);
this.dom.header = domHeader;
this.dom.fields = [];
if (this.columns) {
// create fields
var columns = this.columns,
padding = this.options.padding;
for (var i = 0, iMax = columns.length; i < iMax; i++) {
if (!this.dom.fields[i]) {
var column = this.columns[i];
var domField = document.createElement('DIV');
domField.className = 'treegrid-header-field';
domField.style.position = 'absolute';
domField.style.top = '0px';
domField.innerHTML = column.text || column.name || '';
domField.title = column.title || '';
domHeader.appendChild(domField);
this.dom.fields[i] = domField;
}
}
}
}
// reposition the header
var absTop = Math.max(this.getAbsTop(), 0);
domHeader.style.top = absTop + 'px';
domHeader.style.left = this.getAbsLeft() + 'px';
domHeader.style.height = this.height + 'px';
domHeader.style.width = this.width + 'px';
/* TODO: width of the header?
if (this.left) {
var lastColumn = this.columns[this.columns.length-1];
header.dom.style.width = lastColumn.left+ lastColumn.width + 'px';
}
else {
header.dom.style.width = '100%';
}*/
// position the columns
var domFields = this.dom.fields;
var columns = this.columns;
for (var i = 0, iMax = domFields.length; i < iMax; i++) {
domFields[i].style.left = columns[i].left + 'px';
}
}
else {
// not visible.
// remove the header DOM
if (this.dom.header) {
this.dom.header.parentNode.removeChild(this.dom.header);
this.dom.header = undefined;
this.dom.fields = undefined;
}
}
};
/**
* Recalculate the size of the DOM elements of the header
* @return {Boolean} resized
*/
links.TreeGrid.Header.prototype.reflow = function () {
var resized = false;
// calculate maximum height of the fields
var domFields = this.dom ? this.dom.fields : undefined,
fieldCount = domFields ? domFields.length : 0,
fieldsHeight = this.options.items.minHeight;
if (domFields) {
for (var i = 0; i < fieldCount; i++) {
if (domFields[i]) {
fieldsHeight = Math.max(fieldsHeight, domFields[i].clientHeight);
}
}
this.fieldsHeight = fieldsHeight;
}
else if (!this.columns) {
// zero fields available, reset the fieldsHeight
this.fieldsHeight = 0;
}
else {
// leave fieldsHeight as it is...
}
/* TODO: needed for auto sizing with
// calculate the width of the header
var contentWidth = 0;
var lastColumn = this.columns ? this.columns[this.columns.length - 1] : undefined;
if (lastColumn) {
contentWidth = lastColumn.left + lastColumn.width;
}
resized = resized || (this.contentWidth != contentWidth);
this.contentWidth = contentWidth;
*/
this.width = this.getVisibleWindow().width - this.getAbsLeft();
// calculate total height
var height = 0;
height += this.fieldsHeight;
var diffHeight = (height - this.height);
if (diffHeight) {
resized = true;
this.height = height;
this.onUpdateHeight(diffHeight);
}
return resized;
};
/**
* store a link to the columns
* TODO: comment
* @param {Array} columns
*/
links.TreeGrid.Header.prototype.setFields = function (columns) {
if (columns) {
this.columns = columns;
}
};
/**
* Calculate the width of the fields from the HTML DOM
* @return {Number[]} widths
*/
links.TreeGrid.Header.prototype.getFieldWidths = function () {
var widths = [];
if (this.dom.fields) {
var fields = this.dom.fields;
for (var i = 0, iMax = fields.length; i < iMax; i++) {
widths[i] = fields[i].clientWidth;
}
}
return widths;
};
/**
* Set the number of items
* @param {Number} itemCount
*/
links.TreeGrid.Grid.prototype.setItemCount = function (itemCount) {
var defaultHeight = this.options.items.defaultHeight;
var diff = (itemCount - (this.itemCount || 0));
//console.log('setItemCount', this.itemCount, itemCount);
if (diff > 0) {
// items added
var diffHeight = (defaultHeight + this.dropAreaHeight) * diff;
this.itemsHeight += diffHeight;
}
if (diff < 0) {
// there are items removed
var oldItemCount = this.itemCount;
var items = this.items;
for (var i = itemCount; i < oldItemCount; i++) {
var item = items[i];
if (item) {
item.hide();
delete items[i];
}
var itemHeight = item ? item.getHeight() : defaultHeight;
this.itemsHeight -= (itemHeight + this.dropAreaHeight);
}
if (itemCount == 0) {
// TODO: not so nice to reset the header this way
this.header.clearFields();
}
}
this.itemCount = itemCount || 0;
};
/**
* Add an item to the list with expanded grids.
* This list is used to update all grids.
*/
links.TreeGrid.Grid.prototype.registerExpandedItem = function (item) {
var index = this.expandedItems.indexOf(item);
if (index == -1) {
this.expandedItems.push(item);
}
};
/**
* Add an item to the list with expanded items
* This list is used to update all grids.
*/
links.TreeGrid.Grid.prototype.unregisterExpandedItem = function (item) {
var index = this.expandedItems.indexOf(item);
if (index != -1) {
this.expandedItems.splice(index, 1);
}
};
/**
* Get the number of items
* @return {Number} itemCount
*/
links.TreeGrid.Grid.prototype.getItemCount = function () {
return this.itemCount;
};
/**
* retrieve item at given index. If the node doesn't exist, it will be created
* The node will also be created when the index is out of range
* @param {Number} index
* @return {links.TreeGrid.Item} item
*/
links.TreeGrid.Grid.prototype.getItem = function (index) {
var item = this.items[index];
if (!item ) {
// create node when not existing
item = new links.TreeGrid.Item({
'options': this.options,
'index': index, // TODO: remove this index
'top': this._calculateItemTop(index),
'height': this.options.items.defaultHeight
});
item.setParent(this);
this.items[index] = item;
}
return item;
};
/**
* Calculate the top of an item, by calculating the bottom of the
* previous item .
* This method is used when an items top and height are still undefined
* @param {Number} index
* @return {Number} top
*/
links.TreeGrid.Grid.prototype._calculateItemTop = function(index) {
var items = this.items,
defaultHeight = this.options.items.defaultHeight,
prevBottom = 0,
prev = undefined;
// find the last defined item before this item
for (var i = index - 1; i >= 0; i--) {
prev = items[i];
if (prev && prev.top) {
prevBottom += prev.top + prev.height;
break;
}
else {
prevBottom += defaultHeight;
}
}
// use the bottom of the previous item as top, or, if none of the
// previous items is defined, just calculate based on the default height
// of an item
var top = (prev != undefined) ?
(prevBottom + this.dropAreaHeight) :
(this.headerHeight + defaultHeight * index + this.dropAreaHeight * (index + 1));
return top;
};
/**
* @constructor links.TreeGrid.Item
* @param {Object} params. A key-value map containing parameters:
* index, top, options
*/
links.TreeGrid.Item = function (params) {
if (params) {
this.options = params.options;
this.index = params.index || 0; // TODO: remove this index
this.top = params.top || 0;
}
// objects
this.height = this.options.items.defaultHeight;
this.data = undefined; // link to the original data of this item
this.fields = undefined; // array with the fields
this.fieldsHeight = 0;
this.grid = undefined;
this.gridHeight = 0;
// status
this.dirty = false;
this.loading = false;
this.loadingHeight = 0;
this.error = undefined;
this.errorheight = 0;
this.dataTransfer = {}; // for drag and drop properties
// html dom
this.dom = {};
};
links.TreeGrid.Item.prototype = new links.TreeGrid.Node();
/**
* Evaluate given function with a custom current object
* When the given fn is a string, it will be evaluated
* WARNING: evaluating fn when it is a string is unsafe. It is safer to provide
* fn as a javascript function.
* @param {function or String} fn
* @param {Object} obj
*/
links.TreeGrid.eval = function (fn, obj) {
var t = typeof(fn);
if (t == 'function') {
return fn.call(obj);
}
else if (t == 'string') {
var evalHistory = links.TreeGrid.evalHistory;
if (!evalHistory) {
evalHistory = {};
links.TreeGrid.evalHistory = evalHistory;
}
var f = evalHistory[fn];
if (!f) {
f = eval('f=(' + fn + ');');
evalHistory[fn] = f;
}
return f.call(obj);
}
else {
throw new Error('Function must be of type function or string');
}
};
/**
* read the field values from the item data
* @param {Object} data Item data
* @param {Array} columns Array with column objects, the column objects
* contain a name, left, and width of the column
*/
links.TreeGrid.Item.prototype.setFields = function (data, columns) {
if (data && columns) {
// read the field values from the columns
var fields = [];
for (var i = 0, iMax = columns.length; i < iMax; i++) {
var col = columns[i];
if (col.format) {
fields[i] = links.TreeGrid.eval(col.format, data) || '';
}
else {
fields[i] = (data[col.name] || '');
}
}
this.fields = fields;
this.columns = columns;
// link to the icons
this.icons = data._icons;
// link to the actions
this.actions = data._actions;
// find dataconnectors
var dataconnectors = [];
for (var name in data) {
if (data.hasOwnProperty(name) && name.charAt(0) != '_') {
var value = data[name];
if (links.TreeGrid.isArray(value)) {
dataconnectors.push({
'name': name,
'data': new links.DataTable(value)
});
}
else if (value instanceof links.DataConnector) {
dataconnectors.push({
'name': name,
'data': value
});
}
}
// TODO: remove warning in the future
if (name == '_childs') {
try {
console.log('WARNING: special field _childs encountered. ' +
'This field is no longer in use for subgrids, and is now a regular hidden field. ' +
'Use a fieldname without underscore instead for subgrids.');
}
catch (err) {}
}
}
// create dataconnector
var dataconnector = undefined;
if (dataconnectors.length == 1) {
// a single dataconnector
dataconnector = dataconnectors[0].data;
}
else if (dataconnectors.length > 1) {
// create a new dataconnector containing multipe dataconnectors
var options = {'showHeader': false};
dataconnector = new links.DataTable(dataconnectors, options);
}
if (dataconnector) {
// TODO: is it needed to store childs as a dataConnector here in Item?
this.dataConnector = dataconnector;
if (this.grid) {
this.grid.setData(this.dataConnector);
this.grid.update();
}
}
else {
// no data connector
if (this.dataConnector) {
delete this.dataConnector;
}
if (this.grid) {
this.grid.hide();
this.gridHeight = 0; // TODO: not so nice to set the height and expanded to zero like this
delete this.grid;
}
if (this.expanded) {
this.expanded = false;
this.parent.unregisterExpandedItem(this);
}
}
}
};
/**
* Calculate the width of the fields from the HTML DOM
* @return {Number[]} widths
*/
links.TreeGrid.Item.prototype.getFieldWidths = function () {
var widths = [];
if (this.dom.fields) {
var fields = this.dom.fields;
for (var i = 0, iMax = this.columns.length; i < iMax; i++) {
widths[i] = fields[i] ? fields[i].clientWidth : 0;
}
}
return widths;
};
/**
* Calculate the total width of the icons (if any)
* @return {Number} width
*/
links.TreeGrid.Item.prototype.getIconsWidth = function () {
if (this.dom.icons) {
return this.dom.icons.clientWidth;
}
return 0;
};
/**
* Update the height of this item, because a child's height has been changed.
* This will not cause any repaints, but just updates the height of this node.
* updateHeight() is called via an onUpdateHeight() from a child node.
* @param {links.TreeGrid.Node} child
* @param {Number} diffHeight change in height
*/
links.TreeGrid.Item.prototype.updateHeight = function (child, diffHeight) {
if (child == this.grid) {
this.gridHeight += diffHeight;
}
};
/**
* trigger an event
* @param {String} event Event name. For example 'expand' or 'collapse'
*/
links.TreeGrid.Item.prototype.onEvent = function (event) {
var params = {
//'index': this.index, // TODO: dangerous, invalid when items are deleted/inserted...
'items': [this.data]
};
// send the event to the treegrid
links.events.trigger(this.getTreeGrid(), event, params);
// send the event to the dataconnector
var dataConnector = this.parent.dataConnector; // TODO: not so nice accessing dataconnector like this
if (dataConnector) {
dataConnector._onEvent(event, params);
}
};
/**
* Expand or collapse the item
*/
links.TreeGrid.Item.prototype.onExpand = function () {
if (this.expanded) {
// collapse the item
this.dom.buttonExpand.className = 'treegrid-fold';
this.expanded = false;
this.parent.unregisterExpandedItem(this);
if (this.grid) {
this.grid.setVisible(false);
this.gridHeight -= (this.grid.getHeight() + this.options.padding);
}
this.onEvent('collapse');
}
else {
// expand the item
this.dom.buttonExpand.className = 'treegrid-unfold';
this.expanded = true;
this.parent.registerExpandedItem(this);
if (this.dataConnector) {
if (!this.grid) {
// create a grid for the child data
this.grid = new links.TreeGrid.Grid(this.dataConnector, this.options);
this.grid.setParent(this);
this.grid.setLeft(this.left + this.options.indentationWidth);
this.grid.setTop(this.height);
}
// if grid was already loaded before, make it visible
this.grid.setVisible(true);
this.gridHeight += (this.grid.getHeight() + this.options.padding);
}
this.onEvent('expand');
}
this.onResize();
};
/**
* Event handler for drag start event
*/
links.TreeGrid.Item.prototype.onDragOver = function(event) {
if (this.dataTransfer.dragging) {
return; // we cannot drop the item onto itself
}
// we need to repaint on every dragover event.
// because the item consists of various elements, the dragenter and dragleave
// events are fired every time we enter/leave one of the elements.
// this causes a dragleave executed last wrongly
var threshold = this.fieldsHeight / 2;
var dragbefore = (((event.offsetY || event.layerY) < threshold) || !this.dataConnector);
dragbefore = false; // TODO: cleanup the dragbefore thing, and create a separate drop area for dropping inbetween
this.dataTransfer.dragover = !dragbefore;
this.dataTransfer.dragbefore = dragbefore;
// TODO: get the correct vertical offset, independent of the child
this.repaint();
links.TreeGrid.preventDefault(event);
return false;
};
/**
* Event handler for drag enter event
* this will highlight the current item
*/
links.TreeGrid.Item.prototype.onDragEnter = function(event) {
if (this.dataTransfer.dragging) {
return; // we cannot drop the item onto itself
}
/* TODO
event.dataTransfer.allowedEffect = this.dataConnector ? 'move' : 'none';
event.dataTransfer.dropEffect = this.dataConnector ? 'move' : 'none';
*/
//console.log('onDragEnter', this.dragcount, event.target);
//this.dataTransfer.dragover = true;
//this.repaint();
return false;
};
/**
* Event handler for drag leave event
*/
links.TreeGrid.Item.prototype.onDragLeave = function(event) {
if (this.dataTransfer.dragging) {
return; // we cannot drop the item onto itself
}
//console.log('onDragLeave', this.dragcount, event.target);
this.dataTransfer.dragover = false;
this.dataTransfer.dragbefore = false;
this.repaint();
return false;
};
/**
* Event handler for drop event
*/
links.TreeGrid.Item.prototype.onDrop = function(event) {
var items = event.dataTransfer.getData('items');
this.dataTransfer.dragover = false;
this.dataTransfer.dragbefore = false;
this.repaint();
//console.log('onDrop', data, event.dataTransfer.dropEffect);
// TODO: trigger event
if (this.dataConnector) {
var me = this;
var callback = function () {
//* TODO
if (me.expanded) {
me.onResize();
}
else {
me.onExpand();
}
//*/
};
var errback = callback;
// console.log('drop', items);
// prevent a circular loop, when an item is dropped on one of its own
// childs. So, remove items from which this item is a child
var i = 0;
while (i < items.length) {
var checkItem = this;
while (checkItem && checkItem != items[i]) {
checkItem = checkItem.parent;
}
if (checkItem == items[i]) {
items.splice(i, 1);
}
else {
i++;
}
}
var itemsData = [];
for (var i = 0; i < items.length; i++) {
itemsData.push(items[i].data);
}
this.dataConnector.appendItems(itemsData, callback, errback);
}
else if (this.parent && this.parent.dataConnector &&
event.dataTransfer.dropEffect == 'link') {
var targetItemData = this.data;
var callback = function (resp) {
// TODO: redraw on callback?
};
var errback = function (err) {
console.log(err);
};
var sourceItemsData = [];
for (var i = 0; i < items.length; i++) {
sourceItemsData.push(items[i].data);
}
this.parent.dataConnector.linkItems(sourceItemsData, targetItemData,
callback, errback);
}
else {
console.log('dropped but do nothing', event.dataTransfer.dropEffect); // TODO
}
links.TreeGrid.preventDefault(event);
};
/**
* Redraw the node
*/
links.TreeGrid.Item.prototype.repaint = function () {
this._repaintError();
this._repaintLoading();
this._repaintFields();
this._repaintGrid();
};
/**
* Update the data of the child grid (if there is a child grid)
*/
links.TreeGrid.Item.prototype.update = function() {
if (this.grid && this.expanded) {
this.grid.update();
}
};
/**
* Recalculate the size of the DOM elements of the header
* @return {Boolean} resized
*/
links.TreeGrid.Item.prototype.reflow = function () {
var resized = false;
// update and reflow the grid
if (this.grid && this.expanded) {
var gridResized = this.grid.reflow();
resized = resized || gridResized;
}
/* TODO: needed for auto width
// calculate the width of the item
var width = 0;
var lastColumn = this.columns ? this.columns[this.columns.length - 1] : undefined;
if (lastColumn) {
width = lastColumn.left + lastColumn.width;
}
resized = resized || (this.width != width);
this.width = width;
*/
this.width = this.getVisibleWindow().width - this.getAbsLeft();
if (this.isVisible()) {
var fieldsHeight = this.options.items.minHeight,
fields = this.dom.fields,
actions = this.dom.actions,
icons = this.dom.icons,
expandButton = this.dom.expandButton,
fieldCount = fields ? fields.length : 0;
// calculate width of the icons
if (icons) {
var iconsWidth = icons.clientWidth;
if (iconsWidth != this.iconsWidth) {
resized = true;
}
this.iconsWidth = iconsWidth;
}
else {
// leave iconsWidth as it is
}
// calculate maximum height of the fields
if (fields || actions || icons || expandButton || actions) {
for (var i = 0; i < fieldCount; i++) {
if (fields[i]) {
fieldsHeight = Math.max(fieldsHeight, fields[i].clientHeight);
}
}
if (actions) {
fieldsHeight = Math.max(fieldsHeight, actions.clientHeight);
}
if (icons) {
fieldsHeight = Math.max(fieldsHeight, icons.clientHeight);
}
if (expandButton) {
fieldsHeight = Math.max(fieldsHeight, expandButton.clientHeight);
}
this.fieldsHeight = fieldsHeight;
}
else {
// leave the fieldsheight as it is
}
}
else {
// leave the fieldsHeight as it is
}
// update the height of loading message
if (this.loading) {
if (this.dom.loading) {
this.loadingHeight = this.dom.loading.clientHeight;
}
else {
// leave the height as it is
}
}
else {
this.loadingHeight = 0;
}
// update the height of error message
if (this.error) {
if (this.dom.error) {
this.errorHeight = this.dom.error.clientHeight;
}
else {
// leave the height as it is
}
}
else {
this.errorHeight = 0;
}
// update the height of the fields empty, error, and loading
var height = 0;
height += this.fieldsHeight;
height += this.loadingHeight;
height += this.errorHeight;
height += this.gridHeight;
if (height == 0) {
height = this.options.items.defaultHeight;
}
var diffHeight = (height - this.height);
if (diffHeight) {
resized = true;
this.height = height;
this.onUpdateHeight(diffHeight);
}
return resized;
};
/**
* Get the visible range from the given window
* @param {Object} window An object with parameters top, left, width,
* height.
* @param {Object} currentRange optional, current range. makes getting the range
* faster. Object containing a parameter offset and
* limit
* @return {Object} range An object with parameters offset and limit
*/
links.TreeGrid.Grid.prototype._getRangeFromWindow = function(window, currentRange) {
// use the current range as start
var defaultHeight = this.options.items.defaultHeight,
itemCount = (this.itemCount != undefined) ? this.itemCount : Math.ceil(window.height / defaultHeight),
windowTop = -this.getAbsTop() + this.header.getHeight(), // normalize the top
windowBottom = windowTop + window.height - this.header.getHeight(),
newOffset = currentRange ? currentRange.offset : 0,
newLimit = 0;
var item, top, height, bottom;
//console.log('_getRangeFromWindow', window.top, window.top + window.height, this.top, this.top + this.height)
// find the first visible item
item = this.items[newOffset];
top = item ? item.top : this._calculateItemTop(newOffset);
height = item ? item.getHeight() : defaultHeight;
bottom = top + height;
while ((newOffset < itemCount - 1) && (bottom < windowTop)) {
newOffset++;
item = this.items[newOffset];
top = bottom + this.dropAreaHeight;
height = item ? item.getHeight() : defaultHeight;
bottom = top + height;
}
while ((newOffset > 0) && top > windowTop) {
newOffset--;
item = this.items[newOffset];
height = item ? item.getHeight() : defaultHeight;
bottom = top;
top = top - height - this.dropAreaHeight;
}
// find the last visible item
while ((newOffset + newLimit < itemCount - 1) && (top < windowBottom)) {
newLimit++;
item = this.items[newOffset + newLimit];
top = bottom + this.dropAreaHeight;
height = item ? item.getHeight() : defaultHeight;
bottom = top + height;
//console.log('item', newOffset + newLimit, top, height, bottom, windowBottom) // TODO: cleanup
}
if (top < windowBottom && bottom > windowTop && newOffset + newLimit < itemCount) {
newLimit++;
}
// console.log('range', this.left, newOffset, newLimit, newLimit ? newOffset + newLimit-1 : undefined); // TODO: cleanup
return {
'offset': newOffset,
'limit': newLimit
};
};
/**
* Repaint the HTML DOM fields of this item
*/
links.TreeGrid.Item.prototype._repaintFields = function() {
var field;
if (this.isVisible() && this.fields) {
// check if the fields are changed
var fields = this.fields;
var prevFields = this.prevFields;
if (fields != prevFields) {
// fields are changed. remove old dom
if (this.dom.frame) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
delete this.dom.frame;
}
this.prevFields = fields;
}
var domFrame = this.dom.frame;
if (!domFrame) {
// create the dom frame
var domFrame = document.createElement('DIV');
domFrame.className = 'treegrid-item';
domFrame.style.position = 'absolute'; // TODO
//domFrame.style.position = 'relative';
domFrame.item = this;
domFrame.treeGridType = 'item';
this.dom.frame = domFrame;
//this.getContainer().appendChild(domFrame); // TODO
// create expand button
if (this.dataConnector) {
var buttonExpand = document.createElement('button');
buttonExpand.treeGridType = 'expand';
buttonExpand.className = this.expanded ? 'treegrid-unfold' : 'treegrid-fold';
buttonExpand.style.position = 'absolute';
buttonExpand.grid = this; // TODO: is this used and needed?
buttonExpand.node = this;
buttonExpand.index = this.index; // TODO: remove this index, use the node instead
domFrame.appendChild(buttonExpand);
this.dom.buttonExpand = buttonExpand;
}
// create icons
var icons = this.icons;
if (icons) {
var domIcons = document.createElement('DIV');
domIcons.className = 'treegrid-icons';
domIcons.style.position = 'absolute';
domIcons.style.top = '0px';
for (var i = 0, iMax = icons.length; i < iMax; i++) {
var icon = icons[i];
if (icon && icon.image) {
var domIcon = document.createElement('img');
domIcon.className = 'treegrid-icon';
domIcon.src = icon.image;
domIcon.title = icon.title ? icon.title : '';
domIcon.style.width = icon.width ? icon.width : '';
domIcon.style.height = icon.height ? icon.height : '';
domIcons.appendChild(domIcon);
}
}
domFrame.appendChild(domIcons);
this.dom.icons = domIcons;
}
// create the fields
var domFields = [];
this.dom.fields = domFields;
var padding = this.options.padding;
var fields = this.fields;
var height = this.height;
for (var i = 0, iMax = fields.length; i < iMax; i++) {
var field = fields[i];
var domField = document.createElement('DIV');
domField.className = 'treegrid-item-field';
domField.style.position = 'absolute';
//domField.style.position = 'relative';
domField.style.top = '0px';
var col = this.columns[i];
if (col.fixedWidth) {
domField.style.width = col.width + 'px';
}
domField.innerHTML = field;
domFrame.appendChild(domField);
domFields.push(domField);
}
// create the actions
if (this.actions) {
var actions = this.actions;
var domActions = document.createElement('DIV');
this.dom.actions = domActions;
domActions.style.position = 'absolute';
domActions.className = 'treegrid-actions';
domActions.style.top = 0 + 'px';
domActions.style.right = 24 + 'px'; // reckon with width of the scrollbar
for (var i = 0, iMax = actions.length; i < iMax; i++) {
var action = actions[i];
if (action.event) {
if (action.image) {
// create an image button
var domAction = document.createElement('INPUT');
domAction.treeGridType = 'action';
domAction.type = 'image';
domAction.className = 'treegrid-action-image';
domAction.title = action.title || '';
domAction.src = action.image;
domAction.event = action.event;
domAction.item = this;
domActions.appendChild(domAction);
}
else {
// create a text link
var domAction = document.createElement('A');
domAction.treeGridType = 'action';
domAction.className = 'treegrid-action-link';
domAction.href = '#';
domAction.title = action.title || '';
domAction.innerHTML = action.text ? action.text : action.event;
domAction.event = action.event;
domAction.item = this;
domActions.appendChild(domAction);
}
}
else {
// TODO: throw warning?
}
}
domFrame.appendChild(domActions);
}
// create event handlers for drag and drop
var item = this;
// TODO: not so nice accessing the parent grid like this
var dataTransfer = this.dataConnector ? this.dataConnector.getOptions().dataTransfer : undefined;
if (dataTransfer) {
if (dataTransfer.dropEffect != undefined && dataTransfer.dropEffect != 'none') {
this.dataTransfer.dropEffect = dataTransfer.dropEffect;
links.dnd.makeDroppable(domFrame, {
'dropEffect':dataTransfer.dropEffect,
'drop':function (event) {
item.onDrop(event);
},
'dragEnter':function (event) {
item.onDragEnter(event);
},
'dragOver':function (event) {
item.onDragOver(event);
},
'dragLeave':function (event) {
item.onDragLeave(event);
}
});
}
}
else if (this.parent && this.parent.dataConnector) {
// Check if the items parent has a dataconnector with dropEffect 'link'
var dataTransfer = this.parent.dataConnector.getOptions().dataTransfer;
if (dataTransfer && dataTransfer.dropEffect == 'link') {
this.dataTransfer.dropEffect = dataTransfer.dropEffect;
links.dnd.makeDroppable(domFrame, {
'dropEffect':dataTransfer.dropEffect,
'drop':function (event) {
item.onDrop(event);
},
'dragEnter':function (event) {
item.onDragEnter(event);
},
'dragOver':function (event) {
item.onDragOver(event);
},
'dragLeave':function (event) {
item.onDragLeave(event);
}
});
}
}
}
if (!domFrame.parentNode) {
this.getContainer().appendChild(domFrame);
}
// position the frame
domFrame.style.top = this.getAbsTop() + 'px';
domFrame.style.left = this.getAbsLeft() + 'px';
// TODO
//domFrame.style.top = 0 + 'px';
//domFrame.style.left = 0 + 'px';
domFrame.style.height = this.fieldsHeight + 'px';
domFrame.style.width = this.width - 2 + 'px';
// position the icons
var domIcons = this.dom.icons;
if (domIcons) {
domIcons.style.left = this.options.indentationWidth + 'px';
}
// position the fields
var domFields = this.dom.fields;
if (domFields) {
for (var i = 0, iMax = this.columns.length; i < iMax; i++) {
var col = this.columns[i];
var domField = domFields[i];
if (domField) {
domField.style.left = col.left + 'px';
}
}
}
// show/hide the expand button (hide in case of error, to make place for an error icon)
if (this.dom.buttonExpand) {
this.dom.buttonExpand.style.visibility = (this.error == undefined) ? 'visible' : 'hidden';
}
// check the class name depending on the status
var className = 'treegrid-item';
if (this.selected || this.dataTransfer.dragging) {
className += ' treegrid-item-selected';
}
else if (this.dataTransfer.dragover) {
className += ' treegrid-item-dragover';
}
else if (this.dataTransfer.dragbefore) {
className += ' treegrid-item-dragbefore';
}
if (this.dirty) {
className += ' treegrid-item-dirty';
}
domFrame.className = className;
}
else {
links.dnd.removeDraggable(this.dom.frame);
links.dnd.removeDroppable(this.dom.frame);
if (this.dom.frame && this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
}
if (this.dom.frame) {
this.dom = {};
}
}
};
/**
* Repaint the subgrid of this item, if available.
*/
links.TreeGrid.Item.prototype._repaintGrid = function() {
if (this.grid) {
this.grid.repaint();
}
};
/**
* Repaint the loading text and icon (when the item is being loaded).
*/
links.TreeGrid.Item.prototype._repaintLoading = function() {
// loading icon
if (this.isVisible() && this.loading && (!this.fields || this.error || this.dirty)) {
var domLoadingIcon = this.dom.loadingIcon;
if (!domLoadingIcon) {
// create loading icon
domLoadingIcon = document.createElement('DIV');
domLoadingIcon.className = 'treegrid-loading-icon';
domLoadingIcon.style.position = 'absolute';
//domLoadingIcon.style.top = '0px';
//domLoadingIcon.style.left = '0px';
domLoadingIcon.title = 'loading...';
this.getContainer().appendChild(domLoadingIcon);
this.dom.loadingIcon = domLoadingIcon;
}
// position loading icon
domLoadingIcon.style.top = this.getAbsTop() + 'px';
domLoadingIcon.style.left = this.getAbsLeft() + 'px';
}
else {
// delete loading icon
if (this.dom.loadingIcon) {
this.dom.loadingIcon.parentNode.removeChild(this.dom.loadingIcon);
delete this.dom.loadingIcon;
}
}
// loading text
if (this.isVisible() && this.loading && !this.fields && !this.error) {
var domLoadingText = this.dom.loadingText;
if (!domLoadingText) {
// create loading text
domLoadingText = document.createElement('DIV');
domLoadingText.style.position = 'absolute';
domLoadingText.appendChild(document.createTextNode('loading...'));
domLoadingText.className = 'treegrid-loading';
this.getContainer().appendChild(domLoadingText);
this.dom.loadingText = domLoadingText;
}
// position loading text
domLoadingText.style.top = this.getAbsTop() + 'px';
domLoadingText.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px';
domLoadingText.style.height = this.height + 'px';
}
else {
// delete loading text
if (this.dom.loadingText) {
this.dom.loadingText.parentNode.removeChild(this.dom.loadingText);
delete this.dom.loadingText;
}
}
};
/**
* Repaint error text and icon when the item contains an error
*/
links.TreeGrid.Item.prototype._repaintError = function() {
if (this.isVisible() && this.error && !this.fields) {
// create item error
var domError = this.dom.error;
if (!domError) {
// create the dom
domError = document.createElement('DIV');
domError.style.position = 'absolute';
domError.appendChild(document.createTextNode('Error: ' +
links.TreeGrid.Grid._errorToString(this.error)));
domError.className = 'treegrid-error';
this.getContainer().appendChild(domError);
this.dom.error = domError;
}
// position item error
domError.style.top = this.getAbsTop() + 'px';
domError.style.left = this.getAbsLeft() + this.options.indentationWidth + 'px';
}
else {
// delete item error
if (this.dom.error) {
this.dom.error.parentNode.removeChild(this.dom.error);
delete this.dom.error;
}
}
// redraw error icon
if (this.isVisible() && this.error && !this.loading) {
var domErrorIcon = this.dom.errorIcon;
if (!domErrorIcon) {
// create error icon
domErrorIcon = document.createElement('DIV');
domErrorIcon.className = 'treegrid-error-icon';
domErrorIcon.style.position = 'absolute';
domErrorIcon.title = this.error;
this.getContainer().appendChild(domErrorIcon);
this.dom.errorIcon = domErrorIcon;
}
// position the error icon
var absLeft = this.getAbsLeft();
domErrorIcon.style.top = Math.max(this.getAbsTop(), 0) + 'px';
domErrorIcon.style.left = absLeft + 'px';
}
else {
if (this.dom.errorIcon) {
this.dom.errorIcon.parentNode.removeChild(this.dom.errorIcon);
this.dom.errorIcon = undefined;
}
}
};
/**
* @constructor links.TreeGrid.DropArea
* @param {Object} params. A key-value map containing parameters:
* grid, item, top, height
*/
links.TreeGrid.DropArea = function (params) {
if (params) {
this.dataConnector = params.dataConnector || undefined;
this.item = params.item || undefined;
this.top = params.top || 0;
this.height = params.height || 6;
}
this.dragover = false;
// data
this.dom = {};
};
links.TreeGrid.DropArea.prototype = new links.TreeGrid.Node();
/**
* reflow the DropArea
*/
links.TreeGrid.DropArea.prototype.reflow = function () {
this.width = this.getVisibleWindow().width - this.getAbsLeft();
};
/**
* repaint the droparea
*/
links.TreeGrid.DropArea.prototype.repaint = function () {
if (this.isVisible()) {
var dropArea = this.dom.dropArea;
// create the droparea
if (!dropArea) {
var container = this.getContainer();
dropArea = document.createElement('DIV');
dropArea.style.position = 'absolute';
dropArea.style.left = '0px';
dropArea.style.top = '0px';
dropArea.style.height = this.height + 'px';
container.appendChild(dropArea);
this.dom.dropArea = dropArea;
// drop events
var dataTransfer = this.dataConnector ? this.dataConnector.getOptions().dataTransfer : undefined;
if (dataTransfer) {
var me = this;
this.dropEffect = dataTransfer.dropEffect;
links.TreeGrid.addEventListener(dropArea, 'dragover',
function (event) {
return me.onDragOver(event);
});
links.TreeGrid.addEventListener(dropArea, 'dragenter',
function (event) {
return me.onDragEnter(event);
});
links.TreeGrid.addEventListener(dropArea, 'dragleave',
function (event) {
return me.onDragLeave(event);
});
links.TreeGrid.addEventListener(dropArea, 'drop',
function (event) {
return me.onDrop(event);
});
}
}
// position the droparea
dropArea.className = this.dragover ? 'treegrid-droparea' : '';
dropArea.style.top = this.getAbsTop() + 'px';
dropArea.style.left = this.getAbsLeft() + 'px';
dropArea.style.width = (this.width - 2) + 'px';
}
else {
if (this.dom.dropArea) {
this.dom.dropArea.parentNode.removeChild(this.dom.dropArea);
delete this.dom.dropArea;
}
}
};
/**
* Event handler for drag over event
*/
links.TreeGrid.DropArea.prototype.onDragOver = function(event) {
this.dragover = true;
event.dataTransfer.allowedEffect = 'none';
event.dataTransfer.dropEffect = this.dropEffect || 'none';
this.repaint();
links.TreeGrid.preventDefault(event);
return false;
};
/**
* Event handler for drag enter event
* this will highlight the current item
*/
links.TreeGrid.DropArea.prototype.onDragEnter = function(event) {
this.dragover = true;
this.repaint();
event.dataTransfer.allowedEffect = 'none';
event.dataTransfer.dropEffect = this.dataConnector ? 'move' : 'none';
return false;
};
/**
* Event handler for drag leave event
*/
links.TreeGrid.DropArea.prototype.onDragLeave = function(event) {
//console.log('onDragLeave', event);
this.dragover = false;
this.repaint();
return false;
};
/**
* Event handler for drop event
*/
links.TreeGrid.DropArea.prototype.onDrop = function(event) {
var data = JSON.parse(event.dataTransfer.getData('text'));
this.dragover = false;
this.repaint();
//console.log('onDrop', event);
if (this.dataConnector) {
var me = this;
var callback = function () {
me.parent.onResize();
};
var errback = callback;
var items = [data.item];
var itemBefore = this.item.data;
this.dataConnector.insertItemsBefore(items, itemBefore, callback, errback);
/* TODO: trigger event?
// send drop event
this.dataConnector._onEvent('drop', {
'dataConnector': this.dataConnector,
'dropEffect': event.dataTransfer.dropEffect,
'items': items
});
*/
}
else {
console.log('dropped but do nothing', event.dataTransfer.dropEffect);
}
links.TreeGrid.preventDefault(event);
};
/**
* Convert an error to string
* @param {*} err
* @return {String} err
*/
links.TreeGrid.Grid._errorToString = function(err) {
if (typeof(err) == 'string') {
return err;
}
else if (err instanceof Object) {
if (err.message && typeof(err.message) == 'string') {
return err.message;
}
if (err.error && typeof(err.error) == 'string') {
return err.error;
}
if (JSON) {
return JSON.stringify(err);
}
}
return String(err);
};
/**
* @prototype VerticalScroll
* creates a vertical scrollbar in given HTML DOM element
* @param {Element} container Scroll bar will be created inside this
* container
* @param {Number} min Minimum value for the scrollbar
* @param {Number} max Maximum value for the scrollbar
* @param {Number} value Current value of the scrollbar
*/
links.TreeGrid.VerticalScroll = function (container, min, max, value) {
this.container = container;
this.dom = {};
this.height = 0;
this.min = 0;
this.max = 0;
this.value = 0;
// eventParams can contain event data for example on mouse down.
this.eventParams = {};
this.onChangeHandlers = [];
this.setInterval(min, max);
this.set(value);
};
/**
* Redraw the scrollbar
*/
links.TreeGrid.VerticalScroll.prototype.redraw = function () {
var background = this.dom.background;
if (!background) {
background = document.createElement('div');
background.className = 'treegrid-verticalscroll-background';
background.style.width = '100%';
background.style.height = '100%';
this.container.appendChild(background);
this.dom.background = background;
}
var bar = this.dom.bar;
if (!bar) {
bar = document.createElement('div');
bar.className = 'treegrid-verticalscroll-bar';
bar.style.position = 'absolute';
bar.style.left = '20%';
bar.style.width = '60%';
bar.style.right = '20%';
bar.style.top = '0px';
bar.style.height = '0px';
this.container.appendChild(bar);
var me = this;
var params = this.eventParams;
params._onMouseDown = function (event) {
me._onMouseDown(event);
};
links.TreeGrid.addEventListener(bar, 'mousedown', params._onMouseDown);
this.dom.bar = bar;
}
// position the bar
var interval = (this.max - this.min);
if (interval > 0) {
var height = this.height;
var borderWidth = 2; // TODO: retrieve borderWidth from css?
var barHeight = Math.max(height * height / (interval + height), 20);
var barTop = this.value * (height - barHeight - 2 * borderWidth) / interval;
bar.style.height = barHeight + 'px';
bar.style.top = barTop + 'px';
bar.style.display = '';
}
else {
bar.style.display = 'none';
}
};
/**
* Check if the scrollbar is resized and if so, redraw the scrollbar
* @return {Boolean} resized
*/
links.TreeGrid.VerticalScroll.prototype.checkResize = function () {
var resized = this.reflow();
if (resized) {
this.redraw();
}
return resized;
};
/**
* Recalculate the size of the elements of the scrollbar
*/
links.TreeGrid.VerticalScroll.prototype.reflow = function () {
var resized = false;
this.height = this.dom.background.clientHeight;
return resized;
};
/**
* Set the interval for the vertical scroll bar
* @param {Number} min Minimum value, start of the interval
* @param {Number} max Maximum value, end of the interval
*/
links.TreeGrid.VerticalScroll.prototype.setInterval = function (min, max) {
this.min = min || 0;
this.max = max || 0;
if (this.max < this.min) {
this.max = this.min;
}
// value may be out of range now, so set it again
this.set(this.value);
};
/**
* Set the current value of the scrollbar
* The value must be within the range of the scrollbar
* @param {Number} value
*/
links.TreeGrid.VerticalScroll.prototype.set = function (value) {
this.value = value || this.min;
if (this.value < this.min) {
this.value = this.min;
}
if (this.value > this.max) {
this.value = this.max;
}
this.redraw();
};
/**
* Increase or decrease the value of the scrollbar by a delta
* @param {Number} delta A positive or negative value
*/
links.TreeGrid.VerticalScroll.prototype.increase = function (delta) {
var value = this.get();
value += delta;
this.set(value);
};
/**
* Retrieve the current value of the scrollbar
* @return {Number} value
*/
links.TreeGrid.VerticalScroll.prototype.get = function () {
return this.value;
};
/**
* Handler for mouse down event for scrollbar
* @param {Event} event
*/
links.TreeGrid.VerticalScroll.prototype._onMouseDown = function(event) {
var params = this.eventParams;
event = event || window.event;
params.startMouseX = event.clientX;
params.startMouseY = event.clientY;
params.startValue = this.value;
var me = this;
if (!params._onMouseMove) {
params._onMouseMove = function (event) {me._onMouseMove(event);};
links.TreeGrid.addEventListener(document, "mousemove", params._onMouseMove);
}
if (!params._onMouseUp) {
params._onMouseUp = function (event) {me._onMouseUp(event);};
links.TreeGrid.addEventListener(document, "mouseup", params._onMouseUp);
}
links.TreeGrid.preventDefault(event);
links.TreeGrid.stopPropagation(event);
};
/**
* Handler for mouse move event for scrollbar
* @param {Event} event
*/
links.TreeGrid.VerticalScroll.prototype._onMouseMove = function(event) {
var params = this.eventParams;
event = event || window.event;
var mouseX = event.clientX;
var mouseY = event.clientY;
var diffX = mouseX - params.startMouseX;
var diffY = mouseY - params.startMouseY;
var interval = (this.max - this.min);
var diff = (diffY / this.height) * (interval + this.height);
this.set(params.startValue + diff);
this._callbackOnChangeHandlers();
links.TreeGrid.preventDefault(event);
links.TreeGrid.stopPropagation(event);
};
/**
* Handler for mouse up event for scrollbar
* @param {Event} event
*/
links.TreeGrid.VerticalScroll.prototype._onMouseUp = function(event) {
var params = this.eventParams;
var me = this;
// remove event listeners
if (params._onMouseMove) {
links.TreeGrid.removeEventListener(document, "mousemove", params._onMouseMove);
params._onMouseMove = undefined;
}
if (!params.onMouseUp) {
links.TreeGrid.removeEventListener(document, "mouseup", params._onMouseUp);
params._onMouseUp = undefined;
}
links.TreeGrid.preventDefault(event);
links.TreeGrid.stopPropagation(event);
};
/**
* Add a callback hander which is executed when the value of the scroll
* bar is changed by the user (not after the method set() is executed)
* The callback is executed with the new value as parameter
* @param {Function} callback
*/
links.TreeGrid.VerticalScroll.prototype.addOnChangeHandler = function(callback) {
this.removeOnChangeHandler(callback);
this.onChangeHandlers.push(callback);
};
/**
* Remove an onchange callback hander
* @param {Function} callback Handler to be removed
*/
links.TreeGrid.VerticalScroll.prototype.removeOnChangeHandler = function(callback) {
var index = this.onChangeHandlers.indexOf(callback);
this.onChangeHandlers.splice(index, 1);
};
/**
* Call all onchange callback handlers
*/
links.TreeGrid.VerticalScroll.prototype._callbackOnChangeHandlers = function() {
var handlers = this.onChangeHandlers;
var value = this.value;
for (var i = 0, iMax = handlers.length; i < iMax; i++) {
handlers[i](value);
}
};
/**
* @constructor links.DataConnector
* this prototype should be inherited and its methods must be overwritten
* @param {Object} options
*/
links.DataConnector = function (options) {
this.options = options || {};
this.eventListeners = []; // registered event handlers
};
/**
* Trigger an event
* @param {String} event
* @param {Object} params
*/
links.DataConnector.prototype.trigger = function (event, params) {
// send the event to the treegrid
links.events.trigger(this, event, params);
// trigger the google event bus
if (google && google.visualization && google.visualization.events) {
google.visualization.events.trigger(this, event, params);
}
// TODO: remove this code?
// send the event to all event listeners
var eventListeners = this.eventListeners;
for (var i = 0, iMax = eventListeners.length; i < iMax; i++) {
var callback = eventListeners[i];
callback (event, params);
}
};
/**
* Asynchronously check for changes for a number of items.
* The method will return the items which are changed.
* The changed items can be updated via the method getItems.
* @param {Number} index Index of the first item to be checked
* @param {Number} num Number of items to be checked
* @param {Object[]} items A list with the current versions of these items
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The changed items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.getChanges = function (index, num, items,
callback, errback) {
throw 'Error: method getChanges is not implemented';
};
/**
* Asynchronously get a number of items by index
* @param {Number} index Index of the first item to be retrieved
* @param {Number} num Number of items to be retrieved
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.getItems = function (index, num, callback, errback) {
errback('Error: method getItems is not implemented');
};
/**
* Asynchronously update a number of items.
* The callback returns the updated items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be updated
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The updated items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.updateItems = function (items, callback, errback) {
errback('Error: method updateItems is not implemented');
};
/**
* Asynchronously append a number of items.
* The callback returns the appended items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be added
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The appended items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.appendItems = function (items, callback, errback) {
errback('Error: method appendItems is not implemented');
};
/**
* Asynchronously insert a number of items.
* The callback returns the inserted items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be inserted
* @param {Object} beforeItem The items will be inserted before this item.
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The inserted items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.insertItemsBefore = function (items, beforeItem, callback, errback) {
errback('Error: method insertItemsBefore is not implemented');
};
/**
* Asynchronously move a number of items.
* The callback returns the moved items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be moved
* @param {Object} beforeItem The items will be inserted before this item.
* When beforeItem is undefined, the items will be
* moved to the end of the data.
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The moved items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.moveItems = function (items, beforeItem, callback, errback) {
errback('Error: method moveItems is not implemented');
};
/**
* Asynchronously remove a number of items.
* The callback returns the removed items.
* @param {Object[]} items A list with items to be removed
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The removed items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.removeItems = function (items, callback, errback) {
errback('Error: method removeItems is not implemented');
};
/**
* Asynchronously link a source item to a target item.
* The callback returns the linked items.
* @param {Object[]} sourceItems
* @param {Object} targetItem
* @param {function} callback Callback method called on success. Called with
* one object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The removed items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataConnector.prototype.linkItems = function (sourceItems, targetItem, callback, errback) {
errback('Error: method linkItems is not implemented');
};
/**
* internal onEvent handler
* @param {String} event
* @param {Object} params. Object containing index (Number),
* and item (Object).
*/
links.DataConnector.prototype._onEvent = function (event, params) {
this.trigger(event, params);
this.onEvent(event, params);
};
/**
* onEvent handler
* @param {String} event
* @param {Object} params. Object containing index (Number),
* and item (Object).
*/
links.DataConnector.prototype.onEvent = function (event, params) {
// this method can be overwritten
};
// TODO: comment
links.DataConnector.prototype.setFilters = function (filters) {
errback('Error: method setFilters is not implemented');
};
/**
* Add an event listener to the DataConnector
* @param {function} callback The callback method will be called with two
* parameters:
* {String} event
* {Object} params
*/
links.DataConnector.prototype.addEventListener = function (callback) {
var index = this.eventListeners.indexOf(callback);
if (index == -1) {
this.eventListeners.push(callback);
}
};
/**
* Remove an event listener from the DataConnector
* @param {function} callback The registered callback method
*/
links.DataConnector.prototype.removeEventListener = function (callback) {
var index = this.eventListeners.indexOf(callback);
if (index != -1) {
this.eventListeners.splice(index, 1);
}
};
/**
* Set options for the dataconnector
* @param {Object} options Available options:
* 'columns':
* An array containing objects, each object
* contains parameters 'name', and optionally
* 'text' and 'title'. The provided fields will
* be displayed in the given order.
* 'dataTransfer':
* An object containing the parameters:
* 'allowedEffect':
* A string value 'none', 'link', 'move', or 'copy'
* 'dropEffect':
* A string value 'none', 'link', 'move', or 'copy
*/
links.DataConnector.prototype.setOptions = function (options) {
this.options = options || {};
};
/**
* Get the currently set options
*/
links.DataConnector.prototype.getOptions = function () {
return this.options;
};
/**
* @constructor links.DataTable
* Asynchronous link to a data table
* @param {Array} data A javascript array containing objects
* @param {Object} options
*/
links.DataTable = function (data, options) {
this.data = data || [];
this.setOptions(options);
this.filteredData = this.data;
};
links.DataTable.prototype = new links.DataConnector();
/**
* Asynchronously get a number of items by index
* @param {Number} index Index of the first item to be retrieved
* @param {Number} num Number of items to be retrieved
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.getItems = function (index, num, callback, errback) {
var items = [],
filteredData = this.filteredData,
count = filteredData.length;
for (var i = index, iMax = Math.min(index + num, count) ; i < iMax; i++) {
items.push(filteredData[i]);
}
callback({
'totalItems': filteredData.length,
'items': items
});
};
/**
* Asynchronously update a number of items.
* The callback returns the updated items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be updated
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The updated items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.updateItems = function (items, callback, errback) {
var num = items.length;
var data = this.data;
for (var i = 0; i < num; i++) {
var item = items[i];
var index = data.indexOf(item);
if (index != -1) {
data[index] = item;
}
else {
errback("Cannot find item"); // TODO: better error
return;
}
}
// perform filtering and sorting again if there is a filter set
this.updateFilters();
callback({
'totalItems': this.filteredData.length,
'items': items
});
this.trigger('change', undefined);
};
/**
* Asynchronously append a number of items.
* The callback returns the appended items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be added
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The appended items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.appendItems = function (items, callback, errback) {
var num = items.length;
for (var i = 0; i < num; i++) {
this.data.push(items[i]);
}
// perform filtering and sorting again if there is a filter set
this.updateFilters();
callback({
'totalItems': this.filteredData.length,
'items': items
});
this.trigger('change', undefined);
};
/**
* Asynchronously insert a number of items.
* The callback returns the inserted items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be inserted
* @param {Object} beforeItem The items will be inserted before this item.
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The inserted items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.insertItemsBefore = function (items, beforeItem, callback, errback) {
// find the item before which the new items will be inserted
var data = this.data;
var beforeIndex = data.indexOf(beforeItem);
if (beforeIndex == -1) {
errback("Cannot find item"); // TODO: better error
return;
}
// insert the new data
var num = items.length;
for (var i = 0; i < num; i++) {
data.splice(beforeIndex + i, 0, items[i]);
}
// perform filtering and sorting again if there is a filter set
this.updateFilters();
callback({
'totalItems': this.filteredData.length,
'items': items
});
this.trigger('change', undefined);
};
/**
* Asynchronously move a number of items.
* The callback returns the moved items, which may be newly instantiated objects .
* @param {Object[]} items A list with items to be moved
* @param {Object} beforeItem The items will be inserted before this item.
* When beforeItem is undefined, the items will be
* moved to the end of the data.
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The moved items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.moveItems = function (items, beforeItem, callback, errback) {
// find the index of the before item
var beforeIndex = this.data.indexOf(beforeItem);
if (beforeIndex == -1) {
errback("Cannot find item"); // TODO: better error
return;
}
// find the indexes of all items
var num = items.length;
var indexes = [];
for (var i = 0; i < num; i++) {
var index = this.data.indexOf(items[i]);
if (index != -1) {
indexes[i] = index;
}
else {
errback("Cannot find item"); // TODO: better error
return;
}
}
// if all items are found, move them
for (var i = 0; i < num; i++) {
this.data.splice(indexes[i], 1);
this.data.splice(beforeIndex, 0, items[i]);
}
// perform filtering and sorting again if there is a filter set
this.updateFilters();
callback({
'totalItems': this.filteredData.length,
'items': items
});
this.trigger('change', undefined);
};
/**
* Asynchronously remove a number of items.
* The callback returns the removed items.
* @param {Object[]} items A list with items to be removed
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The removed items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.removeItems = function (items, callback, errback) {
var num = items.length;
for (var i = 0; i < num; i++) {
var index = this.data.indexOf(items[i]);
if (index != -1) {
this.data.splice(index, 1);
}
else {
errback("Cannot find item"); // TODO: better error
return;
}
}
// perform filtering and sorting again if there is a filter set
this.updateFilters();
callback({
'totalItems': this.filteredData.length,
'items': items
});
this.trigger('change', undefined);
};
/**
* Asynchronously check for changes for a number of items.
* The method will return the items which are changed.
* The changed items can be updated via the method getItems.
* @param {Number} index Index of the first item to be checked
* @param {Number} num Number of items to be checked
* @param {Object[]} items A list with items to be checked for changes.
* @param {function} callback Callback method called on success. Called with one
* object as parameter, containing fields:
* {Number} totalItems
* {Array with Objects} items The changed items
* @param {function} errback Callback method called on failure. Called with
* an error message as parameter.
*/
links.DataTable.prototype.getChanges = function (index, num, items, callback, errback) {
var changedItems = [],
filteredData = this.filteredData,
count = filteredData.length;
for (var i = 0; i < num; i++) {
var item = items[i];
if (item != filteredData[index + i]) {
changedItems.push(item);
}
}
callback({
'totalItems': this.filteredData.length,
'items': changedItems
});
};
/**
* Force the DataTable to be changed by incrementing the update sequence
*/
links.DataTable.prototype.update = function () {
this.updateFilters();
this.trigger('change', undefined);
};
/**
* onEvent handler. Can be overwritten by an implementation
* @param {String} event
* @param {Object} params
*/
// TODO: remove the onEvent handler?
links.DataTable.prototype.onEvent = function (event, params) {
};
/**
* Update the filters (if any).
* This method is executed after the data has been changed.
*/
links.DataTable.prototype.updateFilters = function () {
if (this.filters) {
this.setFilters(this.filters);
}
else {
this.filteredData = this.data;
}
};
/**
* Set a filter for this DataTable
* @param {Object[]} filters An array containing filter objects.
* a filter object contains parameters
* field, value, startValue, endValue,
* values, order
*/
// TODO: comment
links.DataTable.prototype.setFilters = function (filters) {
var data = this.data;
var filteredData = [];
this.filteredData = filteredData;
this.filters = filters;
// filter the data
for (var i = 0, iMax = data.length; i < iMax; i++) {
var item = data[i];
var emit = true;
for (var f = 0, fMax = filters.length; f < fMax; f++) {
var filter = filters[f];
if (filter.field) {
var value = item[filter.field];
if (filter.value && (value != filter.value)) {
emit = false;
}
if (filter.startValue && value < filter.startValue) {
emit = false;
}
if (filter.endValue && value > filter.endValue) {
emit = false;
}
if (filter.values && (filter.values.indexOf(value) == -1)) {
emit = false;
}
}
}
if (emit) {
filteredData.push(item);
}
}
// create a list with fields that need to be ordered
var orders = [];
for (var f = 0, fMax = filters.length; f < fMax; f++) {
var filter = filters[f];
if (filter.field && filter.order) {
var order = filter.order.toUpperCase();
if (order == 'ASC' || order == 'DESC') {
orders.push({
'field': filter.field,
'direction': ((order == 'ASC') ? 1 : -1)
});
}
else {
throw 'Unknown order "' + order + '". ' +
'Available values: "ASC", "DESC".';
}
}
}
// order the filtered data
var ordersLength = orders.length;
if (ordersLength) {
filteredData.sort(function (a, b) {
for (var i = 0; i < ordersLength; i++) {
var order = orders[i],
field = order.field,
direction = order.direction;
if (a[field] == b[field]) {
if (i == ordersLength - 1) {
return 0;
}
else {
// compare with the next filter
}
}
else {
return (a[field] > b[field]) ? direction : -direction;
}
}
});
}
};
/**
* @constructor links.CouchConnector
* @param {String} url Url can point to a database or to a view
*/
// TODO: update the couchconnector
links.CouchConnector = function (url) {
this.url = url;
this.data = [];
this.filter = undefined;
this.updateSeq = undefined;
this.blockSize = 16; // data will be retrieved in blocks of this blockSize
// TODO: make blockSize customizable
this.totalItems = this.blockSize;
};
links.CouchConnector.prototype = new links.DataConnector();
links.CouchConnector.prototype.getItems = function (index, num, callback, errback) {
// first check if the requested data is already loaded
var me = this;
var data = this.data;
var dataComplete = true;
for (var i = index, iMax = index + num; i < iMax; i++) {
if (data[i] == undefined) {
dataComplete = false;
break;
}
}
// TODO: smarter retrieve only the missing parts of the data, not the whole interval again.
function getSubset (index, num) {
var dataSubset = [];
for (var i = index, iMax = index + num; i < iMax; i++) {
var d = data[i];
dataSubset.push(d ? d.value : undefined);
}
return dataSubset;
}
if (dataComplete) {
var dataChanged = false;
// if all data is available, check if data has changed on the server
// TODO: check change of update sequence
if (dataChanged) {
// clear all data
this.data = [];
data = this.data;
dataComplete = false;
}
}
if (!dataComplete) {
// choose skip and limit to match block size
var skipRem = index % this.blockSize,
skip = index - skipRem,
limitRem = (num + skipRem) % this.blockSize,
limit = (num + skipRem - limitRem) + (limitRem != 0 ? this.blockSize : 0);
// cut off the part of items which are already loaded
while (data[skip] && limit > 0) {
skip++;
limit--;
}
while (data[skip + limit - 1] && limit > 0) {
limit--;
}
// find a startkey, to spead up the request
var startkey = undefined;
var startKeyIndex = skip - 1;
while (startKeyIndex > 0 && data[startKeyIndex] == undefined) {
startKeyIndex--;
}
if (data[startKeyIndex]) {
startkey = data[startKeyIndex].key;
}
var separator = (this.url.indexOf('?') == -1) ? '?' : '&';
var url;
if (startkey) {
url = this.url + separator +
'skip=' + (skip - startKeyIndex) +
'&limit=' + limit +
'&startkey=' + escape(JSON.stringify(startkey));
// TODO: reckon with filter?
}
else {
url = this.url + separator + 'skip=' + skip + '&limit=' + limit;
if (this.filter) {
if (this.filter.value != undefined) {
url += '&key=' + escape(JSON.stringify(this.filter.value));
}
if (this.filter.startValue != undefined) {
url += '&startkey=' + escape(JSON.stringify(this.filter.startValue));
}
if (this.filter.endValue != undefined) {
url += '&endkey=' + escape(JSON.stringify(this.filter.endValue));
}
}
}
/* TODO: descending order
if (this.filter && this.filter.order) {
if (this.filter.order == 'DESC') {
url += '&descending=true';
// TODO: startkey and endkey must be interchanged
}
}
*/
// TODO: reckon with filter values
// TODO: reckon with filter order: asc/desc
// TODO: using skip for paginating results is a very bad solution, very slow
// create a smarter solution to retrieve results with an as small as
// possible skip, starting at the closest retrieved document key
//console.log('Retrieving data from server url=' + url);
links.getJSONP(url, function (response) {
if (response.error) {
errback(response);
return;
}
var rows = response.rows;
var dataSubset = [];
for (var i = 0, iMax = rows.length; i < iMax; i++) {
data[i + skip] = rows[i];
}
// set the number of total items
me.totalItems = Math.min(me.data.length + me.blockSize, response.total_rows);
var dataSubset = getSubset(index, num);
callback({
'totalItems': me.totalItems,
'items': dataSubset
});
}, errback);
}
else {
// all data is already loaded
var dataSubset = getSubset(index, num);
callback({
'totalItems': me.totalItems,
'items': dataSubset
});
}
};
links.CouchConnector.prototype._getUpdateSeq = function (callback, errback) {
var viewIndex = this.url.indexOf('_view');
if (viewIndex == -1) {
errback('Error: cannot get information on this view, url is no view');
return;
}
// TODO: check _change?since=3
// http://guide.couchdb.org/draft/notifications.html
var url = this.url.substring(0, viewIndex) + '_info';
links.getJSONP(url, function (info) {
if (data.error) {
errback(data);
return;
}
var update_seq = info.view_index.update_seq;
callback(update_seq);
}, errback);
};
links.CouchConnector.prototype.getChanges = function (index, num, items,
callback, errback) {
// TODO: implement CouchConnector.getChanges, use real update_seq from couch
var changedItems = [];
// TODO: check for changes in the items and in the total count
callback({
'totalItems': (this.totalItems || 10),
'items': changedItems
});
return changedItems;
};
/**
* Set a filter for this DataTable
* @param {Object[]} filters An array containing filter objects.
* a filter object contains parameters
* field, value, startValue, endValue, order
*/
links.CouchConnector.prototype.setFilters = function (filters) {
if (filters.length > 1) {
throw "CouchConnector can currently only handle one filter";
}
else if (filters.length > 0) {
this.filter = filters[0];
}
// TODO: invalidate currently retrieved data
};
/**
* Retrieve a JSON response via javascript injection.
* Note1: it is not possible to know when the injection failed, but you can
* create a timeout which checks if the callback has been called succesfully
* and if not, throw an error.
* Note2: jsonp must be enabled on the server side. (For example in the
* couchdb configuration the option 'allow_jsonp' must be set true)
* @author Jos de Jong
* @param {String} url The url to be retrieved
* @param {function} callback. On response, the callback function will be called
* with the retrieved data as parameter (JSON object)
* @param {function} errback On error, the errback function will be called
* without parameters
*/
links.getJSONP = function (url, callback, errback) {
//console.log('getJSONP ' + url) // TODO: cleanup
// create a random function name to use as temporary callback function
var callbackName = 'callback' + Math.round(Math.random() * 1e10);
// create a script to be injected in the document
var script = document.createElement('script');
var separator = (url.indexOf('?') == -1) ? '?' : '&';
script.src = url + separator + 'callback=' + callbackName;
script.onerror = function (event) {
// clean up created function and script
document.body.removeChild(script);
delete window[callbackName];
if (errback) {
errback();
}
};
script.type = 'text/javascript';
// create the temporary callback function
window[callbackName] = function (data) {
// clean up created function and script
document.body.removeChild(script);
delete window[callbackName];
// call callback function with retrieved data
if (callback) {
callback(data);
}
};
// inject the script in the document
document.body.appendChild(script);
// TODO: built something to check for an error. only possible with a timeout?
};
/**
* Event handler for drag start event
*/
links.TreeGrid.Frame.prototype.onDragStart = function(event) {
// create a copy of the selection array
/* TODO: cleanup
var items = [];
for (var i = 0; i < this.selection.length; i++) {
var sel = this.selection[i];
items.push(sel);
}
var dragImage = this.dom.dragImage;
if (dragImage) {
var count = items.length;
dragImage.innerHTML = count + ' item' + ((count != 1) ? 's' : '');
}
event.dataTransfer.setData('items', items);
*/
// check if there are selected items that can be dragged
var items = [];
for (var i = 0; i < this.selection.length; i++) {
var sel = this.selection[i];
var parent = sel.parent;
var dataConnector = parent ? parent.dataConnector : undefined;
var options = dataConnector ? dataConnector.options : undefined;
var dataTransfer = options ? options.dataTransfer : undefined;
var allowedEffect = dataTransfer ? dataTransfer.allowedEffect : undefined;
// validate whether at least one of the items can be moved or copied
if (allowedEffect != undefined && allowedEffect.toLowerCase() != 'none') {
items.push(sel);
}
}
if (items.length > 0) {
var dragImage = this.dom.dragImage;
if (dragImage) {
var count = items.length;
dragImage.innerHTML = count + ' item' + ((count != 1) ? 's' : '');
}
event.dataTransfer.setData('items', items);
return true;
}
else {
return false;
}
};
/**
* Event handler for drag start event
*/
links.TreeGrid.Frame.prototype.onDragEnd = function(event) {
var dropEffect = event.dataTransfer.dropEffect;
if (dropEffect == 'move') {
var frame = this;
var items = event.dataTransfer.getData('items');
var callbacksInProgress = items.length;
var callback = function () {
callbacksInProgress--;
if (callbacksInProgress == 0) {
frame.unselect();
frame.onResize();
}
};
var errback = callback;
for (var i = 0; i < items.length; i++) {
var item = items[i];
// FIXME: removing the item is a temporary hack. This only works
// in case of a single user, and prevents the state of the items
// (expanded or not) from being shifted.
// The real solution is to be able to really store the state of an
// not on an index basis, but on an item basis.
// Use a linked list instead of an array?
item.parent._removeItem(item);
// TODO: not so nice accessing the parent grid this way...
item.parent.dataConnector.removeItems([item.data], callback, errback);
}
}
/* TODO
else if (dropEffect == 'link') {
// TODO: linkedItems
}
else if (dropEffect == 'copy') {
// TODO: copiedItems
}
*/
// TODO: trigger event?
this.repaint();
};
/**
* A click event
* @param {event} event The event that occurred
*/
links.TreeGrid.Frame.prototype.onMouseDown = function(event) {
event = event || window.event;
// only react on left mouse button down
var params = this.eventParams;
var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
if (!leftButtonDown && !params.touchDown) {
return;
}
var target = links.TreeGrid.getTarget(event);
if (target.treeGridType && target.treeGridType == 'expand') {
target.grid.onExpand();
links.TreeGrid.stopPropagation(event); // TODO: does not work
}
else if (target.treeGridType && target.treeGridType == 'action') {
target.item.onEvent(target.event);
links.TreeGrid.stopPropagation(event); // TODO: does not work
}
else {
var item = links.TreeGrid.getItemFromTarget(target);
if (item) {
// select the item
var keepSelection = event.ctrlKey;
var selectRange = event.shiftKey;
this.select(item, keepSelection, selectRange);
}
else {
this.unselect();
}
}
links.TreeGrid.preventDefault(event);
};
/**
* Set given node selected
* @param {links.TreeGrid.Node} node
* @param {Boolean} keepSelection If true, the current node is added to the
* selection. append is false by default
* @param {Boolean} selectRange If true, a range of nodes is selected from
* the last selected node to this node
*/
links.TreeGrid.Frame.prototype.select = function(node, keepSelection, selectRange) {
var triggerEvent = false;
if (selectRange) {
var startNode = this.selection.pop();
var endNode = node;
// ensure having nodes in the same grid
if (startNode && startNode.parent != endNode.parent) {
startNode.unselect();
startNode.repaint();
startNode = undefined;
}
if (!startNode) {
startNode = endNode;
}
// remove selection
while (this.selection.length) {
var selectedNode = this.selection.pop();
selectedNode.unselect();
selectedNode.repaint();
}
var parent = startNode.parent;
var startIndex = parent.items.indexOf(startNode);
var endIndex = (startNode == endNode) ? startIndex : parent.items.indexOf(endNode);
if (endIndex >= startIndex) {
var index = startIndex;
while (index <= endIndex) {
var node = parent.items[index];
node.select();
node.repaint();
this.selection.unshift(node);
index++;
}
}
else {
var index = startIndex;
while (index >= endIndex) {
var node = parent.items[index];
node.select();
node.repaint();
// important to add to the beginning of the array, we want to keep
// our 'start' node at the end of the selection array, needed when
// we adjust this range.
this.selection.unshift(node);
index--;
}
}
}
else if (keepSelection) {
// append this node to the selection
var index = this.selection.indexOf(node);
if (index == -1) {
node.select();
node.repaint();
this.selection.push(node);
}
else {
node.unselect();
node.repaint();
this.selection.splice(index, 1);
}
}
else {
if (!node.selected) {
// remove selection
while (this.selection.length) {
var selectedNode = this.selection.pop();
selectedNode.unselect();
selectedNode.repaint();
}
// append this node to the selection
node.select();
node.repaint();
this.selection.push(node);
}
}
// trigger selection event
this.trigger('select', {
//'index': node.index, // TOOD: cleanup
'items': this.getSelection()
});
};
/**
* Unselect all selected nodes
* @param {Boolean} triggerEvent Optional. True by default
*/
links.TreeGrid.Frame.prototype.unselect = function(triggerEvent) {
var selection = this.selection;
for (var i = 0, iMax = selection.length; i < iMax; i++) {
selection[i].unselect();
selection[i].repaint();
}
this.selection = [];
if (triggerEvent == undefined) {
triggerEvent = true;
}
// trigger selection event
if (triggerEvent) {
this.trigger('select', {
'items': []
});
}
};
/**
* Get the selected items
* @param {Object[]} selected items
*/
links.TreeGrid.Frame.prototype.getSelection = function() {
// create an array with the data of the selected items (instead of the items
// themselves)
var selection = this.selection;
var selectedData = [];
for (var i = 0, iMax = selection.length; i < iMax; i++) {
selectedData.push(selection[i].data);
}
return selectedData;
};
/**
* Event handler for touchstart event on mobile devices
*/
links.TreeGrid.Frame.prototype.onTouchStart = function(event) {
var params = this.eventParams,
me = this;
if (params.touchDown) {
// if already moving, return
return;
}
params.startClientY = event.targetTouches[0].clientY;
params.currentClientY = params.startClientY;
params.previousClientY = params.startClientY;
params.startScrollValue = this.verticalScroll.get();
params.touchDown = true;
if (!params.onTouchMove) {
params.onTouchMove = function (event) {me.onTouchMove(event);};
links.TreeGrid.addEventListener(document, "touchmove", params.onTouchMove);
}
if (!params.onTouchEnd) {
params.onTouchEnd = function (event) {me.onTouchEnd(event);};
links.TreeGrid.addEventListener(document, "touchend", params.onTouchEnd);
}
// don't do preventDefault here, it will block onclick events...
//links.TreeGrid.preventDefault(event);
};
/**
* Event handler for touchmove event on mobile devices
*/
links.TreeGrid.Frame.prototype.onTouchMove = function(event) {
var params = this.eventParams;
var clientY = event.targetTouches[0].clientY;
var diff = (clientY - params.startClientY);
this.verticalScroll.set(params.startScrollValue - diff);
this.onResize();
params.previousClientY = params.currentClientY;
params.currentClientY = clientY;
this.trigger('rangechange', undefined);
links.TreeGrid.preventDefault(event);
};
/**
* Event handler for touchend event on mobile devices
*/
links.TreeGrid.Frame.prototype.onTouchEnd = function(event) {
var params = this.eventParams,
me = this;
params.touchDown = false;
var diff = (params.currentClientY - params.startClientY);
var speed = (params.currentClientY - params.previousClientY);
var decellerate = function () {
if (!params.touchDown) {
me.verticalScroll.set(params.startScrollValue - diff);
me.onRangeChange();
diff += speed;
speed *= 0.8;
if (Math.abs(speed) > 1) {
setTimeout(decellerate, 50);
}
}
};
decellerate();
this.trigger("rangechanged", undefined);
if (params.onTouchMove) {
links.TreeGrid.removeEventListener(document, "touchmove", params.onTouchMove);
delete params.onTouchMove;
}
if (params.onTouchEnd) {
links.TreeGrid.removeEventListener(document, "touchend", params.onTouchEnd);
delete params.onTouchEnd;
}
links.TreeGrid.preventDefault(event);
};
/**
* Event handler for mouse wheel event,
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {event} event
*/
links.TreeGrid.Frame.prototype.onMouseWheel = function(event) {
if (!event) { /* For IE. */
event = window.event;
}
// retrieve delta
var delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta/120;
} else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail/3;
}
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta) {
// TODO: on FireFox, the window is not redrawn within repeated scroll-events
// -> use a delayed redraw? Make a zoom queue?
this.verticalScroll.increase(-delta * 50);
this.onRangeChange();
// fire a rangechanged event
this.trigger('rangechanged', undefined);
}
// Prevent default actions caused by mouse wheel.
// That might be ugly, but we handle scrolls somehow
// anyway, so don't bother here...
links.TreeGrid.preventDefault(event);
};
/**
* fire an event
* @param {String} event The name of an event, for example 'rangechange' or 'edit'
* @param {Object} params Optional object with parameters
*/
links.TreeGrid.prototype.trigger = function (event, params) {
// trigger the links event bus
links.events.trigger(this, event, params);
// trigger the google event bus
if (google && google.visualization && google.visualization.events) {
google.visualization.events.trigger(this, event, params);
}
};
/** ------------------------------------------------------------------------ **/
/**
* Drag and Drop library
*
* This module allows to create draggable and droppable elements in a webpage,
* and easy transfer of data of any type between drag and drop areas.
*
* The interface of the library is equal to the 'real' drag and drop API.
* However, this library works on all browsers without issues (in contrast to
* the official drag and drop API).
* https://developer.mozilla.org/En/DragDrop/Drag_and_Drop
*
* The library is tested on: Chrome, Firefox, Opera, Safari,
* Internet Explorer 5.5+
*
* DOCUMENTATION
*
* To create a draggable area, use the method makeDraggable:
* dnd.makeDraggable(element, options);
*
* with parameters:
* {HTMLElement} element The element to become draggable.
* {Object} options An object with options.
*
* available options:
* {String} effectAllowed The allowed drag effect. Available values:
* 'copy', 'move', 'link', 'copyLink',
* 'copyMove', 'linkMove', 'all', 'none'.
* Default value is 'all'.
* {String or HTMLElement} dragImage
* Image to be used as drag image. If no
* drag image is provided, an opague clone
* of the drag area is used.
* {Number} dragImageOffsetX
* Horizontal offset for the drag image
* {Number} dragImageOffsetY
* Vertical offset for the drag image
* {function} dragStart Method called once on start of a drag.
* The method is called with an event object
* as parameter. The event object contains a
* parameter 'data' to pass data to a drop
* event. This data can be any type.
* {function} drag Method called repeatedly while dragging.
* The method is called with an event object
* as parameter.
* {function} dragEnd Method called after the drag event is
* finished. The method is called with an
* event object as parameter. This event
* object contains a parameter 'dropEffect'
* with the applied drop effect, which is
* undefined when no drop occurred.
*
* To make a droppable area, use the method makeDroppable:
* dnd.makeDroppable(element, options);
*
* with parameters:
* {HTMLElement} element The element to become droppable.
* {Object} options An object with options.
*
* available options:
* {String} dropEffect The drop effect. Available
* values: 'copy', 'move', 'link', 'none'.
* Default value is 'link'
* {function} dragEnter Method called once when the dragged image
* enters the drop area. Can be used to
* apply visual effects to the drop area.
* {function} dragLeave Method called once when the dragged image
* leaves the drop area. Can be used to
* remove visual effects from the drop area.
* {function} dragOver Method called repeatedly when moving
* over the drop area.
* {function} drop Method called when the drag image is
* dropped on this drop area.
* The method is called with an event object
* as parameter. The event object contains a
* parameter 'data' which can contain data
* provided by the drag area.
*
* Created draggable or doppable areas are registed in the drag and
* drop module. To remove a draggable or droppable area, the
* following methods can be used respectively:
* dnd.removeDraggable(element);
* dnd.removeDroppable(element);
*
* which removes all drag and drop functionality from the concerning
* element.
*
*
* EXAMPLE
*
* var drag = document.getElementById('drag');
* dnd.makeDraggable(drag, {
* 'dragStart': function (event) {
* event.data = 'Hello World!'; // data can be any type
* }
* });
*
* var drop = document.getElementById('drop');
* dnd.makeDroppable(drop, {
* 'drop': function (event) {
* alert(event.data); // will alert 'Hello World!'
* },
* 'dragEnter': function (event) {
* drop.style.backgroundColor = 'yellow'; // set visual effect
* },
* 'dragLeave': function (event) {
* drop.style.backgroundColor = ''; // remove visual effect
* }
* });
*
*
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Copyright (c) 2011-2012 Almende B.V. <http://www.almende.com>
*
* @author Jos de Jong <jos@almende.org>
* @date 2012-02-09
*/
links.dnd = function () {
var dragAreas = []; // all registered drag areas
var dropAreas = []; // all registered drop areas
var dragArea = undefined; // currently dragged area
var dragImage = undefined;
var dragImageOffsetX = 0;
var dragImageOffsetY = 0;
var dragEvent = {}; // object with event properties, passed to each event
var mouseMove = undefined;
var mouseUp = undefined;
var originalCursor = undefined;
function isDragging() {
return (dragArea != undefined);
}
/**
* Make an HTML element draggable
* @param {Element} element
* @param {Object} options. available parameters:
* {String} effectAllowed
* {String or HTML DOM} dragImage
* {function} dragStart
* {function} dragEnd
*/
function makeDraggable (element, options) {
// create an object holding the dragarea and options
var newDragArea = {
'element': element
};
if (options) {
links.TreeGrid.extend(newDragArea, options);
}
dragAreas.push(newDragArea);
var mouseDown = function (event) {
event = event || window.event;
var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
if (!leftButtonDown) {
return;
}
// create mousemove listener
mouseMove = function (event) {
if (!isDragging()) {
dragStart(event, newDragArea);
}
dragOver(event);
preventDefault(event);
};
addEventListener(document, 'mousemove', mouseMove);
// create mouseup listener
mouseUp = function (event) {
if (isDragging()) {
dragEnd(event);
}
// remove event listeners
if (mouseMove) {
removeEventListener(document, 'mousemove', mouseMove);
mouseMove = undefined;
}
if (mouseUp) {
removeEventListener(document, 'mouseup', mouseUp);
mouseUp = undefined;
}
preventDefault(event);
};
addEventListener(document, 'mouseup', mouseUp);
preventDefault(event);
};
addEventListener(element, 'mousedown', mouseDown);
newDragArea.mouseDown = mouseDown;
}
/**
* Make an HTML element droppable
* @param {Element} element
* @param {Object} options. available parameters:
* {String} dropEffect
* {function} dragEnter
* {function} dragLeave
* {function} drop
*/
function makeDroppable (element, options) {
var newDropArea = {
'element': element,
'mouseOver': false
};
if (options) {
links.TreeGrid.extend(newDropArea, options);
}
if (!newDropArea.dropEffect) {
newDropArea.dropEffect = 'link';
}
dropAreas.push(newDropArea);
}
/**
* Remove draggable functionality from element
* @param {Element} element
*/
function removeDraggable (element) {
var i = 0;
while (i < dragAreas.length) {
var d = dragAreas[i];
if (d.element == element) {
removeEventListener(d.element, 'mousedown', d.mouseDown);
dragAreas.splice(i, 1);
}
else {
i++;
}
}
}
/**
* Remove droppabe functionality from element
* @param {Element} element
*/
function removeDroppable (element) {
var i = 0;
while (i < dropAreas.length) {
if (dropAreas[i].element == element) {
dropAreas.splice(i, 1);
}
else {
i++;
}
}
}
function dragStart(event, newDragArea) {
// register the dragarea
if (dragArea) {
return;
}
dragArea = newDragArea;
// trigger event
var proceed = true;
if (dragArea.dragStart) {
var data = {};
dragEvent = {
'dataTransfer' : {
'dragArea': dragArea.element,
'dropArea': undefined,
'data': data,
'getData': function (key) {
return data[key];
},
'setData': function (key, value) {
data[key] = value;
},
'clearData': function (key) {
delete data[key];
}
},
'clientX': event.clientX,
'clientY': event.clientY
};
var ret = dragArea.dragStart(dragEvent);
proceed = (ret !== false);
}
if (!proceed) {
// cancel dragevent
dragArea = undefined;
return;
}
// create dragImage
var clone = undefined;
dragImage = document.createElement('div');
dragImage.style.position = 'absolute';
if (typeof(dragArea.dragImage) == 'string') {
// create dragImage from HTML string
dragImage.innerHTML = dragArea.dragImage;
dragImageOffsetX = -dragArea.dragImageOffsetX || 0;
dragImageOffsetY = -dragArea.dragImageOffsetY || 0;
}
else if (dragArea.dragImage) {
// create dragImage from HTML DOM element
dragImage.appendChild(dragArea.dragImage);
dragImageOffsetX = -dragArea.dragImageOffsetX || 0;
dragImageOffsetY = -dragArea.dragImageOffsetY || 0;
}
else {
// clone the drag area
clone = dragArea.element.cloneNode(true);
dragImageOffsetX = (event.clientX || 0) - getAbsoluteLeft(dragArea.element);
dragImageOffsetY = (event.clientY || 0) - getAbsoluteTop(dragArea.element);
clone.style.left = '0px';
clone.style.top = '0px';
clone.style.opacity = '0.7';
clone.style.filter = 'alpha(opacity=70)';
dragImage.appendChild(clone);
}
document.body.appendChild(dragImage);
// adjust the cursor
if (originalCursor == undefined) {
originalCursor = document.body.style.cursor;
}
document.body.style.cursor = 'move';
}
function dragOver (event) {
if (!dragImage) {
return;
}
// adjust position of the dragImage
if (dragImage) {
dragImage.style.left = (event.clientX - dragImageOffsetX) + 'px';
dragImage.style.top = (event.clientY - dragImageOffsetY) + 'px';
}
// adjust event properties
dragEvent.clientX = event.clientX;
dragEvent.clientY = event.clientY;
// find center of the drag area
var left = (event.clientX - dragImageOffsetX);
var top = (event.clientY - dragImageOffsetY);
var width = dragImage.clientWidth || dragArea.element.clientWidth;
var height = dragImage.clientHeight || dragArea.element.clientHeight;
var x = left + width / 2;
var y = top + height / 2;
// console.log(dragImageOffsetX, x, left, width, dragImageOffsetY, y, top, height)
// check if there is a droparea overlapping with current dragarea
var currentDropArea = undefined;
for (var i = 0; i < dropAreas.length; i++) {
var dropArea = dropAreas[i];
var left = getAbsoluteLeft(dropArea.element);
var top = getAbsoluteTop(dropArea.element);
var width = dropArea.element.clientWidth;
var height = dropArea.element.clientHeight;
if (x > left && x < left + width && y > top && y < top + height &&
!currentDropArea &&
getAllowedDropEffect(dragArea.effectAllowed, dropArea.dropEffect)) {
// on droparea
currentDropArea = dropArea;
if (!dropArea.mouseOver) {
if (dropArea.dragEnter) {
dragEvent.dataTransfer.dropArea = dropArea;
dragEvent.dataTransfer.dropEffect = undefined;
dropArea.dragEnter(dragEvent);
}
dropArea.mouseOver = true;
}
}
else {
// not on droparea
if (dropArea.mouseOver) {
if (dropArea.dragLeave) {
dragEvent.dataTransfer.dropArea = dropArea;
dragEvent.dataTransfer.dropEffect = undefined;
dropArea.dragLeave(dragEvent);
}
dropArea.mouseOver = false;
}
}
}
// adjust event properties
if (currentDropArea) {
dragEvent.dataTransfer.dropArea = currentDropArea.element;
dragEvent.dataTransfer.dropEffect =
getAllowedDropEffect(dragArea.effectAllowed, currentDropArea.dropEffect);
if (currentDropArea.dragOver) {
currentDropArea.dragOver(dragEvent);
// TODO
// // dropEffecct may be changed during dragOver
//currentDropArea.dropEffect = dragEvent.dataTransfer.dropEffect;
}
}
else {
dragEvent.dataTransfer.dropArea = undefined;
dragEvent.dataTransfer.dropEffect = undefined;
}
if (dragArea.drag) {
// dragEvent.dataTransfer.effectAllowed = dragArea.effectAllowed;
dragArea.drag(dragEvent);
// TODO
// // effectAllowed may be changed during drag
// dragArea.effectAllowed = dragEvent.dataTransfer.effectAllowed;
}
}
function dragEnd (event) {
// remove the dragImage
if (dragImage && dragImage.parentNode) {
dragImage.parentNode.removeChild(dragImage);
}
dragImage = undefined;
// restore cursor
document.body.style.cursor = originalCursor || '';
originalCursor = undefined;
var currentDropArea = undefined;
for (var i = 0; i < dropAreas.length; i++) {
var dropArea = dropAreas[i];
// perform mouse leave event
if (dropArea.mouseOver) {
if (dropArea.dragLeave) {
dropArea.dragLeave(dragEvent);
}
dropArea.mouseOver = false;
// perform drop event
if (!currentDropArea) {
currentDropArea = dropArea;
}
}
}
if (currentDropArea) {
// adjust event properties
dragEvent.dataTransfer.dropArea = currentDropArea.element;
dragEvent.dataTransfer.dropEffect =
getAllowedDropEffect(dragArea.effectAllowed, currentDropArea.dropEffect);
// trigger drop event
if (dragEvent.dataTransfer.dropEffect) {
if (currentDropArea.drop) {
currentDropArea.drop(dragEvent);
}
}
}
else {
dragEvent.dataTransfer.dropArea = undefined;
dragEvent.dataTransfer.dropEffect = undefined;
}
// trigger dragEnd event
if (dragArea.dragEnd) {
dragArea.dragEnd(dragEvent);
}
// remove the dragArea
dragArea = undefined;
// clear event data
dragEvent = {};
}
/**
* Return the current dropEffect, taking into account the allowed drop effects
* @param {String} effectAllowed
* @param {String} dropEffect
* @return allowedDropEffect the allowed dropEffect, or undefined when
* not allowed
*/
function getAllowedDropEffect (effectAllowed, dropEffect) {
if (!dropEffect || dropEffect == 'none') {
// none
return undefined;
}
if (!effectAllowed || effectAllowed.toLowerCase() == 'all') {
// all
return dropEffect;
}
if (effectAllowed.toLowerCase().indexOf(dropEffect.toLowerCase()) != -1 ) {
return dropEffect;
}
return undefined;
}
/**
* Add and event listener. Works for all browsers
* @param {Element} element An html element
* @param {string} action The action, for example 'click',
* without the prefix 'on'
* @param {function} listener The callback function to be executed
* @param {boolean} useCapture
*/
function addEventListener (element, action, listener, useCapture) {
if (element.addEventListener) {
if (useCapture === undefined) {
useCapture = false;
}
if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
action = 'DOMMouseScroll'; // For Firefox
}
element.addEventListener(action, listener, useCapture);
} else {
element.attachEvent('on' + action, listener); // IE browsers
}
}
/**
* Remove an event listener from an element
* @param {Element} element An html dom element
* @param {string} action The name of the event, for example 'mousedown'
* @param {function} listener The listener function
* @param {boolean} useCapture
*/
function removeEventListener (element, action, listener, useCapture) {
if (element.removeEventListener) {
// non-IE browsers
if (useCapture === undefined) {
useCapture = false;
}
if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
action = 'DOMMouseScroll'; // For Firefox
}
element.removeEventListener(action, listener, useCapture);
} else {
// IE browsers
element.detachEvent('on' + action, listener);
}
}
/**
* Stop event propagation
*/
function stopPropagation (event) {
if (!event)
event = window.event;
if (event.stopPropagation) {
event.stopPropagation(); // non-IE browsers
}
else {
event.cancelBubble = true; // IE browsers
}
}
/**
* Cancels the event if it is cancelable, without stopping further propagation of the event.
*/
function preventDefault (event) {
if (!event)
event = window.event;
if (event.preventDefault) {
event.preventDefault(); // non-IE browsers
}
else {
event.returnValue = false; // IE browsers
}
}
/**
* Retrieve the absolute left value of a DOM element
* @param {Element} elem A dom element, for example a div
* @return {number} left The absolute left position of this element
* in the browser page.
*/
function getAbsoluteLeft (elem) {
var left = 0;
while( elem != null ) {
left += elem.offsetLeft;
//left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too
elem = elem.offsetParent;
}
return left;
}
/**
* Retrieve the absolute top value of a DOM element
* @param {Element} elem A dom element, for example a div
* @return {number} top The absolute top position of this element
* in the browser page.
*/
function getAbsoluteTop (elem) {
var top = 0;
while( elem != null ) {
top += elem.offsetTop;
//left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too
elem = elem.offsetParent;
}
return top;
}
// return public methods
return {
'makeDraggable': makeDraggable,
'makeDroppable': makeDroppable,
'removeDraggable': removeDraggable,
'removeDroppable': removeDroppable
};
}();
/** ------------------------------------------------------------------------ **/
/**
* Event bus for adding and removing event listeners and for triggering events.
* This is a singleton.
*/
links.events = links.events || {
'listeners': [],
/**
* Find a single listener by its object
* @param {Object} object
* @return {Number} index -1 when not found
*/
'indexOf': function (object) {
var listeners = this.listeners;
for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
var listener = listeners[i];
if (listener && listener.object == object) {
return i;
}
}
return -1;
},
/**
* Add an event listener
* @param {Object} object
* @param {String} event The name of an event, for example 'select'
* @param {function} callback The callback method, called when the
* event takes place
*/
'addListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (!listener) {
listener = {
'object': object,
'events': {}
};
this.listeners.push(listener);
}
var callbacks = listener.events[event];
if (!callbacks) {
callbacks = [];
listener.events[event] = callbacks;
}
// add the callback if it does not yet exist
if (callbacks.indexOf(callback) == -1) {
callbacks.push(callback);
}
},
/**
* Remove an event listener
* @param {Object} object
* @param {String} event The name of an event, for example 'select'
* @param {function} callback The registered callback method
*/
'removeListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
var callbackIndex = callbacks.indexOf(callback);
if (callbackIndex != -1) {
callbacks.splice(callbackIndex, 1);
}
// remove the array when empty
if (callbacks.length == 0) {
delete listener.events[event];
}
}
// count the number of registered events. remove listener when empty
var count = 0;
var events = listener.events;
for (var e in events) {
if (events.hasOwnProperty(e)) {
count++;
}
}
if (count == 0) {
delete this.listeners[index];
}
}
},
/**
* Remove all registered event listeners
*/
'removeAllListeners': function () {
this.listeners = [];
},
/**
* Trigger an event. All registered event handlers will be called
* @param {Object} object
* @param {String} event
* @param {Object} params (optional)
*/
'trigger': function (object, event, params) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
callbacks[i](params);
}
}
}
}
};
/** ------------------------------------------------------------------------ **/
/**
* Add and event listener. Works for all browsers
* @param {Element} element An html element
* @param {string} action The action, for example 'click',
* without the prefix 'on'
* @param {function} listener The callback function to be executed
* @param {boolean} useCapture
*/
links.TreeGrid.addEventListener = function (element, action, listener, useCapture) {
if (element.addEventListener) {
if (useCapture === undefined) {
useCapture = false;
}
if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
action = 'DOMMouseScroll'; // For Firefox
}
element.addEventListener(action, listener, useCapture);
} else {
element.attachEvent('on' + action, listener); // IE browsers
}
};
/**
* Remove an event listener from an element
* @param {Element} element An html dom element
* @param {string} action The name of the event, for example 'mousedown'
* @param {function} listener The listener function
* @param {boolean} useCapture
*/
links.TreeGrid.removeEventListener = function(element, action, listener, useCapture) {
if (element.removeEventListener) {
// non-IE browsers
if (useCapture === undefined) {
useCapture = false;
}
if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
action = 'DOMMouseScroll'; // For Firefox
}
element.removeEventListener(action, listener, useCapture);
} else {
// IE browsers
element.detachEvent('on' + action, listener);
}
};
/**
* Get HTML element which is the target of the event
* @param {MouseEvent} event
* @return {Element} target element
*/
links.TreeGrid.getTarget = function (event) {
// code from http://www.quirksmode.org/js/events_properties.html
if (!event) {
event = window.event;
}
var target;
if (event.target) {
target = event.target;
}
else if (event.srcElement) {
target = event.srcElement;
}
if (target.nodeType !== undefined && target.nodeType == 3) {
// defeat Safari bug
target = target.parentNode;
}
return target;
};
/**
* Recursively find the treegrid item of which this target element is a part of.
* @param {Element} target
* @return {links.TreeGrid.Item} item Item or undefined when not found
*/
links.TreeGrid.getItemFromTarget = function (target) {
var elem = target;
while (elem) {
if (elem.treeGridType == 'item' && elem.item) {
return elem.item;
}
elem = elem.parentElement;
}
return undefined;
};
/**
* Stop event propagation
*/
links.TreeGrid.stopPropagation = function (event) {
if (!event) {
event = window.event;
}
if (event.stopPropagation) {
event.stopPropagation(); // non-IE browsers
}
else {
event.cancelBubble = true; // IE browsers
}
};
/**
* Cancels the event if it is cancelable, without stopping further propagation of the event.
*/
links.TreeGrid.preventDefault = function (event) {
if (!event) {
event = window.event;
}
if (event.preventDefault) {
event.preventDefault(); // non-IE browsers
}
else {
event.returnValue = false; // IE browsers
}
};