mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-15 20:44:27 +01:00
571 lines
14 KiB
JavaScript
571 lines
14 KiB
JavaScript
/**
|
|
* eGroupWare eTemplate2
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage dataview
|
|
* @link http://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
* @copyright Stylite 2011-2012
|
|
* @version $Id$
|
|
*/
|
|
|
|
/*egw:uses
|
|
et2_core_common;
|
|
et2_core_inheritance;
|
|
|
|
et2_dataview_interfaces;
|
|
et2_dataview_view_row;
|
|
*/
|
|
|
|
/**
|
|
* The fetch timeout specifies the time during which the controller tries to
|
|
* consolidate requests for rows.
|
|
*/
|
|
var ET2_DATAVIEW_FETCH_TIMEOUT = 50;
|
|
|
|
/**
|
|
* The et2_dataview_controller class is the intermediate layer between a grid
|
|
* instance and the corresponding data source. It manages updating the grid,
|
|
* as well as inserting and deleting rows.
|
|
*/
|
|
var et2_dataview_controller = Class.extend({
|
|
|
|
/**
|
|
* Constructor of the et2_dataview_controller, connects to the grid
|
|
* callback.
|
|
*/
|
|
init: function (_grid, _dataProvider, _rowCallback, _context)
|
|
{
|
|
// Copy the given arguments
|
|
this._grid = _grid;
|
|
this._dataProvider = _dataProvider;
|
|
this._rowCallback = _rowCallback;
|
|
this._rowContext = _context;
|
|
|
|
// Initialize the "index map" which contains all currently displayed
|
|
// containers hashed by the "index"
|
|
this._indexMap = {};
|
|
|
|
// "lastModified" contains the last timestap which was returned from the
|
|
// server.
|
|
this._lastModification = null;
|
|
|
|
// Timer used for queing fetch requests
|
|
this._queueTimer = null;
|
|
|
|
// Array used for queing the requests
|
|
this._queue = [];
|
|
|
|
// Register the dataFetch callback
|
|
this._grid.setDataCallback(this._gridCallback, this);
|
|
},
|
|
|
|
destroy: function () {
|
|
|
|
this._clearTimer();
|
|
|
|
this._super();
|
|
},
|
|
|
|
/**
|
|
* The update function queries the server for changes in the currently
|
|
* managed index range -- those changes are then merged into the current
|
|
* view without a complete rebuild of every row.
|
|
*/
|
|
update: function () {
|
|
// Clear the fetch queue
|
|
this._queue = [];
|
|
this._clearTimer();
|
|
|
|
// Get the currently visible range from the grid
|
|
var range = this._grid.getIndexRange();
|
|
|
|
// Force range.top and range.bottom to contain an integer
|
|
if (range.top === false)
|
|
{
|
|
range.top = range.bottom = 0;
|
|
}
|
|
|
|
// Require that range from the server
|
|
this._queueFetch(range.top, range.bottom - range.top + 1,
|
|
this._lastModification !== null, true);
|
|
},
|
|
|
|
/**
|
|
* Rebuilds the complete grid.
|
|
*/
|
|
reset: function () {
|
|
// Throw away all internal mappings and reset the timestamp
|
|
this._indexMap = {};
|
|
this._lastModification = null;
|
|
|
|
// Clear the grid
|
|
this._grid.clear();
|
|
|
|
// Update the data
|
|
this.update();
|
|
},
|
|
|
|
|
|
/* -- PRIVATE FUNCTIONS -- */
|
|
|
|
|
|
_getIndexEntry: function (_idx) {
|
|
// Create an entry in the index map if it does not exist yet
|
|
if (typeof this._indexMap[_idx] === "undefined")
|
|
{
|
|
this._indexMap[_idx] = {
|
|
"row": null,
|
|
"uid": null
|
|
};
|
|
}
|
|
|
|
// Always update the index of the entries before returning them. This is
|
|
// neccessary, as when we remove the uid from an entry without row, its
|
|
// index does not get updated any further
|
|
this._indexMap[_idx]["idx"] = _idx;
|
|
|
|
return this._indexMap[_idx];
|
|
},
|
|
|
|
/**
|
|
* Inserts a new data row into the grid. index and uid are derived from the
|
|
* given management entry. If the data for the given uid does not exist yet,
|
|
* a "loading" placeholder will be shown instead. The function will do
|
|
* nothing if there already is a row associated to the entry. This function
|
|
* will not re-insert a row if the entry already had a row.
|
|
*
|
|
* @param _entry is the management entry for the index the row will be
|
|
* displayed at.
|
|
* @param _update specifies whether the row should be updated if _entry.row
|
|
* already exists.
|
|
* @return true, if all data for the row has been available, false
|
|
* otherwise.
|
|
*/
|
|
_insertDataRow: function (_entry, _update) {
|
|
// Abort if the entry already has a row but the _insert flag is not set
|
|
if (_entry.row && !_update)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Context used for the callback functions
|
|
var ctx = {"self": this, "entry": _entry};
|
|
|
|
// Create a new row instance, if it does not exist yet
|
|
var createdRow = false;
|
|
if (!_entry.row)
|
|
{
|
|
createdRow = true;
|
|
_entry.row = new et2_dataview_row(this._grid);
|
|
_entry.row.setDestroyCallback(this._destroyCallback, ctx);
|
|
}
|
|
|
|
// Load the row data if we have a uid for the entry
|
|
this.hasData = false; // Gets updated by the _dataCallback
|
|
if (_entry.uid)
|
|
{
|
|
// Register the callback / immediately load the data
|
|
this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback,
|
|
ctx);
|
|
}
|
|
|
|
// Display the loading "row prototype" if we don't have data for the row
|
|
if (!this.hasData)
|
|
{
|
|
// Get the average height, the "-5" derives from the td padding
|
|
var avg = Math.round(this._grid.getAverageHeight() - 5) + "px";
|
|
var prototype = this._grid.getRowProvider().getPrototype("loading");
|
|
$j("div", prototype).css("height", avg);
|
|
var node = _entry.row.getJNode();
|
|
node.empty();
|
|
node.append(prototype.children());
|
|
}
|
|
|
|
// Insert the row into the table -- the same row must never be inserted
|
|
// twice into the grid, so this function only executes the following
|
|
// code only if it is a newly created row.
|
|
if (createdRow)
|
|
{
|
|
this._grid.insertRow(_entry.idx, _entry.row);
|
|
}
|
|
|
|
return this.hasData;
|
|
},
|
|
|
|
/**
|
|
* Function which gets called by the grid when data is requested.
|
|
*
|
|
* @param _idxStart is the index of the first row for which data is
|
|
* requested.
|
|
* @param _idxEnd is the index of the last requested row.
|
|
*/
|
|
_gridCallback: function (_idxStart, _idxEnd) {
|
|
|
|
var needsData = false;
|
|
|
|
// Iterate over all elements the dataview requested and create a row
|
|
// which indicates that we are currently loading data
|
|
for (var i = _idxStart; i <= _idxEnd; i++)
|
|
{
|
|
var entry = this._getIndexEntry(i);
|
|
|
|
// Insert the row for the entry -- do not update rows which are
|
|
// already existing, as we do not have new data for those.
|
|
if (!this._insertDataRow(entry, false) && needsData === false)
|
|
{
|
|
needsData = i;
|
|
}
|
|
}
|
|
|
|
// Queue fetching that data range
|
|
if (needsData !== false)
|
|
{
|
|
this._queueFetch(needsData, _idxEnd - needsData + 1, false);
|
|
}
|
|
},
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_queueFetch: function (_start, _numRows, _refresh, _immediate) {
|
|
|
|
// Force immediate to be false
|
|
// _immediate = _immediate ? _immediate : false;
|
|
_immediate = true;
|
|
|
|
// Push the request onto the request queue
|
|
this._queue.push({
|
|
"start": _start,
|
|
"num_rows": _numRows,
|
|
"refresh": _refresh
|
|
});
|
|
|
|
// Start the queue timer, if this has not already been done
|
|
if (this._queueTimer === null && !_immediate)
|
|
{
|
|
var self = this;
|
|
this._queueTimer = window.setTimeout(function () {
|
|
self._flushQueue();
|
|
}, ET2_DATAVIEW_FETCH_TIMEOUT);
|
|
}
|
|
|
|
if (_immediate)
|
|
{
|
|
this._flushQueue();
|
|
}
|
|
},
|
|
|
|
_flushQueue: function () {
|
|
|
|
function consolidateQueries(_q) {
|
|
var didConsolidation = false;
|
|
|
|
var _new = [];
|
|
var skip = {};
|
|
|
|
for (var i = 0; i < _q.length; i++)
|
|
{
|
|
var r1 = et2_range(_q[i].start, _q[i].num_rows);
|
|
|
|
var intersected = false;
|
|
|
|
for (var j = i + 1; j < _q.length; j++)
|
|
{
|
|
if (skip[j])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var r2 = et2_range(_q[j].start, _q[j].num_rows);
|
|
|
|
if (et2_rangeIntersect(r1, r2))
|
|
{
|
|
var n = et2_bounds(Math.min(r1.top, r2.top),
|
|
Math.max(r1.botom, r2.bottom));
|
|
_new.push({
|
|
"start": n.top,
|
|
"num_rows": n.bottom - n.top + 1,
|
|
"refresh": _q[i].refresh
|
|
});
|
|
skip[i] = true;
|
|
skip[j] = true;
|
|
intersected = true;
|
|
}
|
|
}
|
|
|
|
if (!intersected)
|
|
{
|
|
_new.push(_q[i]);
|
|
skip[i] = true;
|
|
}
|
|
}
|
|
|
|
if (didConsolidation) {
|
|
return consolidateQueries(_new);
|
|
}
|
|
|
|
return _new;
|
|
}
|
|
|
|
// Clear any still existing timer
|
|
this._clearTimer();
|
|
|
|
// Calculate the refresh flag (refresh = false is stronger)
|
|
var refresh = true;
|
|
for (var i = 0; i < this._queue.length; i++)
|
|
{
|
|
refresh = refresh && this._queue[i].refresh;
|
|
}
|
|
|
|
// Extend all ranges into bottom direction, initialize the queries array
|
|
for (var i = 0; i < this._queue.length; i++)
|
|
{
|
|
this._queue[i].num_rows += 10;
|
|
this._queue[i].refresh = refresh;
|
|
}
|
|
|
|
// Consolidate all queries
|
|
var queries = consolidateQueries(this._queue);
|
|
|
|
// Execute all queries
|
|
for (var i = 0; i < queries.length; i++)
|
|
{
|
|
// Sanitize the requests
|
|
queries[i].start = Math.max(0, queries[i].start);
|
|
queries[i].num_rows = Math.min(this._grid.getTotalCount(),
|
|
queries[i].start + queries[i].num_rows) - queries[i].start;
|
|
|
|
// Context used in the callback function
|
|
var ctx = {
|
|
"self": this,
|
|
"start": queries[i].start,
|
|
"count": queries[i].num_rows
|
|
};
|
|
|
|
// Call the callback
|
|
this._dataProvider.dataFetch(queries[i], this._lastModification,
|
|
this._fetchCallback, ctx);
|
|
}
|
|
|
|
// Flush the queue
|
|
this._queue = [];
|
|
},
|
|
|
|
_clearTimer: function () {
|
|
|
|
// Reset the queue timer upon destruction
|
|
if (this._queueTimer)
|
|
{
|
|
window.clearTimeout(this._queueTimer);
|
|
this._queueTimer = null;
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_dataCallback: function (_data) {
|
|
// Set the "hasData" flag
|
|
this.self.hasData = true;
|
|
|
|
// Call the row callback with the new data -- the row callback then
|
|
// generates the row DOM nodes that will be inserted into the grid
|
|
if (this.self._rowCallback)
|
|
{
|
|
// Remove everything from the current row
|
|
this.entry.row.clear();
|
|
|
|
// Fill the row DOM Node with data
|
|
this.self._rowCallback.call(
|
|
this.self._rowContext,
|
|
_data,
|
|
this.entry.row.getDOMNode(),
|
|
this.entry.idx,
|
|
this.entry
|
|
);
|
|
|
|
// Invalidate the current row entry
|
|
this.entry.row.invalidate();
|
|
}
|
|
},
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_destroyCallback: function (_row) {
|
|
// There is no further row connected to the entry
|
|
this.entry.row = null;
|
|
|
|
// Unregister the data callback
|
|
this.self._dataProvider.dataUnregisterUID(this.entry.uid,
|
|
this.self._dataCallback, null);
|
|
},
|
|
|
|
/**
|
|
* Returns an array containing "_count" index mapping entries starting from
|
|
* the index given in "_start".
|
|
*/
|
|
_getIndexMapping: function (_start, _count) {
|
|
var result = [];
|
|
|
|
for (var i = _start; i < _start + _count; i++)
|
|
{
|
|
result.push(this._getIndexEntry(i));
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Updates the grid according to the new order. The function simply does the
|
|
* following: It iterates along the new order (given in _order) and the old
|
|
* order given in _idxMap. Iteration variables used are
|
|
* a) i -- points to the current entry in _order
|
|
* b) idx -- points to the current grid row that will be effected by
|
|
* this operation.
|
|
* c) mapIdx -- points to the current entry in _indexMap
|
|
* The following cases may occur:
|
|
* a) The current entry in the old order has no uid or no row -- in that
|
|
* case the row at the current position is simply updated,
|
|
* the old pointer will be incremented.
|
|
* b) The two uids differ -- insert a new row with the new uid, do not
|
|
* increment the old pointer.
|
|
* c) The two uids are the same -- increment the old pointer.
|
|
* In a last step all rows that are left in the old order are deleted. All
|
|
* newly created index entries are returned. This function does not update
|
|
* the internal mapping in _idxMap.
|
|
*/
|
|
_updateOrder: function (_start, _count, _idxMap, _order) {
|
|
// The result contains the newly created index map entries which have to
|
|
// be merged with the result
|
|
var result = [];
|
|
|
|
// Iterate over the new order
|
|
var mapIdx = 0;
|
|
var idx = _start;
|
|
for (var i = 0; i < _order.length; i++, idx++)
|
|
{
|
|
var current = _idxMap[mapIdx];
|
|
|
|
if (!current.row || !current.uid)
|
|
{
|
|
// If there is no row yet at the current position or the uid
|
|
// of that entry is unknown, simply update the entry.
|
|
current.uid = _order[i];
|
|
current.idx = idx;
|
|
|
|
// Only update the row, if it is displayed (e.g. has a "loading"
|
|
// row displayed) -- this is needed for prefetching
|
|
if (current.row)
|
|
{
|
|
this._insertDataRow(current, true);
|
|
}
|
|
|
|
mapIdx++;
|
|
}
|
|
else if (current.uid !== _order[i])
|
|
{
|
|
// Insert a new row at the new position
|
|
var entry = {
|
|
"idx": idx,
|
|
"uid": _order[i],
|
|
"row": null
|
|
};
|
|
this._insertDataRow(entry, true);
|
|
|
|
// Remember the new entry
|
|
result.push(entry);
|
|
}
|
|
else
|
|
{
|
|
// Do nothing, the uids do not differ, just update the index of
|
|
// the element
|
|
current.idx = idx;
|
|
mapIdx++;
|
|
}
|
|
}
|
|
|
|
// Delete as many rows as we have left
|
|
for (var i = mapIdx; i < _idxMap.length; i++)
|
|
{
|
|
this._grid.deleteRow(idx);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_mergeResult: function (_newEntries, _invalidStartIdx, _diff) {
|
|
|
|
if (_newEntries.length > 0 || _diff > 0)
|
|
{
|
|
// Create a new index map
|
|
var newMap = {};
|
|
|
|
// Insert all new entries into the new index map
|
|
for (var i = 0; i < _newEntries.length; i++)
|
|
{
|
|
newMap[_newEntries[i].idx] = _newEntries[i];
|
|
}
|
|
|
|
// Insert all old entries that have a row into the new index map
|
|
// while adjusting their indices
|
|
for (var key in this._indexMap)
|
|
{
|
|
// Get the corresponding index entry
|
|
var entry = this._indexMap[key];
|
|
|
|
// Only keep index entries which are currently displayed
|
|
if (entry.row)
|
|
{
|
|
// Calculate the new index -- if rows were deleted, we'll
|
|
// have to adjust the index
|
|
var newIdx = entry.idx >= _invalidStartIdx
|
|
? entry.idx - _diff : entry.idx;
|
|
entry.idx = key;
|
|
newMap[newIdx] = entry;
|
|
}
|
|
}
|
|
|
|
// Make the new index map the current index map
|
|
this._indexMap = newMap;
|
|
}
|
|
|
|
},
|
|
|
|
_fetchCallback: function (_response) {
|
|
// Do nothing if _response.order evaluates to false
|
|
if (!_response.order)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Copy the last modification
|
|
this.self._lastModification = _response.lastModification;
|
|
|
|
// Make sure _response.order.length is not longer than the requested
|
|
// count
|
|
var order = _response.order.splice(0, this.count);
|
|
|
|
// Get the current index map for the updated region
|
|
var idxMap = this.self._getIndexMapping(this.start, this.count);
|
|
|
|
// Update the grid using the new order. The _updateOrder function does
|
|
// not update the internal mapping while inserting and deleting rows, as
|
|
// this would move us to another asymptotic runtime level.
|
|
var res = this.self._updateOrder(this.start, this.count, idxMap, order);
|
|
|
|
// Merge the new indices, update all indices with rows that were not
|
|
// affected and invalidate all indices if there were changes
|
|
this.self._mergeResult(res, this.start + order.length,
|
|
idxMap.length - order.length);
|
|
|
|
// Update the total element count in the grid
|
|
this.self._grid.setTotalCount(_response.total);
|
|
}
|
|
|
|
});
|
|
|