egroupware/api/js/etemplate/et2_dataview_controller_selection.js

525 lines
21 KiB
JavaScript

/**
* 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_dialog } from "./et2_widget_dialog";
import { et2_createWidget } from "./et2_core_widget";
import { et2_dataview_rowAOI } from "./et2_dataview_view_aoi";
import { egwActionObjectInterface } from "../egw_action/egw_action.js";
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.js";
import { egwBitIsSet, egwSetBit } from "../egw_action/egw_action_common.js";
/**
* 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 {
/**
* 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) {
// _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) {
var that = this;
var record_count = 0;
var range_index = 0;
var range_count = queryRanges.length;
var cont = true;
var fetchPromise = new Promise(function (resolve) {
resolve();
});
// Found after dialog loads
var progressbar;
var parent = et2_dialog._create_parent();
var dialog = et2_createWidget("dialog", {
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": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), id: 'dialog[cancel]', image: 'cancel' }],
width: 300
}, parent);
jQuery(dialog.template.DOMContainer).on('load', function () {
// Get access to template widgets
progressbar = dialog.template.widgetContainer.getWidgetById('progressbar');
});
for (var i = 0; i < queryRanges.length; i++) {
if (record_count + (queryRanges[i].bottom - queryRanges[i].top + 1) > that.MAX_SELECTION) {
egw.message(egw.lang('Too many rows selected.<br />Select all, or less than %1 rows', that.MAX_SELECTION));
break;
}
else {
record_count += (queryRanges[i].bottom - queryRanges[i].top + 1);
fetchPromise = fetchPromise.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]));
}
}
fetchPromise.finally(function () {
dialog.destroy();
});
}
}
// Maximum number of rows we can safely fetch for selection
// Actual selection may have more rows if we already have some
et2_dataview_selectionManager.MAX_SELECTION = 1000;
//# sourceMappingURL=et2_dataview_controller_selection.js.map