Et2Tree now only binds on eventhandler for contextmenu and one for default instead of binding one for every item

-- EgwPopupActionImplementation now only binds one Handler iff FindActionTarget is implemented and actionObjectInterface has attribute tree set. This is only the case for EgwDragDropShoelaceTree
This commit is contained in:
milan 2024-07-25 15:37:28 +02:00
parent d5ffc615af
commit 6271f71a12
4 changed files with 131 additions and 38 deletions

View File

@ -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

View File

@ -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 <a> 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 {

View File

@ -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);

View File

@ -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 };
}