From 57aaf6d756b00edc5f874f06740419cdff3f8499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20St=C3=B6ckel?= Date: Thu, 10 Mar 2011 20:58:35 +0000 Subject: [PATCH] Dynamic loading of content tested and optimized, resolved bugs, implemented support for data trees in the view classes. The whole progess can be seen in the test/test_grid_view.html file. --- phpgwapi/js/egw_action/egw_action.js | 8 +- phpgwapi/js/egw_action/egw_action_common.js | 1 + phpgwapi/js/egw_action/egw_grid_data.js | 392 ++++++++---- phpgwapi/js/egw_action/egw_grid_view.js | 560 +++++++++++++----- phpgwapi/js/egw_action/test/grid.css | 201 +++---- .../js/egw_action/test/imgs/ajax-loader.gif | Bin 0 -> 1683 bytes .../egw_action/test/imgs/focused_hatching.png | Bin 0 -> 202 bytes .../egw_action/test/imgs/focused_hatching.svg | 169 ++++++ .../js/egw_action/test/test_grid_view.html | 80 ++- 9 files changed, 1028 insertions(+), 383 deletions(-) create mode 100644 phpgwapi/js/egw_action/test/imgs/ajax-loader.gif create mode 100644 phpgwapi/js/egw_action/test/imgs/focused_hatching.png create mode 100644 phpgwapi/js/egw_action/test/imgs/focused_hatching.svg diff --git a/phpgwapi/js/egw_action/egw_action.js b/phpgwapi/js/egw_action/egw_action.js index 1390cf45b9..c1e68ea622 100644 --- a/phpgwapi/js/egw_action/egw_action.js +++ b/phpgwapi/js/egw_action/egw_action.js @@ -395,8 +395,6 @@ function egwActionObject(_id, _parent, _iface, _manager, _flags) this.focusedChild = null; this.setAOI(_iface); - this.iface.setStateChangeCallback(this._ifaceCallback, this); - this.iface.setReconnectActionsCallback(this._reconnectCallback, this); } /** @@ -418,6 +416,8 @@ egwActionObject.prototype.setAOI = function(_aoi) // Replace the interface object this.iface = _aoi; + this.iface.setStateChangeCallback(this._ifaceCallback, this); + this.iface.setReconnectActionsCallback(this._reconnectCallback, this); } /** @@ -764,7 +764,7 @@ egwActionObject.prototype._ifaceCallback = function(_newState, _changedBit, _shi // and set their select state. if (egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK)) { - var focused = this.getRootObject().getFocusedObject(); + var focused = this.getFocusedObject(); if (focused) { objs = this.traversePath(focused); @@ -915,7 +915,7 @@ egwActionObject.prototype.setAllSelected = function(_selected, _informParent) */ egwActionObject.prototype.updateSelectedChildren = function(_child, _selected) { - var id = this.selectedChildren.indexOf(_child); + var id = this.selectedChildren.indexOf(_child); // TODO Replace by binary search, insert children sorted by index! var wasEmpty = this.selectedChildren.length == 0; // Add or remove the given child from the selectedChildren list diff --git a/phpgwapi/js/egw_action/egw_action_common.js b/phpgwapi/js/egw_action/egw_action_common.js index 04a60ceebc..1770cf0c9a 100644 --- a/phpgwapi/js/egw_action/egw_action_common.js +++ b/phpgwapi/js/egw_action/egw_action_common.js @@ -91,6 +91,7 @@ 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; } diff --git a/phpgwapi/js/egw_action/egw_grid_data.js b/phpgwapi/js/egw_action/egw_grid_data.js index 9c6d6338ad..7b8e0190ff 100644 --- a/phpgwapi/js/egw_action/egw_grid_data.js +++ b/phpgwapi/js/egw_action/egw_grid_data.js @@ -64,6 +64,7 @@ function egwGridDataElement(_id, _parent, _columns, _readQueue, _objectManager) this.canHaveChildren = false; this.type = egwGridViewRow; this.userData = null; + this.updatedGrid = null; this.gridViewObj = null; } @@ -132,7 +133,8 @@ egwGridDataElement.prototype.set_data = function(_value) this.data[col_id] = { "data": data, - "sortData": sortData + "sortData": sortData, + "queued": false } } } @@ -168,8 +170,13 @@ egwGridDataElement.prototype.set_data = function(_value) * "canHaveChildren": [true|false] // Specifies whether the row "open/close" button is displayed * } */ -egwGridDataElement.prototype.loadData = function(_data) +egwGridDataElement.prototype.loadData = function(_data, _doCallUpdate) { + if (typeof _doCallUpdate == "undefined") + { + _doCallUpdate = false; + } + if (_data.constructor == Array) { var virgin = this.children.length == 0; @@ -197,6 +204,7 @@ egwGridDataElement.prototype.loadData = function(_data) { var count = typeof entry.count == "number" && entry.count >= 0 ? entry.count : 1; var prefix = typeof entry.prefix == "string" ? entry.prefix : "elem_"; + var canHaveChildren = typeof entry.canHaveChildren == "boolean" ? entry.canHaveChildren : false; var index = last_element ? last_element.index + 1 : 0; for (var j = 0; j < count; j++) @@ -204,6 +212,7 @@ egwGridDataElement.prototype.loadData = function(_data) var id = prefix + (index + j); element = this.insertElement(index + j, id); element.type = type; // Type can only be set directly after creation + element.canHaveChildren = canHaveChildren; } } else if (entryType == EGW_DATA_TYPE_ELEMENT) @@ -240,7 +249,12 @@ egwGridDataElement.prototype.loadData = function(_data) this.loadData(_data.children); } - this.gridViewObj.callGridViewObjectUpdate(); + if (_doCallUpdate) + { + this.callBeginUpdate(); + } + + this.callGridViewObjectUpdate(); } } @@ -328,7 +342,7 @@ egwGridDataElement.prototype.getElementById = function(_id, _depth) { for (var i = 0; i < this.children.length; i++) { - var elem = this.children.getElementById(_id, _depth - 1); + var elem = this.children[i].getElementById(_id, _depth - 1); if (elem) { @@ -348,13 +362,13 @@ egwGridDataElement.prototype.getChildren = function(_callback, _context) { if (this.children.length > 0) { - _callback.call(_context, this.children); + _callback.call(_context, this.children, true); } else if (this.canHaveChildren) { // If the children havn't been loaded yet, request them via queue call. - this.readQueue.queue(this, EGW_DATA_QUEUE_CHILDREN, function() { - _callback.call(_context, this.children); + this.readQueue.queueCall(this, EGW_DATA_QUEUE_CHILDREN, function() { + _callback.call(_context, this.children, false); }, this); } } @@ -386,16 +400,21 @@ egwGridDataElement.prototype.hasColumn = function(_columnId, _returnData) res = true; } } + + if (!_returnData && typeof (this.data[_columnId]) != "undefined" && this.data[_columnId].queued) + { + res = true; + } } else { // Check whether the column data of this column has been read, // if yes, return it. - if (typeof this.data[_columnIds] != "undefined") + if (typeof this.data[_columnId] != "undefined") { - if (_returnData) + if (_returnData && typeof this.data[_columnId].data != "undefined") { - res = this.data[_columnIds].data; + res = this.data[_columnId].data; } else { @@ -456,7 +475,7 @@ egwGridDataElement.prototype.getData = function(_columnIds) // in the readQueue if (queryList.length > 0) { - this.readQueue.queue(this, queryList); + this.readQueue.queueCall(this, queryList); } return result; @@ -467,11 +486,16 @@ egwGridDataElement.prototype.getData = function(_columnIds) * Calls the row object update function - checks whether the row object implements * this interface and whether it is set. */ -egwGridDataElement.prototype.callGridViewObjectUpdate = function() +egwGridDataElement.prototype.callGridViewObjectUpdate = function(_immediate) { + if (typeof _immediate == "undefined") + { + _immediate = false; + } + if (this.gridViewObj && typeof this.gridViewObj.doUpdateData == "function") { - this.gridViewObj.doUpdateData(); + this.gridViewObj.doUpdateData(_immediate); } } @@ -516,7 +540,63 @@ egwGridDataElement.prototype.setGridViewObj = function(_obj) } } +/** + * Returns the root element + */ +egwGridDataElement.prototype.getRootElement = function() +{ + if (!this.parent) + { + return this; + } + else + { + return this.parent.getRootElement(); + } +} +/** + * Returns the depth of this element in the document tree + */ +egwGridDataElement.prototype.getDepth = function() +{ + return (this.parent) ? (this.parent.getDepth() + 1) : 0; +} + +/** + * Calls the beginUpdate function of the grid associated to the grid view object + */ +egwGridDataElement.prototype.callBeginUpdate = function() +{ + if (this.gridViewObj) + { + var root = this.getRootElement(); + + if (root.updatedGrid != this.gridViewObj.grid) + { + if (root.updatedGrid) + { + root.updatedGrid.endUpdate(); + } + root.updatedGrid = this.gridViewObj.grid; + root.updatedGrid.beginUpdate(); + } + } +} + +/** + * Calls the end update function of the currently active updated grid + */ +egwGridDataElement.prototype.callEndUpdate = function() +{ + var root = this.getRootElement(); + + if (root.updatedGrid) + { + root.updatedGrid.endUpdate(); + root.updatedGrid = null; + } +} /** - egwGridDataReadQueue -- **/ @@ -558,27 +638,56 @@ egwGridDataQueue.prototype.setDataRoot = function(_dataRoot) */ egwGridDataQueue.prototype._queue = function(_obj) { + this.timeoutId++; + + // Push the queue object onto the queue this.queue.push(_obj); if (this.queue.length > EGW_DATA_QUEUE_MAX_ELEM_COUNT) { - this.flushQueue(); + this.flushQueue(false); return false; } + else + { + // Specify that the element data is queued + for (var i = 0; i < this.queueColumns.length; i++) + { + if (typeof _obj.elem.data[this.queueColumns[i]] == "undefined") + { + _obj.elem.data[this.queueColumns[i]] = { + "queued": true + } + } + } + + // Set the flush queue timeout + var tid = this.timeoutId; + var self = this; + window.setTimeout(function() { + if (self.timeoutId == tid) + { + self.flushQueue(true); + } + }, EGW_DATA_QUEUE_FLUSH_TIMEOUT); + } return true; } -egwGridDataQueue.prototype.inQueue = function(_elem) +egwGridDataQueue.prototype._accumulateQueueColumns = function(_columns) { - for (var i = 0; i < this.queue.length; i++) + if (this.dataRoot.columns.columns.length > this.queueColumns.length) { - if (this.queue[i].elem == _elem) + // Merge the specified columns into the queueColumns variable + for (var i = 0; i < _columns.length; i++) { - return true; + if (this.queueColumns.indexOf(_columns[i]) == -1) + { + this.queueColumns.push(_columns[i]); + } } } - return false; } /** @@ -594,7 +703,7 @@ egwGridDataQueue.prototype.inQueue = function(_elem) * @param object _context is the context in which the callback function will * be executed. */ -egwGridDataQueue.prototype.queue = function(_elem, _columns, _callback, _context) +egwGridDataQueue.prototype.queueCall = function(_elem, _columns, _callback, _context) { if (typeof _callback == "undefined") { @@ -610,7 +719,7 @@ egwGridDataQueue.prototype.queue = function(_elem, _columns, _callback, _context if (!this._queue({ "elem": _elem, "type": EGW_DATA_QUEUE_CHILDREN, - "proc": _callback, + "callback": _callback, "context": _context })) { @@ -619,73 +728,125 @@ egwGridDataQueue.prototype.queue = function(_elem, _columns, _callback, _context } else { - // Merge the specified columns into the queueColumns variable - for (var i = 0; i < _columns.length; i++) - { - if (this.queueColumns.indexOf(_columns[i]) == -1) - { - this.queueColumns.push(_columns[i]); - } - } + // Accumulate the queue columns ids + this._accumulateQueueColumns(_columns); // Queue the element and search in the elements around the given one for // elements whose data isn't loaded yet. - var done = !this._queue({ + this._queue({ "elem": _elem, "type": EGW_DATA_QUEUE_ELEM, - "proc": _callback, + "callback": _callback, "context": _context }); + } +} - // Prefetch other elements around the given element - var parent = _elem.parent; - if (parent) +egwGridDataQueue.prototype._getQueuePlanes = function() +{ + var planes = []; + var curPlane = null; + + for (var i = 0; i < this.queue.length; i++) + { + var elem = this.queue[i].elem; + + if (!curPlane || elem.parent != curPlane.parent) { - // Initialize the start prefetch index and the max prefetch count - var prefetch = EGW_DATA_QUEUE_PREFETCH_COUNT; - var idx = Math.floor(Math.max(0, _elem.index - prefetch / 2)); - - while (!done && prefetch > 0 && idx < parent.children.length) + curPlane = null; + for (var j = 0; j < planes.length; j++) { - - // Don't prefetch the element itself - if (idx != _elem.idx) + if (planes[j].parent == elem.parent) { - // Fetch the element with the current index from the children - // of the parent of the element. - var elem = parent.children[idx]; + curPlane = planes[j]; + break; + } + } - // Check whether this element has all data columns loaded and is - // not already in the queue - if (!this.inQueue(elem)) + if (!curPlane) + { + curPlane = { + "parent": elem.parent, + "cnt": 0, + "min": 0, + "max": 0, + "idx": 0, + "done": false + }; + planes.push(curPlane); + } + } + + if (curPlane.cnt == 0 || elem.index < curPlane.min) + { + curPlane.min = elem.index; + } + if (curPlane.cnt == 0 || elem.index > curPlane.max) + { + curPlane.max = elem.index; + } + + curPlane.cnt++; + } + + return planes; +} + +egwGridDataQueue.prototype.prefetch = function(_cnt) +{ + var cnt = _cnt; + var planes = this._getQueuePlanes(); + + // Set the start indices + for (var i = 0; i < planes.length; i++) + { + planes[i].idx = Math.max(0, Math.ceil(planes[i].min - _cnt / (2 * planes.length))); + } + + // Add as many elements as specified to the prefetched elements + var done = 0; + var plane = 0; + while (cnt > 0 && done < planes.length) + { + if (!planes[plane].done) + { + var idx = planes[plane].idx; + + if (idx == planes[plane].parent.children.length) + { + planes[plane].done = true; + done++; + } + else + { + var hasData = true; + var elem = planes[plane].parent.children[idx]; + for (var j = 0; j < this.queueColumns.length; j++) + { + if (!elem.hasColumn(this.queueColumns[i], false)) { - var hasColumns = true; - for (var j = 0; j < this.queueColumns.length; j++) - { - var res = elem.hasColumn(this.queueColumns[i], false); - if (!res) - { - hasColumns = false; - break; - } - } - - if (!hasColumns) - { - done = !this._queue({ - "elem": elem, - "type": EGW_DATA_QUEUE_ELEM, - "proc": null, - "context": null - }); - prefetch--; - } + hasData = false; + break; } } - idx++; + if (!hasData) + { + this._queue({ + "elem": elem, + "type": EGW_DATA_QUEUE_ELEM, + "callback": null, + "context": null + }); + cnt--; + } + + planes[plane].idx++; } } + + // Go to the next plane + plane = (plane + 1) % planes.length; } } @@ -693,14 +854,32 @@ egwGridDataQueue.prototype.queue = function(_elem, _columns, _callback, _context * Empties the queue and calls the fetch callback which cares about retrieving * the data from the server. */ -egwGridDataQueue.prototype.flushQueue = function() +egwGridDataQueue.prototype.flushQueue = function(_doPrefetch) { var ids = []; + if (_doPrefetch) + { + // Get the count of elements which will be dynamically added to the list, "prefetched" + var prefetch_cnt = Math.min(EGW_DATA_QUEUE_PREFETCH_COUNT, + Math.max(0, EGW_DATA_QUEUE_MAX_ELEM_COUNT - this.queue.length)); + + this.prefetch(prefetch_cnt); + } + // Generate a list of element ids for (var i = 0; i < this.queue.length; i++) { - ids.push(this.queue[i].elem.id); + var id = this.queue[i].elem.id; + if (id == this.queue[i].elem.id) + { + if (this.queue[i].type == EGW_DATA_QUEUE_CHILDREN) + { + id = "[CHILDREN]" + id; + } + } + + ids.push(id); } // Call the fetch callback and save a snapshot of the current queue @@ -711,53 +890,60 @@ egwGridDataQueue.prototype.flushQueue = function() this.queue = []; this.queueColumns = []; + this.timeoutId = 0; } egwGridDataQueue.prototype.dataCallback = function(_data, _queue) { var rootData = []; - - // Iterate over the given data and check whether the data coresponds to one - // of the queue elements - if yes, call their (probably) specified callback. - // All elements for which no queue element can be found are added to the - // "rootData" list, which is then loaded by the "dataRoot" data object. - for (var i = 0; i < _data.length; i++) + try { - var hasTarget = false; - - // Search for a queue element which belongs to the given data entry. - if (_queue.length > 0 && typeof _data[i].id != "undefined") + // Iterate over the given data and check whether the data coresponds to one + // of the queue elements - if yes, call their (probably) specified callback. + // All elements for which no queue element can be found are added to the + // "rootData" list, which is then loaded by the "dataRoot" data object. + var i = 0; + for (var i = 0; i < _data.length; i++) { - var id = _data[i].id; + var hasTarget = false; - for (var j = 0; j < _queue.length; j++) + // Search for a queue element which belongs to the given data entry. + if (_queue.length > 0 && typeof _data[i].id != "undefined") { - if (_queue[j].elem.id == id) + var id = _data[i].id; + + for (var j = 0; j < _queue.length; j++) { - // The element has been found, update its data - _queue[j].elem.loadData(_data[i]); - - // Call the queue object callback (if specified) - if (_queue[j].callback) + if (_queue[j].elem.id == id) { - _queue[j].callback.call(_queue[j].context); + _queue[j].elem.loadData(_data[i], true); + + // Call the queue object callback (if specified) + if (_queue[j].callback) + { + _queue[j].callback.call(_queue[j].context); + } + + // Delete this queue element + _queue.splice(j, 1); + + hasTarget = true; + break; } - - // Delete this queue element - _queue.splice(i, 1); - - hasTarget = true; - break; } } + + if (!hasTarget) + { + rootData.push(_data[i]); + } } - if (!hasTarget) - { - rootData.push(_queue[i]); - } + this.dataRoot.loadData(rootData, true); + } + finally + { + this.dataRoot.callEndUpdate(); } - - this.dataRoot.loadData(rootData); } diff --git a/phpgwapi/js/egw_action/egw_grid_view.js b/phpgwapi/js/egw_action/egw_grid_view.js index d6fa0629df..f2f2537858 100644 --- a/phpgwapi/js/egw_action/egw_grid_view.js +++ b/phpgwapi/js/egw_action/egw_grid_view.js @@ -102,7 +102,7 @@ function egwGridViewOuter(_parentNode, _dataRoot) this.scrollbarWidth = Math.max(10, this.getScrollbarWidth()); // Start value for the average row height - this.avgRowHeight = 23.0; + this.avgRowHeight = 19.0; this.avgRowCnt = 1; // Insert the base grid container into the DOM-Tree @@ -121,6 +121,19 @@ egwGridViewOuter.prototype.addHeightToAvg = function(_value) this.avgRowHeight = this.avgRowHeight * (1 - frac) + _value * frac; } +/** + * Removes the height from the average container height + */ +egwGridViewOuter.prototype.remHeightFromAvg = function(_value) +{ + if (this.avgRowCnt > 1) + { + var sum = this.avgRowHeight * this.avgRowCnt - _value; + this.avgRowCnt--; + this.avgRowCount = sum / this.avgRowCnt; + } +} + /** * Removes all containers from the base grid and replaces it with spacers again. * As only partial data is displayed, this method is faster than updating every @@ -287,6 +300,8 @@ function egwGridViewContainer(_grid, _heightChangeProc) this.assumedHeight = false; this.index = 0; this.viewArea = false; + this.containerClass = ""; + this.heightInAvg = false; this.doInsertIntoDOM = null; this.doSetViewArea = null; @@ -323,7 +338,10 @@ egwGridViewContainer.prototype.setVisible = function(_visible, _force) // While the element has been invisible, the viewarea might have changed, // so check it now - this.checkViewArea(); + if (this.visible) + { + this.checkViewArea(); + } // As the element is now (in)visible, its height has changed. Inform the // parent about it. @@ -514,8 +532,10 @@ function egwGridViewGrid(_grid, _heightChangeProc, _scrollable, _outer) container.scrollable = _scrollable; container.scrollHeight = 100; container.scrollEvents = 0; + container.inUpdate = 0; container.didUpdate = false; container.updateIndex = 0; + container.triggerID = 0; container.setupContainer = egwGridViewGrid_setupContainer; container.insertContainer = egwGridViewGrid_insertContainer; container.removeContainer = egwGridViewGrid_removeContainer; @@ -526,8 +546,12 @@ function egwGridViewGrid(_grid, _heightChangeProc, _scrollable, _outer) container.empty = egwGridViewGrid_empty; container.getOuter = egwGridViewGrid_getOuter; container.updateAssumedHeights = egwGridViewGrid_updateAssumedHeights; + container.beginUpdate = egwGridViewGrid_beginUpdate; + container.endUpdate = egwGridViewGrid_endUpdate; + container.triggerUpdateAssumedHeights = egwGridViewGrid_triggerUpdateAssumedHeights; container.children = []; container.outer = _outer; + container.containerClass = "grid"; // Overwrite the abstract container interface functions container.invalidateHeightCache = egwGridViewGrid_invalidateHeightCache; @@ -538,6 +562,67 @@ function egwGridViewGrid(_grid, _heightChangeProc, _scrollable, _outer) return container; } +function egwGridViewGrid_beginUpdate() +{ + if (this.inUpdate == 0) + { + this.didUpdate = false; + + if (this.grid) + { + this.grid.beginUpdate(); + } + } + this.inUpdate++; +} + +function egwGridViewGrid_triggerUpdateAssumedHeights() +{ + this.triggerID++; + var self = this; + var id = this.triggerID; + window.setTimeout(function() { + if (id = self.triggerID) + { + self.triggerID = 0; + self.updateAssumedHeights(20); + } + }, + EGW_GRID_UPDATE_HEIGHTS_TIMEOUT); +} + +function egwGridViewGrid_endUpdate(_recPrev) +{ + if (typeof _recPrev == "undefined") + { + _recPrev = false; + } + + if (this.inUpdate > 0) + { + this.inUpdate--; + + if (this.inUpdate == 0 && this.grid) + { + this.grid.endUpdate(); + } + + if (this.inUpdate == 0 && this.didUpdate) + { + // If an update has been done, check whether any height assumptions have been + // done. This procedure is executed with some delay, as this gives the browser + // the time to insert the newly generated objects into the DOM-Tree and allows + // us to read their height at a very fast rate. + if (this.didUpdate && !_recPrev) + { + this.triggerUpdateAssumedHeights(); + } + } + } + + this.didUpdate = false; +} + function egwGridViewGrid_getOuter() { if (this.outer) @@ -571,6 +656,7 @@ function egwGridViewGrid_setupContainer() */ this.outerNode = $(document.createElement("td")); + this.outerNode.addClass("frame"); if (this.scrollable) { @@ -623,7 +709,6 @@ function egwGridViewGrid_scrollCallback(_event) var area = egwArea(this.scrollarea.scrollTop() - EGW_GRID_VIEW_EXT, this.scrollHeight + EGW_GRID_VIEW_EXT * 2); - // Set view area sets the "didUpdate" variable to false this.setViewArea(area); this.scrollEvents = 0; @@ -638,7 +723,7 @@ function egwGridViewGrid_updateAssumedHeights(_maxCount) try { - this.inUpdate = true; + this.beginUpdate(); while (traversed < this.children.length && cnt > 0) { @@ -658,7 +743,16 @@ function egwGridViewGrid_updateAssumedHeights(_maxCount) var oldHeight = child.assumedHeight; child.invalidateHeightCache(); var newHeight = child.getHeight(); - outer.addHeightToAvg(newHeight); + + if (child.containerClass == "row") + { + if (child.heightInAvg) + { + outer.remHeightFromAvg(oldHeight); + } + outer.addHeightToAvg(newHeight); + child.heightInAvg = true; + } // Offset the position of all following elements by the delta. var delta = newHeight - oldHeight; @@ -682,7 +776,7 @@ function egwGridViewGrid_updateAssumedHeights(_maxCount) } finally { - this.inUpdate = false; + this.endUpdate(true); } var self = this; @@ -690,9 +784,7 @@ function egwGridViewGrid_updateAssumedHeights(_maxCount) if (cnt == 0) { // If the maximum-update-count has been exhausted, retrigger this function - window.setTimeout(function() { - self.updateAssumedHeights(_maxCount); - }, EGW_GRID_UPDATE_HEIGHTS_TIMEOUT); + this.triggerUpdateAssumedHeights(); } else { @@ -706,79 +798,100 @@ function egwGridViewGrid_updateAssumedHeights(_maxCount) function egwGridViewGrid_insertContainer(_after, _class, _params) { - this.didUpdate = true; - - var container = new _class(this, this.heightChangeHandler, _params); - - var idx = this.children.length; - if (typeof _after == "number") + this.beginUpdate(); + try { - idx = Math.max(-1, Math.min(this.children.length, _after)) + 1; + this.didUpdate = true; + + var container = new _class(this, this.heightChangeHandler, _params); + + var idx = this.children.length; + if (typeof _after == "number") + { + idx = Math.min(this.children.length, Math.max(-1, _after)) + 1; + } + else if (typeof _after == "object" && _after) + { + idx = _after.index + 1; + } + + // Insert the element at the given position + this.children.splice(idx, 0, container); + + // Create a table row for that element + var tr = $(document.createElement("tr")); + + // Insert the table row after the container specified in the _after parameter + // and set the top position of the node + container.index = idx; + + if (idx == 0) + { + this.innerNode.prepend(tr); + container.setPosition(0); + } + else + { + tr.insertAfter(this.children[idx - 1].parentNode); + container.setPosition(this.children[idx - 1].getArea().bottom); + } + + // Insert the container into the table row + container.insertIntoDOM(tr, this.columns); + + // Offset the position of all following elements by the height of the container + // and move the index of those elements + var height = this.getOuter().avgRowHeight; + container.assumedHeight = height; + for (var i = idx + 1; i < this.children.length; i++) + { + this.children[i].offsetPosition(height); + this.children[i].index++; + } + + return container; } - else if (typeof _after == "object" && _after) + finally { - idx = _after.index + 1; + this.endUpdate(); } - // Insert the element at the given position - this.children.splice(idx, 0, container); - - // Create a table row for that element - var tr = $(document.createElement("tr")); - - // Insert the table row after the container specified in the _after parameter - // and set the top position of the node - container.index = idx; - - if (idx == 0) - { - this.innerNode.prepend(tr); - container.setPosition(0); - } - else - { - tr.insertAfter(this.children[idx - 1].parentNode); - container.setPosition(this.children[idx - 1].getArea().bottom); - } - - // Insert the container into the table row - container.insertIntoDOM(tr, this.columns); - - // Offset the position of all following elements by the height of the container - // and move the index of those elements - var height = this.getOuter().avgRowHeight; //container.getHeight(); // This took a lot of time. - container.assumedHeight = height; - for (var i = idx + 1; i < this.children.length; i++) - { - this.children[i].offsetPosition(height); - this.children[i].index++; - } - - return container; + this.callHeightChangeProc(); } function egwGridViewGrid_removeContainer(_container) { this.didUpdate = true; - var idx = _container.index; - - // Offset the position of the folowing children back - var height = _container.getHeight(); - for (var i = idx + 1; i < this.children.length; i++) + try { - this.children[i].offsetPosition(-height); - this.children[i].index--; + this.beginUpdate(); + + var idx = _container.index; + + // Offset the position of the folowing children back + var height = _container.getHeight(); + for (var i = idx + 1; i < this.children.length; i++) + { + this.children[i].offsetPosition(-height); + this.children[i].index--; + } + + // Delete the parent node of the container object + if (_container.parentNode) + { + _container.parentNode.remove(); + _container.parentNode = null; + } + + this.children.splice(idx, 1); + } + finally + { + this.endUpdate(); } - // Delete the parent node of the container object - if (_container.parentNode) - { - _container.parentNode.remove(); - _container.parentNode = null; - } - - this.children.splice(idx, 1); + this.callHeightChangeProc(); } function egwGridViewGrid_empty(_newColumns) @@ -807,6 +920,7 @@ function egwGridViewGrid_invalidateHeightCache(_children) } this.height = false; + this.assumedHeight = false; if (_children) { @@ -837,17 +951,17 @@ function egwGridViewGrid_heightChangeHandler(_elem) { this.didUpdate = true; - // Get the height-change + // The old height of the element is now only an assumed height - the next + // time the "updateAssumedHeights" functions is triggered, this will be + // updated. var oldHeight = _elem.assumedHeight !== false ? _elem.assumedHeight : - (_elem.height === false ? 0 : _elem.height); + (_elem.height === false ? this.getOuter().avgRowHeight : _elem.height); _elem.invalidateHeightCache(false); - var newHeight = _elem.getHeight(); - var offs = newHeight - oldHeight; + _elem.assumedHeight = oldHeight; - // Set the offset of all elements succeding the given element correctly - for (var i = _elem.index + 1; i < this.children.length; i++) + if (_elem.containerClass == "grid" && !this.inUpdate) { - this.children[i].offsetPosition(offs); + this.triggerUpdateAssumedHeights(); } // As a result of the height of one of the children, the height of this element @@ -864,8 +978,13 @@ function egwGridViewGrid_doInsertIntoDOM() this.outerNode.attr("colspan", this.columns.length + (this.scrollable ? 1 : 0)); } -function egwGridViewGrid_doSetviewArea(_area) +function egwGridViewGrid_doSetviewArea(_area, _recPrev) { + if (typeof _recPrev == "undefined") + { + _recPrev == false; + } + // Do a binary search for elements which are inside the given area this.didUpdate = false; var elem = null; @@ -927,29 +1046,22 @@ function egwGridViewGrid_doSetviewArea(_area) } } - this.inUpdate = true; - - // Call the setViewArea function of visible child elements - // Imporant: The setViewArea function has to work on a copy of children, - // as the container may start to remove themselves or add new elements using - // the insertAfter function. - for (var i = 0; i < elems.length; i++) + try { - elems[i].setViewArea(_area, true); + this.beginUpdate(); + + // Call the setViewArea function of visible child elements + // Imporant: The setViewArea function has to work on a copy of children, + // as the container may start to remove themselves or add new elements using + // the insertAfter function. + for (var i = 0; i < elems.length; i++) + { + elems[i].setViewArea(_area, true); + } } - - this.inUpdate = false; - - // If an update has been done, check whether any height assumptions have been - // done. This procedure is executed with some delay, as this gives the browser - // the time to insert the newly generated objects into the DOM-Tree and allows - // us to read their height at a very fast rate. - if (this.didUpdate) + finally { - var self = this; - window.setTimeout(function() { - self.updateAssumedHeights(20); - }, EGW_GRID_UPDATE_HEIGHTS_TIMEOUT); + this.endUpdate(_recPrev); } } @@ -968,10 +1080,17 @@ function egwGridViewRow(_grid, _heightChangeProc, _item) container.aoiSetup = egwGridViewRow_aoiSetup; container.getAOI = egwGridViewRow_getAOI; container.checkOdd = egwGridViewRow_checkOdd; + container._columnClick = egwGridViewRow__columnClick; + container.setOpen = egwGridViewRow_setOpen; + container.tdObjects = []; + container.containerClass = "row"; + container.childGrid = null; + container.opened = false; // Overwrite the inherited abstract functions container.doInsertIntoDOM = egwGridViewRow_doInsertIntoDOM; container.doSetViewArea = egwGridViewRow_doSetViewArea; + container.doUpdateData = egwGridViewRow_doUpdateData; return container; } @@ -1015,6 +1134,16 @@ function egwGridViewRow_getAOI() return this.aoi; } +function egwGridViewRow__columnClick(_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); +} + var EGW_GRID_VIEW_ROW_BORDER = false; @@ -1032,26 +1161,25 @@ function egwGridViewRow_doInsertIntoDOM() // Check whether this element is odd this.checkOdd(); - // Read the column data - /*var ids = []; - for (var i = 0; i < this.columns.length; i++) - { - ids.push(this.columns[i].id); - } - - data = this.item.getData(ids);*/ - for (var i = 0; i < this.columns.length; i++) { var col = this.columns[i]; var td = $(document.createElement("td")); - //if (typeof data[this.columns[i].id] != "undefined") - { - td.html("col" + i); - } this.parentNode.append(td); + // Assign the click event to the column + td.mousedown(egwPreventSelect); + td.click({"item": this, "col": col.id}, function(e) { + this.onselectstart = null; + e.data.item._columnClick(egwGetShiftState(e), e.data.col); + }); + + if (i == 0) + { + td.addClass("first"); + } + // Set the column width if (EGW_GRID_VIEW_ROW_BORDER === false) { @@ -1059,11 +1187,117 @@ function egwGridViewRow_doInsertIntoDOM() } td.css("width", col.drawnWidth - EGW_GRID_VIEW_ROW_BORDER); + // Store the column in the td object array + this.tdObjects.push({ + "col": col, + "td": td + }); } + this.doUpdateData(true); + this.checkViewArea(); } +function egwGridViewRow_doUpdateData(_immediate) +{ + var ids = []; + for (var i = 0; i < this.columns.length; i++) + { + ids.push(this.columns[i].id); + } + + data = this.item.getData(ids); + + for (var i = 0; i < this.tdObjects.length; i++) + { + var td = this.tdObjects[i].td; + var col = this.tdObjects[i].col; + if (typeof data[col.id] != "undefined") + { + td.empty(); + if (col.type == EGW_COL_TYPE_NAME_ICON_FIXED) + { + // Insert the indentation spacer + var depth = this.item.getDepth() - 1; + if (depth > 0) + { + // Build the indentation object + var indentation = $(document.createElement("span")); + indentation.addClass("indentation"); + indentation.css("width", (depth * 12) + "px"); + td.append(indentation); + } + + // Insert the open/close arrow + if (this.item.canHaveChildren) + { + var arrow = $(document.createElement("span")); + arrow.addClass("arrow"); + arrow.addClass(this.item.opened ? "opened" : "closed"); + arrow.click(this, function(e) { + $this = $(this); + + if (!e.data.opened) + { + $this.addClass("opened"); + $this.removeClass("closed"); + } + else + { + $this.addClass("closed"); + $this.removeClass("opened"); + } + + e.data.setOpen(!e.data.opened); + }); + td.append(arrow); + } + + // Insert the icon + if (data[col.id].iconUrl) + { + // Build the icon element + var icon = $(document.createElement("img")); + icon.attr("src", data[col.id].iconUrl); + icon.load(this, function(e) { + e.data.callHeightChangeProc(); + }); + icon.addClass("icon"); + td.append(icon); + } + + // Build the caption + if (data[col.id].caption) + { + var caption = $(document.createElement("span")); + caption.html(data[col.id].caption); + td.append(caption); + } + } + else + { + td.html(data[col.id]); + } + td.toggleClass("queued", false); + } + else + { + td.toggleClass("queued", true); + } + } + + // Set the open state + this.setOpen(this.item.opened); + + // If the call is not from inside the doInsertIntoDOM function, we have to + // inform the parent about a possible height change + if (!_immediate) + { + this.callHeightChangeProc(); + } +} + function egwGridViewRow_checkOdd() { if (this.item && this.parentNode) @@ -1084,6 +1318,44 @@ function egwGridViewRow_doSetViewArea() this.checkOdd(); } +function egwGridViewRow_setOpen(_open) +{ + if (_open != this.opened) + { + if (_open) + { + if (!this.childGrid) + { + // Get the arrow and put it to "loading" state + var arrow = $(".arrow", this.parentNode); + arrow.removeClass("closed"); + arrow.addClass("loading"); + + // Create the "child grid" + this.childGrid = this.grid.insertContainer(this.index, egwGridViewGrid, + false); + this.childGrid.setVisible(false); + var spacer = this.childGrid.insertContainer(-1, egwGridViewSpacer, + this.grid.getOuter().avgRowHeight); + this.item.getChildren(function(_children) { + arrow.removeClass("loading"); + arrow.removeClass("closed"); + arrow.addClass("opened"); + spacer.setItemList(_children); + }); + } + } + + if (this.childGrid) + { + this.childGrid.setVisible(_open); + } + + this.opened = _open; + this.item.opend = _open; + } +} + /** -- egwGridViewSpacer Class -- **/ @@ -1139,42 +1411,46 @@ function egwGridViewSpacer_doInsertIntoDOM() */ function egwGridViewSpacer_doSetViewArea() { - var avgHeight = this.grid.getOuter().avgRowHeight; - - // Get all items which are in the view area - var top = Math.max(0, Math.floor(this.viewArea.top / this.itemHeight)); - var bot = Math.min(this.items.length, Math.ceil(this.viewArea.bottom / this.itemHeight)); - - // Split the item list into three parts - var it_top = this.items.slice(0, top); - var it_mid = this.items.slice(top, bot); - var it_bot = this.items.slice(bot, this.items.length); - - this.items = []; - var idx = this.index; - - // Insert the new rows in the parent grid in front of the spacer container - for (var i = it_mid.length - 1; i >= 0; i--) + if (this.items.length > 0) { - this.grid.insertContainer(idx - 1, it_mid[i].type, it_mid[i]); - } + var avgHeight = this.grid.getOuter().avgRowHeight; - // If top was greater than 0, insert a new spacer in front of the - if (it_top.length > 0) - { - var spacer = this.grid.insertContainer(idx - 1, egwGridViewSpacer, avgHeight); - spacer.setItemList(it_top) - } + // Get all items which are in the view area + var top = Math.max(0, Math.floor(this.viewArea.top / this.itemHeight)); + var bot = Math.min(this.items.length, Math.ceil(this.viewArea.bottom / this.itemHeight)); - // If there are items left at the bottom of the spacer, set theese as items of this spacer - if (it_bot.length > 0) - { - this.itemHeight = avgHeight; - this.setItemList(it_bot); - } - else - { - this.grid.removeContainer(this); + // Split the item list into three parts + var it_top = this.items.slice(0, top); + var it_mid = this.items.slice(top, bot); + var it_bot = this.items.slice(bot, this.items.length); + + this.items = []; + var idx = this.index; + + // Insert the new rows in the parent grid in front of the spacer container + for (var i = it_mid.length - 1; i >= 0; i--) + { + this.grid.insertContainer(idx - 1, it_mid[i].type, it_mid[i]); + } + + // If top was greater than 0, insert a new spacer in front of the newly + // created elements. + if (it_top.length > 0) + { + var spacer = this.grid.insertContainer(idx - 1, egwGridViewSpacer, avgHeight); + spacer.setItemList(it_top) + } + + // If there are items left at the bottom of the spacer, set theese as items of this spacer + if (it_bot.length > 0) + { + this.itemHeight = avgHeight; + this.setItemList(it_bot); + } + else + { + this.grid.removeContainer(this); + } } } diff --git a/phpgwapi/js/egw_action/test/grid.css b/phpgwapi/js/egw_action/test/grid.css index 8d468088e4..06a95e28f9 100644 --- a/phpgwapi/js/egw_action/test/grid.css +++ b/phpgwapi/js/egw_action/test/grid.css @@ -14,6 +14,26 @@ body, td, th { border-collapse: collapse; } +.egwGridView_outer td.queued { + background-image: url(imgs/ajax-loader.gif); + background-position: center; + background-repeat: no-repeat; + height: 19px; +} + +.egwGridView_grid tr.focused td { + background-image: url(imgs/focused_hatching.png); + background-repeat: repeat; +} + +.egwGridView_grid tr.selected td { + background-color: #b7c3ff; +} + +.egwGridView_grid tr.selected.odd td { + background-color: #9dadff; +} + .egwGridView_scrollarea { width: 100%; overflow: auto; @@ -37,16 +57,71 @@ body, td, th { margin: 0; } -.egwGridView_grid td, .egwGridView_grid tr { +.egwGridView_grid td { border-right: 1px solid silver; - padding: 4px 3px 4px 4px; + padding: 2px 3px 2px 4px; margin: 0; } +.egwGridView_grid tr { + padding: 2px 3px 2px 4px; + margin: 0; +} + +.egwGridView_grid tr.hidden { + display: none; +} + .egwGridView_grid tr.odd { background-color: #F1F1F1; } +.egwGridView_grid span.indentation { + display: inline-block; +} + +.egwGridView_grid span { + vertical-align: middle; +} + +.egwGridView_grid img.icon { + vertical-align: middle; + margin: 2px 5px 2px 2px; + -moz-user-select: none; + -khtml-user-select: none; + user-select: none; +} + +.egwGridView_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; +} + +.egwGridView_grid span.arrow.opened { + cursor: pointer; + background-image: url(imgs/arrows.png); + background-position: -8px 0; +} + +.egwGridView_grid span.arrow.closed { + cursor: pointer; + background-image: url(imgs/arrows.png); + background-position: 0 0; +} + +.egwGridView_grid span.arrow.loading { + cursor: pointer; + background-image: url(imgs/ajax-loader.gif); + background-position: 0 0; +} + .egwGridView_outer thead th { background-color: #E0E0E0; font-weight: normal; @@ -66,32 +141,6 @@ body, td, th { text-align: center; } - -/*----------------------------------------------------------------------------*/ - -.grid_outer { - border-spacing: 0; - border-collapse: collapse; - padding: 0; - margin: 0; -} - -.grid_outer div.scrollarea { - overflow: auto; - width:100%; -} - -.grid_outer td, .grid_outer tr { - padding: 0; - margin: 0; -} - -.horizontal_spacer { - display: block; - background-image: url(imgs/non_loaded_bg.png); - background-position: top left; -} - .selectcols { display: inline-block; width: 10px; @@ -104,99 +153,9 @@ body, td, th { background-repeat: no-repeat; } -.grid { - border-spacing: 0; - border-collapse: collapse; +.frame { + padding: 0 !important; + border-right: 0 none silver !important; } -.grid th.optcol { - width: 6px; - padding: 0; - text-align: center; -} - -.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; - border-left: 1px solid silver; - border-top: 1px solid silver; - border-right: 1px solid gray; - border-bottom: 1px solid gray; - background-image: url(imgs/header_overlay.png); - background-position: center; - background-repeat: repeat-x; -} - -.grid td { - padding: 0; - 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/ajax-loader.gif b/phpgwapi/js/egw_action/test/imgs/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ef515f1e3e696287044739512d372b680994cf8 GIT binary patch literal 1683 zcmaJ>eOwcD7@vvY3!2t63$x?s#TMIcV`FYI6?dCZjRq_=Df8HF+coZGcQ=_3L?ILg zg+LSmMMM+@A`p@gF&u+{tf`r%wcg)fq_P)#vl}t&kG9|EbD#VDp8I~E@ALeg=bp`} zy0uCqiWkK@q2Sr=c89}JTwL7U-R*QbhlYllo14eS$A^c9D=I4b`uZj&CUSFgtE#Fx zIyx*C3y$OE<>g+lcW`hJ!?2o~n)deg($dn_*4Cz`rqR*S{QUf)qN2LGx}Kh%va+(u z%1W2Z+I}oYip~mttCm4VVJS8F{{;DP*Bj@+dDZq z+1S|F(9qD*(o$Gh*nWD77h=U;Sd^wUW@?fRTB!({JBNdDrfqry03$5Iy^}V$D8yt( zSs)v=VuTtzJ1_(Sm{|>OmFporr9o|2N*;}7=A{`;d3KZ13~pEtB)L>BoWfBS0bFl>31T-}yjRJBJBs4*Z5RfPlNfKlVg(4o1LJ~*} zNyQSWP@+&JNL3I7%q);oqs0Q7lFhK)@rCO*&aM$VK{Z3OF_EmS9BF>4;1uE_NZ5SR#S~ zDFuOgeRwF2&!QPN1HBmU|2k%jyD3zhfik3%HlZ9mE+9pzG&G8^ByA+goZu}QY$QuE zHj)A~nQ~w&MwrQ5W*V*6t5OMuMF? z&*5SB&})OQ4)h;-rSD+x%RSv)2Riq6w7>LX+Y8UPKG*VW^D|9PKh@Y!|76`0kJmm{ zQ(d*M^3g{s${*fa_Rxc+C3}h=xWDMW!h(DA^LD#(cR5*xc2FdN=h*MTY*q_uHf19_ z@7{6Oomt!O*tT`c?Z(WEbi-|F`de?gIW=XoP77-`C2!oYUY)c~#Z@~|E=!PdKa@xq zf0N+GH8)&;-L-LGEO5>0t5?Nbb>+${RxIaVe%Z36mo8Zxy=dWr`B9PcBIaJgjfU9s z5epXb6ukJrFbNDmUgT z%HWUm#UKKiEJEw)aD<~A0QgIY5skxO@JIV%b_!N4nD0HRr@~QuApaNvXYRA4`$1p5 zS+Hl`4(%ynSjy5k{(fR)o1-M%FY?7?3!;~}U&Kt=AJE{-7) zt#2@rCV%~(r&&Ms7Bqg6Hd(;P mo8iDLb)kU`%v_NDfT=CXR&V{2gt + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpgwapi/js/egw_action/test/test_grid_view.html b/phpgwapi/js/egw_action/test/test_grid_view.html index 3cfff59101..5ac78ce336 100644 --- a/phpgwapi/js/egw_action/test/test_grid_view.html +++ b/phpgwapi/js/egw_action/test/test_grid_view.html @@ -17,7 +17,8 @@ -

Test for dynamically displaying and loading grid lines (0,1 Mio Entries)

+

Test for dynamically displaying and loading grid lines

+ Simulates network trafic by using window.setTimeout(), 100ms network latency