egroupware_official/api/js/etemplate/et2_dataview_controller_selection.ts
Milan f430b66d3b converted egw_action from javascript to typescript
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)
2023-09-13 10:40:32 +02:00

765 lines
19 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_dataview_view_aoi;
egw_action.egw_keymanager;
*/
import {egw} from "../jsapi/egw_global";
import {et2_bounds} from "./et2_core_common";
import {et2_dataview_rowAOI} from "./et2_dataview_view_aoi";
import {egwActionObjectInterface} from "../egw_action/egw_action";
import {
EGW_AO_SHIFT_STATE_BLOCK,
EGW_AO_SHIFT_STATE_MULTI,
EGW_AO_STATE_FOCUSED,
EGW_AO_STATE_NORMAL,
EGW_AO_STATE_SELECTED
} from "../egw_action/egw_action_constants";
import {egwBitIsSet, egwSetBit} from "../egw_action/egw_action_common";
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
/**
* The selectioManager is internally used by the et2_dataview_controller class
* to manage the row selection.
* As the action system does not allow selection of entries which are currently
* not in the dom tree, we have to manage this in this class. The idea is to
* manage an external action object interface for each visible row and proxy all
* state changes between an dummy action object, that does no selection handling,
* and the external action object interface.
*
* @augments Class
*/
export class et2_dataview_selectionManager
{
// Maximum number of rows we can safely fetch for selection
// Actual selection may have more rows if we already have some
public static readonly MAX_SELECTION = 1000;
private _parent: any;
private _indexMap: any;
private _actionObjectManager: any;
private _makeVisibleCallback: any;
private _queryRangeCallback: any;
private select_callback: null;
private _context: any;
_registeredRows: {};
_focusedEntry: null;
private _invertSelection: boolean;
private _selectAll: boolean;
private _inUpdate: boolean;
private _total: number;
private _children: any[];
/**
* Constructor
*
* @param _parent
* @param _indexMap
* @param _actionObjectManager
* @param _queryRangeCallback
* @param _makeVisibleCallback
* @param _context
* @memberOf et2_dataview_selectionManager
*/
constructor(_parent, _indexMap, _actionObjectManager,
_queryRangeCallback, _makeVisibleCallback, _context)
{
// Copy the arguments
this._parent = _parent;
this._indexMap = _indexMap;
this._actionObjectManager = _actionObjectManager;
this._queryRangeCallback = _queryRangeCallback;
this._makeVisibleCallback = _makeVisibleCallback;
this._context = _context;
// Attach this manager to the parent manager if one is given
if (this._parent)
{
this._parent._children.push(this);
}
// Use our selection instead of object manager's to handle not-loaded rows
if(_actionObjectManager)
{
this._actionObjectManager.getAllSelected = jQuery.proxy(
this.getAllSelected, this
);
}
// Internal map which contains all curently selected uids and their
// state
this._registeredRows = {};
this._focusedEntry = null;
this._invertSelection = false;
this._selectAll = false;
this._inUpdate = false;
this._total = 0;
this._children = [];
// Callback for when the selection changes
this.select_callback = null;
}
destroy( )
{
// If we have a parent, unregister from that
if (this._parent)
{
var idx = this._parent._children.indexOf(this);
this._parent._children.splice(idx, 1);
}
// Destroy all children
for (var i = this._children.length - 1; i >= 0; i--)
{
this._children[i].destroy();
}
// Delete all still registered rows
for (var key in this._registeredRows)
{
this.unregisterRow(key, this._registeredRows[key].tr);
}
this.select_callback = null;
}
clear( )
{
for (var key in this._registeredRows)
{
this.unregisterRow(key, this._registeredRows[key].tr);
delete this._registeredRows[key];
}
if(this._actionObjectManager)
{
this._actionObjectManager.clear();
}
for (key in this._indexMap) {
delete this._indexMap[key];
}
this._total = 0;
this._focusedEntry = null;
this._invertSelection = false;
this._selectAll = false;
this._inUpdate = false;
}
setIndexMap( _indexMap)
{
this._indexMap = _indexMap;
}
setTotalCount( _total)
{
this._total = _total;
}
registerRow( _uid, _idx, _tr, _links)
{
// Get the corresponding entry from the registered rows array
var entry = this._getRegisteredRowsEntry(_uid);
// If the row has changed unregister the old one and do not delete
// entry from the entry map
if (entry.tr && entry.tr !== _tr) {
this.unregisterRow(_uid, entry.tr, true);
}
// Create the AOI for the tr
if (!entry.tr && _links)
{
this._attachActionObjectInterface(entry, _tr, _uid);
this._attachActionObject(entry, _tr, _uid, _links, _idx);
}
// Update the entry
if(entry.ao) entry.ao._index;
entry.idx = _idx;
entry.tr = _tr;
// Update the visible state of the _tr
this._updateEntryState(entry, entry.state);
}
unregisterRow( _uid, _tr, _noDelete? : boolean)
{
// _noDelete defaults to false
_noDelete = _noDelete ? true : false;
if (typeof this._registeredRows[_uid] !== "undefined"
&& this._registeredRows[_uid].tr === _tr)
{
this._inUpdate = true;
// Don't leave focusedEntry
// @ts-ignore
if(this._focusedEntry !== null && this._focusedEntry.uid == _uid)
{
this.setFocused(_uid, false);
}
this._registeredRows[_uid].tr = null;
this._registeredRows[_uid].aoi = null;
// Remove the action object from its container
if (this._registeredRows[_uid].ao)
{
this._registeredRows[_uid].ao.remove();
this._registeredRows[_uid].ao = null;
}
if (!_noDelete
&& this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL)
{
delete this._registeredRows[_uid];
}
this._inUpdate = false;
}
}
resetSelection( )
{
this._invertSelection = false;
this._selectAll = false;
this._actionObjectManager.setAllSelected(false);
for (var key in this._registeredRows)
{
this.setSelected(key, false);
}
for(var i = 0; i < this._children.length; i++)
{
this._children[i].resetSelection();
}
}
setSelected( _uid, _selected)
{
this._selectAll = false;
var entry = this._getRegisteredRowsEntry(_uid);
this._updateEntryState(entry,
egwSetBit(entry.state, EGW_AO_STATE_SELECTED, _selected));
}
getAllSelected()
{
var selected = this.getSelected();
return selected.all || (selected.ids.length === this._total);
}
setFocused( _uid, _focused)
{
// Reset the state of the currently focused entry
if (this._focusedEntry)
{
this._updateEntryState(this._focusedEntry,
egwSetBit(this._focusedEntry.state, EGW_AO_STATE_FOCUSED,
false));
this._focusedEntry = null;
}
// Mark the new given uid as focused
if (_focused)
{
//console.log('et2_dataview_controller_selection::setFocused -> UID:'+_uid+' is focused by:'+this._actionObjectManager.name);
var entry = this._focusedEntry = this._getRegisteredRowsEntry(_uid);
this._updateEntryState(entry,
egwSetBit(entry.state, EGW_AO_STATE_FOCUSED, true));
}
}
selectAll( )
{
// Reset the selection
this.resetSelection();
this._selectAll = true;
// Run as a range if there's less then the max
if(egw.dataKnownUIDs(this._context._dataProvider.dataStorePrefix).length !== this._total &&
this._total <= et2_dataview_selectionManager.MAX_SELECTION
)
{
this._selectRange(0, this._total);
}
// Tell action manager to do all
this._actionObjectManager.setAllSelected(true);
// Update the selection
for (var key in this._registeredRows)
{
var entry = this._registeredRows[key];
this._updateEntryState(entry, entry.state);
}
this._selectAll = true;
}
getSelected( )
{
// Collect all currently selected ids
var ids = [];
for (var key in this._registeredRows)
{
if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED))
{
ids.push(key);
}
}
// Push all events of the child managers onto the list
for (var i = 0; i < this._children.length; i++)
{
ids = ids.concat(this._children[i].getSelected().ids);
}
// Return an array containing those ids
// RB: we are currently NOT using "inverted"
return {
//"inverted": this._invertSelection,
"all": this._selectAll,
"ids": ids
};
}
/** -- PRIVATE FUNCTIONS -- **/
_attachActionObjectInterface( _entry, _tr, _uid)
{
// Create the AOI which is used internally in the selection manager
// this AOI is not connected to the AO, as the selection manager
// cares about selection etc.
_entry.aoi = new et2_dataview_rowAOI(_tr);
_entry.aoi.setStateChangeCallback(
function (_newState, _changedBit, _shiftState) {
if (_changedBit === EGW_AO_STATE_SELECTED)
{
// Call the select handler
this._handleSelect(
_uid,
_entry,
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK),
egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)
);
}
}, this);
}
_getDummyAOI( _entry, _tr, _uid, _idx)
{
// Create AOI
var dummyAOI = new egwActionObjectInterface();
var self = this;
// Handling for Action Implementations updating the state
dummyAOI.doSetState = function (_state) {
if (!self._inUpdate)
{
// Update the "focused" flag
self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED));
// Generally update the state
self._updateState(_uid, _state);
}
};
// Handle the "make visible" event, pass the request to the parent
// controller
dummyAOI.doMakeVisible = function () {
self._makeVisibleCallback.call(self._context, _idx);
};
// Connect the the two AOIs
dummyAOI.doTriggerEvent = _entry.aoi.doTriggerEvent;
// Implementation of the getDOMNode function, so that the event
// handlers can be properly bound
dummyAOI.getDOMNode = function () { return _tr; };
return dummyAOI;
}
_attachActionObject( _entry, _tr, _uid, _links, _idx)
{
// Get the dummyAOI which connects the action object to the tr but
// does no selection handling
var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx);
// Create an action object for the tr and connect it to a dummy AOI
if(this._actionObjectManager)
{
if(this._actionObjectManager.getObjectById(_uid))
{
var state = _entry.state;
this._actionObjectManager.getObjectById(_uid).remove();
_entry.state = state;
}
_entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI);
}
// Force context (actual widget) in here, it's the last place it's available
_entry.ao._context = this._context;
_entry.ao.updateActionLinks(_links);
_entry.ao._index = _idx;
// Overwrite some functions like "traversePath", "getNext" and
// "getPrevious"
var self = this;
function getIndexAO (_idx) {
// Check whether the index is in the index map
if (typeof self._indexMap[_idx] !== "undefined"
&& self._indexMap[_idx].uid)
{
return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao;
}
return null;
}
function getElementRelatively (_step) {
var total = self._total || Object.keys(self._indexMap).length;
var max_index = Math.max.apply(Math,Object.keys(self._indexMap));
// Get a reasonable number of iterations - not all
var count = Math.max(1,Math.min(self._total,50));
var element = null;
var idx = _entry.idx;
while(element == null && count > 0 && max_index > 0)
{
count--;
element = getIndexAO(Math.max(0,
Math.min(max_index, idx += _step)));
}
return element;
}
_entry.ao.getPrevious = function (_step) {
return getElementRelatively(-_step);
};
_entry.ao.getNext = function (_step) {
return getElementRelatively(_step);
};
_entry.ao.traversePath = function (_obj) {
// Get the start and the stop index
var s = Math.min(this._index, _obj._index);
var e = Math.max(this._index, _obj._index);
var result = [];
for (var i = s; i < e; i++)
{
var ao = getIndexAO(i);
if (ao)
{
result.push(ao);
}
}
return result;
};
}
_updateState( _uid, _state)
{
var entry = this._getRegisteredRowsEntry(_uid);
this._updateEntryState(entry, _state);
return entry;
}
_updateEntryState( _entry, _state)
{
if (this._selectAll)
{
_state |= EGW_AO_STATE_SELECTED;
}
else if (this._invertSelection)
{
_state ^= EGW_AO_STATE_SELECTED;
}
// Attach ao if not there, happens for rows loaded for selection, but
// not displayed yet
if(!_entry.ao && _entry.uid && this._actionObjectManager)
{
var _links = [];
for (var key in this._registeredRows)
{
if(this._registeredRows[key].ao && this._registeredRows[key].ao.actionLinks)
{
_links = this._registeredRows[key].ao.actionLinks;
break;
}
}
if(_links.length)
{
this._attachActionObjectInterface(_entry, null, _entry.uid);
this._attachActionObject(_entry, null, _entry.uid, _links, _entry.idx);
}
}
// Update the state if it has changed
if ((_entry.aoi && _entry.aoi.getState() !== _state) || _entry.state != _state)
{
this._inUpdate = true; // Recursion prevention
// Update the state of the action object
if (_entry.ao)
{
_entry.ao.setSelected(egwBitIsSet(_state, EGW_AO_STATE_SELECTED));
_entry.ao.setFocused(egwBitIsSet(_state, EGW_AO_STATE_FOCUSED));
}
this._inUpdate = false;
// Delete the element if state was set to "NORMAL" and there is
// no tr
if (_state === EGW_AO_STATE_NORMAL && !_entry.tr)
{
delete this._registeredRows[_entry.uid];
}
}
// Update the visual state
if (_entry.aoi && _entry.aoi.doSetState)
{
_entry.aoi.doSetState(_state);
}
// Update the state of the entry
_entry.state = _state;
}
_getRegisteredRowsEntry( _uid)
{
if (typeof this._registeredRows[_uid] === "undefined")
{
this._registeredRows[_uid] = {
"uid": _uid,
"idx": null,
"state": EGW_AO_STATE_NORMAL,
"tr": null,
"aoi": null,
"ao": null
};
}
return this._registeredRows[_uid];
}
_handleSelect( _uid, _entry, _shift, _ctrl)
{
// If not "_ctrl" is set, reset the selection
if (!_ctrl)
{
var top = this;
while(top._parent !== null)
{
top = top._parent;
}
top.resetSelection();
this._actionObjectManager.setAllSelected(false); // needed for hirachical stuff
}
// Mark the element that was clicked as selected
var entry = this._getRegisteredRowsEntry(_uid);
this.setSelected(_uid,
!_ctrl || !egwBitIsSet(entry.state, EGW_AO_STATE_SELECTED));
// Focus the element if shift is not pressed
if (!_shift)
{
this.setFocused(_uid, true);
}
else if (this._focusedEntry)
{
this._selectRange(this._focusedEntry.idx, _entry.idx);
}
if(this.select_callback && typeof this.select_callback == "function")
{
this.select_callback.apply(this._context, arguments);
}
}
_selectRange( _start, _stop)
{
// Contains ranges that are not currently in the index map and that have
// to be queried
var queryRanges = [];
// Iterate over the given range and select the elements in the range
// from _start to _stop
var naStart = false;
var s = Math.min(_start, _stop);
var e = Math.max(_stop, _start);
var RANGE_MAX = 50;
var range_break = s + RANGE_MAX;
for (var i = s; i <= e; i++)
{
if (typeof this._indexMap[i] !== "undefined" &&
this._indexMap[i].uid && egw.dataGetUIDdata(this._indexMap[i].uid))
{
// Add the range to the "queryRanges"
if (naStart !== false)
{
queryRanges.push(et2_bounds(naStart, i - 1));
naStart = false;
range_break += RANGE_MAX;
}
// Select the element, unless flagged for exclusion
// Check for no_actions flag via data
var data = egw.dataGetUIDdata(this._indexMap[i].uid);
if(data && data.data && !data.data.no_actions)
{
this.setSelected(this._indexMap[i].uid, true);
}
}
else if (naStart === false)
{
naStart = i;
range_break = naStart + RANGE_MAX;
}
else if(i >= range_break)
{
queryRanges.push(et2_bounds(naStart ? naStart : s, i - 1));
naStart = i;
range_break += RANGE_MAX;
}
}
// Add the last range to the "queryRanges"
if (naStart !== false)
{
queryRanges.push(et2_bounds(naStart, i - 1));
naStart = false;
}
// Query all unknown ranges from the server
if(queryRanges.length > 0)
{
this._query_ranges(queryRanges);
}
}
_query_ranges(queryRanges)
{
let that = this;
let record_count = 0;
let range_index = 0;
let range_count = queryRanges.length;
let cont = true;
let fetchResolver;
let fetchPromise = new Promise(function(resolve)
{
fetchResolver = resolve;
});
let fetchList = [fetchPromise];
// Found after dialog loads
var progressbar;
let dialog = new Et2Dialog(this._context._widget.egw());
dialog.transformAttributes({
callback:
// Abort the long task if they canceled the data load
function() {cont = false},
template: egw.webserverUrl + '/api/templates/default/long_task.xet',
message: egw.lang('Loading'),
title: egw.lang('please wait...'),
buttons: [{
button_id: Et2Dialog.CANCEL_BUTTON,
label: egw.lang('cancel'),
id: 'dialog[cancel]',
image: 'cancel'
}],
width: 300
});
(this._context._widget.getDOMNode() || document.body).append(dialog);
dialog.updateComplete.then(() =>
{
dialog.template.DOMContainer.addEventListener('load', () =>
{
// Get access to template widgets
progressbar = dialog.template.widgetContainer.getWidgetById('progressbar');
let rangePromise = fetchPromise;
for(var i = 0; i < queryRanges.length; i++)
{
if(record_count + (queryRanges[i].bottom - queryRanges[i].top + 1) > et2_dataview_selectionManager.MAX_SELECTION)
{
egw.message(egw.lang('Too many rows selected.<br />Select all, or less than %1 rows', et2_dataview_selectionManager.MAX_SELECTION));
break;
}
else
{
record_count += (queryRanges[i].bottom - queryRanges[i].top + 1);
// We want to chain these one after the other, not fire them all right away
rangePromise = rangePromise.then((function()
{
// Check for abort
if(!cont)
{
return;
}
return new Promise(function(resolve)
{
that._queryRangeCallback.call(that._context, this,
function(_order)
{
for(var j = 0; j < _order.length; j++)
{
// Check for no_actions flag via data since entry isn't there/available
var data = egw.dataGetUIDdata(_order[j]);
if(!data || data && data.data && !data.data.no_actions)
{
var entry = this._getRegisteredRowsEntry(_order[j]);
this._updateEntryState(entry,
egwSetBit(entry.state, EGW_AO_STATE_SELECTED, true));
}
}
progressbar.set_value(100 * (++range_index / range_count));
resolve();
}, that);
}.bind(this));
}).bind(queryRanges[i]));
fetchList.push(rangePromise);
}
}
// Start the first fetch
fetchResolver();
Promise.all(fetchList).finally(function()
{
dialog.close();
});
})
});
}
}