/** * 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.eTemplate.DOMContainer.addEventListener('load', () => { // Get access to template widgets progressbar = dialog.eTemplate.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.
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(); }); }) }); } }