egroupware/phpgwapi/js/egw_action/egw_grid.js

587 lines
14 KiB
JavaScript
Raw Normal View History

/**
* eGroupWare egw_action framework - egw action framework
*
* @link http://www.egroupware.org
* @author Andreas Stöckel <as@stylite.de>
* @copyright 2011 by Andreas Stöckel
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package egw_action
* @version $Id$
*/
/**
* Contains classes which are able to display an dynamic data view which
*/
/*
uses
egw_action,
egw_action_common,
egw_menu,
jquery;
*/
/**
* Main class for the grid view. The grid view is a combination of a classic
* list view with multiple columns and the tree view.
*
* @param object _parentNode is the DOM-Node the grid-view should be inserted into
* @param array _columns is an array of all colloumns the grid should be able to
* display.
* TODO: A column should be an object and become much more mighty (sorting,
* visibility, etc.)
*/
function egwGrid(_parentNode, _columns)
{
this.parentNode = _parentNode;
this.columns = _columns;
this.updateElems = [];
this.children = [];
this.inUpdate = false;
this.tbody = null;
// Append the grid base elements to the parent node
$(this.parentNode).append(this._buildBase());
}
/**
* Adds a new item to the grid and returns it
*
* @param string _id is an unique identifier of the new grid item. It can be searched
* lateron with the "getItemById" function.
* @param string _caption is the caption of the new element
* @param string _icon is the URL to the icon image
* @param object _columns is an object which can contain string entries with html
* for every column_id available in the grid.
* @returns object the newly created egwGridItem
*/
egwGrid.prototype.addItem = function(_id, _caption, _icon, _columns)
{
var item = new egwGridItem(this, null, _id, _caption, _icon, _columns);
this.children.push(item);
this.update(item);
return item;
}
/**
* Updates the given element - if the element is visible, it is added to the
* DOM-Tree if it hasn't been attached to it yet. If the object is already in
* the DOM-Tree, it will be rebuilt, which means, that the DOM-Data of its row
* will be replaced by a new one.
* If multiple updates are in progress, it is wise to group those by using
* the beginUpdate and endUpdate functions, as this saves some redundancy
* like re-colorizing the rows if a new one has been added.
*
* @param object _elem is the egwGridItem object, which will be updated
*/
egwGrid.prototype.update = function(_elem)
{
if (_elem.isVisible())
{
if (!this.inUpdate)
{
if (this._updateElement(_elem))
{
this._colorizeRows();
}
}
else
{
if (this.updateElems.indexOf(_elem) == -1)
{
this.updateElems.push(_elem);
}
}
}
}
/**
* Starts a group of updates: After beginUpdate has been called, the update function
* will collect the update wishes and execute them as soon as endUpdate is called.
* This brings a major performance improvement if lots of elements are added.
*/
egwGrid.prototype.beginUpdate = function()
{
this.inUpdate = true;
}
/**
* Ends the update grouping and actually executes the updates.
*/
egwGrid.prototype.endUpdate = function()
{
// Call the update function for all elements which wanted to be updated
// since the last beginUpdate call.
var added = false;
this.inUpdate = false;
for (var i = 0; i < this.updateElems.length; i++)
{
added = this._updateElement(this.updateElems[i]) || added;
}
this.updateElems = [];
// If elements have been (visibly) added to the tree, call the colorize rows
// function.
if (added)
{
this._colorizeRows();
}
}
/**
* Builds the base DOM-Structure of the grid and returns the outer object.
*/
egwGrid.prototype._buildBase = function()
{
var table = $(document.createElement("table"));
table.addClass("grid");
this.tbody = $(document.createElement("tbody"));
table.append(this.tbody);
this.tbody.append(this._buildHeader());
return table;
}
/**
* Builds the header row and returns it
*/
egwGrid.prototype._buildHeader = function()
{
var row = document.createElement("tr");
for (var i = 0; i < this.columns.length; i++)
{
var column = $(document.createElement("th"));
if (i == 0)
{
column.addClass("front");
}
column.html(this.columns[i].caption);
if (typeof this.columns[i].width != "undefined")
{
column.css("width", this.columns[i].width);
}
$(row).append(column);
}
return row;
}
/**
* Gives odd rows the additional "odd" CSS-class which creates the "zebra" structure.
*/
egwGrid.prototype._colorizeRows = function()
{
this.tbody.children().removeClass("odd");
$("tr:not(.hidden):odd", this.tbody).addClass("odd");
}
/**
* Internal function which actually performs the update of the given element as
* it is described in the update function.
*/
egwGrid.prototype._updateElement = function(_elem)
{
// If the element has to be inserted into the dom tree first, search for the
// proper position:
var insertAfter = null;
var oldRow = null;
if (_elem.isVisible())
{
if (!_elem.domData)
{
var parentChildren = _elem.parent ? _elem.parent.children : this.children;
var index = _elem.index();
// Fetch the node after which this one should be inserted
if (index > 0)
{
insertAfter = parentChildren[index - 1].lastInsertedChild().domData.row;
}
else
{
insertAfter = _elem.parent ? _elem.parent.domData.row :
this.tbody.children().get(0);
}
}
// Check whether the element already has a row attached to it
var row = _elem.buildRow(_elem.domData ? _elem.domData.row : null);
// Insert the row after the fetched element
if (insertAfter)
{
$(row).insertAfter(insertAfter);
return _elem.isVisible();
}
}
return false;
}
/** egwGridItem Class **/
/**
* The egwGridItem represents a single row inside an egwGrid. Each egwGridItem
* contains an egwActionObjectInterface-Object (can be recieved by calling getAOI()),
* which is used to interconnect with the egw_action framework.
*
* Don't create new egwGridItems yourself, use the addItem functions supplied by
* egwGrid and egwGridItem.
*
* @param object _grid is the parent egwGrid object
* @param object _parent is the parent egwGridItem. Null if no parent exists.
* @param string _caption is the caption of the grid item
* @param string _icon is the url to the icon image
* @param object _columns is an object which can contain string entries with html
* for every column_id available in the grid.
* TODO: Remove _canHaveChildren and replace with something better
*/
function egwGridItem(_grid, _parent, _id, _caption, _icon, _columns, _canHaveChildren)
{
if (typeof _canHaveChildren == "undefined")
{
_canHaveChildren = true;
}
// Setup the egwActionObjectInterface
this._setupAOI();
this.clickCallback = null;
this.grid = _grid;
this.parent = _parent;
this.id = _id;
this.caption = _caption;
this.icon = _icon;
this.columns = _columns;
if (_canHaveChildren)
{
this.children = [];
}
else
{
this.children = false;
}
this.opened = false;
this.domData = null;
}
/**
* Creates AOI instance of the item and overwrites all necessary functions.
*/
egwGridItem.prototype._setupAOI = function()
{
this.aoi = new egwActionObjectInterface;
// The default state of an aoi is EGW_AO_STATE_NORMAL || EGW_AO_STATE_VISIBLE -
// egwGridItems are not necessarily visible by default
this.aoi._state = EGW_AO_STATE_NORMAL;
this.aoi.gridItem = this;
this.aoi.doSetState = gridAOIDoSetState;
this.aoi.getDOMNode = gridAOIGetDOMNode;
}
function gridAOIDoSetState(_state, _shiftState)
{
if (this.gridItem.domData)
{
$(this.gridItem.domData.row).toggleClass("selected", egwBitIsSet(_state,
EGW_AO_STATE_SELECTED));
$(this.gridItem.domData.row).toggleClass("focused", egwBitIsSet(_state,
EGW_AO_STATE_FOCUSED));
}
}
function gridAOIGetDOMNode()
{
return this.gridItem.domData ? this.gridItem.domData.row : null;
}
/**
* Returns the actionObjectInterface object of this grid item.
*/
egwGridItem.prototype.getAOI = function()
{
return this.aoi;
}
/**
* Returns whether the grid item is actually visible - in this case this means,
* whether the element is a child of an item which is currently not opened.
*/
egwGridItem.prototype.isVisible = function()
{
return (this.parent ? this.parent.opened && this.parent.isVisible() : true);
}
/**
* Returns the depth of this entry inside the item tree.
*/
egwGridItem.prototype.getDepth = function()
{
return (this.parent ? this.parent.getDepth() + 1 : 0);
}
/**
* Returns the last child which is inserted into the DOM-Tree. Used by the update
* function to determine where new rows should be inserted into the dom tree.
*/
egwGridItem.prototype.lastInsertedChild = function()
{
for (var i = (this.children.length - 1); i >= 0; i--)
{
if (this.children[i].domData)
{
return this.children[i].lastInsertedChild();
}
}
return this;
}
/**
* Adds a new child item to this item. Parameters are equivalent to those of
* egwGrid.addItem.
*/
egwGridItem.prototype.addItem = function(_id, _caption, _icon, _columns)
{
//If the element was not designed to have children, update it in the grid
if (!this.children)
{
this.children = [];
this.grid.update(this);
}
var item = new egwGridItem(this.grid, this, _id, _caption, _icon, _columns);
this.children.push(item);
this.grid.update(item);
return item;
}
/**
* Returns the position of this element inside it's parent children list.
*/
egwGridItem.prototype.index = function()
{
if (this.parent)
{
return this.parent.children.indexOf(this);
}
else
{
return this.grid.children.indexOf(this);
}
}
/**
* Internal function which updates the visibility of this item and its children
* if the open state of the element is changed.
*/
egwGridItem.prototype._updateVisibility = function(_visible)
{
// Set the visibility of this object - if it is at all inserted in the dom-
// tree.
if (this.domData)
{
$(this.domData.row).toggleClass("hidden", !_visible);
// Deselect this row, if it is no longer visible
this.aoi.updateState(EGW_AO_STATE_VISIBLE, _visible);
// Update the visibility of all children
for (var i = 0; i < this.children.length; i++)
{
this.children[i]._updateVisibility(_visible && this.opened);
}
}
}
/**
* Toggles the visibility of the child elements.
*
* @param boolean _open specifies whether this item is opened or closed.
*/
egwGridItem.prototype.setOpened = function(_open)
{
var self = this;
function doSetOpen()
{
// Set the arrow direction
if (_open)
{
self.domData.arrow.addClass("opened");
self.domData.arrow.removeClass("closed");
}
else
{
self.domData.arrow.addClass("closed");
self.domData.arrow.removeClass("opened");
}
// Update all DOM rows
if (_open)
{
self.grid.beginUpdate();
for (var i = 0; i < self.children.length; i++)
{
var child = self.children[i];
if (!child.domData)
{
self.grid.update(child);
}
}
self.grid.endUpdate();
}
// And make them (in)visible
self._updateVisibility(true);
self.grid._colorizeRows();
}
if (this.opened != _open && this.children !== false)
{
this.opened = _open;
if (this.children.length == 0 && _open)
{
// alert("JSON Callback")
}
else
{
doSetOpen();
}
}
}
/**
* Builds the actual DOM-representation of the item and attaches all events to the
* DOM-Nodes.
*
* @param object _row If an existing DOM-Item should be updated, it can be passed
* here and the updated objects will be inserted inside of the row object. Defaults
* to null.
*/
egwGridItem.prototype.buildRow = function(_row)
{
// Build the container row
var row = null;
if (typeof _row == "undefined" || !_row)
{
row = document.createElement("tr");
}
else
{
row = _row;
$(row).empty();
}
$(row).toggleClass("hidden", !this.isVisible())
// Build the indentation object
var indentation = $(document.createElement("span"));
indentation.addClass("indentation");
indentation.css("width", (this.getDepth() * 12) + "px");
// Build the arrow element
var arrow = $(document.createElement("span"));
arrow.addClass("arrow");
if (this.children !== false)
{
arrow.addClass(this.opened ? "opened" : "closed");
arrow.click(this, function(e) {
e.data.setOpened(!e.data.opened);
return false;
});
}
// Build the icon element
icon = $(document.createElement("img"));
if (this.icon)
{
icon.attr("src", this.icon);
}
icon.addClass("icon");
// Build the caption
var caption = $(document.createElement("span"));
caption.text(this.caption);
caption.addClass("caption");
// Build the td surrounding those elements
var column_caption = $(document.createElement("td"));
column_caption.append(indentation, arrow, icon, caption);
column_caption.mousedown(egwPreventSelect);
column_caption.click(this, function(e) {
egwResetPreventSelect(this);
this.onselectstart = null;
e.data._columnClick(egwGetShiftState(e), 0);
});
// Append the column to the row
$(row).append(column_caption);
for (var i = 1; i < this.grid.columns.length; i++) // Skips the front column
{
var content = "";
var gridcol = this.grid.columns[i];
if (typeof this.columns[gridcol.id] != "undefined")
{
content = this.columns[gridcol.id];
}
else
{
if (typeof gridcol["default"] != "undefined")
{
content = gridcol["default"];
}
}
// Create a column and append it to the row
var column = $(document.createElement("td"));
column.html(content);
column.mousedown(egwPreventSelect);
column.click({"item": this, "col": gridcol.id}, function(e) {
egwPreventSelect(this);
e.data.item._columnClick(egwGetShiftState(e), e.data.col);
});
$(row).append(column);
}
this.domData = {
"row": row,
"arrow": arrow,
"icon": icon,
"caption": caption
}
// The item is now visible
this.aoi.updateState(EGW_AO_STATE_VISIBLE, true);
return row;
}
egwGridItem.prototype._columnClick = function(_shiftState, _column)
{
var state = this.aoi.getState();
var isSelected = egwBitIsSet(state, EGW_AO_STATE_SELECTED);
this.aoi.updateState(EGW_AO_STATE_SELECTED,
!egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI) || !isSelected,
_shiftState);
}