mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 02:43:22 +01:00
f430b66d3b
classes are now uppercase and in their own files. lowercase classes are deprecated.
Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
(cherry picked from commit 5e3c67a5cf
)
1174 lines
29 KiB
TypeScript
1174 lines
29 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage dataview
|
|
* @link https://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
* @copyright EGroupware GmbH 2011-2021
|
|
*/
|
|
|
|
/*egw:uses
|
|
et2_core_common;
|
|
et2_core_inheritance;
|
|
|
|
et2_dataview_interfaces;
|
|
et2_dataview_controller_selection;
|
|
et2_dataview_view_row;
|
|
et2_dataview_view_tile;
|
|
|
|
egw_action.egw_action;
|
|
*/
|
|
|
|
import {et2_IDataProvider} from "./et2_dataview_interfaces";
|
|
import {et2_dataview_selectionManager} from "./et2_dataview_controller_selection";
|
|
import {et2_dataview_row} from "./et2_dataview_view_row";
|
|
import {et2_dataview_grid} from "./et2_dataview_view_grid";
|
|
import {et2_arrayIntKeys, et2_bounds} from "./et2_core_common";
|
|
import {egw} from "../jsapi/egw_global";
|
|
import {egwBitIsSet} from "../egw_action/egw_action_common";
|
|
import {EGW_AO_STATE_NORMAL, EGW_AO_STATE_SELECTED} from "../egw_action/egw_action_constants";
|
|
|
|
/**
|
|
* The fetch timeout specifies the time during which the controller tries to
|
|
* consolidate requests for rows.
|
|
*/
|
|
export const ET2_DATAVIEW_FETCH_TIMEOUT = 50;
|
|
|
|
export const ET2_DATAVIEW_STEPSIZE = 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.
|
|
*/
|
|
export class et2_dataview_controller
|
|
{
|
|
// Maximum concurrent data requests. Additional ones are held in the queue.
|
|
public static readonly CONCURRENT_REQUESTS = 5;
|
|
|
|
protected _parentController: any;
|
|
protected _grid: et2_dataview_grid;
|
|
protected dataStorePrefix: any;
|
|
private _dataProvider: any;
|
|
protected _rowCallback: any;
|
|
protected _linkCallback: any;
|
|
private _context: any;
|
|
|
|
protected _children: any[];
|
|
protected _indexMap: any = {};
|
|
|
|
private _queueTimer: number;
|
|
private _lastModification: number;
|
|
private _queue: {};
|
|
private _request_queue: any[];
|
|
protected _selectionMgr: et2_dataview_selectionManager;
|
|
|
|
protected _objectManager: any;
|
|
private hasData: boolean;
|
|
|
|
/**
|
|
* Constructor of the et2_dataview_controller, connects to the grid
|
|
* callback.
|
|
*
|
|
* @param _grid is the grid the controller should controll.
|
|
* @param _rowCallback is the callback function that gets called when a row
|
|
* is requested.
|
|
* @param _linkCallback is the callback function that gets called for
|
|
* requesting action links for a row. The row data, the index of the row and
|
|
* the uid are passed as parameters to the function.
|
|
* uid is passed to the function.
|
|
* @param _actionObjectManager is the object that manages the action
|
|
* objects.
|
|
*/
|
|
constructor (_parentController, _grid)
|
|
{
|
|
// Copy the given arguments
|
|
this._parentController = _parentController;
|
|
this._grid = _grid;
|
|
|
|
// Initialize list of child controllers
|
|
this._children = [];
|
|
|
|
// Initialize the "index map" which contains all currently displayed
|
|
// containers hashed by the "index"
|
|
this._indexMap = {};
|
|
|
|
// Timer used for queing fetch requests
|
|
this._queueTimer = null;
|
|
|
|
// Array which contains all currently queued row indices in the form of
|
|
// an associative array
|
|
this._queue = {};
|
|
|
|
// Current concurrent requests we have
|
|
this._request_queue = [];
|
|
|
|
// Register the dataFetch callback
|
|
this._grid.setDataCallback(this._gridCallback, this);
|
|
|
|
|
|
// Record the child
|
|
if(this._parentController != null)
|
|
{
|
|
this._parentController._children.push(this);
|
|
}
|
|
}
|
|
|
|
destroy( )
|
|
{
|
|
|
|
// Destroy the selection manager
|
|
this._selectionMgr.destroy();
|
|
|
|
// Clear the selection timeout
|
|
this._clearTimer();
|
|
|
|
// Remove the child from the child list
|
|
if(this._parentController != null)
|
|
{
|
|
var idx = this._parentController._children.indexOf(this);
|
|
|
|
if (idx >= 0)
|
|
{
|
|
// This element is no longer parent of the child
|
|
this._parentController._children.splice(idx, 1);
|
|
this._parentController = null;
|
|
}
|
|
}
|
|
|
|
this._grid = null;
|
|
}
|
|
|
|
/**
|
|
* @param value is an object implementing the et2_IDataProvider
|
|
* interface
|
|
*/
|
|
setDataProvider(value: et2_IDataProvider)
|
|
{
|
|
this._dataProvider = value;
|
|
}
|
|
|
|
setRowCallback(value: any) {
|
|
this._rowCallback = value;
|
|
}
|
|
|
|
setLinkCallback(value: any) {
|
|
this._linkCallback = value;
|
|
}
|
|
|
|
/**
|
|
* @param value is the context in which the _rowCallback and the
|
|
* _linkCallback are called.
|
|
*/
|
|
setContext(value: any) {
|
|
this._context = value;
|
|
}
|
|
|
|
setActionObjectManager(_actionObjectManager: any)
|
|
{
|
|
if(this._selectionMgr)
|
|
{
|
|
this._selectionMgr.destroy();
|
|
}
|
|
|
|
// Create the selection manager
|
|
this._selectionMgr = new et2_dataview_selectionManager(
|
|
this._parentController ? this._parentController._selectionMgr : null,
|
|
this._indexMap,
|
|
_actionObjectManager,
|
|
this._selectionFetchRange,
|
|
this._makeIndexVisible,
|
|
this
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {boolean} clear Skip the fancy stuff, dump everything and start again.
|
|
* Completely clears the grid and selection.
|
|
*/
|
|
update( clear? : boolean)
|
|
{
|
|
// Avoid update after destroy
|
|
// Happens sometimes if AJAX response comes after etemplate unload
|
|
if(!this._grid) return;
|
|
|
|
// ---------
|
|
|
|
// TODO: Actually stuff here should be done if the server responds that
|
|
// there at all were some changes (needs implementation of "refresh")
|
|
|
|
// Tell the grid not to try and update itself while we do this
|
|
this._grid.doInvalidate = false;
|
|
|
|
if(clear)
|
|
{
|
|
// Scroll to top
|
|
this._grid.makeIndexVisible(0);
|
|
this._grid.clear();
|
|
|
|
// Free selection manager
|
|
this._selectionMgr.clear();
|
|
|
|
// Clear object manager
|
|
this._objectManager.clear();
|
|
|
|
// Clear the map
|
|
this._indexMap = {};
|
|
// Update selection manager, it uses this by reference
|
|
this._selectionMgr.setIndexMap(this._indexMap);
|
|
|
|
// Clear the queue
|
|
this._queue = {};
|
|
|
|
// Invalidate the change detection, re-fetches any known rows
|
|
this._lastModification = 0;
|
|
}
|
|
// Remove all rows which are outside the view range
|
|
this._grid.cleanup();
|
|
|
|
// 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;
|
|
}
|
|
this._request_queue = [];
|
|
|
|
// Require that range from the server
|
|
this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true);
|
|
}
|
|
|
|
/**
|
|
* Rebuilds the complete grid.
|
|
*/
|
|
reset( )
|
|
{
|
|
// Throw away all internal mappings and reset the timestamp
|
|
this._indexMap = {};
|
|
// Update selection manager, it uses this by reference
|
|
this._selectionMgr.setIndexMap(this._indexMap);
|
|
|
|
// Clear the grid
|
|
this._grid.clear();
|
|
|
|
// Clear the row queue
|
|
this._queue = {};
|
|
|
|
// Reset the request queue
|
|
this._request_queue = [];
|
|
|
|
// Update the data
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* Loads the initial order. Do not call multiple times.
|
|
*/
|
|
loadInitialOrder( order)
|
|
{
|
|
for (var i = 0; i < order.length; i++)
|
|
{
|
|
this._getIndexEntry(i).uid = order[i];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load initial data
|
|
*
|
|
* @param {string} uid_key Name of the unique row identifier field
|
|
* @param {Object} data Key / Value mapping of initial data.
|
|
*/
|
|
loadInitialData( uid_prefix, uid_key, data)
|
|
{
|
|
var idx = 0;
|
|
for(var key in data)
|
|
{
|
|
// Skip any extra keys
|
|
if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue;
|
|
|
|
// Add to row / uid map
|
|
var entry = this._getIndexEntry(idx++);
|
|
entry.uid = data[key][uid_key]+"";
|
|
if(entry.uid.indexOf(uid_prefix) < 0)
|
|
{
|
|
entry.uid = uid_prefix + "::" + entry.uid;
|
|
}
|
|
|
|
// Add to data cache so grid will find it
|
|
egw.dataStoreUID(entry.uid, data[key])
|
|
|
|
// Don't try to insert the rows, grid will do that automatically
|
|
}
|
|
if(idx == 0)
|
|
{
|
|
// No rows, start with an empty
|
|
this._selectionMgr.clear();
|
|
this._emptyRow(this._grid._total == 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the depth of the controller instance.
|
|
*/
|
|
getDepth( )
|
|
{
|
|
|
|
if (this._parentController)
|
|
{
|
|
return this._parentController.getDepth() + 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Set the data cache prefix
|
|
* The default is to use appname, but if you need to set it explicitly to
|
|
* something else to avoid conflicts. Use the same prefix everywhere for
|
|
* each type of data. eg. infolog for infolog entries, even if accessed via addressbook
|
|
*/
|
|
setPrefix( prefix)
|
|
{
|
|
this.dataStorePrefix = prefix;
|
|
}
|
|
|
|
/**
|
|
* Returns the row information of the passed node, or null if not available
|
|
*
|
|
* @param {DOMNode} node
|
|
* @return {string|false} UID, or false if not found
|
|
*/
|
|
getRowByNode( node)
|
|
{
|
|
// Whatever the node, find a TR
|
|
var row_node = jQuery(node).closest('tr');
|
|
var row = null;
|
|
|
|
// Check index map - simple case
|
|
var indexed = this._getIndexEntry(row_node.index());
|
|
if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
|
|
{
|
|
row = indexed;
|
|
}
|
|
else
|
|
{
|
|
// Check whole index map
|
|
for(var index in this._indexMap)
|
|
{
|
|
indexed = this._indexMap[index];
|
|
if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
|
|
{
|
|
row = indexed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check children
|
|
for(var i = 0; !row && i < this._children.length; i++)
|
|
{
|
|
var child_row = this._children[i].getRowByNode(node);
|
|
if(child_row !== false) row = child_row;
|
|
}
|
|
if(row && !row.controller)
|
|
{
|
|
row.controller = this;
|
|
}
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Returns the current "total" count.
|
|
*/
|
|
getTotalCount() : number
|
|
{
|
|
return this._grid.getTotalCount();
|
|
}
|
|
|
|
/* -- PRIVATE FUNCTIONS -- */
|
|
|
|
|
|
_getIndexEntry( _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( _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 = this._createRow(ctx);
|
|
_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");
|
|
jQuery("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 && _entry.row)
|
|
{
|
|
this._grid.insertRow(_entry.idx, _entry.row);
|
|
}
|
|
|
|
// Remove 'No matches found' row
|
|
var row = jQuery(".egwGridView_empty",this._grid.innerTbody).remove();
|
|
if(row.length)
|
|
{
|
|
this._selectionMgr.unregisterRow("", 0);
|
|
}
|
|
|
|
// Update index map only for push (autorefresh disabled)
|
|
if(this._indexMap[_entry.idx] && this._indexMap[_entry.idx].uid !== _entry.uid)
|
|
{
|
|
let max = parseInt(Object.keys(this._indexMap).reduce((a, b) => this._indexMap[a] > this._indexMap[b] ? a : b));
|
|
for(let idx = max; idx >= _entry.idx; idx--)
|
|
{
|
|
let entry = this._indexMap[idx];
|
|
this._indexMap[idx].idx = idx+1;
|
|
this._indexMap[this._indexMap[idx].idx] = this._indexMap[idx];
|
|
if(this._selectionMgr && this._selectionMgr._registeredRows[entry.uid])
|
|
{
|
|
this._selectionMgr._registeredRows[entry.uid].idx = entry.idx;
|
|
}
|
|
}
|
|
}
|
|
this._indexMap[_entry.idx] = _entry;
|
|
|
|
return this.hasData;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new row.
|
|
*
|
|
* @param {type} ctx
|
|
* @returns {et2_dataview_container}
|
|
*/
|
|
_createRow( ctx)
|
|
{
|
|
return new et2_dataview_row(this._grid);
|
|
}
|
|
|
|
/**
|
|
* 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( _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(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The _queueFetch function is used to queue a fetch request.
|
|
* TODO: Refresh is currently not used
|
|
*/
|
|
_queueFetch( _range, _direction, _isUpdate)
|
|
{
|
|
|
|
// Force immediate to be false
|
|
_isUpdate = _isUpdate ? _isUpdate : false;
|
|
|
|
// Push the requests onto the request queue
|
|
var start = Math.max(0, _range.top);
|
|
var end = Math.min(this._grid.getTotalCount(), _range.bottom);
|
|
for (var i = start; i < end; i++)
|
|
{
|
|
if (typeof this._queue[i] === "undefined")
|
|
{
|
|
this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current
|
|
}
|
|
}
|
|
|
|
// Start the queue timer, if this has not already been done
|
|
if (this._queueTimer === null && !_isUpdate)
|
|
{
|
|
var self = this;
|
|
egw.debug('log', 'Dataview queue: ', _range);
|
|
this._queueTimer = window.setTimeout(function () {
|
|
self._flushQueue(false);
|
|
}, ET2_DATAVIEW_FETCH_TIMEOUT);
|
|
}
|
|
|
|
if (_isUpdate)
|
|
{
|
|
this._flushQueue(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flushes the queue.
|
|
*/
|
|
_flushQueue( _isUpdate)
|
|
{
|
|
|
|
// Clear any still existing timer
|
|
this._clearTimer();
|
|
|
|
// Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE
|
|
var marked = {};
|
|
var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2);
|
|
var total = this._grid.getTotalCount();
|
|
for (var key in this._queue)
|
|
{
|
|
if (this._queue[key] > 1)
|
|
continue;
|
|
|
|
key = parseInt(key);
|
|
|
|
var b = Math.max(0, key - r + (r * this._queue[key]));
|
|
var t = Math.min(key + r + (r * this._queue[key]), total - 1);
|
|
var c = 0;
|
|
for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++)
|
|
{
|
|
if (typeof this._queue[i] == "undefined"
|
|
|| this._queue[i] <= 1)
|
|
{
|
|
this._queue[i] = 2; // Stage 2 -- pending or available
|
|
marked[i] = true;
|
|
c++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a list with start indices and counts
|
|
var fetchList = [];
|
|
var entry = null;
|
|
var last = 0;
|
|
|
|
// Get the int keys and sort the array numeric
|
|
var arr = et2_arrayIntKeys(marked).sort(
|
|
function(a,b){return a > b ? 1 : (a == b ? 0 : -1)});
|
|
|
|
for (var i = 0; i < arr.length; i++)
|
|
{
|
|
if (i == 0 || arr[i] - last > 1)
|
|
{
|
|
if (entry)
|
|
{
|
|
fetchList.push(entry);
|
|
}
|
|
entry = {
|
|
"start": arr[i],
|
|
"count": 1
|
|
};
|
|
}
|
|
else
|
|
{
|
|
entry.count++;
|
|
}
|
|
|
|
last = arr[i];
|
|
}
|
|
|
|
if (entry)
|
|
{
|
|
fetchList.push(entry);
|
|
}
|
|
|
|
// Special case: If there are no entries in the fetch list and this is
|
|
// an update, create an dummy entry, so that we'll get the current count
|
|
if (fetchList.length === 0 && _isUpdate)
|
|
{
|
|
fetchList.push({
|
|
"start": 0, "count": 0
|
|
});
|
|
|
|
// Disable grid invalidate, or it might request again before we're done
|
|
this._grid.doInvalidate = false;
|
|
}
|
|
|
|
egw.debug("log", "Dataview flush", fetchList);
|
|
// Execute all queries
|
|
for (var i = 0; i < fetchList.length; i++)
|
|
{
|
|
// Build the query
|
|
var query = {
|
|
"start": fetchList[i].start,
|
|
"num_rows": fetchList[i].count,
|
|
"refresh": false
|
|
};
|
|
|
|
// Context used in the callback function
|
|
var ctx = {
|
|
"self": this,
|
|
"start": query.start,
|
|
"count": query.num_rows,
|
|
"lastModification": this._lastModification,
|
|
prefix: undefined
|
|
};
|
|
if(this.dataStorePrefix)
|
|
{
|
|
ctx.prefix = this.dataStorePrefix;
|
|
}
|
|
|
|
this._queueRequest(query, ctx);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue a request for data
|
|
* @param {Object} query
|
|
* @param {Object} ctx
|
|
*/
|
|
_queueRequest(query, ctx)
|
|
{
|
|
this._request_queue.push({
|
|
query: query,
|
|
context: ctx,
|
|
// Start pending, set to 1 when request sent
|
|
status: 0
|
|
});
|
|
|
|
this._fetchQueuedRequest();
|
|
}
|
|
|
|
/**
|
|
* Fetch data for a queued request, subject to rate limit
|
|
*/
|
|
_fetchQueuedRequest()
|
|
{
|
|
// Check to see if there's room
|
|
var count = 0;
|
|
for (var i = 0; i < this._request_queue.length; i++)
|
|
{
|
|
if(this._request_queue[i].status > 0) count++;
|
|
}
|
|
// Too many requests, will try again after response is received
|
|
if(count >= et2_dataview_controller.CONCURRENT_REQUESTS || this._request_queue.length === 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Keep at least 1 previous pending
|
|
var keep = 1;
|
|
|
|
// The most recent is the one the user's most interested in
|
|
var request = null;
|
|
for(var i = this._request_queue.length - 1; i >= 0; i--)
|
|
{
|
|
// Only interested in pending requests (status 0)
|
|
if(this._request_queue[i].status != 0)
|
|
{
|
|
continue;
|
|
}
|
|
if(request == null)
|
|
{
|
|
request = this._request_queue[i];
|
|
}
|
|
else if (keep > 0)
|
|
{
|
|
keep--;
|
|
}
|
|
else if (keep <= 0)
|
|
{
|
|
// Cancel pending, they've probably scrolled past.
|
|
this._request_queue.splice(i,1);
|
|
}
|
|
}
|
|
if(request == null) return;
|
|
|
|
// Request being sent
|
|
request.status = 1;
|
|
|
|
// Call the callback
|
|
this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context);
|
|
}
|
|
|
|
_clearTimer( )
|
|
{
|
|
|
|
// Reset the queue timer upon destruction
|
|
if (this._queueTimer)
|
|
{
|
|
window.clearTimeout(this._queueTimer);
|
|
this._queueTimer = null;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Called by the data source when the data changes
|
|
*
|
|
* @param _data Object|null New data, or null. Null will remove the row.
|
|
*/
|
|
_dataCallback( _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();
|
|
|
|
// If there's no data, stop
|
|
if(typeof _data == "undefined" || _data == null)
|
|
{
|
|
this.self._destroyCallback.call(
|
|
this,
|
|
this.entry.row
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Fill the row DOM Node with data
|
|
this.self._rowCallback.call(
|
|
this.self._context,
|
|
_data,
|
|
this.entry.row,
|
|
this.entry.idx,
|
|
this.entry
|
|
);
|
|
|
|
// Attach the "subgrid" tag to the row, if the depth of this
|
|
// controller is larger than zero
|
|
var tr = this.entry.row.getDOMNode();
|
|
var d = this.self.getDepth();
|
|
if (d > 0)
|
|
{
|
|
jQuery(tr).addClass("subentry");
|
|
jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation");
|
|
|
|
if(this.entry.idx == 0)
|
|
{
|
|
// Set the CSS for the level - required so columns line up
|
|
var indent = jQuery("<span class='indentation'/>").appendTo('body');
|
|
egw.css(".subentry td div.innerContainer.level_"+d,
|
|
"margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px"
|
|
);
|
|
indent.remove();
|
|
}
|
|
}
|
|
|
|
var links = null;
|
|
|
|
// Look for a flag in the row to avoid actions. Use for sums or extra header rows.
|
|
if(!_data.no_actions)
|
|
{
|
|
// Get the action links if the links callback is set
|
|
if (this.self._linkCallback)
|
|
{
|
|
links = this.self._linkCallback.call(
|
|
this.self._context,
|
|
_data,
|
|
this.entry.idx,
|
|
this.entry.uid
|
|
);
|
|
}
|
|
|
|
// Register the row in the selection manager
|
|
this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx,
|
|
tr, links);
|
|
}
|
|
else
|
|
{
|
|
// Remember that
|
|
this.entry.no_actions = true;
|
|
}
|
|
|
|
// Invalidate the current row entry
|
|
this.entry.row.invalidate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
_destroyCallback( _row)
|
|
{
|
|
|
|
// Unregister the row from the selection manager, if not selected
|
|
// If it is selected, leave it there - allows selecting rows and scrolling
|
|
var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid);
|
|
if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED))
|
|
{
|
|
var tr = this.entry.row.getDOMNode();
|
|
this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL);
|
|
this.self._selectionMgr.unregisterRow(this.entry.uid, tr);
|
|
}
|
|
|
|
// 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, this);
|
|
}
|
|
|
|
/**
|
|
* Returns an array containing "_count" index mapping entries starting from
|
|
* the index given in "_start".
|
|
*/
|
|
_getIndexMapping( _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( _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, invalidate the corresponding
|
|
// index entry
|
|
for (var i = mapIdx; i < _idxMap.length; i++)
|
|
{
|
|
if(typeof _idxMap[i] != 'undefined')
|
|
{
|
|
_idxMap[i].uid = null;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
_mergeResult( _newEntries, _invalidStartIdx, _diff, _total)
|
|
{
|
|
|
|
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];
|
|
}
|
|
|
|
// Merge the old map with all old entries
|
|
for (var key in this._indexMap)
|
|
{
|
|
// Get the corresponding index entry
|
|
var entry = this._indexMap[key];
|
|
|
|
// 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;
|
|
if (newIdx >= 0 && newIdx < _total
|
|
&& typeof newMap[newIdx] === "undefined")
|
|
{
|
|
entry.idx = newIdx;
|
|
newMap[newIdx] = entry;
|
|
}
|
|
else if(newMap[newIdx]!==this._indexMap[key])
|
|
{
|
|
// Make sure the old entry gets invalidated
|
|
entry.idx = null;
|
|
entry.row = null;
|
|
}
|
|
}
|
|
|
|
// Make the new index map the current index map
|
|
this._indexMap = newMap;
|
|
this._selectionMgr.setIndexMap(newMap);
|
|
}
|
|
|
|
}
|
|
|
|
_fetchCallback( _response)
|
|
{
|
|
// Remove answered request from queue
|
|
var request = null;
|
|
for(var i = 0; i < this.self._request_queue.length; i++)
|
|
{
|
|
if(this.self._request_queue[i].context == this)
|
|
{
|
|
request = this.self._request_queue[i];
|
|
this.self._request_queue.splice(i,1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.self._lastModification = _response.lastModification;
|
|
|
|
// Do nothing if _response.order evaluates to false
|
|
if (!_response.order)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure _response.order.length is not longer than the requested
|
|
// count, if a specific count was requested
|
|
var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order;
|
|
|
|
// Remove from queue, or it will not be fetched again
|
|
if(_response.total < this.count)
|
|
{
|
|
// Less rows than we expected
|
|
// Clear the queue, or the remnants will never be loaded again
|
|
this.self._queue = {};
|
|
}
|
|
else
|
|
{
|
|
for(var i = this.start; i < this.start + order.length; i++)
|
|
delete this.self._queue[i];
|
|
}
|
|
|
|
// Get the current index map for the updated region
|
|
var idxMap = this.self._getIndexMapping(this.start, order.length);
|
|
|
|
// 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, _response.total);
|
|
|
|
if(_response.total == 0)
|
|
{
|
|
this.self._emptyRow(true);
|
|
}
|
|
else
|
|
{
|
|
var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove();
|
|
this.self._selectionMgr.unregisterRow("",0,row.get(0));
|
|
}
|
|
|
|
// Now it's OK to invalidate, if it wasn't before
|
|
this.self._grid.doInvalidate = true;
|
|
|
|
// Update the total element count in the grid
|
|
this.self._grid.setTotalCount(_response.total);
|
|
this.self._selectionMgr.setTotalCount(_response.total);
|
|
|
|
// Schedule an invalidate, in case total is the same
|
|
this.self._grid.invalidate();
|
|
|
|
// Check if requests are waiting
|
|
this.self._fetchQueuedRequest();
|
|
}
|
|
|
|
/**
|
|
* Insert an empty / placeholder row when there is no data to display
|
|
*/
|
|
_emptyRow(_noRows)
|
|
{
|
|
var noRows = !_noRows ? false : true;
|
|
jQuery(".egwGridView_empty",this._grid.innerTbody).remove();
|
|
if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty"))
|
|
{
|
|
var placeholder = this._grid._rowProvider.getPrototype("empty");
|
|
if(jQuery("td",placeholder).length == 1)
|
|
{
|
|
jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px")
|
|
}
|
|
placeholder.appendTo(this._grid.innerTbody);
|
|
|
|
// Register placeholder action only if no rows
|
|
if (noRows)
|
|
{
|
|
// Get the action links if the links callback is set
|
|
var links = null;
|
|
if (this._linkCallback)
|
|
{
|
|
links = this._linkCallback.call(
|
|
this._context,
|
|
{},
|
|
0,
|
|
""
|
|
);
|
|
}
|
|
this._selectionMgr.registerRow("",0,placeholder.get(0), links);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback function used by the selection manager to translate the selected
|
|
* range to uids.
|
|
*/
|
|
_selectionFetchRange( _range, _callback, _context)
|
|
{
|
|
this._dataProvider.dataFetch(
|
|
{ "start": _range.top, "num_rows": _range.bottom - _range.top + 1,
|
|
"no_data": true },
|
|
function (_response) {
|
|
_callback.call(_context, _response.order);
|
|
},
|
|
_context
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Tells the grid to make the given index visible.
|
|
*/
|
|
_makeIndexVisible( _idx)
|
|
{
|
|
this._grid.makeIndexVisible(_idx);
|
|
}
|
|
|
|
}
|
|
|