diff --git a/api/js/egw_action/EgwAction.ts b/api/js/egw_action/EgwAction.ts index a516d79aff..264116bfe6 100644 --- a/api/js/egw_action/EgwAction.ts +++ b/api/js/egw_action/EgwAction.ts @@ -134,9 +134,10 @@ export class EgwAction { * @param {string} _iconUrl * @param {(string|function)} _onExecute * @param {boolean} _allowOnMultiple - * @returns {EgwAction} + * @returns EgwAction **/ - constructor(_parent: EgwAction, _id: string, _caption: string = "", _iconUrl: string = "", _onExecute: string | Function = null, _allowOnMultiple: boolean = true) { + 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!"; } @@ -294,9 +295,11 @@ export class EgwAction { 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); - } + } else //elem.icon and elem.iconUrl is still undefined + { //if there is no icon and none can be found remove icon tag from the object delete elem.icon; + } } // always add shortcut for delete diff --git a/api/js/egw_action/EgwPopupActionImplementation.ts b/api/js/egw_action/EgwPopupActionImplementation.ts index 13f5098828..465948316a 100644 --- a/api/js/egw_action/EgwPopupActionImplementation.ts +++ b/api/js/egw_action/EgwPopupActionImplementation.ts @@ -17,20 +17,46 @@ import {EgwActionImplementation} from "./EgwActionImplementation"; import {EgwActionObject} from "./EgwActionObject"; import {EgwPopupAction} from "./EgwPopupAction"; import {egw} from "../jsapi/egw_global"; +import {Et2Tree} from "../etemplate/Et2Tree/Et2Tree"; +import {FindActionTarget} from "../etemplate/FindActionTarget"; export class EgwPopupActionImplementation implements EgwActionImplementation { type = "popup"; auto_paste = true; + parent?: FindActionTarget //currently only implemented by Et2Tree registerAction = (_aoi, _callback, _context) => { + const parent = _aoi.tree; // maybe expand this to aoi.?? for other actionObjectInterfaces + let isNew = parent?.findActionTarget != null const node = _aoi.getDOMNode(); + if (node == this.parent) return true //Event Listener already bound on parent + if (isNew) + { + if (this.parent && this.parent == parent) + { + return true // already added Event Listener on parent no need to register on children + } else + { - if (node) { - this._registerDefault(node, _callback, _context); - this._registerContext(node, _callback, _context); - return true; + this.parent = parent // this only exists for the EgwDragDropShoelaceTree ActionObjectInterface atm + + //if a parent is available the context menu Event-listener will only be bound once on the parent + this._registerDefault(parent, _callback, _context); + this._registerContext(parent, _callback, _context); + + return true; + } + } else + { + + if (node) + { + this._registerDefault(node, _callback, _context); + this._registerContext(node, _callback, _context); + return true; + } + return false; } - return false; }; unregisterAction = function (_aoi) { @@ -94,12 +120,24 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { */ private _registerDefault = (_node, _callback, _context)=> { const defaultHandler = (e)=> { + const x = _node + //use different node and context for callback if event happens on parent + let nodeToUse; + let contextToUse; + if (x.findActionTarget) + { + const y = x.findActionTarget(e); + nodeToUse = y?.target; + contextToUse = y?.action; + e.originalEvent = e; + + } //allow bubbling of the expand folder event //do not stop bubbling of events if the event is supposed to be handled by the et2-tree - if (window.egwIsMobile() && e.currentTarget.tagName == "SL-TREE-ITEM") return true; + if (window.egwIsMobile() && (nodeToUse || e.currentTarget).tagName == "SL-TREE-ITEM") return true; // a tag should be handled by default event // Prevent bubbling bound event on tag, on touch devices - if (window.egwIsMobile() && e.target.tagName == "A") return true; + if (window.egwIsMobile() && (nodeToUse || e.target).tagName == "A") return true; if (typeof document["selection"] != "undefined" && typeof document["selection"].empty != "undefined") { @@ -109,9 +147,13 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { sel.removeAllRanges(); } - if (!(_context.manager.getActionsByAttr('singleClick', true).length > 0 && - e.target.classList.contains('et2_clickable'))) { - _callback.call(_context, "default", this); + if (! + ((contextToUse || _context).manager.getActionsByAttr('singleClick', true).length > 0 && + (nodeToUse || e.target).classList.contains('et2_clickable') + ) + ) + { + _callback.call(contextToUse || _context, "default", this); } // Stop action from bubbling up to parents @@ -240,8 +282,19 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { * @returns {boolean} */ private _registerContext = (_node, _callback, _context) => { - const contextHandler = (e) => { + const contextHandler = (e) => { + const x = _node + //use different node and context for callback if event happens on parent + let nodeToUse; + let contextToUse; + if (x.findActionTarget) + { + const y = x.findActionTarget(e); + nodeToUse = y?.target; + contextToUse = y?.action; + e.originalEvent = e; + } //Obtain the event object, this should not happen at any point if (!e) { e = window.event; @@ -250,25 +303,35 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { // Close any open tooltip so they don't get in the way egw(window).tooltipCancel(); - if (_egw_active_menu) { + if (_egw_active_menu) + { _egw_active_menu.hide(); } else if (!e.ctrlKey && e.which == 3 || e.which === 0 || e.type === 'tapandhold') // tap event indicates by 0 { const _xy = this._getPageXY(e); - const _implContext = {event: e, posx: _xy.posx, posy: _xy.posy}; - _callback.call(_context, _implContext, this); + const _implContext = { + event: e, posx: _xy.posx, + posy: _xy.posy, + innerText: nodeToUse.innerText || _node.innerText,//nodeToUse only exists on widgets that define findActionTarget + target: nodeToUse.target || _node, + }; + _callback.call(contextToUse || _context, _implContext, this); } e.cancelBubble = !e.ctrlKey || e.which == 1; if (e.stopPropagation && e.cancelBubble) { e.stopPropagation(); + e.preventDefault() } return !e.cancelBubble; }; // Safari still needs the taphold to trigger contextmenu // Chrome has default event on touch and hold which acts like right click this._handleTapHold(_node, contextHandler); - if (!window.egwIsMobile()) jQuery(_node).on('contextmenu', contextHandler); + if (!window.egwIsMobile()) + { + _node.addEventListener('contextmenu', contextHandler); + } }; /** @@ -512,7 +575,7 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { offset: {top: 0, left: 0} }; if (this._context.event) { - const event = this._context.event.originalEvent; + const event = this._context.event.originalEvent || this._context.event; ui.position = {top: event.pageY, left: event.pageX}; ui.offset = {top: event.offsetY, left: event.offsetX}; } @@ -598,9 +661,9 @@ export class EgwPopupActionImplementation implements EgwActionImplementation { } let os_clipboard_caption = ""; if (this._context.event) { - os_clipboard_caption = this._context.event.originalEvent.target.innerText.trim(); + os_clipboard_caption = this._context.innerText.trim(); clipboard_action.set_caption(window.egw.lang('Copy "%1"', os_clipboard_caption.length > 20 ? os_clipboard_caption.substring(0, 20) + '...' : os_clipboard_caption)); - clipboard_action.data.target = this._context.event.originalEvent.target; + clipboard_action.data.target = this._context.target; } jQuery(clipboard_action.data.target).off('copy').on('copy', function (event) { try { diff --git a/api/js/etemplate/Et2Tree/Et2Tree.ts b/api/js/etemplate/Et2Tree/Et2Tree.ts index 28f557c503..b9e00250e6 100644 --- a/api/js/etemplate/Et2Tree/Et2Tree.ts +++ b/api/js/etemplate/Et2Tree/Et2Tree.ts @@ -12,6 +12,7 @@ import {et2_action_object_impl} from "../et2_core_DOMWidget"; import {EgwActionObject} from "../../egw_action/EgwActionObject"; import {EgwAction} from "../../egw_action/EgwAction"; import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree"; +import {FindActionTarget} from "../FindActionTarget"; export type TreeItemData = SelectOption & { focused?: boolean; @@ -73,7 +74,7 @@ export const composedPathContains = (_ev: any, tag?: string, className?: string) * //TODO add for other events * @since 23.1.x */ -export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) +export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements FindActionTarget { //does not work because it would need to be run on the shadow root //@query("sl-tree-item[selected]") selected: SlTreeItem; @@ -131,6 +132,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) selectedNodes: SlTreeItem[] private _actionManager: EgwAction; + widget_object: EgwActionObject; private get _tree() { return this.shadowRoot.querySelector('sl-tree') ?? null}; @@ -462,7 +464,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) return this._currentSlTreeItem } - getDomNode(_id): SlTreeItem|null + getDomNode(_id: string): SlTreeItem | null { return this.shadowRoot.querySelector('sl-tree-item[id="' + _id.replace(/"/g, '\\"') + '"'); } @@ -576,7 +578,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) } /** - * getTreeNodeOpenItems TODO + * getTreeNodeOpenItems * * @param {string} _nodeID the nodeID where to start from (initial node) 0 means for all items * @param {string} mode the mode to run in: "forced" fakes the initial node openState to be open @@ -584,9 +586,6 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) */ getTreeNodeOpenItems(_nodeID: string | 0, mode?: string) { - - - //let z:string[] = this.input.getSubItems(_nodeID).split(this.input.dlmtr); let subItems = (_nodeID == 0) ? this._selectOptions.map(option => this.getDomNode(option.id)) ://NodeID == 0 means that we want all tree Items @@ -595,7 +594,6 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) let PoS: 0 | 1 | -1; let rv: string[]; let returnValue = (_nodeID == 0) ? [] : [_nodeID]; // do not keep 0 in the return value... - // it is not needed and only throws a php warning later TODO check with ralf what happens in mail_ui.inc.php with ajax_setFolderStatus let modetorun = "none"; if (mode) { @@ -817,6 +815,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) const parentNode = selectOption ?? this.getNode(selectOption.id) ?? this.optionSearch(selectOption.id, this._selectOptions, 'id', 'item'); parentNode.item = [...result.item] this.requestUpdate("_selectOptions") + this._link_actions(this.actions) }) } @@ -933,7 +932,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) { this.onopenend(event.detail.id, this, -1) } - this._link_actions(this.actions) + } } @@ -976,12 +975,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) } // Get the top level element for the tree let objectManager = egw_getAppObjectManager(true); - let widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) + this.widget_object = objectManager.getObjectById(this.id); + if (this.widget_object == null) { // Add a new container to the object manager which will hold the widget // objects - widget_object = objectManager.insertObject(false, new EgwActionObject( + this.widget_object = objectManager.insertObject(false, new EgwActionObject( //@ts-ignore this.id, objectManager, (new et2_action_object_impl(this, this)).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager @@ -989,12 +988,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) } else { // @ts-ignore - widget_object.setAOI((new et2_action_object_impl(this, this)).getAOI()); + this.widget_object.setAOI((new et2_action_object_impl(this, this)).getAOI()); } // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); + this.widget_object.clear(); + this.widget_object.unregisterActions(); // Go over the widget & add links - this is where we decide which actions are // 'allowed' for this widget at this time @@ -1006,7 +1005,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) // Iterate over the options (leaves) and add action to each one let apply_actions = function (treeObj: EgwActionObject, option: TreeItemData) { // Add a new action object to the object manager - let id = option.value ?? (typeof option.value == 'number' ? String(option.id) : option.id); + let id = option.value ?? (typeof option.id == 'number' ? String(option.id) : option.id); // @ts-ignore let obj : EgwActionObject = treeObj.addObject(id, new EgwDragDropShoelaceTree(self, id)); obj.updateActionLinks(action_links); @@ -1020,12 +1019,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) for (const selectOption of this._selectOptions) { - apply_actions.call(this, widget_object, selectOption) + apply_actions.call(this, this.widget_object, selectOption) } } - widget_object.updateActionLinks(action_links); + this.widget_object.updateActionLinks(action_links); } /** @@ -1116,6 +1115,23 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) } } } + + /** + * returns the closest SlItem to the click position, and the corresponding EgwActionObject + * @param _event the click event + * @returns { target:SlTreeItem, action:EgwActionObject } + */ + findActionTarget(_event): { target: SlTreeItem, action: EgwActionObject } + { + let e = _event.composedPath ? _event : _event.originalEvent; + let target = e.composedPath().find(element => { + return element.tagName == "SL-TREE-ITEM" + }) + let action: EgwActionObject = this.widget_object.children.find(elem => { + return elem.id == target.id + }) + return {target: target, action: action} + } } customElements.define("et2-tree", Et2Tree); \ No newline at end of file diff --git a/api/js/etemplate/FindActionTarget.ts b/api/js/etemplate/FindActionTarget.ts new file mode 100644 index 0000000000..9c5b2ece68 --- /dev/null +++ b/api/js/etemplate/FindActionTarget.ts @@ -0,0 +1,11 @@ +import {EgwActionObject} from "../egw_action/EgwActionObject"; + +export interface FindActionTarget +{ + /** + * returns the closest Item to the click position, and the corresponding EgwActionObject + * @param _event the click event + * @returns { target:HTMLElement, action:EgwActionObject } + */ + findActionTarget(_event): { target: HTMLElement, action: EgwActionObject }; +} \ No newline at end of file