mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-22 07:53:39 +01:00
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:
parent
d5ffc615af
commit
6271f71a12
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
11
api/js/etemplate/FindActionTarget.ts
Normal file
11
api/js/etemplate/FindActionTarget.ts
Normal 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 };
|
||||
}
|
Loading…
Reference in New Issue
Block a user