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} _iconUrl
* @param {(string|function)} _onExecute * @param {(string|function)} _onExecute
* @param {boolean} _allowOnMultiple * @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") { if (_parent && (typeof _id != "string" || !_id) && _parent.type !== "actionManager") {
throw "EgwAction _id must be a non-empty string!"; throw "EgwAction _id must be a non-empty string!";
} }
@ -294,10 +295,12 @@ 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.icon = this.defaultIcons[elem.id]; // only works if default Icon is available
if (typeof elem.icon != "undefined") { if (typeof elem.icon != "undefined") {
elem.iconUrl = localEgw.image(elem.icon); 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 //if there is no icon and none can be found remove icon tag from the object
delete elem.icon; delete elem.icon;
} }
}
// always add shortcut for delete // always add shortcut for delete
if (elem.id == "delete" && typeof elem.shortcut == "undefined") { if (elem.id == "delete" && typeof elem.shortcut == "undefined") {

View File

@ -17,20 +17,46 @@ import {EgwActionImplementation} from "./EgwActionImplementation";
import {EgwActionObject} from "./EgwActionObject"; import {EgwActionObject} from "./EgwActionObject";
import {EgwPopupAction} from "./EgwPopupAction"; import {EgwPopupAction} from "./EgwPopupAction";
import {egw} from "../jsapi/egw_global"; import {egw} from "../jsapi/egw_global";
import {Et2Tree} from "../etemplate/Et2Tree/Et2Tree";
import {FindActionTarget} from "../etemplate/FindActionTarget";
export class EgwPopupActionImplementation implements EgwActionImplementation { export class EgwPopupActionImplementation implements EgwActionImplementation {
type = "popup"; type = "popup";
auto_paste = true; auto_paste = true;
parent?: FindActionTarget //currently only implemented by Et2Tree
registerAction = (_aoi, _callback, _context) => { 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(); 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.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._registerDefault(node, _callback, _context);
this._registerContext(node, _callback, _context); this._registerContext(node, _callback, _context);
return true; return true;
} }
return false; return false;
}
}; };
unregisterAction = function (_aoi) { unregisterAction = function (_aoi) {
@ -94,12 +120,24 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
*/ */
private _registerDefault = (_node, _callback, _context)=> { private _registerDefault = (_node, _callback, _context)=> {
const defaultHandler = (e)=> { 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 //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 //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 // a tag should be handled by default event
// Prevent bubbling bound event on <a> tag, on touch devices // 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") { if (typeof document["selection"] != "undefined" && typeof document["selection"].empty != "undefined") {
@ -109,9 +147,13 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
sel.removeAllRanges(); sel.removeAllRanges();
} }
if (!(_context.manager.getActionsByAttr('singleClick', true).length > 0 && if (!
e.target.classList.contains('et2_clickable'))) { ((contextToUse || _context).manager.getActionsByAttr('singleClick', true).length > 0 &&
_callback.call(_context, "default", this); (nodeToUse || e.target).classList.contains('et2_clickable')
)
)
{
_callback.call(contextToUse || _context, "default", this);
} }
// Stop action from bubbling up to parents // Stop action from bubbling up to parents
@ -240,8 +282,19 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
* @returns {boolean} * @returns {boolean}
*/ */
private _registerContext = (_node, _callback, _context) => { 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 //Obtain the event object, this should not happen at any point
if (!e) { if (!e) {
e = window.event; e = window.event;
@ -250,25 +303,35 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
// Close any open tooltip so they don't get in the way // Close any open tooltip so they don't get in the way
egw(window).tooltipCancel(); egw(window).tooltipCancel();
if (_egw_active_menu) { if (_egw_active_menu)
{
_egw_active_menu.hide(); _egw_active_menu.hide();
} else if (!e.ctrlKey && e.which == 3 || e.which === 0 || e.type === 'tapandhold') // tap event indicates by 0 } else if (!e.ctrlKey && e.which == 3 || e.which === 0 || e.type === 'tapandhold') // tap event indicates by 0
{ {
const _xy = this._getPageXY(e); const _xy = this._getPageXY(e);
const _implContext = {event: e, posx: _xy.posx, posy: _xy.posy}; const _implContext = {
_callback.call(_context, _implContext, this); 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; e.cancelBubble = !e.ctrlKey || e.which == 1;
if (e.stopPropagation && e.cancelBubble) { if (e.stopPropagation && e.cancelBubble) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault()
} }
return !e.cancelBubble; return !e.cancelBubble;
}; };
// Safari still needs the taphold to trigger contextmenu // Safari still needs the taphold to trigger contextmenu
// Chrome has default event on touch and hold which acts like right click // Chrome has default event on touch and hold which acts like right click
this._handleTapHold(_node, contextHandler); 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} offset: {top: 0, left: 0}
}; };
if (this._context.event) { 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.position = {top: event.pageY, left: event.pageX};
ui.offset = {top: event.offsetY, left: event.offsetX}; ui.offset = {top: event.offsetY, left: event.offsetX};
} }
@ -598,9 +661,9 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
} }
let os_clipboard_caption = ""; let os_clipboard_caption = "";
if (this._context.event) { 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.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) { jQuery(clipboard_action.data.target).off('copy').on('copy', function (event) {
try { try {

View File

@ -12,6 +12,7 @@ import {et2_action_object_impl} from "../et2_core_DOMWidget";
import {EgwActionObject} from "../../egw_action/EgwActionObject"; import {EgwActionObject} from "../../egw_action/EgwActionObject";
import {EgwAction} from "../../egw_action/EgwAction"; import {EgwAction} from "../../egw_action/EgwAction";
import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree"; import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree";
import {FindActionTarget} from "../FindActionTarget";
export type TreeItemData = SelectOption & { export type TreeItemData = SelectOption & {
focused?: boolean; focused?: boolean;
@ -73,7 +74,7 @@ export const composedPathContains = (_ev: any, tag?: string, className?: string)
* //TODO add for other events * //TODO add for other events
* @since 23.1.x * @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 //does not work because it would need to be run on the shadow root
//@query("sl-tree-item[selected]") selected: SlTreeItem; //@query("sl-tree-item[selected]") selected: SlTreeItem;
@ -131,6 +132,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
selectedNodes: SlTreeItem[] selectedNodes: SlTreeItem[]
private _actionManager: EgwAction; private _actionManager: EgwAction;
widget_object: EgwActionObject;
private get _tree() { return this.shadowRoot.querySelector('sl-tree') ?? null}; private get _tree() { return this.shadowRoot.querySelector('sl-tree') ?? null};
@ -462,7 +464,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
return this._currentSlTreeItem return this._currentSlTreeItem
} }
getDomNode(_id): SlTreeItem|null getDomNode(_id: string): SlTreeItem | null
{ {
return this.shadowRoot.querySelector('sl-tree-item[id="' + _id.replace(/"/g, '\\"') + '"'); 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} _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 * @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) getTreeNodeOpenItems(_nodeID: string | 0, mode?: string)
{ {
//let z:string[] = this.input.getSubItems(_nodeID).split(this.input.dlmtr);
let subItems = let subItems =
(_nodeID == 0) ? (_nodeID == 0) ?
this._selectOptions.map(option => this.getDomNode(option.id)) ://NodeID == 0 means that we want all tree Items 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 PoS: 0 | 1 | -1;
let rv: string[]; let rv: string[];
let returnValue = (_nodeID == 0) ? [] : [_nodeID]; // do not keep 0 in the return value... 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"; let modetorun = "none";
if (mode) 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'); const parentNode = selectOption ?? this.getNode(selectOption.id) ?? this.optionSearch(selectOption.id, this._selectOptions, 'id', 'item');
parentNode.item = [...result.item] parentNode.item = [...result.item]
this.requestUpdate("_selectOptions") 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.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 // Get the top level element for the tree
let objectManager = egw_getAppObjectManager(true); let objectManager = egw_getAppObjectManager(true);
let widget_object = objectManager.getObjectById(this.id); this.widget_object = objectManager.getObjectById(this.id);
if (widget_object == null) if (this.widget_object == null)
{ {
// Add a new container to the object manager which will hold the widget // Add a new container to the object manager which will hold the widget
// objects // objects
widget_object = objectManager.insertObject(false, new EgwActionObject( this.widget_object = objectManager.insertObject(false, new EgwActionObject(
//@ts-ignore //@ts-ignore
this.id, objectManager, (new et2_action_object_impl(this, this)).getAOI(), this.id, objectManager, (new et2_action_object_impl(this, this)).getAOI(),
this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager
@ -989,12 +988,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
} else } else
{ {
// @ts-ignore // @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 // Delete all old objects
widget_object.clear(); this.widget_object.clear();
widget_object.unregisterActions(); this.widget_object.unregisterActions();
// Go over the widget & add links - this is where we decide which actions are // Go over the widget & add links - this is where we decide which actions are
// 'allowed' for this widget at this time // '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 // Iterate over the options (leaves) and add action to each one
let apply_actions = function (treeObj: EgwActionObject, option: TreeItemData) { let apply_actions = function (treeObj: EgwActionObject, option: TreeItemData) {
// Add a new action object to the object manager // 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 // @ts-ignore
let obj : EgwActionObject = treeObj.addObject(id, new EgwDragDropShoelaceTree(self, id)); let obj : EgwActionObject = treeObj.addObject(id, new EgwDragDropShoelaceTree(self, id));
obj.updateActionLinks(action_links); obj.updateActionLinks(action_links);
@ -1020,12 +1019,12 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
for (const selectOption of this._selectOptions) 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); 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 };
}