/** * EGroupware egw_action framework - egw action framework * * @link https://www.egroupware.org * @author Andreas Stöckel * @copyright 2011 by Andreas Stöckel * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package egw_action */ import {EgwActionLink} from "./EgwActionLink"; import {EgwActionManager} from "./EgwActionManager"; import {egwBitIsSet, egwObjectLength, egwQueueCallback, egwSetBit} from "./egw_action_common"; import { EGW_AO_EXEC_SELECTED, EGW_AO_EXEC_THIS, EGW_AO_FLAG_IS_CONTAINER, EGW_AO_SHIFT_STATE_BLOCK, EGW_AO_SHIFT_STATE_MULTI, EGW_AO_SHIFT_STATE_NONE, EGW_AO_STATE_FOCUSED, EGW_AO_STATE_NORMAL, EGW_AO_STATE_SELECTED, EGW_AO_STATE_VISIBLE, EGW_KEY_A, EGW_KEY_ARROW_DOWN, EGW_KEY_ARROW_UP, EGW_KEY_PAGE_DOWN, EGW_KEY_PAGE_UP, EGW_KEY_SPACE } from "./egw_action_constants"; import type {EgwActionObjectInterface} from "./EgwActionObjectInterface"; import {egwActionObjectInterface} from "./egw_action"; /** * The egwActionObject represents an abstract object to which actions may be * applied. Communication with the DOM tree is established by using the * egwActionObjectInterface (AOI), which is passed in the constructor. * egwActionObjects are organized in a tree structure. * * @param {string} _id is the identifier of the object which * @param {EgwActionObject} _parent is the parent object in the hierarchy. This may be set to NULL * @param {egwActionObjectInterface} _iface is the egwActionObjectInterface which connects the object * to the outer world. * @param {EgwActionManager} _manager is the action manager this object is connected to * this object to the DOM tree. If the _manager isn't supplied, the parent manager * is taken. * @param {number} _flags a set of additional flags being applied to the object, * defaults to 0 */ export class EgwActionObject { readonly id: string readonly parent: EgwActionObject public readonly children: EgwActionObject[] = [] private actionLinks: EgwActionLink[] = [] iface: EgwActionObjectInterface readonly manager: EgwActionManager readonly flags: number data: any = null private readonly setSelectedCallback: any = null; private registeredImpls: any[] = []; // Two variables which help fast travelling through the object tree, when // searching for the selected/focused object. private selectedChildren = []; private focusedChild:EgwActionObject = null; private readonly onBeforeTrigger: Function = undefined _context: any = undefined constructor(_id: string, _parent, _interface:EgwActionObjectInterface, _manager?, _flags: number=0) { if (typeof _manager == "undefined" && typeof _parent == "object" && _parent) _manager = _parent.manager; if (typeof _flags == "undefined") _flags = 0; this.id = _id this.parent = _parent this.iface = _interface this.manager = _manager this.flags = _flags this.setAOI(_interface) } /** * Sets the action object interface - if "NULL" is given, the iface is set * to a dummy interface which is used to store the temporary data. * * @param {egwActionObjectInterface} _aoi */ setAOI(_aoi) { if (_aoi == null) { //TODo replace DummyInterface _aoi = new egwActionObjectInterface(); } // Copy the state from the old interface if (this.iface) { _aoi.setState(this.iface.getState()); } // Replace the interface object this.iface = _aoi; this.iface.setStateChangeCallback(this._ifaceCallback, this); this.iface.setReconnectActionsCallback(this._reconnectCallback, this); }; /** // * Returns the object from the tree with the given ID // * // * @param {string} _id // * @param {number} _search_depth // * @return {egwActionObject} description // * @todo Add search function to egw_action_commons.js // */ getObjectById(_id, _search_depth) { if (this.id == _id) { return this; } if (typeof _search_depth == "undefined") { _search_depth = Number.MAX_VALUE; } for (let i = 0; i < this.children.length && _search_depth > 0; i++) { const obj = this.children[i].getObjectById(_id, _search_depth - 1); if (obj) { return obj; } } return null; }; /** * Adds an object as child to the actionObject and returns it - if the supplied * parameter is an object, the object will be added directly, otherwise an object * with the given id will be created. * * @param {(string|object)} _id Id of the object which will be created or the object * that will be added. * @param {object} _interface if _id was a string, _interface defines the interface which * will be connected to the newly generated object. * @param {number} _flags are the flags will which be supplied to the newly generated * object. May be omitted. * @returns object the generated object */ addObject(_id: any, _interface: EgwActionObjectInterface=null, _flags: number=0) { return this.insertObject(false, _id, _interface, _flags); }; /** * Inserts an object as child to the actionObject and returns it - if the supplied * parameter is an object, the object will be added directly, otherwise an object * with the given id will be created. * * @param {number} _index Position where the object will be inserted, "false" will add it * to the end of the list. * @param {string|object} _id Id of the object which will be created or the object * that will be added. * @param {object} _iface if _id was a string, _iface defines the interface which * will be connected to the newly generated object. * @param {number} _flags are the flags will which be supplied to the newly generated * object. May be omitted. * @returns object the generated object */ insertObject(_index: number | boolean, _id: string | EgwActionObject, _iface: EgwActionObjectInterface, _flags: number) { if (_index === false) _index = this.children.length; let obj = null; if (typeof _id == "object") { obj = _id; // Set the parent to null and reset the focus of the object obj.parent = null; obj.setFocused(false); // Set the parent to this object obj.parent = this; } else if (typeof _id == "string") { obj = new EgwActionObject(_id, this, _iface, this.manager, _flags); } if (obj) { // Add the element to the children this.children.splice(_index as number, 0, obj); } else { throw "Error while adding new element to the ActionObjects!"; } return obj; }; /** * Deletes all children of the egwActionObject */ clear() { // Remove all children while (this.children.length > 0) { this.children[0].remove(); } // Delete all other references this.selectedChildren = []; this.focusedChild = null; // Remove links this.actionLinks = []; }; /** * Deletes this object from the parent container */ remove() { // Remove focus and selection from this element this.setFocused(false); this.setSelected(false); this.setAllSelected(false); // Unregister all registered action implementations this.unregisterActions(); // Clear the child-list this.clear(); // Remove this element from the parent list if (this.parent != null) { const idx = this.parent.children.indexOf(this); if (idx >= 0) { this.parent.children.splice(idx, 1); } } }; /** * Searches for the root object in the action object tree and returns it. */ getRootObject() { if (this.parent === null) { return this; } else { return this.parent.getRootObject(); } }; /** * Returns a list with all parents of this object. */ getParentList() { if (this.parent === null) { return []; } else { const list = this.parent.getParentList(); list.unshift(this.parent); return list; } }; /** * Returns the first parent which has the container flag */ getContainerRoot(): EgwActionObject { if (egwBitIsSet(this.flags, EGW_AO_FLAG_IS_CONTAINER) || this.parent === null) { return this; } else { return this.parent.getContainerRoot(); } }; /** * Returns all selected objects which are in the current subtree. * * @param {function} _test is a function, which gets an object and checks whether * it will be added to the list. * @param {array} _list is internally used to fetch all selected elements, please * omit this parameter when calling the function. */ getSelectedObjects(_test?, _list?) { if (typeof _test == "undefined") _test = null; if (typeof _list == "undefined") { _list = {"elements": []}; } if ((!_test || _test(this)) && this.getSelected()) _list.elements.push(this); if (this.selectedChildren) { for (let i = 0; i < this.selectedChildren.length; i++) { this.selectedChildren[i].getSelectedObjects(_test, _list); } } return _list.elements; }; /** * Returns whether all objects in this tree are selected */ getAllSelected() { if (this.children.length == this.selectedChildren.length) { for (let i = 0; i < this.children.length; i++) { if (!this.children[i].getAllSelected()) return false; } // If this element is a container *and* does not have any children, we // should return false. If this element is not a container we have to // return true has this is the recursion base case return (!egwBitIsSet(this.flags, EGW_AO_FLAG_IS_CONTAINER)) || (this.children.length > 0); } return false; }; /** * Toggles the selection of all objects. * * @param _select boolean specifies whether the objects should get selected or not. * If this parameter is not supplied, the selection will be toggled. */ toggleAllSelected(_select?) { if (typeof _select == "undefined") { _select = !this.getAllSelected(); } // Check for a select_all action if (_select && this.manager && this.manager.getActionById('select_all')) { return this.manager.getActionById('select_all').execute(this); } this.setAllSelected(_select); }; /** * Creates a list which contains all items of the element tree. * * @param {boolean} _visibleOnly * @param {object} _obj is used internally to pass references to the array inside * the object. * @return {array} */ flatList(_visibleOnly?: boolean, _obj?: { elements: EgwActionObject[] }) { if (typeof (_obj) == "undefined") { _obj = { "elements": [] }; } if (typeof (_visibleOnly) == "undefined") { _visibleOnly = false; } if (!_visibleOnly || this.getVisible()) { _obj.elements.push(this); } for (const child of this.children) { child.flatList(_visibleOnly, _obj); } return _obj.elements; }; /** * Returns a traversal list with all objects which are in between the given object * and this one. The operation returns an empty list, if a container object is * found on the way. * * @param {object} _to * @return {array} * @todo Remove flatList here! */ traversePath(_to) { const contRoot: EgwActionObject = this.getContainerRoot(); if (contRoot) { // Get a flat list of all the hncp elements and search for this object // and the object supplied in the _to parameter. const flatList = contRoot.flatList(); const thisId = flatList.indexOf(this); const toId = flatList.indexOf(_to); // Check whether both elements have been found in this part of the tree, // return the slice of that list. if (thisId !== -1 && toId !== -1) { const from = Math.min(thisId, toId); const to = Math.max(thisId, toId); return flatList.slice(from, to + 1); } } return []; }; /** * Returns the index of this object in the children list of the parent object. */ getIndex() { if (this.parent === null) { //TODO check: should be -1 for invalid return 0; } else { return this.parent.children.indexOf(this); } }; /** * Returns the deepest object which is currently focused. Objects with the * "container"-flag will not be returned. */ getFocusedObject() { return this.focusedChild || null; }; /** * Internal function which is connected to the ActionObjectInterface associated * with this object in the constructor. It gets called, whenever the object * gets (de)selected. * * @param {number} _newState is the new state of the object * @param {number} _changedBit * @param {number} _shiftState is the status of extra keys being pressed during the * selection process. */ _ifaceCallback(_newState: number, _changedBit: number, _shiftState?: number) { if (typeof _shiftState == "undefined") _shiftState = EGW_AO_SHIFT_STATE_NONE; let selected: boolean = egwBitIsSet(_newState, EGW_AO_STATE_SELECTED); const visible: boolean = egwBitIsSet(_newState, EGW_AO_STATE_VISIBLE); // Check whether the visibility of the object changed if (_changedBit == EGW_AO_STATE_VISIBLE && visible != this.getVisible()) { // Deselect the object if (!visible) { this.setSelected(false); this.setFocused(false); return EGW_AO_STATE_NORMAL; } else { // Auto-register the actions attached to this object this.registerActions(); } } // Remove the focus from all children on the same level if (this.parent && visible && _changedBit == EGW_AO_STATE_SELECTED) { selected = egwBitIsSet(_newState, EGW_AO_STATE_SELECTED); let objs = []; if (selected) { // Deselect all other objects inside this container, if the "MULTI" shift-state is not set if (!egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)) { this.getContainerRoot().setAllSelected(false); } // If the LIST state is active, get all objects in between this one and the focused one // and set their select state. if (egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK)) { const focused = this.getFocusedObject(); if (focused) { objs = this.traversePath(focused); for (let i = 0; i < objs.length; i++) { objs[i].setSelected(true); } } } } // If the focused element didn't belong to this container, or the "list" // shift-state isn't active, set the focus to this element. if (objs.length == 0 || !egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK)) { this.setFocused(true); _newState = egwSetBit(EGW_AO_STATE_FOCUSED, _newState, true); } this.setSelected(selected); } return _newState; }; /** * Handler for key presses * * @param {number} _keyCode * @param {boolean} _shift * @param {boolean} _ctrl * @param {boolean} _alt * @returns {boolean} */ handleKeyPress(_keyCode, _shift, _ctrl, _alt) { switch (_keyCode) { case EGW_KEY_ARROW_UP: case EGW_KEY_ARROW_DOWN: case EGW_KEY_PAGE_UP: case EGW_KEY_PAGE_DOWN: if (!_alt) { const intval = (_keyCode == EGW_KEY_ARROW_UP || _keyCode == EGW_KEY_ARROW_DOWN) ? 1 : 10; if (this.children.length > 0) { // Get the focused object const focused = this.getFocusedObject(); // Determine the object which should get selected let selObj = null; if (!focused) { selObj = this.children[0]; } else { selObj = (_keyCode == EGW_KEY_ARROW_UP || _keyCode == EGW_KEY_PAGE_UP) ? focused.getPrevious(intval) : focused.getNext(intval); } if (selObj != null) { if (!_shift && !(this.parent && this.parent.data && this.parent.data.keyboard_select)) { this.setAllSelected(false); } else if (!(this.parent && this.parent.data && this.parent.data.keyboard_select)) { const objs = focused.traversePath(selObj); for (let i = 0; i < objs.length; i++) { objs[i].setSelected(true); } } if (!(this.parent.data && this.parent.data.keyboard_select)) { selObj.setSelected(true); } selObj.setFocused(true); // Tell the aoi of the object to make it visible selObj.makeVisible(); } return true; } } break; // Space bar toggles selected for current row case EGW_KEY_SPACE: if (this.children.length <= 0) { break; } // Mark that we're selecting by keyboard, or arrows will reset selection if (!this.parent.data) { this.parent.data = {}; } this.parent.data.keyboard_select = true; // Get the focused object const focused = this.getFocusedObject(); focused.setSelected(!focused.getSelected()); // Tell the aoi of the object to make it visible focused.makeVisible(); return true; // Handle CTRL-A to select all elements in the current container case EGW_KEY_A: if (_ctrl && !_shift && !_alt) { this.toggleAllSelected(); return true; } break; } return false; }; getPrevious(_intval) { if (this.parent != null) { if (this.getFocused() && !this.getSelected()) { return this; } const flatTree = this.getContainerRoot().flatList(); let idx = flatTree.indexOf(this); if (idx > 0) { idx = Math.max(1, idx - _intval); return flatTree[idx]; } } return this; }; getNext(_intval) { if (this.parent != null) { if (this.getFocused() && !this.getSelected()) { return this; } const flatTree = this.getContainerRoot().flatList(true); let idx = flatTree.indexOf(this); if (idx < flatTree.length - 1) { idx = Math.min(flatTree.length - 1, idx + _intval); return flatTree[idx]; } } return this; }; /** * Returns whether the object is currently selected. */ getSelected() { return egwBitIsSet(this.getState(), EGW_AO_STATE_SELECTED); }; /** * Returns whether the object is currently focused. */ getFocused() { return egwBitIsSet(this.getState(), EGW_AO_STATE_FOCUSED); }; /** * Returns whether the object currently is visible - visible means, that the * AOI has a dom node and is visible. */ getVisible() { return egwBitIsSet(this.getState(), EGW_AO_STATE_VISIBLE); }; /** * Returns the complete state of the object. */ getState() { return this.iface.getState(); }; /** * Sets the focus of the element. The formerly focused element in the tree will * be de-focused. * * @param {boolean} _focused - whether to remove or set the focus. Defaults to true */ setFocused(_focused) { if (typeof _focused == "undefined") _focused = true; const state = this.iface.getState(); if (egwBitIsSet(state, EGW_AO_STATE_FOCUSED) != _focused) { // Un-focus the currently focused object const currentlyFocused = this.getFocusedObject(); if (currentlyFocused && currentlyFocused != this) { currentlyFocused.setFocused(false); } this.iface.setState(egwSetBit(state, EGW_AO_STATE_FOCUSED, _focused)); if (this.parent) { this.parent.updateFocusedChild(this, _focused); } } if (this.focusedChild != null && _focused == false) { this.focusedChild.setFocused(false); } }; /** * Sets the selected state of the element. * * @param {boolean} _selected * @TODO Callback */ setSelected(_selected) { const state = this.iface.getState(); if ((egwBitIsSet(state, EGW_AO_STATE_SELECTED) != _selected) && egwBitIsSet(state, EGW_AO_STATE_VISIBLE)) { this.iface.setState(egwSetBit(state, EGW_AO_STATE_SELECTED, _selected)); if (this.parent) { this.parent.updateSelectedChildren(this, _selected || this.selectedChildren.length > 0); } } }; /** * Sets the selected state of all elements, including children * * @param {boolean} _selected * @param {boolean} _informParent */ setAllSelected(_selected, _informParent = true) { const state = this.iface.getState(); // Update this element if (egwBitIsSet(state, EGW_AO_STATE_SELECTED) != _selected) { this.iface.setState(egwSetBit(state, EGW_AO_STATE_SELECTED, _selected)); if (_informParent && this.parent) { this.parent.updateSelectedChildren(this, _selected); } if (this.parent?.data && this.parent?.data?.keyboard_select) { this.parent.data.keyboard_select = false; } } // Update the children if they should be selected or if they should be // deselected and there are selected children. if (_selected || this.selectedChildren.length > 0) { for (let i = 0; i < this.children.length; i++) { this.children[i].setAllSelected(_selected, false); } } // Copy the selected children list this.selectedChildren = []; if (_selected) { for (let i = 0; i < this.children.length; i++) { this.selectedChildren.push(this.children[i]); } } // Call the setSelectedCallback egwQueueCallback(this.setSelectedCallback, [], this, "setSelectedCallback"); }; /** * Updates the selectedChildren array each actionObject has in order to determine * all selected children in a very fast manner. * * @param {(string|egwActionObject} _child * @param {boolean} _selected * @todo Has also to be updated, if an child is added/removed! */ updateSelectedChildren(_child, _selected) { const id: number = this.selectedChildren.indexOf(_child); // TODO Replace by binary search, insert children sorted by index! const wasEmpty: boolean = this.selectedChildren.length == 0; // Add or remove the given child from the selectedChildren list if (_selected && id == -1) { this.selectedChildren.push(_child); } else if (!_selected && id != -1) { this.selectedChildren.splice(id, 1); } // If the emptiness of the selectedChildren array has changed, update the // parent selected children array. if (wasEmpty != (this.selectedChildren.length == 0) && this.parent) { this.parent.updateSelectedChildren(this, wasEmpty); } // Call the setSelectedCallback egwQueueCallback(this.setSelectedCallback, this.getContainerRoot().getSelectedObjects(), this, "setSelectedCallback"); }; /** * Updates the focusedChild up to the container boundary. * * @param {(string|egwActionObject} _child * @param {boolean} _focused */ updateFocusedChild(_child: EgwActionObject, _focused: boolean) { if (_focused) { this.focusedChild = _child; } else { if (this.focusedChild == _child) { this.focusedChild = null; } } if (this.parent /*&& !egwBitIsSet(this.flags, EGW_AO_FLAG_IS_CONTAINER)*/) { this.parent.updateFocusedChild(_child, _focused); } }; /** * Updates the actionLinks of the given ActionObject. * * @param {array} _actionLinks contains the information about the actionLinks which * should be updated as an array of objects. Example * [ * { * "actionId": "file_delete", * "enabled": true * } * ] * string[] or {actionID:string,enabled:boolean}[] * If an supplied link doesn't exist yet, it will be created (if _doCreate is true) * and added to the list. Otherwise, the information will just be updated. * @param {boolean} _recursive If true, the settings will be applied to all child * object (default false) * @param {boolean} _doCreate If true, not yet existing links will be created (default true) */ updateActionLinks(_actionLinks: string[] | { actionId: string, enabled: boolean }[], _recursive: boolean = false, _doCreate: boolean = true) { for (let elem of _actionLinks) { // Allow single strings for simple action links. if (typeof elem == "string") { elem = { actionId: elem, enabled: true }; } if (typeof elem.actionId != "undefined" && elem.actionId) { //Get the action link object, if it doesn't exist yet, create it let actionLink = this.getActionLink(elem.actionId); if (!actionLink && _doCreate) { actionLink = new EgwActionLink(this.manager); this.actionLinks.push(actionLink); } //Set the supplied data if (actionLink) { actionLink.updateLink(elem); } } } if (_recursive) { for (let i = 0; i < this.children.length; i++) { this.children[i].updateActionLinks(_actionLinks, true, _doCreate); } } if (this.getVisible() && this.iface != null) { this.registerActions(); } }; /** * Reconnects the actions. */ _reconnectCallback() { this.registeredImpls = []; this.registerActions(); }; /** * Registers the action implementations inside the DOM-Tree. */ registerActions() { const groups = this.getActionImplementationGroups(); for (const group in groups) { // Get the action implementation for each group if (typeof window._egwActionClasses[group] != "undefined" && window._egwActionClasses[group].implementation && this.iface) { const impl = window._egwActionClasses[group].implementation(); if (this.registeredImpls.indexOf(impl) == -1) { // Register a handler for that action with the iface of that object, // the callback and this object as context for the callback if (impl.registerAction(this.iface, this.executeActionImplementation, this)) { this.registeredImpls.push(impl); } } } } }; /** * Unregisters all action implementations registered to this element */ unregisterActions() { while (this.registeredImpls.length > 0) { const impl = this.registeredImpls.pop(); if (this.iface) { impl.unregisterAction(this.iface); } } }; protected triggerCallback(): boolean { if (this.onBeforeTrigger) { return this.onBeforeTrigger() } return true; } makeVisible() { this.iface.makeVisible(); }; /** * Executes the action implementation which is associated to the given action type. * * @param {object} _implContext is data which should be delivered to the action implementation. * E.g. in case of the popup action implementation, the x and y coordinates where the * menu should open, and contextmenu event are transmitted. * @param {string} _implType is the action type for which the implementation should be * executed. * @param {number} _execType specifies in which context the execution should take place. * defaults to EGW_AO_EXEC_SELECTED */ executeActionImplementation(_implContext, _implType, _execType) { if (typeof _execType == "undefined") { _execType = EGW_AO_EXEC_SELECTED; } if (typeof _implType == "string") { _implType = window._egwActionClasses[_implType].implementation(); } if (typeof _implType == "object" && _implType) { let selectedActions; if (_execType == EGW_AO_EXEC_SELECTED) { if (!(egwBitIsSet(EGW_AO_FLAG_IS_CONTAINER, this.flags))) { this.forceSelection(); } selectedActions = this.getSelectedLinks(_implType.type); } else if (_execType == EGW_AO_EXEC_THIS) { selectedActions = this._getLinks([this], _implType.type); } if (selectedActions.selected.length > 0 && egwObjectLength(selectedActions.links) > 0) { return _implType.executeImplementation(_implContext, selectedActions.selected, selectedActions.links); } } return false; }; /** * Forces the object to be inside the currently selected objects. If this is * not the case, the object will select itself and deselect all other objects. */ forceSelection() { const selected = this.getContainerRoot().getSelectedObjects(); // Check whether this object is in the list const thisInList: boolean = selected.indexOf(this) != -1; // If not, select it if (!thisInList) { this.getContainerRoot().setAllSelected(false); this.setSelected(true); } this.setFocused(true); }; /** * Returns all selected objects, and all action links of those objects, which are * of the given implementation type, actionLink properties such as * "enabled" and "visible" are accumulated. * * Objects have the chance to change their action links or to deselect themselves * in the onBeforeTrigger event, which is evaluated by the triggerCallback function. * * @param _actionType is the action type for which the actionLinks should be collected. * @returns object An object which contains a "links" and a "selected" section with * an array of links/selected objects- */ getSelectedLinks(_actionType) { // Get all objects in this container which are currently selected const selected = this.getContainerRoot().getSelectedObjects(); return this._getLinks(selected, _actionType); }; /** * * @param {array} _objs * @param {string} _actionType * @return {object} with attributes "selected" and "links" */ _getLinks(_objs, _actionType) { const actionLinks:any = {}; const testedSelected = []; const test = function (olink,obj) { // Test whether the action type is of the given implementation type if (olink.actionObj.type == _actionType) { if (typeof actionLinks[olink.actionId] == "undefined") { actionLinks[olink.actionId] = { "actionObj": olink.actionObj, "enabled": (testedSelected.length == 1), "visible": false, "cnt": 0 }; } // Accumulate the action link properties const llink = actionLinks[olink.actionId]; llink.enabled = llink.enabled && olink.actionObj.enabled.exec(olink.actionObj, _objs, obj) && olink.enabled && olink.visible; llink.visible = (llink.visible || olink.visible); llink.cnt++; // Add in children, so they can get checked for visible / enabled if (olink.actionObj && olink.actionObj.children.length > 0) { for (let j = 0; j < olink.actionObj.children.length; j++) { const child = olink.actionObj.children[j]; test({ actionObj: child, actionId: child.id, enabled: olink.enabled, visible: olink.visible },obj); } } } }; for (const obj of _objs) { if (!egwBitIsSet(obj.flags, EGW_AO_FLAG_IS_CONTAINER) && obj.triggerCallback()) { testedSelected.push(obj); obj.actionLinks.forEach(item => { test(item,obj); //object link }); } } // Check whether all objects supported the action for (let k in actionLinks) { actionLinks[k].enabled = actionLinks[k].enabled && (actionLinks[k].cnt >= testedSelected.length) && ((actionLinks[k].actionObj.allowOnMultiple === true) || (actionLinks[k].actionObj.allowOnMultiple == "only" && _objs.length > 1) || (actionLinks[k].actionObj.allowOnMultiple == false && _objs.length === 1) || (typeof actionLinks[k].actionObj.allowOnMultiple === 'number' && _objs.length == actionLinks[k].actionObj.allowOnMultiple)); if (!window.egwIsMobile()) actionLinks[k].actionObj.hideOnMobile = false; actionLinks[k].visible = actionLinks[k].visible && !actionLinks[k].actionObj.hideOnMobile && (actionLinks[k].enabled || !actionLinks[k].actionObj.hideOnDisabled); } // Return an object which contains the accumulated actionLinks and all selected // objects. return { "selected": testedSelected, "links": actionLinks }; }; /** * Returns the action link, which contains the association to the action with * the given actionId. * * @param {string} _actionId name of the action associated to the link */ getActionLink(_actionId: string) { for (let i = 0; i < this.actionLinks.length; i++) { if (this.actionLinks[i].actionObj?.id == _actionId) { return this.actionLinks[i]; } } return null; }; /** * Returns all actions associated to the object tree, grouped by type. * * @param {function} _test gets an egwActionObject and should return, whether the * actions of this object are added to the result. Defaults to an "always true" * function. * @param {object} _groups is an internally used parameter, may be omitted. */ getActionImplementationGroups(_test?, _groups?) { // If the _groups parameter hasn't been given preset it to an empty object // (associative array). if (typeof _groups == "undefined") _groups = {}; if (typeof _test == "undefined") _test = function (_obj) { return true; }; this.actionLinks.forEach(item => { const action = item.actionObj; if (typeof action != "undefined" && _test(this)) { if (typeof _groups[action.type] == "undefined") { _groups[action.type] = []; } _groups[action.type].push({ "object": this, "link": item }); } }); // Recursively add the actions of the children to the result (as _groups is // an object, only the reference is passed). this.children.forEach(item => { item.getActionImplementationGroups(_test, _groups); }); return _groups; }; /** * Check if user tries to get dragOut action * * keys for dragOut: * -Mac: Command + Shift * -Others: Alt + Shift * * @param {event} _event * @return {boolean} return true if Alt+Shift keys and left mouse click are pressed, otherwise false */ isDragOut(_event) { return (_event.altKey || _event.metaKey) && _event.shiftKey && _event.which == 1; }; /** * Check if user tries to get selection action * * Keys for selection: * -Mac: Command key * -Others: Ctrl key * * @param {type} _event * @returns {Boolean} return true if left mouse click and Ctrl/Alt key are pressed, otherwise false */ isSelection(_event) { return !(_event.shiftKey) && _event.which == 1 && (_event.metaKey || _event.ctrlKey || _event.altKey); }; }