mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 14:41:29 +01:00
0c2f211ada
- tree drop wouldn't work on newly added folders - tree drop actions sometimes targeted a parent leaf - flickering on drop hover
1135 lines
38 KiB
TypeScript
1135 lines
38 KiB
TypeScript
/**
|
|
* EGroupware egw_action framework - egw action framework
|
|
*
|
|
* @link https://www.egroupware.org
|
|
* @author Andreas Stöckel <as@stylite.de>
|
|
* @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 {
|
|
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
|
|
|
|
/**
|
|
* Some widgets handle DOM events for child objects, so we only bind one DOM event listener.
|
|
* Set this in the child action object, pointing to the parent that will listen for the DOM events.
|
|
* findActionTargetHandler must have an implementation of ActionTargetHandler
|
|
*/
|
|
public findActionTargetHandler : EgwActionObject
|
|
|
|
|
|
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=Number.MAX_VALUE):EgwActionObject {
|
|
if (this.id == _id) {
|
|
return this;
|
|
}
|
|
|
|
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);
|
|
};
|
|
|
|
}
|