1
|
|
|
/** |
2
|
|
|
* EGroupware eTemplate2 |
3
|
|
|
* |
4
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License |
5
|
|
|
* @package etemplate |
6
|
|
|
* @subpackage dataview |
7
|
|
|
* @link http://www.egroupware.org |
8
|
|
|
* @author Andreas Stöckel |
9
|
|
|
* @copyright Stylite 2011-2012 |
10
|
|
|
* @version $Id$ |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
/*egw:uses |
14
|
|
|
et2_core_common; |
15
|
|
|
et2_core_inheritance; |
16
|
|
|
|
17
|
|
|
et2_dataview_interfaces; |
18
|
|
|
et2_dataview_controller_selection; |
19
|
|
|
et2_dataview_view_row; |
20
|
|
|
|
21
|
|
|
egw_action.egw_action; |
22
|
|
|
*/ |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* The fetch timeout specifies the time during which the controller tries to |
26
|
|
|
* consolidate requests for rows. |
27
|
|
|
*/ |
28
|
|
|
var ET2_DATAVIEW_FETCH_TIMEOUT = 50; |
29
|
|
|
|
30
|
|
|
var ET2_DATAVIEW_STEPSIZE = 50; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* The et2_dataview_controller class is the intermediate layer between a grid |
34
|
|
|
* instance and the corresponding data source. It manages updating the grid, |
35
|
|
|
* as well as inserting and deleting rows. |
36
|
|
|
*/ |
37
|
|
|
var et2_dataview_controller = (function(){ "use strict"; return Class.extend({ |
38
|
|
|
|
39
|
|
|
// Maximum concurrent data requests. Additional ones are held in the queue. |
40
|
|
|
CONCURRENT_REQUESTS: 5, |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Constructor of the et2_dataview_controller, connects to the grid |
44
|
|
|
* callback. |
45
|
|
|
* |
46
|
|
|
* @param _grid is the grid the controller should controll. |
47
|
|
|
* @param _dataProvider is an object implementing the et2_IDataProvider |
48
|
|
|
* interface. |
49
|
|
|
* @param _rowCallback is the callback function that gets called when a row |
50
|
|
|
* is requested. |
51
|
|
|
* @param _linkCallback is the callback function that gets called for |
52
|
|
|
* requesting action links for a row. The row data, the index of the row and |
53
|
|
|
* the uid are passed as parameters to the function. |
54
|
|
|
* uid is passed to the function. |
55
|
|
|
* @param _context is the context in which the _rowCallback and the |
56
|
|
|
* _linkCallback are called. |
57
|
|
|
* @param _actionObjectManager is the object that manages the action |
58
|
|
|
* objects. |
59
|
|
|
*/ |
60
|
|
|
init: function (_parentController, _grid, _dataProvider, _rowCallback, |
61
|
|
|
_linkCallback, _context, _actionObjectManager) |
62
|
|
|
{ |
63
|
|
|
// Copy the given arguments |
64
|
|
|
this._parentController = _parentController; |
65
|
|
|
this._grid = _grid; |
66
|
|
|
this._dataProvider = _dataProvider; |
67
|
|
|
this._rowCallback = _rowCallback; |
68
|
|
|
this._linkCallback = _linkCallback; |
69
|
|
|
this._context = _context; |
70
|
|
|
|
71
|
|
|
// Initialize list of child controllers |
72
|
|
|
this._children = []; |
73
|
|
|
|
74
|
|
|
// Initialize the "index map" which contains all currently displayed |
75
|
|
|
// containers hashed by the "index" |
76
|
|
|
this._indexMap = {}; |
77
|
|
|
|
78
|
|
|
// Timer used for queing fetch requests |
79
|
|
|
this._queueTimer = null; |
80
|
|
|
|
81
|
|
|
// Array which contains all currently queued row indices in the form of |
82
|
|
|
// an associative array |
83
|
|
|
this._queue = {}; |
84
|
|
|
|
85
|
|
|
// Current concurrent requests we have |
86
|
|
|
this._request_queue = []; |
87
|
|
|
|
88
|
|
|
// Register the dataFetch callback |
89
|
|
|
this._grid.setDataCallback(this._gridCallback, this); |
90
|
|
|
|
91
|
|
|
// Create the selection manager |
92
|
|
|
this._selectionMgr = new et2_dataview_selectionManager( |
93
|
|
|
this._parentController ? this._parentController._selectionMgr : null, |
94
|
|
|
this._indexMap, |
95
|
|
|
_actionObjectManager, |
96
|
|
|
this._selectionFetchRange, |
97
|
|
|
this._makeIndexVisible, |
98
|
|
|
this |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
// Record the child |
102
|
|
|
if(this._parentController != null) |
103
|
|
|
{ |
104
|
|
|
this._parentController._children.push(this); |
105
|
|
|
} |
106
|
|
|
}, |
107
|
|
|
|
108
|
|
|
destroy: function () { |
109
|
|
|
|
110
|
|
|
// Destroy the selection manager |
111
|
|
|
this._selectionMgr.free(); |
112
|
|
|
|
113
|
|
|
// Clear the selection timeout |
114
|
|
|
this._clearTimer(); |
115
|
|
|
|
116
|
|
|
// Remove the child from the child list |
117
|
|
|
if(this._parentController != null) |
118
|
|
|
{ |
119
|
|
|
var idx = this._parentController._children.indexOf(this); |
120
|
|
|
|
121
|
|
|
if (idx >= 0) |
122
|
|
|
{ |
123
|
|
|
// This element is no longer parent of the child |
124
|
|
|
this._parentController._children.splice(idx, 1); |
125
|
|
|
this._parentController = null; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
}, |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* The update function queries the server for changes in the currently |
132
|
|
|
* managed index range -- those changes are then merged into the current |
133
|
|
|
* view without a complete rebuild of every row. |
134
|
|
|
* |
135
|
|
|
* @param {boolean} clear Skip the fancy stuff, dump everything and start again. |
136
|
|
|
* Completely clears the grid and selection. |
137
|
|
|
*/ |
138
|
|
|
update: function (clear) { |
139
|
|
|
|
140
|
|
|
// --------- |
141
|
|
|
|
142
|
|
|
// TODO: Actually stuff here should be done if the server responds that |
143
|
|
|
// there at all were some changes (needs implementation of "refresh") |
144
|
|
|
|
145
|
|
|
// Tell the grid not to try and update itself while we do this |
146
|
|
|
this._grid.doInvalidate = false; |
147
|
|
|
|
148
|
|
|
if(clear) |
149
|
|
|
{ |
150
|
|
|
// Scroll to top |
151
|
|
|
this._grid.makeIndexVisible(0); |
152
|
|
|
this._grid.clear(); |
153
|
|
|
|
154
|
|
|
// Free selection manager |
155
|
|
|
this._selectionMgr.clear(); |
156
|
|
|
|
157
|
|
|
// Clear object manager |
158
|
|
|
this._objectManager.clear(); |
159
|
|
|
|
160
|
|
|
// Clear the map |
161
|
|
|
this._indexMap = {} |
162
|
|
|
// Update selection manager, it uses this by reference |
163
|
|
|
this._selectionMgr.setIndexMap(this._indexMap); |
164
|
|
|
|
165
|
|
|
// Clear the queue |
166
|
|
|
this._queue = {}; |
167
|
|
|
|
168
|
|
|
// Invalidate the change detection, re-fetches any known rows |
169
|
|
|
this._lastModification = 0; |
170
|
|
|
} |
171
|
|
|
// Remove all rows which are outside the view range |
172
|
|
|
this._grid.cleanup(); |
173
|
|
|
|
174
|
|
|
// Get the currently visible range from the grid |
175
|
|
|
var range = this._grid.getIndexRange(); |
176
|
|
|
|
177
|
|
|
// Force range.top and range.bottom to contain an integer |
178
|
|
|
if (range.top === false) |
179
|
|
|
{ |
180
|
|
|
range.top = range.bottom = 0; |
181
|
|
|
} |
182
|
|
|
this._request_queue = []; |
183
|
|
|
|
184
|
|
|
// Require that range from the server |
185
|
|
|
this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true); |
186
|
|
|
}, |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Rebuilds the complete grid. |
190
|
|
|
*/ |
191
|
|
|
reset: function () { |
192
|
|
|
// Throw away all internal mappings and reset the timestamp |
193
|
|
|
this._indexMap = {}; |
194
|
|
|
// Update selection manager, it uses this by reference |
195
|
|
|
this._selectionMgr.setIndexMap(this._indexMap); |
196
|
|
|
|
197
|
|
|
// Clear the grid |
198
|
|
|
this._grid.clear(); |
199
|
|
|
|
200
|
|
|
// Clear the row queue |
201
|
|
|
this._queue = {}; |
202
|
|
|
|
203
|
|
|
// Reset the request queue |
204
|
|
|
this._request_queue = []; |
205
|
|
|
|
206
|
|
|
// Update the data |
207
|
|
|
this.update(); |
208
|
|
|
}, |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Loads the initial order. Do not call multiple times. |
212
|
|
|
*/ |
213
|
|
|
loadInitialOrder: function (order) { |
214
|
|
|
for (var i = 0; i < order.length; i++) |
215
|
|
|
{ |
216
|
|
|
this._getIndexEntry(i).uid = order[i]; |
217
|
|
|
} |
218
|
|
|
}, |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Load initial data |
222
|
|
|
* |
223
|
|
|
* @param {string} uid_key Name of the unique row identifier field |
224
|
|
|
* @param {Object} data Key / Value mapping of initial data. |
225
|
|
|
*/ |
226
|
|
|
loadInitialData: function (uid_prefix, uid_key, data) { |
227
|
|
|
var idx = 0; |
228
|
|
|
for(var key in data) |
229
|
|
|
{ |
230
|
|
|
// Skip any extra keys |
231
|
|
|
if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue; |
232
|
|
|
|
233
|
|
|
// Add to row / uid map |
234
|
|
|
var entry = this._getIndexEntry(idx++); |
235
|
|
|
entry.uid = data[key][uid_key]+""; |
236
|
|
|
if(entry.uid.indexOf(uid_prefix) < 0) |
237
|
|
|
{ |
238
|
|
|
entry.uid = uid_prefix + "::" + entry.uid; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
// Add to data cache so grid will find it |
242
|
|
|
egw.dataStoreUID(entry.uid, data[key]) |
243
|
|
|
|
244
|
|
|
// Don't try to insert the rows, grid will do that automatically |
245
|
|
|
} |
246
|
|
|
if(idx == 0) |
247
|
|
|
{ |
248
|
|
|
// No rows, start with an empty |
249
|
|
|
this._selectionMgr.clear(); |
250
|
|
|
this._emptyRow(this._grid._total == 0); |
251
|
|
|
} |
252
|
|
|
}, |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Returns the depth of the controller instance. |
256
|
|
|
*/ |
257
|
|
|
getDepth: function () { |
258
|
|
|
|
259
|
|
|
if (this._parentController) |
260
|
|
|
{ |
261
|
|
|
return this._parentController.getDepth() + 1; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
return 0; |
265
|
|
|
}, |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Set the data cache prefix |
269
|
|
|
* The default is to use appname, but if you need to set it explicitly to |
270
|
|
|
* something else to avoid conflicts. Use the same prefix everywhere for |
271
|
|
|
* each type of data. eg. infolog for infolog entries, even if accessed via addressbook |
272
|
|
|
*/ |
273
|
|
|
setPrefix: function(prefix) { |
274
|
|
|
this.dataStorePrefix = prefix; |
275
|
|
|
}, |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Returns the row information of the passed node, or null if not available |
279
|
|
|
* |
280
|
|
|
* @param {DOMNode} node |
281
|
|
|
* @return {string|false} UID, or false if not found |
282
|
|
|
*/ |
283
|
|
|
getRowByNode: function(node) { |
284
|
|
|
// Whatever the node, find a TR |
285
|
|
|
var row_node = jQuery(node).closest('tr'); |
286
|
|
|
var row = false |
287
|
|
|
|
288
|
|
|
// Check index map - simple case |
289
|
|
|
var indexed = this._getIndexEntry(row_node.index()); |
290
|
|
|
if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) |
291
|
|
|
{ |
292
|
|
|
row = indexed; |
293
|
|
|
} |
294
|
|
|
else |
295
|
|
|
{ |
296
|
|
|
// Check whole index map |
297
|
|
|
for(var index in this._indexMap) |
298
|
|
|
{ |
299
|
|
|
indexed = this._indexMap[index]; |
300
|
|
|
if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) |
301
|
|
|
{ |
302
|
|
|
row = indexed; |
303
|
|
|
break; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// Check children |
309
|
|
|
for(var i = 0; !row && i < this._children.length; i++) |
310
|
|
|
{ |
311
|
|
|
var child_row = this._children[i].getRowByNode(node); |
312
|
|
|
if(child_row !== false) row = child_row; |
313
|
|
|
} |
314
|
|
|
if(row && !row.controller) |
315
|
|
|
{ |
316
|
|
|
row.controller = this; |
317
|
|
|
} |
318
|
|
|
return row; |
319
|
|
|
}, |
320
|
|
|
|
321
|
|
|
/* -- PRIVATE FUNCTIONS -- */ |
322
|
|
|
|
323
|
|
|
|
324
|
|
|
_getIndexEntry: function (_idx) { |
325
|
|
|
// Create an entry in the index map if it does not exist yet |
326
|
|
|
if (typeof this._indexMap[_idx] === "undefined") |
327
|
|
|
{ |
328
|
|
|
this._indexMap[_idx] = { |
329
|
|
|
"row": null, |
330
|
|
|
"uid": null |
331
|
|
|
}; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// Always update the index of the entries before returning them. This is |
335
|
|
|
// neccessary, as when we remove the uid from an entry without row, its |
336
|
|
|
// index does not get updated any further |
337
|
|
|
this._indexMap[_idx]["idx"] = _idx; |
338
|
|
|
|
339
|
|
|
return this._indexMap[_idx]; |
340
|
|
|
}, |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* Inserts a new data row into the grid. index and uid are derived from the |
344
|
|
|
* given management entry. If the data for the given uid does not exist yet, |
345
|
|
|
* a "loading" placeholder will be shown instead. The function will do |
346
|
|
|
* nothing if there already is a row associated to the entry. This function |
347
|
|
|
* will not re-insert a row if the entry already had a row. |
348
|
|
|
* |
349
|
|
|
* @param _entry is the management entry for the index the row will be |
350
|
|
|
* displayed at. |
351
|
|
|
* @param _update specifies whether the row should be updated if _entry.row |
352
|
|
|
* already exists. |
353
|
|
|
* @return true, if all data for the row has been available, false |
354
|
|
|
* otherwise. |
355
|
|
|
*/ |
356
|
|
|
_insertDataRow: function (_entry, _update) { |
357
|
|
|
// Abort if the entry already has a row but the _insert flag is not set |
358
|
|
|
if (_entry.row && !_update) |
359
|
|
|
{ |
360
|
|
|
return true; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
// Context used for the callback functions |
364
|
|
|
var ctx = {"self": this, "entry": _entry}; |
365
|
|
|
|
366
|
|
|
// Create a new row instance, if it does not exist yet |
367
|
|
|
var createdRow = false; |
368
|
|
|
if (!_entry.row) |
369
|
|
|
{ |
370
|
|
|
createdRow = true; |
371
|
|
|
_entry.row = this._createRow(ctx); |
372
|
|
|
_entry.row.setDestroyCallback(this._destroyCallback, ctx); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
// Load the row data if we have a uid for the entry |
376
|
|
|
this.hasData = false; // Gets updated by the _dataCallback |
377
|
|
|
if (_entry.uid) |
378
|
|
|
{ |
379
|
|
|
// Register the callback / immediately load the data |
380
|
|
|
this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, |
381
|
|
|
ctx); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
// Display the loading "row prototype" if we don't have data for the row |
385
|
|
|
if (!this.hasData) |
386
|
|
|
{ |
387
|
|
|
// Get the average height, the "-5" derives from the td padding |
388
|
|
|
var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; |
389
|
|
|
var prototype = this._grid.getRowProvider().getPrototype("loading"); |
390
|
|
|
jQuery("div", prototype).css("height", avg); |
391
|
|
|
var node = _entry.row.getJNode(); |
392
|
|
|
node.empty(); |
393
|
|
|
node.append(prototype.children()); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
// Insert the row into the table -- the same row must never be inserted |
397
|
|
|
// twice into the grid, so this function only executes the following |
398
|
|
|
// code only if it is a newly created row. |
399
|
|
|
if (createdRow && _entry.row) |
400
|
|
|
{ |
401
|
|
|
this._grid.insertRow(_entry.idx, _entry.row); |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
return this.hasData; |
405
|
|
|
}, |
406
|
|
|
|
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Create a new row. |
410
|
|
|
* |
411
|
|
|
* @param {type} ctx |
412
|
|
|
* @returns {et2_dataview_container} |
413
|
|
|
*/ |
414
|
|
|
_createRow: function(ctx) { |
415
|
|
|
return new et2_dataview_row(this._grid); |
416
|
|
|
}, |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* Function which gets called by the grid when data is requested. |
420
|
|
|
* |
421
|
|
|
* @param _idxStart is the index of the first row for which data is |
422
|
|
|
* requested. |
423
|
|
|
* @param _idxEnd is the index of the last requested row. |
424
|
|
|
*/ |
425
|
|
|
_gridCallback: function (_idxStart, _idxEnd) { |
426
|
|
|
|
427
|
|
|
var needsData = false; |
428
|
|
|
|
429
|
|
|
// Iterate over all elements the dataview requested and create a row |
430
|
|
|
// which indicates that we are currently loading data |
431
|
|
|
for (var i = _idxStart; i <= _idxEnd; i++) |
432
|
|
|
{ |
433
|
|
|
var entry = this._getIndexEntry(i); |
434
|
|
|
|
435
|
|
|
// Insert the row for the entry -- do not update rows which are |
436
|
|
|
// already existing, as we do not have new data for those. |
437
|
|
|
if (!this._insertDataRow(entry, false) && needsData === false) |
438
|
|
|
{ |
439
|
|
|
needsData = i; |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
// Queue fetching that data range |
444
|
|
|
if (needsData !== false) |
445
|
|
|
{ |
446
|
|
|
this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false); |
447
|
|
|
} |
448
|
|
|
}, |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* The _queueFetch function is used to queue a fetch request. |
452
|
|
|
* TODO: Refresh is currently not used |
453
|
|
|
*/ |
454
|
|
|
_queueFetch: function (_range, _direction, _isUpdate) { |
455
|
|
|
|
456
|
|
|
// Force immediate to be false |
457
|
|
|
_isUpdate = _isUpdate ? _isUpdate : false; |
458
|
|
|
|
459
|
|
|
// Push the requests onto the request queue |
460
|
|
|
var start = Math.max(0, _range.top); |
461
|
|
|
var end = Math.min(this._grid.getTotalCount(), _range.bottom); |
462
|
|
|
for (var i = start; i < end; i++) |
463
|
|
|
{ |
464
|
|
|
if (typeof this._queue[i] === "undefined") |
465
|
|
|
{ |
466
|
|
|
this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current |
467
|
|
|
} |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
// Start the queue timer, if this has not already been done |
471
|
|
|
if (this._queueTimer === null && !_isUpdate) |
472
|
|
|
{ |
473
|
|
|
var self = this; |
474
|
|
|
egw.debug('log', 'Dataview queue: ', _range); |
475
|
|
|
this._queueTimer = window.setTimeout(function () { |
476
|
|
|
self._flushQueue(false); |
477
|
|
|
}, ET2_DATAVIEW_FETCH_TIMEOUT); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
if (_isUpdate) |
481
|
|
|
{ |
482
|
|
|
this._flushQueue(true); |
483
|
|
|
} |
484
|
|
|
}, |
485
|
|
|
|
486
|
|
|
/** |
487
|
|
|
* Flushes the queue. |
488
|
|
|
*/ |
489
|
|
|
_flushQueue: function (_isUpdate) { |
490
|
|
|
|
491
|
|
|
// Clear any still existing timer |
492
|
|
|
this._clearTimer(); |
493
|
|
|
|
494
|
|
|
// Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE |
495
|
|
|
var marked = {}; |
496
|
|
|
var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2); |
497
|
|
|
var total = this._grid.getTotalCount(); |
498
|
|
|
for (var key in this._queue) |
499
|
|
|
{ |
500
|
|
|
if (this._queue[key] > 1) |
501
|
|
|
continue; |
502
|
|
|
|
503
|
|
|
key = parseInt(key); |
504
|
|
|
|
505
|
|
|
var b = Math.max(0, key - r + (r * this._queue[key])); |
506
|
|
|
var t = Math.min(key + r + (r * this._queue[key]), total - 1); |
507
|
|
|
var c = 0; |
508
|
|
|
for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++) |
509
|
|
|
{ |
510
|
|
|
if (typeof this._queue[i] == "undefined" |
511
|
|
|
|| this._queue[i] <= 1) |
512
|
|
|
{ |
513
|
|
|
this._queue[i] = 2; // Stage 2 -- pending or available |
514
|
|
|
marked[i] = true; |
515
|
|
|
c++; |
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
// Create a list with start indices and counts |
521
|
|
|
var fetchList = []; |
522
|
|
|
var entry = null; |
523
|
|
|
var last = 0; |
524
|
|
|
|
525
|
|
|
// Get the int keys and sort the array numeric |
526
|
|
|
var arr = et2_arrayIntKeys(marked).sort( |
527
|
|
|
function(a,b){return a > b ? 1 : (a == b ? 0 : -1)}); |
528
|
|
|
|
529
|
|
|
for (var i = 0; i < arr.length; i++) |
530
|
|
|
{ |
531
|
|
|
if (i == 0 || arr[i] - last > 1) |
532
|
|
|
{ |
533
|
|
|
if (entry) |
534
|
|
|
{ |
535
|
|
|
fetchList.push(entry); |
536
|
|
|
} |
537
|
|
|
entry = { |
538
|
|
|
"start": arr[i], |
539
|
|
|
"count": 1 |
540
|
|
|
}; |
541
|
|
|
} |
542
|
|
|
else |
543
|
|
|
{ |
544
|
|
|
entry.count++; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
last = arr[i]; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
if (entry) |
551
|
|
|
{ |
552
|
|
|
fetchList.push(entry); |
553
|
|
|
} |
554
|
|
|
|
555
|
|
|
// Special case: If there are no entries in the fetch list and this is |
556
|
|
|
// an update, create an dummy entry, so that we'll get the current count |
557
|
|
|
if (fetchList.length === 0 && _isUpdate) |
558
|
|
|
{ |
559
|
|
|
fetchList.push({ |
560
|
|
|
"start": 0, "count": 0 |
561
|
|
|
}); |
562
|
|
|
|
563
|
|
|
// Disable grid invalidate, or it might request again before we're done |
564
|
|
|
this._grid.doInvalidate = false; |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
egw.debug("log", "Dataview flush", fetchList); |
568
|
|
|
// Execute all queries |
569
|
|
|
for (var i = 0; i < fetchList.length; i++) |
570
|
|
|
{ |
571
|
|
|
// Build the query |
572
|
|
|
var query = { |
573
|
|
|
"start": fetchList[i].start, |
574
|
|
|
"num_rows": fetchList[i].count, |
575
|
|
|
"refresh": false |
576
|
|
|
}; |
577
|
|
|
|
578
|
|
|
// Context used in the callback function |
579
|
|
|
var ctx = { |
580
|
|
|
"self": this, |
581
|
|
|
"start": query.start, |
582
|
|
|
"count": query.num_rows, |
583
|
|
|
"lastModification": this._lastModification |
584
|
|
|
}; |
585
|
|
|
if(this.dataStorePrefix) |
586
|
|
|
{ |
587
|
|
|
ctx.prefix = this.dataStorePrefix; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
this._queueRequest(query, ctx); |
591
|
|
|
} |
592
|
|
|
}, |
593
|
|
|
|
594
|
|
|
/** |
595
|
|
|
* Queue a request for data |
596
|
|
|
* @param {Object} query |
597
|
|
|
* @param {Object} ctx |
598
|
|
|
*/ |
599
|
|
|
_queueRequest: function _queueRequest(query, ctx) |
600
|
|
|
{ |
601
|
|
|
this._request_queue.push({ |
602
|
|
|
query: query, |
603
|
|
|
context: ctx, |
604
|
|
|
// Start pending, set to 1 when request sent |
605
|
|
|
status: 0 |
606
|
|
|
}); |
607
|
|
|
|
608
|
|
|
this._fetchQueuedRequest(); |
609
|
|
|
}, |
610
|
|
|
|
611
|
|
|
/** |
612
|
|
|
* Fetch data for a queued request, subject to rate limit |
613
|
|
|
*/ |
614
|
|
|
_fetchQueuedRequest: function _fetchQueuedRequest() |
615
|
|
|
{ |
616
|
|
|
// Check to see if there's room |
617
|
|
|
var count = 0; |
618
|
|
|
for (var i = 0; i < this._request_queue.length; i++) |
619
|
|
|
{ |
620
|
|
|
if(this._request_queue[i].status > 0) count++; |
621
|
|
|
} |
622
|
|
|
// Too many requests, will try again after response is received |
623
|
|
|
if(count >= this.CONCURRENT_REQUESTS || this._request_queue.length === 0) |
624
|
|
|
{ |
625
|
|
|
return; |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
// Keep at least 1 previous pending |
629
|
|
|
var keep = 1; |
630
|
|
|
|
631
|
|
|
// The most recent is the one the user's most interested in |
632
|
|
|
var request = null; |
633
|
|
|
for(var i = this._request_queue.length - 1; i >= 0; i--) |
634
|
|
|
{ |
635
|
|
|
// Only interested in pending requests (status 0) |
636
|
|
|
if(this._request_queue[i].status != 0) |
637
|
|
|
{ |
638
|
|
|
continue; |
639
|
|
|
} |
640
|
|
|
if(request == null) |
641
|
|
|
{ |
642
|
|
|
request = this._request_queue[i]; |
643
|
|
|
} |
644
|
|
|
else if (keep > 0) |
645
|
|
|
{ |
646
|
|
|
keep--; |
647
|
|
|
} |
648
|
|
|
else if (keep <= 0) |
649
|
|
|
{ |
650
|
|
|
// Cancel pending, they've probably scrolled past. |
651
|
|
|
this._request_queue.splice(i,1); |
652
|
|
|
} |
653
|
|
|
} |
654
|
|
|
if(request == null) return; |
655
|
|
|
|
656
|
|
|
// Request being sent |
657
|
|
|
request.status = 1; |
658
|
|
|
|
659
|
|
|
// Call the callback |
660
|
|
|
this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context); |
661
|
|
|
}, |
662
|
|
|
|
663
|
|
|
_clearTimer: function () { |
664
|
|
|
|
665
|
|
|
// Reset the queue timer upon destruction |
666
|
|
|
if (this._queueTimer) |
667
|
|
|
{ |
668
|
|
|
window.clearTimeout(this._queueTimer); |
669
|
|
|
this._queueTimer = null; |
670
|
|
|
} |
671
|
|
|
|
672
|
|
|
}, |
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* Called by the data source when the data changes |
676
|
|
|
* |
677
|
|
|
* @param _data Object|null New data, or null. Null will remove the row. |
678
|
|
|
*/ |
679
|
|
|
_dataCallback: function (_data) { |
680
|
|
|
// Set the "hasData" flag |
681
|
|
|
this.self.hasData = true; |
682
|
|
|
|
683
|
|
|
// Call the row callback with the new data -- the row callback then |
684
|
|
|
// generates the row DOM nodes that will be inserted into the grid |
685
|
|
|
if (this.self._rowCallback) |
686
|
|
|
{ |
687
|
|
|
// Remove everything from the current row |
688
|
|
|
this.entry.row.clear(); |
689
|
|
|
|
690
|
|
|
// If there's no data, stop |
691
|
|
|
if(typeof _data == "undefined" || _data == null) |
692
|
|
|
{ |
693
|
|
|
this.self._destroyCallback.call( |
694
|
|
|
this, |
695
|
|
|
this.entry.row |
696
|
|
|
); |
697
|
|
|
return; |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
// Fill the row DOM Node with data |
701
|
|
|
this.self._rowCallback.call( |
702
|
|
|
this.self._context, |
703
|
|
|
_data, |
704
|
|
|
this.entry.row, |
705
|
|
|
this.entry.idx, |
706
|
|
|
this.entry |
707
|
|
|
); |
708
|
|
|
|
709
|
|
|
// Attach the "subgrid" tag to the row, if the depth of this |
710
|
|
|
// controller is larger than zero |
711
|
|
|
var tr = this.entry.row.getDOMNode(); |
712
|
|
|
var d = this.self.getDepth(); |
713
|
|
|
if (d > 0) |
714
|
|
|
{ |
715
|
|
|
jQuery(tr).addClass("subentry"); |
716
|
|
|
jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation"); |
717
|
|
|
|
718
|
|
|
if(this.entry.idx == 0) |
719
|
|
|
{ |
720
|
|
|
// Set the CSS for the level - required so columns line up |
721
|
|
|
var indent = jQuery("<span class='indentation'/>").appendTo('body'); |
722
|
|
|
egw.css(".subentry td div.innerContainer.level_"+d, |
723
|
|
|
"margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px" |
724
|
|
|
); |
725
|
|
|
indent.remove(); |
726
|
|
|
} |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
var links = null; |
730
|
|
|
|
731
|
|
|
// Look for a flag in the row to avoid actions. Use for sums or extra header rows. |
732
|
|
|
if(!_data.no_actions) |
733
|
|
|
{ |
734
|
|
|
// Get the action links if the links callback is set |
735
|
|
|
if (this.self._linkCallback) |
736
|
|
|
{ |
737
|
|
|
links = this.self._linkCallback.call( |
738
|
|
|
this.self._context, |
739
|
|
|
_data, |
740
|
|
|
this.entry.idx, |
741
|
|
|
this.entry.uid |
742
|
|
|
); |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
// Register the row in the selection manager |
746
|
|
|
this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx, |
747
|
|
|
tr, links); |
748
|
|
|
} |
749
|
|
|
else |
750
|
|
|
{ |
751
|
|
|
// Remember that |
752
|
|
|
this.entry.no_actions = true; |
753
|
|
|
} |
754
|
|
|
|
755
|
|
|
// Invalidate the current row entry |
756
|
|
|
this.entry.row.invalidate(); |
757
|
|
|
} |
758
|
|
|
}, |
759
|
|
|
|
760
|
|
|
/** |
761
|
|
|
* |
762
|
|
|
*/ |
763
|
|
|
_destroyCallback: function (_row) { |
764
|
|
|
|
765
|
|
|
// Unregister the row from the selection manager, if not selected |
766
|
|
|
// If it is selected, leave it there - allows selecting rows and scrolling |
767
|
|
|
var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid); |
768
|
|
|
if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED)) |
769
|
|
|
{ |
770
|
|
|
var tr = this.entry.row.getDOMNode(); |
771
|
|
|
this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL) |
772
|
|
|
this.self._selectionMgr.unregisterRow(this.entry.uid, tr); |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
// There is no further row connected to the entry |
776
|
|
|
this.entry.row = null; |
777
|
|
|
|
778
|
|
|
// Unregister the data callback |
779
|
|
|
this.self._dataProvider.dataUnregisterUID(this.entry.uid, |
780
|
|
|
this.self._dataCallback, this); |
781
|
|
|
}, |
782
|
|
|
|
783
|
|
|
/** |
784
|
|
|
* Returns an array containing "_count" index mapping entries starting from |
785
|
|
|
* the index given in "_start". |
786
|
|
|
*/ |
787
|
|
|
_getIndexMapping: function (_start, _count) { |
788
|
|
|
var result = []; |
789
|
|
|
|
790
|
|
|
for (var i = _start; i < _start + _count; i++) |
791
|
|
|
{ |
792
|
|
|
result.push(this._getIndexEntry(i)); |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
return result; |
796
|
|
|
}, |
797
|
|
|
|
798
|
|
|
/** |
799
|
|
|
* Updates the grid according to the new order. The function simply does the |
800
|
|
|
* following: It iterates along the new order (given in _order) and the old |
801
|
|
|
* order given in _idxMap. Iteration variables used are |
802
|
|
|
* a) i -- points to the current entry in _order |
803
|
|
|
* b) idx -- points to the current grid row that will be effected by |
804
|
|
|
* this operation. |
805
|
|
|
* c) mapIdx -- points to the current entry in _indexMap |
806
|
|
|
* The following cases may occur: |
807
|
|
|
* a) The current entry in the old order has no uid or no row -- in that |
808
|
|
|
* case the row at the current position is simply updated, |
809
|
|
|
* the old pointer will be incremented. |
810
|
|
|
* b) The two uids differ -- insert a new row with the new uid, do not |
811
|
|
|
* increment the old pointer. |
812
|
|
|
* c) The two uids are the same -- increment the old pointer. |
813
|
|
|
* In a last step all rows that are left in the old order are deleted. All |
814
|
|
|
* newly created index entries are returned. This function does not update |
815
|
|
|
* the internal mapping in _idxMap. |
816
|
|
|
*/ |
817
|
|
|
_updateOrder: function (_start, _count, _idxMap, _order) { |
818
|
|
|
// The result contains the newly created index map entries which have to |
819
|
|
|
// be merged with the result |
820
|
|
|
var result = []; |
821
|
|
|
|
822
|
|
|
// Iterate over the new order |
823
|
|
|
var mapIdx = 0; |
824
|
|
|
var idx = _start; |
825
|
|
|
for (var i = 0; i < _order.length; i++, idx++) |
826
|
|
|
{ |
827
|
|
|
var current = _idxMap[mapIdx]; |
828
|
|
|
|
829
|
|
|
if (!current.row || !current.uid) |
830
|
|
|
{ |
831
|
|
|
// If there is no row yet at the current position or the uid |
832
|
|
|
// of that entry is unknown, simply update the entry. |
833
|
|
|
current.uid = _order[i]; |
834
|
|
|
current.idx = idx; |
835
|
|
|
|
836
|
|
|
// Only update the row, if it is displayed (e.g. has a "loading" |
837
|
|
|
// row displayed) -- this is needed for prefetching |
838
|
|
|
if (current.row) |
839
|
|
|
{ |
840
|
|
|
this._insertDataRow(current, true); |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
mapIdx++; |
844
|
|
|
} |
845
|
|
|
else if (current.uid !== _order[i]) |
846
|
|
|
{ |
847
|
|
|
// Insert a new row at the new position |
848
|
|
|
var entry = { |
849
|
|
|
"idx": idx, |
850
|
|
|
"uid": _order[i], |
851
|
|
|
"row": null |
852
|
|
|
}; |
853
|
|
|
|
854
|
|
|
this._insertDataRow(entry, true); |
855
|
|
|
|
856
|
|
|
// Remember the new entry |
857
|
|
|
result.push(entry); |
858
|
|
|
} |
859
|
|
|
else |
860
|
|
|
{ |
861
|
|
|
// Do nothing, the uids do not differ, just update the index of |
862
|
|
|
// the element |
863
|
|
|
current.idx = idx; |
864
|
|
|
mapIdx++; |
865
|
|
|
} |
866
|
|
|
} |
867
|
|
|
|
868
|
|
|
// Delete as many rows as we have left, invalidate the corresponding |
869
|
|
|
// index entry |
870
|
|
|
for (var i = mapIdx; i < _idxMap.length; i++) |
871
|
|
|
{ |
872
|
|
|
if(typeof _idxMap[i] != 'undefined') |
873
|
|
|
{ |
874
|
|
|
_idxMap[i].uid = null; |
875
|
|
|
} |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
return result; |
879
|
|
|
}, |
880
|
|
|
|
881
|
|
|
_mergeResult: function (_newEntries, _invalidStartIdx, _diff, _total) { |
882
|
|
|
|
883
|
|
|
if (_newEntries.length > 0 || _diff > 0) |
884
|
|
|
{ |
885
|
|
|
// Create a new index map |
886
|
|
|
var newMap = {}; |
887
|
|
|
|
888
|
|
|
// Insert all new entries into the new index map |
889
|
|
|
for (var i = 0; i < _newEntries.length; i++) |
890
|
|
|
{ |
891
|
|
|
newMap[_newEntries[i].idx] = _newEntries[i]; |
892
|
|
|
} |
893
|
|
|
|
894
|
|
|
// Merge the old map with all old entries |
895
|
|
|
for (var key in this._indexMap) |
896
|
|
|
{ |
897
|
|
|
// Get the corresponding index entry |
898
|
|
|
var entry = this._indexMap[key]; |
899
|
|
|
|
900
|
|
|
// Calculate the new index -- if rows were deleted, we'll |
901
|
|
|
// have to adjust the index |
902
|
|
|
var newIdx = entry.idx >= _invalidStartIdx |
903
|
|
|
? entry.idx - _diff : entry.idx; |
904
|
|
|
if (newIdx >= 0 && newIdx < _total |
905
|
|
|
&& typeof newMap[newIdx] === "undefined") |
906
|
|
|
{ |
907
|
|
|
entry.idx = newIdx; |
908
|
|
|
newMap[newIdx] = entry; |
909
|
|
|
} |
910
|
|
|
else |
911
|
|
|
{ |
912
|
|
|
// Make sure the old entry gets invalidated |
913
|
|
|
entry.idx = null; |
914
|
|
|
entry.row = null; |
915
|
|
|
} |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
// Make the new index map the current index map |
919
|
|
|
this._indexMap = newMap; |
920
|
|
|
this._selectionMgr.setIndexMap(newMap); |
921
|
|
|
} |
922
|
|
|
|
923
|
|
|
}, |
924
|
|
|
|
925
|
|
|
_fetchCallback: function (_response) { |
926
|
|
|
// Remove answered request from queue |
927
|
|
|
var request = null; |
928
|
|
|
for(var i = 0; i < this.self._request_queue.length; i++) |
929
|
|
|
{ |
930
|
|
|
if(this.self._request_queue[i].context == this) |
931
|
|
|
{ |
932
|
|
|
request = this.self._request_queue[i]; |
933
|
|
|
this.self._request_queue.splice(i,1); |
934
|
|
|
break; |
935
|
|
|
} |
936
|
|
|
} |
937
|
|
|
|
938
|
|
|
this.self._lastModification = _response.lastModification; |
939
|
|
|
|
940
|
|
|
// Do nothing if _response.order evaluates to false |
941
|
|
|
if (!_response.order) |
942
|
|
|
{ |
943
|
|
|
return; |
944
|
|
|
} |
945
|
|
|
|
946
|
|
|
// Make sure _response.order.length is not longer than the requested |
947
|
|
|
// count, if a specific count was requested |
948
|
|
|
var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order; |
949
|
|
|
|
950
|
|
|
// Remove from queue, or it will not be fetched again |
951
|
|
|
if(_response.total < this.count) |
952
|
|
|
{ |
953
|
|
|
// Less rows than we expected |
954
|
|
|
// Clear the queue, or the remnants will never be loaded again |
955
|
|
|
this.self._queue = {}; |
956
|
|
|
} |
957
|
|
|
else |
958
|
|
|
{ |
959
|
|
|
for(var i = this.start; i < this.start + order.length; i++) |
960
|
|
|
delete this.self._queue[i]; |
961
|
|
|
} |
962
|
|
|
|
963
|
|
|
// Get the current index map for the updated region |
964
|
|
|
var idxMap = this.self._getIndexMapping(this.start, order.length); |
965
|
|
|
|
966
|
|
|
// Update the grid using the new order. The _updateOrder function does |
967
|
|
|
// not update the internal mapping while inserting and deleting rows, as |
968
|
|
|
// this would move us to another asymptotic runtime level. |
969
|
|
|
var res = this.self._updateOrder(this.start, this.count, idxMap, order); |
970
|
|
|
|
971
|
|
|
// Merge the new indices, update all indices with rows that were not |
972
|
|
|
// affected and invalidate all indices if there were changes |
973
|
|
|
this.self._mergeResult(res, this.start + order.length, |
974
|
|
|
idxMap.length - order.length, _response.total); |
975
|
|
|
|
976
|
|
|
if(_response.total == 0) |
977
|
|
|
{ |
978
|
|
|
this.self._emptyRow(true); |
979
|
|
|
} |
980
|
|
|
else |
981
|
|
|
{ |
982
|
|
|
var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove(); |
983
|
|
|
this.self._selectionMgr.unregisterRow("",0,row.get(0)); |
984
|
|
|
} |
985
|
|
|
|
986
|
|
|
// Now it's OK to invalidate, if it wasn't before |
987
|
|
|
this.self._grid.doInvalidate = true; |
988
|
|
|
|
989
|
|
|
// Update the total element count in the grid |
990
|
|
|
this.self._grid.setTotalCount(_response.total); |
991
|
|
|
this.self._selectionMgr.setTotalCount(_response.total); |
992
|
|
|
|
993
|
|
|
// Schedule an invalidate, in case total is the same |
994
|
|
|
this.self._grid.invalidate(); |
995
|
|
|
|
996
|
|
|
// Check if requests are waiting |
997
|
|
|
this.self._fetchQueuedRequest(); |
998
|
|
|
}, |
999
|
|
|
|
1000
|
|
|
/** |
1001
|
|
|
* Insert an empty / placeholder row when there is no data to display |
1002
|
|
|
*/ |
1003
|
|
|
_emptyRow: function(_noRows) |
1004
|
|
|
{ |
1005
|
|
|
var noRows = !_noRows ? false : true; |
1006
|
|
|
jQuery(".egwGridView_empty",this._grid.innerTbody).remove(); |
1007
|
|
|
if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty")) |
1008
|
|
|
{ |
1009
|
|
|
var placeholder = this._grid._rowProvider.getPrototype("empty"); |
1010
|
|
|
if(jQuery("td",placeholder).length == 1) |
1011
|
|
|
{ |
1012
|
|
|
jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px") |
1013
|
|
|
} |
1014
|
|
|
placeholder.appendTo(this._grid.innerTbody); |
1015
|
|
|
|
1016
|
|
|
// Register placeholder action only if no rows |
1017
|
|
|
if (noRows) |
1018
|
|
|
{ |
1019
|
|
|
// Get the action links if the links callback is set |
1020
|
|
|
var links = null; |
1021
|
|
|
if (this._linkCallback) |
1022
|
|
|
{ |
1023
|
|
|
links = this._linkCallback.call( |
1024
|
|
|
this._context, |
1025
|
|
|
{}, |
1026
|
|
|
0, |
1027
|
|
|
"" |
1028
|
|
|
); |
1029
|
|
|
} |
1030
|
|
|
this._selectionMgr.registerRow("",0,placeholder.get(0), links); |
1031
|
|
|
} |
1032
|
|
|
} |
1033
|
|
|
}, |
1034
|
|
|
|
1035
|
|
|
/** |
1036
|
|
|
* Callback function used by the selection manager to translate the selected |
1037
|
|
|
* range to uids. |
1038
|
|
|
*/ |
1039
|
|
|
_selectionFetchRange: function (_range, _callback, _context) { |
1040
|
|
|
this._dataProvider.dataFetch( |
1041
|
|
|
{ "start": _range.top, "num_rows": _range.bottom - _range.top + 1, |
1042
|
|
|
"no_data": true }, |
1043
|
|
|
function (_response) { |
1044
|
|
|
_callback.call(_context, _response.order); |
1045
|
|
|
}, |
1046
|
|
|
_context |
1047
|
|
|
); |
1048
|
|
|
}, |
1049
|
|
|
|
1050
|
|
|
/** |
1051
|
|
|
* Tells the grid to make the given index visible. |
1052
|
|
|
*/ |
1053
|
|
|
_makeIndexVisible: function (_idx) { |
1054
|
|
|
this._grid.makeIndexVisible(_idx); |
1055
|
|
|
} |
1056
|
|
|
|
1057
|
|
|
});}).call(this); |
1058
|
|
|
|
1059
|
|
|
|