egroupware_official/api/js/egw_action/EgwAction.ts
ralf ff4c86f4f5 fix not translated context menu
window.egw with a reference to the main window in a popup, egw(_app, window) gives one with correct window context / translation in a popup (_app does not matter for translations, they are requested to be loaded from server-side)

(cherry picked from commit 40a4d38776)
2023-09-13 10:40:32 +02:00

720 lines
26 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 {egwActionStoreJSON, EgwFnct} from "./egw_action_common";
import {IegwAppLocal} from "../jsapi/egw_global";
import {egw_getObjectManager} from "./egw_action";
export class EgwAction {
public readonly id: string;
private caption: string;
group: number;
order: number;
public set_caption(_value) {
this.caption = _value;
}
private iconUrl: string;
public set_iconUrl(_value) {
this.iconUrl = _value;
}
allowOnMultiple: boolean | string | number;
/**
* The allowOnMultiple property may be true, false, "only" (> 1) or number of select, e.g. 2
*
* @param {(boolean|string|number)} _value
*/
public set_allowOnMultiple(_value: boolean | string | number) {
this.allowOnMultiple = _value
}
public readonly enabled: EgwFnct;
public set_enabled(_value) {
this.enabled.setValue(_value);
}
public hideOnDisabled = false;
public data: any = {}; // Data which can be freely assigned to the action
/**
* @deprecated just set the data parameter with '=' sign to use its setter
* @param _value
*/
public set_data(_value) {
this.data = _value
}
type = "default"; //All derived classes have to override this!
canHaveChildren: boolean | string[] = false; //Has to be overwritten by inheriting action classes
// this is not bool all the time. Can be ['popup'] e.g. List of egwActionClasses that are allowed to have children?
readonly parent: EgwAction;
children: EgwAction[] = []; //i guess
private readonly onExecute = new EgwFnct(this, null, []);
/**
* Set to either a confirmation prompt, or TRUE to indicate that this action
* cares about large selections and to ask the confirmation prompt(s)
*
* --set in egw_action_popup--
* @param {String|Boolean} _value
*/
public confirm_mass_selection: string | boolean = undefined
/**
* The set_onExecute function is the setter function for the onExecute event of
* the EgwAction object. There are three possible types the passed "_value" may
* take:
* 1. _value may be a string with the word "javaScript:" prefixed. The function
* which is specified behind the colon and which has to be in the global scope
* will be executed.
* 2. _value may be a boolean, which specifies whether the external onExecute handler
* (passed as "_handler" in the constructor) will be used.
* 3. _value may be a JS function which will then be called.
* In all possible situation, the called function will get the following parameters:
* 1. A reference to this action
* 2. The senders, an array of all objects (JS)/object ids (PHP) which evoked the event
*
* @param {(string|function|boolean)} _value
*/
public set_onExecute(_value) {
this.onExecute.setValue(_value)
}
public hideOnMobile = false;
public disableIfNoEPL = false;
/**
* Default icons for given id
*/
public defaultIcons = {
view: 'view',
edit: 'edit',
open: 'edit', // does edit if possible, otherwise view
add: 'new',
new: 'new',
delete: 'delete',
cat: 'attach', // add as category icon to api
document: 'etemplate/merge',
print: 'print',
copy: 'copy',
move: 'move',
cut: 'cut',
paste: 'editpaste',
save: 'save',
apply: 'apply',
cancel: 'cancel',
continue: 'continue',
next: 'continue',
finish: 'finish',
back: 'back',
previous: 'back',
close: 'close'
};
/**
* Constructor for EgwAction object
*
* @param {EgwAction} _parent
* @param {string} _id
* @param {string} _caption
* @param {string} _iconUrl
* @param {(string|function)} _onExecute
* @param {boolean} _allowOnMultiple
* @returns {EgwAction}
**/
constructor(_parent: EgwAction, _id: string, _caption: string = "", _iconUrl: string = "", _onExecute: string | Function = null, _allowOnMultiple: boolean = true) {
if (_parent && (typeof _id != "string" || !_id) && _parent.type !== "actionManager") {
throw "EgwAction _id must be a non-empty string!";
}
this.parent = _parent;
this.id = _id;
this.caption = _caption;
this.iconUrl = _iconUrl;
if (_onExecute !== null) {
this.set_onExecute(_onExecute)
}
this.allowOnMultiple = _allowOnMultiple;
this.enabled = new EgwFnct(this, true);
}
/**
* Clears the element and removes it from the parent container
*/
public remove() {
// Remove all references to the child elements
this.children = [];
// Remove this element from the parent list
if (this.parent) {
const idx = this.parent.children.indexOf(this);
if (idx >= 0) {
this.parent.children.splice(idx, 1);
}
}
}
/**
* Searches for a specific action with the given id
*
* @param {(string|number)} _id ID of the action to find
* @param {number} [_search_depth=Infinite] How deep into existing action children
* to search.
*
* @return {(EgwAction|null)}
*/
public getActionById(_id: string, _search_depth: number = Number.MAX_VALUE): EgwAction {
// If the current action object has the given id, return this object
if (this.id == _id) {
return this;
}
// If this element is capable of having children, search those for the given
// action id
if (this.canHaveChildren) {
for (let i = 0; i < this.children.length && _search_depth > 0; i++) {
const elem = this.children[i].getActionById(_id, _search_depth - 1);
if (elem) {
return elem;
}
}
}
return null;
};
/**
* Searches for actions having an attribute with a certain value
*
* Example: actionManager.getActionsByAttr("checkbox", true) returns all checkbox actions
*
* @param {string} _attr attribute name
* @param _val attribute value
* @return array
*/
public getActionsByAttr(_attr: string | number, _val: any = undefined) {
let _actions = [];
// If the current action object has the given attr AND value, or no value was provided, return it
if (typeof this[_attr] != "undefined" && (this[_attr] === _val || typeof _val === "undefined" && this[_attr] !== null)) {
_actions.push(this);
}
// If this element is capable of having children, search those too
if (this.canHaveChildren) {
for (let i = 0; i < this.children.length; i++) {
_actions = _actions.concat(this.children[i].getActionsByAttr(_attr, _val));
}
}
return _actions;
};
/**
* Adds a new action to the child elements.
*
* @param {string} _type
* @param {string} _id
* @param {string} _caption
* @param {string} _iconUrl
* @param {(string|function)} _onExecute
* @param {boolean} _allowOnMultiple
*/
public addAction(_type: string, _id: string, _caption: string = "", _iconUrl: string = "", _onExecute: string | Function = null, _allowOnMultiple: boolean = true): EgwAction {
//Get the constructor for the given action type
if (!(_type in window._egwActionClasses)) {
//TODO doesn't default instead of popup make more sense here??
_type = "popup"
}
// Only allow adding new actions, if this action class allows it.
if (this.canHaveChildren) {
const constructor: any = window._egwActionClasses[_type]?.actionConstructor;
if (typeof constructor == "function") {
const action: EgwAction = new constructor(this, _id, _caption, _iconUrl, _onExecute, _allowOnMultiple);
this.children.push(action);
return action;
} else {
throw "Given action type not registered.";
}
} else {
throw "This action does not allow child elements!";
}
};
/**
* Updates the children of this element
*
* @param {object} _actions { id: action, ...}
* @param {string} _app defaults to egw_getAppname()
*/
public updateActions(_actions: any[] | Object, _app) {
if (this.canHaveChildren) {
if (typeof _app == "undefined") _app = window.egw(window).app_name()
/*
this is an egw Object as defined in egw_core.js
probably not because it changes on runtime
*/
const localEgw: IegwAppLocal = window.egw(_app, window);
//replaced jQuery calls
if (Array.isArray(_actions)) {
//_actions is now an object for sure
//happens in test website
_actions = {..._actions};
}
for (const i in _actions) {
let elem = _actions[i];
if (typeof elem == "string") {
//changes type of elem to Object {caption:string}
_actions[i] = elem = {caption: elem};
}
if (typeof elem == "object") // isn't this always true because of step above? Yes if elem was a string before
{
// use attr name as id, if none given
if (typeof elem.id != "string") elem.id = i;
// if no iconUrl given, check icon and default icons
if (typeof elem.iconUrl == "undefined") {
if (typeof elem.icon == "undefined") elem.icon = this.defaultIcons[elem.id]; // only works if default Icon is available
if (typeof elem.icon != "undefined") {
elem.iconUrl = localEgw.image(elem.icon);
}
//if there is no icon and none can be found remove icon tag from the object
delete elem.icon;
}
// always add shortcut for delete
if (elem.id == "delete" && typeof elem.shortcut == "undefined") {
elem.shortcut = {
keyCode: 46, shift: false, ctrl: false, alt: false, caption: localEgw.lang('Del')
};
}
// translate caption
if (elem.caption && (typeof elem.no_lang == "undefined" || !elem.no_lang)) {
elem.caption = localEgw.lang(elem.caption);
if (typeof elem.hint == "string") elem.hint = localEgw.lang(elem.hint);
}
delete elem.no_lang;
// translate confirm messages and place '?' at the end iff not there yet
for (const attr in {confirm: '', confirm_multiple: ''}) {
if (typeof elem[attr] == "string") {
elem[attr] = localEgw.lang(elem[attr]) + ((elem[attr].substr(-1) != '?') ? '?' : '');
}
}
// set certain enabled functions iff elem.enabled is not set so false
if (typeof elem.enabled == 'undefined' || elem.enabled === true) {
if (typeof elem.enableClass != "undefined") {
elem.enabled = this.enableClass;
} else if (typeof elem.disableClass != "undefined") {
elem.enabled = this.not_disableClass;
} else if (typeof elem.enableId != "undefined") {
elem.enabled = this.enableId;
}
}
//Check whether the action already exists, and if no, add it to the
//actions list
let action = this.getActionById(elem.id);
if (!action) {
//elem will be popup on default
if (typeof elem.type == "undefined") {
elem.type = "popup";
}
let constructor = null;
// Check whether the given type is inside the "canHaveChildren"
// array // here can have children is used as array where possible types of children are stored
if (this.canHaveChildren !== true && this.canHaveChildren.indexOf(elem.type) == -1) {
throw "This child type '" + elem.type + "' is not allowed!";
}
if (typeof window._egwActionClasses[elem.type] != "undefined") {
constructor = window._egwActionClasses[elem.type].actionConstructor;
} else {
throw "Given action type \"" + elem.type + "\" not registered, because type does not exist";
}
if (typeof constructor == "function" && constructor) action = new constructor(this, elem.id); else throw "Given action type \"" + elem.type + "\" not registered.";
this.children.push(action);
}
action.updateAction(elem);
// Add sub-actions to the action
if (elem.children) {
action.updateActions(elem.children, _app);
}
}
}
} else {
throw "This action element cannot have children!";
}
};
/**
* Callback to check if none of _senders rows has disableClass set
*
* @param _action EgwAction object, we use _action.data.disableClass to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if none has disableClass, false otherwise
*/
private not_disableClass(_action: EgwAction, _senders: any, _target: any) {
if (_target.iface.getDOMNode()) {
return !(_target.iface.getDOMNode()).classList.contains(_action.data.disableClass);
} else if (_target.id) {
// Checking on a something that doesn't have a DOM node, like a nm row
// that's not currently rendered
const data = window.egw.dataGetUIDdata(_target.id);
if (data && data.data && data.data.class) {
return -1 === data.data.class.split(' ').indexOf(_action.data.disableClass);
}
}
};
/**
* Callback to check if all of _senders rows have enableClass set
*
* @param _action EgwAction object, we use _action.data.enableClass to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if none has disableClass, false otherwise
*/
//TODO senders is never used in function body??
private enableClass(_action: EgwAction, _senders: any[], _target: any) {
if (typeof _target == 'undefined') {
return false;
} else if (_target.iface.getDOMNode()) {
return (_target.iface.getDOMNode()).classList.contains(_action.data.enableClass);
} else if (_target.id) {
// Checking on a something that doesn't have a DOM node, like a nm row
// that's not currently rendered. Not as good as an actual DOM node check
// since things can get missed, but better than nothing.
const data = window.egw.dataGetUIDdata(_target.id);
if (data && data.data && data.data.class) {
return -1 !== data.data.class.split(' ').indexOf(_action.data.enableClass);
}
}
};
/**
* Enable an _action, if it matches a given regular expression in _action.data.enableId
*
* @param _action EgwAction object, we use _action.data.enableId to check
* @param _senders array of egwActionObject objects
* @param _target egwActionObject object, gets called for every object in _senders
* @returns boolean true if _target.id matches _action.data.enableId
*/
private enableId(_action: EgwAction, _senders: any[], _target: any) {
if (typeof _action.data.enableId == 'string') {
_action.data.enableId = new RegExp(_action.data.enableId);
}
return _target.id.match(_action.data.enableId);
};
/**
* Applies the same onExecute handler to all actions which don't have an execute
* handler set.
*
* @param {(string|function)} _value
*/
public setDefaultExecute(_value: string | Function): void {
// Check whether the onExecute handler of this action should be set
if (this.type != "actionManager" && !this.onExecute.hasHandler()) {
this.onExecute.isDefault = true;
this.onExecute.setValue(_value);
}
// Apply the value to all children
if (this.canHaveChildren) {
for (const elem of this.children) {
elem.setDefaultExecute(_value);
}
}
};
/**
* Executes this action by using the method specified in the onExecute setter.
*
* @param {array} _senders array with references to the objects which caused the action
* @param {object} _target is an optional parameter which may represent e.g. a drag drop target
*/
execute(_senders, _target = null): any {
if (!this._check_confirm_mass_selections(_senders, _target)) {
return this._check_confirm(_senders, _target);
}
};
/**
* If this action needs to confirm mass selections (attribute confirm_mass_selection = true),
* check for any checkboxes that have a confirmation prompt (confirm_mass_selection is a string)
* and are unchecked. We then show the prompt, and set the checkbox to their answer.
*
* * This is only considered if there are more than 20 entries selected.
*
* * Only the first confirmation prompt / checkbox action will be used, others
* will be ignored.
*
* @param {type} _senders
* @param {type} _target
* @returns {Boolean}
*/
private _check_confirm_mass_selections(_senders, _target) {
const obj_manager: any = egw_getObjectManager(this.getManager().parent.id, false);
if (!obj_manager) {
return false;
}
// Action needs to care about mass selection - check for parent that cares too
let confirm_mass_needed = false;
let action: EgwAction = this;
while (action && action !== obj_manager.manager && !confirm_mass_needed) {
confirm_mass_needed = !!action.confirm_mass_selection;
action = action.parent;
}
if (!confirm_mass_needed) return false;
// Check for confirm mass selection checkboxes
const confirm_mass_selections = obj_manager.manager.getActionsByAttr("confirm_mass_selection");
confirm_mass_needed = _senders.length > 20;
//no longer needed because of '=>' notation
//const self = this;
// Find & show prompt
for (let i = 0; confirm_mass_needed && i < confirm_mass_selections.length; i++) {
const check = confirm_mass_selections[i];
if (check.checkbox === false || check.checked === true) {
continue
}
// Show the mass selection prompt
const msg = window.egw.lang(check.confirm_mass_selection, obj_manager.getAllSelected() ? window.egw.lang('all') : _senders.length);
const callback = (_button) => {
// YES = unchecked, NO = checked
check.set_checked(_button === window.Et2Dialog.NO_BUTTON);
if (_button !== window.Et2Dialog.CANCEL_BUTTON) {
this._check_confirm(_senders, _target);
}
};
window.Et2Dialog.show_dialog(callback, msg, this.data.hint, {}, window.Et2Dialog.BUTTONS_YES_NO_CANCEL, window.Et2Dialog.QUESTION_MESSAGE);
return true;
}
return false;
};
/**
* Check to see if action needs to be confirmed by user before we do it
*/
private _check_confirm(_senders, _target) {
// check if actions needs to be confirmed first
if (this.data && (this.data.confirm || this.data.confirm_multiple) &&
this.onExecute.functionToPerform != window.nm_action && typeof window.Et2Dialog != 'undefined') // let old eTemplate run its own confirmation from nextmatch_action.js
{
let msg = this.data.confirm || '';
if (_senders.length > 1) {
if (this.data.confirm_multiple) {
msg = this.data.confirm_multiple;
}
// check if we have all rows selected
const obj_manager = egw_getObjectManager(this.getManager().parent.id, false);
if (obj_manager && obj_manager.getAllSelected()) {
msg += "\n\n" + window.egw.lang('Attention: action will be applied to all rows, not only visible ones!');
}
}
//no longer needed because of '=>' notation
//var self = this;
if (msg.trim().length > 0) {
if (this.data.policy_confirmation && window.egw.app('policy')) {
import(window.egw.link('/policy/js/app.min.js')).then(() => {
if (typeof window.app.policy === 'undefined' || typeof window.app.policy.confirm === 'undefined') {
window.app.policy = new window.app.classes.policy();
}
window.app.policy.confirm(this, _senders, _target);
}
);
return;
}
window.Et2Dialog.show_dialog((_button) => {
if (_button == window.Et2Dialog.YES_BUTTON) {
// @ts-ignore
return this.onExecute.exec(this, _senders, _target);
}
}, msg, this.data.hint, {}, window.Et2Dialog.BUTTONS_YES_NO, window.Et2Dialog.QUESTION_MESSAGE);
return;
}
}
// @ts-ignore
return this.onExecute.exec(this, _senders, _target);
};
private updateAction(_data: Object) {
egwActionStoreJSON(_data, this, "data")
}
/**
* Returns the parent action manager
*/
getManager(): EgwAction {
if (this.type == "actionManager") {
return this;
} else if (this.parent) {
return this.parent.getManager();
} else {
return null;
}
}
/**
* The appendToGraph function generates an action tree which automatically contains
* all parent elements. If the appendToGraph function is called for a
*
* @param {not an array} _tree contains the tree structure - pass an object containing {root:Tree}??TODO
* the empty array "root" to this function {"root": []}. The result will be stored in
* this array.
* @param {boolean} _addChildren is used internally to prevent parent elements from
* adding their children automatically to the tree.
*/
public appendToTree(_tree: { root: Tree }, _addChildren: boolean = true) {
if (typeof _addChildren == "undefined") {
_addChildren = true;
}
// Preset some variables
const root: Tree = _tree.root;
let parentNode: TreeElem = null;
let node: TreeElem = {
"action": this, "children": []
};
if (this.parent && this.type != "actionManager") {
// Check whether the parent container has already been added to the tree
parentNode = _egwActionTreeFind(root, this.parent);
if (!parentNode) {
parentNode = this.parent.appendToTree(_tree, false);
}
// Check whether this element has already been added to the parent container
let added = false;
for (const child of parentNode.children) {
if (child.action == this) {
node = child;
added = true;
break;
}
}
if (!added) {
parentNode.children.push(node);
}
} else {
let added = false;
for (const treeElem of root) {
if (treeElem.action == this) {
node = treeElem;
added = true;
break;
}
}
if (!added) {
// Add this element to the root if it has no parent
root.push(node);
}
}
if (_addChildren) {
for (const child of this.children) {
child.appendToTree(_tree, true);
}
}
return node;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_hideOnDisabled(_value) {
this.hideOnDisabled = _value;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_hideOnMobile(_value) {
this.hideOnMobile = _value;
};
/**
* @deprecated directly set value instead
* @param _value
*/
set_disableIfNoEPL(_value) {
this.disableIfNoEPL = _value;
};
set_hint(hint: string) {
}
}
type TreeElem = { action: EgwAction, children: Tree }
type Tree = TreeElem[]
/**
* finds an egwAction in the given tree
* @param {Tree}_tree where to search
* @param {EgwAction}_elem elem to search
* @returns {TreeElem} the treeElement for corresponding _elem if found, null else
*/
function _egwActionTreeFind(_tree: Tree, _elem: EgwAction): TreeElem {
for (const current of _tree) {
if (current.action == _elem) {
return current;
}
if (typeof current.children != "undefined") {
const elem = _egwActionTreeFind(current.children, _elem);
if (elem) {
return elem;
}
}
}
return null;
}