forked from extern/egroupware
ae7987264e
- Improved egw_action.js: - Bugfixing regaring handling of egwActionObjects organized in trees (hasn't been tested before) - Improved egwActionObjectInterface interface and functionality: AOIs can now mark themselves as invisible/visible and request the action objects to reconnect the action implementations - Action objects do now automatically register the action implementations - Vastly improved speed when working with huge amounts (> 500) of objects organized in trees (as done in the grid test) - Improved egwActionObject functionality: Added new functions regarding selecting groups of objects
587 lines
14 KiB
JavaScript
587 lines
14 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|
|
|