From ae7987264e65ca36b50dfe15f88e62b577ebeaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20St=C3=B6ckel?= Date: Wed, 2 Mar 2011 21:18:20 +0000 Subject: [PATCH] - Added working egw_grid component including test and documentation, codebase will be used to replace the nextmatchWidget in etemplate2 - 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 --- phpgwapi/js/egw_action/egw_action.js | 351 ++++++++--- phpgwapi/js/egw_action/egw_action_common.js | 27 + phpgwapi/js/egw_action/egw_action_popup.js | 3 + phpgwapi/js/egw_action/egw_grid.js | 586 ++++++++++++++++++ phpgwapi/js/egw_action/test/grid.css | 94 +++ .../js/egw_action/test/imgs/arrow_down.png | Bin 0 -> 240 bytes .../js/egw_action/test/imgs/arrow_left.png | Bin 0 -> 239 bytes phpgwapi/js/egw_action/test/imgs/arrows.png | Bin 0 -> 393 bytes phpgwapi/js/egw_action/test/imgs/arrows.svg | 74 +++ .../egw_action/test/imgs/mime16_directory.png | Bin 0 -> 695 bytes .../egw_action/test/imgs/select_overlay.png | Bin 0 -> 377 bytes .../egw_action/test/imgs/select_overlay.svg | 103 +++ phpgwapi/js/egw_action/test/test_action.html | 18 +- phpgwapi/js/egw_action/test/test_grid.html | 175 ++++++ 14 files changed, 1316 insertions(+), 115 deletions(-) create mode 100644 phpgwapi/js/egw_action/egw_grid.js create mode 100644 phpgwapi/js/egw_action/test/grid.css create mode 100755 phpgwapi/js/egw_action/test/imgs/arrow_down.png create mode 100755 phpgwapi/js/egw_action/test/imgs/arrow_left.png create mode 100644 phpgwapi/js/egw_action/test/imgs/arrows.png create mode 100644 phpgwapi/js/egw_action/test/imgs/arrows.svg create mode 100644 phpgwapi/js/egw_action/test/imgs/mime16_directory.png create mode 100644 phpgwapi/js/egw_action/test/imgs/select_overlay.png create mode 100644 phpgwapi/js/egw_action/test/imgs/select_overlay.svg create mode 100644 phpgwapi/js/egw_action/test/test_grid.html diff --git a/phpgwapi/js/egw_action/egw_action.js b/phpgwapi/js/egw_action/egw_action.js index a86bd35c31..4f9e0259cb 100644 --- a/phpgwapi/js/egw_action/egw_action.js +++ b/phpgwapi/js/egw_action/egw_action.js @@ -340,6 +340,7 @@ egwActionLink.prototype.set_actionId = function(_value) var EGW_AO_STATE_NORMAL = 0x00; var EGW_AO_STATE_SELECTED = 0x01; var EGW_AO_STATE_FOCUSED = 0x02; +var EGW_AO_STATE_VISIBLE = 0x04; //< Can only be set by the AOI, means that the object is attached to the DOM-Tree and visible var EGW_AO_EVENT_DRAG_OVER_ENTER = 0x00; var EGW_AO_EVENT_DRAG_OVER_LEAVE = 0x01; @@ -386,14 +387,22 @@ function egwActionObject(_id, _parent, _iface, _manager, _flags) this.manager = _manager; this.flags = _flags; + this.registeredImpls = []; + + // Two variables which help fast travelling through the object tree, when + // searching for the selected/focused object. + this.selectedChildren = []; + this.focusedChild = null; + this.iface = _iface; - this.iface.setStateChangeCallback(this._ifaceCallback, this) + this.iface.setStateChangeCallback(this._ifaceCallback, this); + this.iface.setReconnectActionsCallback(this._reconnectCallback, this); } /** * Returns the object from the tree with the given ID */ -//TODO: Add "ByID"-Suffix to all other of those functions. +//TODO: Add "ById"-Suffix to all other of those functions. //TODO: Add search function to egw_action_commons.js egwActionObject.prototype.getObjectById = function(_id) { @@ -531,24 +540,35 @@ egwActionObject.prototype.getContainerRoot = function() } /** - * Returns all selected objects which are in the current container. + * Returns all selected objects which are in the current subtree. + * + * @param function _test is a function, which gets an object and checks whether + * it will be added to the list. + * @param array _list is internally used to fetch all selected elements, please + * omit this parameter when calling the function. */ -egwActionObject.prototype.getSelectedObjects = function(_test) +egwActionObject.prototype.getSelectedObjects = function(_test, _list) { if (typeof _test == "undefined") _test = null; - var result = []; - var list = this.getContainerRoot().flatList(); - for (var i = 0; i < list.length; i++) + if (typeof _list == "undefined") { - if (list[i].getSelected() && (!_test || _test(list[i]))) + _list = {"elements": []} + } + + if ((!_test || _test(this)) && this.getSelected()) + _list.elements.push(this); + + if (this.selectedChildren) + { + for (var i = 0; i < this.selectedChildren.length; i++) { - result.push(list[i]); + this.selectedChildren[i].getSelectedObjects(_test, _list) } } - return result; + return _list.elements; } /** @@ -556,7 +576,7 @@ egwActionObject.prototype.getSelectedObjects = function(_test) */ egwActionObject.prototype.getAllSelected = function() { - if (this.getSelected()) + if (this.children.length == this.selectedChildren.length) { for (var i = 0; i < this.children.length; i++) { @@ -583,11 +603,7 @@ egwActionObject.prototype.toggleAllSelected = function(_select) _select = !this.getAllSelected(); } - this.setSelected(_select); - for (var i = 0; i < this.children.length; i++) - { - this.children[i].toggleAllSelected(_select); - } + this.setAllSelected(_select); } /** @@ -620,6 +636,7 @@ egwActionObject.prototype.flatList = function(_obj) * and this one. The operation returns an empty list, if a container object is * found on the way. */ +//TODO: Remove flatList here! egwActionObject.prototype.traversePath = function(_to) { var contRoot = this.getContainerRoot(); @@ -667,23 +684,8 @@ egwActionObject.prototype.getIndex = function() */ egwActionObject.prototype.getFocusedObject = function() { - //Search for the focused object in the children - for (var i = 0; i < this.children.length; i++) - { - var obj = this.children[i].getFocusedObject() - if (obj) - { - return obj; - } - } - - //One of the child objects hasn't been focused, probably this object is - if (!egwBitIsSet(this.flags, EGW_AO_FLAG_IS_CONTAINER) && this.getFocused()) - { - return this; - } - - return null; + var cr = this.getContainerRoot(); + return cr ? cr.focusedChild : null; } /** @@ -695,12 +697,33 @@ egwActionObject.prototype.getFocusedObject = function() * @param int _shiftState is the status of extra keys being pressed during the * selection process. */ -egwActionObject.prototype._ifaceCallback = function(_newState, _shiftState) +egwActionObject.prototype._ifaceCallback = function(_newState, _changedBit, _shiftState) { if (typeof _shiftState == "undefined") _shiftState = EGW_AO_SHIFT_STATE_NONE; + + var selected = egwBitIsSet(_newState, EGW_AO_STATE_SELECTED); + var visible = egwBitIsSet(_newState, EGW_AO_STATE_VISIBLE); + + // Check whether the visibility of the object changed + if (_changedBit == EGW_AO_STATE_VISIBLE && visible != this.getVisible()) + { + // Deselect the object + if (!visible) + { + this.setSelected(false); + this.setFocused(false); + return EGW_AO_STATE_NORMAL; + } + else + { + // Auto-register the actions attached to this object + this.registerActions(); + } + } + // Remove the focus from all children on the same level - if (this.parent) + if (this.parent && visible && _changedBit == EGW_AO_STATE_SELECTED) { var selected = egwBitIsSet(_newState, EGW_AO_STATE_SELECTED); var objs = []; @@ -714,14 +737,7 @@ egwActionObject.prototype._ifaceCallback = function(_newState, _shiftState) // state is not set if (!egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)) { - var lst = this.getContainerRoot().flatList(); - for (var i = 0; i < lst.length; i++) - { - if (lst[i] != this) - { - lst[i].setSelected(false); - } - } + var lst = this.getContainerRoot().setAllSelected(false); } // If the LIST state is active, get all objects inbetween this one and the focused one @@ -745,8 +761,13 @@ egwActionObject.prototype._ifaceCallback = function(_newState, _shiftState) if (objs.length == 0 || !egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK)) { this.setFocused(true); + _newState = egwSetBit(EGW_AO_STATE_FOCUSED, _newState, true); } + + this.setSelected(selected); } + + return _newState; } /** @@ -765,6 +786,15 @@ egwActionObject.prototype.getFocused = function() return egwBitIsSet(this.getState(), EGW_AO_STATE_FOCUSED); } +/** + * Returns whether the object currently is visible - visible means, that the + * AOI has a dom node and is visible. + */ +egwActionObject.prototype.getVisible = function() +{ + return egwBitIsSet(this.getState(), EGW_AO_STATE_VISIBLE); +} + /** * Returns the complete state of the object. */ @@ -779,55 +809,28 @@ egwActionObject.prototype.getState = function() * be de-focused. * * @param boolean _focused - whether to remove or set the focus. Defaults to true - * @param object _recPrev is internally used to prevent infinit recursion. Do not touch. */ -egwActionObject.prototype.setFocused = function(_focused, _recPrev) +egwActionObject.prototype.setFocused = function(_focused) { if (typeof _focused == "undefined") _focused = true; - //TODO: When deleting and moving objects is implemented, don't forget to update - // the selection and the focused element!! + var state = this.iface.getState(); - if (typeof _recPrev == "undefined") - _recPrev = false; - - //Check whether the focused state has changed - if (_focused != this.getFocused()) + if (egwBitIsSet(state, EGW_AO_STATE_FOCUSED) != _focused) { - //Reset the focus of the formerly focused element - if (!_recPrev) + // Un-focus the currently focused object + var currentlyFocused = this.getFocusedObject(); + if (currentlyFocused && currentlyFocused != this) { - var focused = this.getRootObject().getFocusedObject(); - if (focused) - { - focused.setFocused(false, this); - } + currentlyFocused.setFocused(false); } - if (!_focused) + this.iface.setState(egwSetBit(state, EGW_AO_STATE_FOCUSED, _focused)); + if (this.parent) { - //If the object is not focused, reset the focus state of all children - for (var i = 0; i < this.children.length; i++) - { - if (this.children[i] != _recPrev) - { - this.children[i].setFocused(false, _recPrev); - } - } + this.parent.updateFocusedChild(this, _focused); } - else - { - //Otherwise set the focused state of the parent to true - if (this.parent) - { - this.parent.setFocused(true, _recPrev); - } - } - - //No perform the actual change in the interface state. - this.iface.setState(egwSetBit(this.iface.getState(), EGW_AO_STATE_FOCUSED, - _focused)); } } @@ -837,8 +840,103 @@ egwActionObject.prototype.setFocused = function(_focused, _recPrev) */ egwActionObject.prototype.setSelected = function(_selected) { - this.iface.setState(egwSetBit(this.iface.getState(), EGW_AO_STATE_SELECTED, - _selected)); + var state = this.iface.getState(); + + if ((egwBitIsSet(state, EGW_AO_STATE_SELECTED) != _selected) && + egwBitIsSet(state, EGW_AO_STATE_VISIBLE)) + { + this.iface.setState(egwSetBit(state, EGW_AO_STATE_SELECTED, _selected)); + if (this.parent) + { + this.parent.updateSelectedChildren(this, _selected || this.selectedChildren.length > 0); + } + } +} + +/** + * Sets the selected state of all elements, including children + */ +egwActionObject.prototype.setAllSelected = function(_selected, _informParent) +{ + if (typeof _informParent == "undefined") + _informParent = true; + + var state = this.iface.getState(); + + // Update this element + if (egwBitIsSet(state, EGW_AO_STATE_SELECTED) != _selected) + { + this.iface.setState(egwSetBit(state, EGW_AO_STATE_SELECTED, _selected)); + if (_informParent && this.parent) + { + this.parent.updateSelectedChildren(this, _selected); + } + } + + // Update the children if the should be selected or if they should be + // deselected and there are selected children. + if (_selected || this.selectedChildren.length > 0) + { + for (var i = 0; i < this.children.length; i++) + { + this.children[i].setAllSelected(_selected, false); + } + } + + // Copy the selected children list + this.selectedChildren = _selected ? this.children : []; +} + + +/** + * Updates the selectedChildren array each actionObject has in order to determine + * all selected children in a very fast manner. + * TODO: Has also to be updated, if an child is added/removed! + */ +egwActionObject.prototype.updateSelectedChildren = function(_child, _selected) +{ + var id = this.selectedChildren.indexOf(_child); + var wasEmpty = this.selectedChildren.length == 0; + + // Add or remove the given child from the selectedChildren list + if (_selected && id == -1) + { + this.selectedChildren.push(_child); + } + else if (!_selected && id != -1) + { + this.selectedChildren.splice(id, 1); + } + + // If the emptieness of the selectedChildren array has changed, update the + // parent selected children array. + if (wasEmpty != this.selectedChildren.length == 0 && this.parent) + { + this.parent.updateSelectedChildren(this, wasEmpty); + } +} + +/** + * Updates the focusedChild up to the container boundary. + */ +egwActionObject.prototype.updateFocusedChild = function(_child, _focused) +{ + if (_focused) + { + this.focusedChild = _child; + } + else + { + if (this.focusedChild = _child) + { + this.focusedChild = null; + } + } + + if (this.parent && !egwBitIsSet(this.flags, EGW_AO_FLAG_IS_CONTAINER)) + { + this.parent.updateFocusedChild(_child, _focused); + } } /** @@ -893,6 +991,20 @@ egwActionObject.prototype.updateActionLinks = function(_actionLinks, _recursive, this.children[i].updateActionLinks(_actionLinks, true, _doCreate); } } + + if (this.getVisible()) + { + this.registerActions(); + } +} + +/** + * Reconnects the actions. + */ +egwActionObject.prototype._reconnectCallback = function() +{ + this.registeredImpls = []; + this.registerActions; } /** @@ -911,9 +1023,15 @@ egwActionObject.prototype.registerActions = function() { var impl = _egwActionClasses[group].implementation(); - // Register a handler for that action with the interface of that object, - // the callback and this object as context for the callback - impl.registerAction(this.iface, this.executeActionImplementation, this); + if (this.registeredImpls.indexOf(impl) == -1) + { + // Register a handler for that action with the interface of that object, + // the callback and this object as context for the callback + if (impl.registerAction(this.iface, this.executeActionImplementation, this)) + { + this.registeredImpls.push(impl); + } + } } } } @@ -964,7 +1082,7 @@ egwActionObject.prototype.executeActionImplementation = function(_implContext, _ */ egwActionObject.prototype.forceSelection = function() { - var selected = this.getSelectedObjects(); + var selected = this.getContainerRoot().getSelectedObjects(); // Check whether this object is in the list var thisInList = selected.indexOf(this) != -1; @@ -972,11 +1090,11 @@ egwActionObject.prototype.forceSelection = function() // If not, select it if (!thisInList) { + this.getContainerRoot().setAllSelected(false); this.setSelected(true); - this._ifaceCallback(egwSetBit(this.getState(), EGW_AO_STATE_SELECTED, true), - EGW_AO_SHIFT_STATE_NONE); - selected = [this]; } + + this.setFocused(true); } /** @@ -994,7 +1112,7 @@ egwActionObject.prototype.forceSelection = function() egwActionObject.prototype.getSelectedLinks = function(_actionType) { // Get all objects in this container which are currently selected - var selected = this.getSelectedObjects(); + var selected = this.getContainerRoot().getSelectedObjects(); var actionLinks = {}; var testedSelected = []; @@ -1138,9 +1256,12 @@ function egwActionObjectInterface() // or not. this.doSetState = function(_state, _outerCall) {}; - this.doTriggerEvent = function(_event) {}; + this._state = EGW_AO_STATE_NORMAL || EGW_AO_STATE_VISIBLE; - this._state = EGW_AO_STATE_NORMAL; + this.stateChangeCallback = null; + this.stateChangeContext = null; + this.reconnectActionsCallback = null; + this.reconnectActionsContext = null; } /** @@ -1153,23 +1274,50 @@ egwActionObjectInterface.prototype.setStateChangeCallback = function(_callback, this.stateChangeContext = _context; } +/** + * Sets the reconnectActions callback, which will be called by the AOI if its + * DOM-Node has been replaced and the actions have to be re-registered. + */ +egwActionObjectInterface.prototype.setReconnectActionsCallback = function(_callback, _context) +{ + this.reconnectActionsCallback = _callback; + this.reconnectActionsContext = _context; +} + +/** + * Will be called by the aoi if the actions have to be re-registered due to a + * DOM-Node exchange. + */ +egwActionObjectInterface.prototype.reconnectActions = function() +{ + if (this.reconnectActionsCallback) + { + this.reconnectActionsCallback.call(this.reconnectActionsContext); + } +} + /** * Internal function which should be used whenever the select status of the object * has been changed by the user. This will automatically calculate the new state of * the object and call the stateChangeCallback (if it has been set) * - * @param boolean _selected Whether the object is selected or not. - * @param int _shiftState special keys which change how the state change will - * be treated. + * @param int _stateBit is the bit in the state bit which should be changed + * @param boolean _set specifies whether the state bit should be set or not */ -egwActionObjectInterface.prototype._selectChange = function(_selected, _shiftState) +egwActionObjectInterface.prototype.updateState = function(_stateBit, _set, _shiftState) { - //Set the EGW_AO_STATE_SELECTED bit accordingly and call the callback - this._state = egwSetBit(this._state, EGW_AO_STATE_SELECTED, _selected); + // Calculate the new state + var newState = egwSetBit(this._state, _stateBit, _set); + + // Call the stateChangeCallback if the state really changed if (this.stateChangeCallback) { - this.stateChangeCallback.call(this.stateChangeContext, - this._state, _shiftState); + this._state = this.stateChangeCallback.call(this.stateChangeContext, newState, + _stateBit, _shiftState); + } + else + { + this._state = newState; } } @@ -1196,7 +1344,7 @@ egwActionObjectInterface.prototype.setState = function(_state) if (_state != this._state) { this._state = _state; - this.doSetState(_state, true); + this.doSetState(_state); } } @@ -1210,7 +1358,6 @@ egwActionObjectInterface.prototype.getState = function() return this._state; } - /** egwActionObjectManager Object **/ /** diff --git a/phpgwapi/js/egw_action/egw_action_common.js b/phpgwapi/js/egw_action/egw_action_common.js index baeb36f17a..ee04d65990 100644 --- a/phpgwapi/js/egw_action/egw_action_common.js +++ b/phpgwapi/js/egw_action/egw_action_common.js @@ -83,3 +83,30 @@ if (typeof Array.prototype.indexOf == "undefined") }; } +/** + * Isolates the shift state from an event object + */ +function egwGetShiftState(e) +{ + var state = EGW_AO_SHIFT_STATE_NONE; + state = egwSetBit(state, EGW_AO_SHIFT_STATE_MULTI, e.ctrkKey || e.metaKey); + state = egwSetBit(state, EGW_AO_SHIFT_STATE_BLOCK, e.shiftKey); + return state; +} + +function egwPreventSelect(e) +{ + if (egwGetShiftState(e) > EGW_AO_SHIFT_STATE_NONE) + { + this.onselectstart = function() { + return false; + } + + return false; + } +} + +function egwResetPreventSelect(elem) +{ +} + diff --git a/phpgwapi/js/egw_action/egw_action_popup.js b/phpgwapi/js/egw_action/egw_action_popup.js index 2e957da678..f02ce0f4ad 100644 --- a/phpgwapi/js/egw_action/egw_action_popup.js +++ b/phpgwapi/js/egw_action/egw_action_popup.js @@ -88,7 +88,10 @@ function egwPopupActionImplementation() } return false; } + + return true; } + return false; } ai.doUnregisterAction = function(_aoi) diff --git a/phpgwapi/js/egw_action/egw_grid.js b/phpgwapi/js/egw_action/egw_grid.js new file mode 100644 index 0000000000..881f2ba10a --- /dev/null +++ b/phpgwapi/js/egw_action/egw_grid.js @@ -0,0 +1,586 @@ +/** + * eGroupWare egw_action framework - egw action framework + * + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @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); +} + diff --git a/phpgwapi/js/egw_action/test/grid.css b/phpgwapi/js/egw_action/test/grid.css new file mode 100644 index 0000000000..9ffbe36183 --- /dev/null +++ b/phpgwapi/js/egw_action/test/grid.css @@ -0,0 +1,94 @@ +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +body, td, th { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 11px; +} + +.grid { + width: 100%; + border-spacing: 0px; +} + +.grid tr.hidden { + display: none; +} + +.grid td, .grid th { + border: 1px solid white; +} + +.grid tr.focused td { + border: 1px dotted black; +} + +.grid tr.selected td { + background-image: url(imgs/select_overlay.png); + background-position: center; + background-repeat: repeat-x; +} + +.grid span.arrow { + display: inline-block; + vertical-align: middle; + width: 8px; + height: 8px; + background-repeat: no-repeat; + margin-right: 2px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} + +.grid span.arrow.opened { + cursor: pointer; + background-image: url(imgs/arrows.png); + background-position: -8px 0; +} + +.grid span.arrow.closed { + cursor: pointer; + background-image: url(imgs/arrows.png); + background-position: 0 0; +} + +.grid tr.odd { + background-color: #F1F1F1; +} + +.grid th { + background-color: #E0E0E0; + font-weight: normal; + padding: 5px; + text-align: left; +} + +.grid td { + padding: 0 5px 0 5px; + vertical-align: middle; +} + +.grid th.front { + font-weight: bold; +} + +.grid span.caption { + vertical-align: middle; +} + +.grid span.indentation { + display: inline-block; +} + +.grid img.icon { + vertical-align: middle; + margin: 2px 5px 2px 2px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} diff --git a/phpgwapi/js/egw_action/test/imgs/arrow_down.png b/phpgwapi/js/egw_action/test/imgs/arrow_down.png new file mode 100755 index 0000000000000000000000000000000000000000..648ebb6c6322ba2eeded92dec83d71bc1f6b9c6e GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR42?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4<^OaFa`YL?l&>JwyX2inWv>FVdQ&MBb@09f8o00000 literal 0 HcmV?d00001 diff --git a/phpgwapi/js/egw_action/test/imgs/arrows.png b/phpgwapi/js/egw_action/test/imgs/arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c4d9fdbd1d5ae23f19e23317e81f850d90e2b0 GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQY^(zo*^7SP{WbZ0pxQQctjQh z)n5l;MkkHg6+l7B64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&**skw zLo_DVPTc5s*g?Q0{rw%8R#o$|=lTqaQeB(ZKjHo%^5l<@sHura;1X|_$4%=CxS}_$ zD|onfslqg$DF#=wx|Yw3+;lC^<;mUZ|GnI+R=sk~{;Q*u^;S4A<(c`a*Ska~Ml1bF zdG<^uWo>PMSMAfvc>;nA4lV^Ya?d3mFJ^LZ4OEGoW}CT||J+Xg?6-HBKHS^xvL*Fd zMo*(5+Zmb8jMsbfuWIsLG6_wMytl^j+|Fu-glEg%|MqlIPD7J7U)PI_wGxu0w&tU@}hO+6~5}u_=wB0SzuVZr9wQlzM-W`4q=UA^1`pcBm lv;P0y!nKw2%%9)mf6=|@QOy4Pp1@#Z@O1TaS?83{1OPB}pl<*G literal 0 HcmV?d00001 diff --git a/phpgwapi/js/egw_action/test/imgs/arrows.svg b/phpgwapi/js/egw_action/test/imgs/arrows.svg new file mode 100644 index 0000000000..e9f5e45595 --- /dev/null +++ b/phpgwapi/js/egw_action/test/imgs/arrows.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/phpgwapi/js/egw_action/test/imgs/mime16_directory.png b/phpgwapi/js/egw_action/test/imgs/mime16_directory.png new file mode 100644 index 0000000000000000000000000000000000000000..4b545f3e34fc49e93a4854031c6ef79505f1130d GIT binary patch literal 695 zcmV;o0!aOdP)z@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ;ElET{RCwBAWPk#ii8P!Xu*FgNJ)JY-;K zy7m7*BS?&q0qjhu{O&q6Mt}ffVfg!-;p-nvlmCMh0afKC+C%sZ5I%$fp_2bz`wtL6 zEdT!eX88OIj~juC6WVTWWny6V#^M16fB<6o4|KstsPmbC_#ce^51~_=Zd^+V5%*5A z7GU6JV}!fx>T3puvRRK900M~R|G&QspMFAYs+)N07b7DR8_fKFK+`g!B(yTD1Q-^d zWnh5&6le%D(DV@4t@ z1%scz{xW>~{QV6;0I~f4^NZohn_pnXciu8Eq^(BS!pMlC2W}{c`10X5!>?~Y9svXp z*b9%Jd;$8Ih2i-ppfi6W3}9eHvK1*gF#%ok@%2}Rf8W1d0|+2+AijG3nc)Y}fY)Du z{{D%i_&=&yFg7zVl)eBB`19lUU4Q@rhr#Vz?-`n6WEcYM8HfTe9z0=4y?USF?6*Ic z0Ro8W|Gz&B-#&h2P>|$eAQpW60d)229}Itge7_G6KtLD#W%zLS1;f1?cNyM4WroK9 zKETDn#PH_zcZMGyzB~d5Ah6ej^%nnUKn^=_=>G?&eXs~PBcO#Qko)#hEF(YwG5#mU dN<07{zyM2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4 + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/phpgwapi/js/egw_action/test/test_action.html b/phpgwapi/js/egw_action/test/test_action.html index 4f7f691668..d89b0a9dae 100644 --- a/phpgwapi/js/egw_action/test/test_action.html +++ b/phpgwapi/js/egw_action/test/test_action.html @@ -106,9 +106,9 @@ var state = getShiftState(e); // "Normal" Listbox behaviour - aoi.doSetState(egwSetBit(aoi.getState(), EGW_AO_STATE_SELECTED, - !egwBitIsSet(state, EGW_AO_SHIFT_STATE_MULTI) || !selected), - false, state); + aoi.updateState(EGW_AO_STATE_SELECTED, + !egwBitIsSet(state, EGW_AO_SHIFT_STATE_MULTI) || !selected, + state); // "PHPMyAdmin" Listbox behaviour // aoi.doSetState(egwSetBit(aoi.getState(), EGW_AO_STATE_SELECTED, @@ -117,22 +117,16 @@ }); $(aoi.checkBox).change(function() { - aoi.doSetState(egwSetBit(aoi.getState(), EGW_AO_STATE_SELECTED, - this.checked), false, EGW_AO_SHIFT_STATE_MULTI); + aoi.updateState(EGW_AO_STATE_SELECTED, this.checked, EGW_AO_SHIFT_STATE_MULTI); }); - aoi.doSetState = function(_state, _outerCall, _shiftState) { + aoi.doSetState = function(_state) { var selected = egwBitIsSet(_state, EGW_AO_STATE_SELECTED); this.checkBox.checked = selected; $(this.node).toggleClass('focused', egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); $(this.node).toggleClass('selected', selected); - if (! _outerCall) - { - this._selectChange(egwBitIsSet(_state, - EGW_AO_STATE_SELECTED), _shiftState); - } } return aoi; @@ -258,8 +252,6 @@ obj.updateActionLinks(listboxFileLinks); else obj.updateActionLinks(listboxFolderLinks); - - obj.registerActions(); }); $("#selectAll").click(function() { diff --git a/phpgwapi/js/egw_action/test/test_grid.html b/phpgwapi/js/egw_action/test/test_grid.html new file mode 100644 index 0000000000..ab75bd93e9 --- /dev/null +++ b/phpgwapi/js/egw_action/test/test_grid.html @@ -0,0 +1,175 @@ + + + Grid Test + + + + + + + + + + + + + + + + + + + + + +
+
+ + +