Fix some tree / drag & drop issues

- tree drop wouldn't work on newly added folders
- tree drop actions sometimes targeted a parent leaf
- flickering on drop hover
This commit is contained in:
nathan 2024-07-31 09:52:24 -06:00
parent aadaa28f86
commit 0c2f211ada
5 changed files with 210 additions and 81 deletions

View File

@ -65,6 +65,13 @@ export class EgwActionObject {
private readonly onBeforeTrigger: Function = undefined
_context: any = undefined
/**
* Some widgets handle DOM events for child objects, so we only bind one DOM event listener.
* Set this in the child action object, pointing to the parent that will listen for the DOM events.
* findActionTargetHandler must have an implementation of ActionTargetHandler
*/
public findActionTargetHandler : EgwActionObject
constructor(_id: string, _parent, _interface:EgwActionObjectInterface, _manager?, _flags: number=0) {
if (typeof _manager == "undefined" && typeof _parent == "object" && _parent) _manager = _parent.manager;

View File

@ -7,19 +7,12 @@
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package egw_action
*/
import {EgwActionObjectInterface} from "./EgwActionObjectInterface";
import {egwActionObjectInterface} from "./egw_action";
import {Et2Tree} from "../etemplate/Et2Tree/Et2Tree";
import {
EGW_AI_DRAG_ENTER,
EGW_AI_DRAG_OUT,
EGW_AI_DRAG_OVER,
EGW_AO_STATE_FOCUSED,
EGW_AO_STATE_SELECTED
} from "./egw_action_constants";
import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT, EGW_AO_STATE_FOCUSED, EGW_AO_STATE_SELECTED} from "./egw_action_constants";
import {egwBitIsSet} from "./egw_action_common";
import {SlTreeItem} from "@shoelace-style/shoelace";
import {EgwActionObject} from "./EgwActionObject";
export const EXPAND_FOLDER_ON_DRAG_DROP_TIMEOUT = 1000
@ -28,12 +21,16 @@ export class EgwDragDropShoelaceTree extends egwActionObjectInterface{
node: SlTreeItem;
id: string;
tree: Et2Tree;
// Reference to the widget that's handling actions for us
public findActionTargetHandler : EgwActionObject;
constructor(_tree:Et2Tree, _itemId: string) {
super();
this.node = _tree.getDomNode(_itemId);
this.id = _itemId
this.tree = _tree
this.findActionTargetHandler = _tree.widget_object;
this.doGetDOMNode = function () {
return this.node;
}
@ -43,7 +40,7 @@ export class EgwDragDropShoelaceTree extends egwActionObjectInterface{
if (_event == EGW_AI_DRAG_ENTER)
{
this.node.classList.add("draggedOver");
this.node.classList.add("draggedOver", "drop-hover");
timeout = setTimeout(() => {
if (this.node.classList.contains("draggedOver"))
{
@ -53,7 +50,7 @@ export class EgwDragDropShoelaceTree extends egwActionObjectInterface{
}
if (_event == EGW_AI_DRAG_OUT)
{
(this.node).classList.remove("draggedOver");
(this.node).classList.remove("draggedOver", "drop-hover");
clearTimeout(timeout)
}
return true

View File

@ -12,6 +12,7 @@ import {EgwActionImplementation} from "./EgwActionImplementation";
import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT, EGW_AO_EXEC_THIS} from "./egw_action_constants";
import {egw_getObjectManager} from "./egw_action";
import {getPopupImplementation} from "./EgwPopupActionImplementation";
import {EgwActionObject} from "./EgwActionObject";
export class EgwDropActionImplementation implements EgwActionImplementation {
type: string = "drop";
@ -20,10 +21,27 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
private currentDropEl = null
registerAction: (_actionObjectInterface: any, _triggerCallback: Function, _context: object) => boolean = (_aoi, _callback, _context)=> {
const node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();
const self:EgwDropActionImplementation = this;
if (node) {
registerAction : (_actionObjectInterface : any, _triggerCallback : Function, _context : EgwActionObject) => boolean = (_aoi, _callback, _context) =>
{
let parentNode = null;
let parentAO = null;
let isNew = false;
let node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();
const self : EgwDropActionImplementation = this;
// Is there a parent that handles action targets?
if(typeof _context.findActionTargetHandler !== "undefined" && typeof _context.findActionTargetHandler?.iface?.getWidget == "function")
{
parentAO = _context.findActionTargetHandler;
parentNode = parentAO.iface.getWidget();
}
if(!_aoi.findActionTargetHandler && parentNode && typeof parentNode.findActionTarget == "function")
{
_aoi.findActionTargetHandler = parentNode;
}
if(node)
{
if(typeof _aoi.handlers == "undefined")
{
_aoi.handlers = {};
@ -46,7 +64,17 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
event.stopPropagation();
event.preventDefault();
self.currentDropEl = event.currentTarget;
if(_aoi.findActionTargetHandler && typeof _aoi.findActionTargetHandler.findActionTarget === "function")
{
// Bubbling up to parent
const parentData = _aoi.findActionTargetHandler.findActionTarget(event);
self.currentDropEl = parentData.target ?? event.currentTarget;
_aoi = parentData.action.iface ?? _aoi;
}
else
{
self.currentDropEl = event.currentTarget;
}
event.dataTransfer.dropEffect = 'link';
const data = {
@ -71,9 +99,16 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
// don't go further if the dragged element is no there (happens when a none et2 dragged element is being dragged)
if (!self.getTheDraggedDOM()) return;
let dropActionObject = _context;
// remove the hover class
this.classList.remove('drop-hover');
if(this.findActionTarget)
{
dropActionObject = this.findActionTarget(event).action ?? _context;
}
const helper = self.getHelperDOM();
let ui = self.getTheDraggedData();
ui.position = {top: event.clientY, left: event.clientX};
@ -82,7 +117,8 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
let data = JSON.parse(event.dataTransfer.getData('application/json'));
if (!self.isAccepted(data, _context, _callback,undefined) || self.isTheDraggedDOM(this)) {
if(!self.isAccepted(data, dropActionObject, _callback, undefined) || self.isTheDraggedDOM(this))
{
// clean up the helper dom
if (helper) helper.remove();
return;
@ -93,7 +129,7 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
});
//links is an Object of DropActions bound to their names
const links = _callback.call(_context, "links", self, EGW_AO_EXEC_THIS);
const links = _callback.call(dropActionObject, "links", self, EGW_AO_EXEC_THIS);
// Disable all links which only accept types which are not
// inside ddTypes
@ -133,7 +169,7 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
if (cnt == 1) {
window.setTimeout(function () {
lnk.actionObj.execute(selected, _context);
lnk.actionObj.execute(selected, dropActionObject);
}, 0);
}
@ -152,7 +188,7 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
window.setTimeout(function () {
popup.executeImplementation(pos, selected, links,
_context);
dropActionObject);
// Reset, popup is reused
popup.auto_paste = true;
}, 0); // Timeout is needed to have it working in IE
@ -174,7 +210,14 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
// don't go further if the dragged element is no there (happens when a none et2 dragged element is being dragged)
if (!self.getTheDraggedDOM() || self.isTheDraggedDOM(this)) return;
const data = {
if(_aoi.findActionTargetHandler && typeof _aoi.findActionTargetHandler.findActionTarget === "function")
{
// Bubbling up to parent
const parentData = _aoi.getWidget().findActionTarget(event);
_aoi = parentData?.action?.iface ?? _aoi;
}
const data = {
event: event,
ui: self.getTheDraggedData()
};
@ -187,18 +230,38 @@ export class EgwDropActionImplementation implements EgwActionImplementation {
return false;
};
// DND Event listeners
node.addEventListener('dragover', dragover, false);
_aoi.handlers[this.type].push({type: 'dragover', listener: dragover});
// Bind events on parent, if provided, instead of individual node
if(_aoi.findActionTargetHandler)
{
// But only bind once
if(parentAO && !parentAO.iface.handlers[this.type])
{
parentAO.iface.handlers[this.type] = parentAO.iface.handlers[this.type] ?? [];
// Swap objects, bind down below
_aoi = parentAO.iface;
node = parentAO.iface.getDOMNode();
}
else
{
return true;
}
}
node.addEventListener('dragenter', dragenter, false);
_aoi.handlers[this.type].push({type: 'dragenter', listener: dragenter});
if(_aoi.handlers[this.type].length == 0)
{
// DND Event listeners
node.addEventListener('dragenter', dragenter, false);
_aoi.handlers[this.type].push({type: 'dragenter', listener: dragenter});
node.addEventListener('drop', drop, false);
_aoi.handlers[this.type].push({type: 'drop', listener: drop});
node.addEventListener('dragleave', dragleave, false);
_aoi.handlers[this.type].push({type: 'dragleave', listener: dragleave});
node.addEventListener('dragleave', dragleave, false);
_aoi.handlers[this.type].push({type: 'dragleave', listener: dragleave});
node.addEventListener('dragover', dragover, false);
_aoi.handlers[this.type].push({type: 'dragover', listener: dragover});
node.addEventListener('drop', drop, false);
_aoi.handlers[this.type].push({type: 'drop', listener: drop});
}
return true;
}

View File

@ -17,7 +17,6 @@ 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 {
@ -26,38 +25,40 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
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)
let parentNode = null;
let parentAO = null;
let isNew = false;
// Is there a parent that handles action targets?
if(typeof _context.findActionTargetHandler !== "undefined" && typeof _context.findActionTargetHandler?.iface?.getWidget == "function")
{
parentAO = _context.findActionTargetHandler;
parentNode = parentAO.iface.getWidget();
}
if(!_aoi.findActionTargetHandler && parentNode && typeof parentNode.findActionTarget == "function")
{
_aoi.findActionTargetHandler = parentNode;
isNew = true;
}
if(isNew)
{
if (this.parent && this.parent == parent)
{
return true // already added Event Listener on parent no need to register on children
} else
{
//if a parent is available the context menu Event-listener will only be bound once on the parent
this._registerDefault(parentNode, _callback, parentAO);
this._registerContext(parentNode, _callback, parentAO);
this.parent = parent // this only exists for the EgwDragDropShoelaceTree ActionObjectInterface atm
return true;
}
else if(node && !parentNode)
{
this._registerDefault(node, _callback, _context);
this._registerContext(node, _callback, _context);
return true;
}
return false;
//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;
}
};
};
unregisterAction = function (_aoi) {
const node = _aoi.getDOMNode();

View File

@ -13,6 +13,7 @@ import {EgwActionObject} from "../../egw_action/EgwActionObject";
import {EgwAction} from "../../egw_action/EgwAction";
import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree";
import {FindActionTarget} from "../FindActionTarget";
import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT} from "../../egw_action/egw_action_constants";
export type TreeItemData = SelectOption & {
focused?: boolean;
@ -804,6 +805,37 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
return this.getNode(_nodeId)?.userdata?.find(elem => elem.name === _name)?.content
}
/**
* Handle drag events from inside the shadowRoot
*
* events get re-targeted to the tree as they bubble, and action can't tell the difference between leaves
* inside the shadowRoot
*
* @param event
* @returns {Promise<void>}
* @protected
*/
protected async handleDragEvent(event)
{
await this.updateComplete;
let option = event.composedPath().find(element =>
{
return element.tagName == "SL-TREE-ITEM"
});
if(!option)
{
return;
}
let id = option.value ?? (typeof option.id == 'number' ? String(option.id) : option.id);
console.log(event.type, id);
const typeMap = {
dragenter: EGW_AI_DRAG_ENTER,
dragleave: EGW_AI_DRAG_OUT
}
this.widget_object.getObjectById(id).iface.triggerEvent(typeMap[event.type], event);
}
/**
* Overridable, add style
* @returns {TemplateResult<1>}
@ -992,7 +1024,8 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
}
}
@dragenter=${(event) => {this.handleDragEvent(event);}}
@dragleave=${(event) => {this.handleDragEvent(event);}}
>
<sl-icon name="chevron-right" slot="expand-icon"></sl-icon>
<sl-icon name="chevron-down" slot="collapse-icon"></sl-icon>
@ -1014,6 +1047,27 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
return result
.then((results) => {
_item = results;
// Add actions
const itemAO = this.widget_object.getObjectById(_item.id);
let parentAO = null;
if(itemAO && itemAO.parent)
{
// Remove previous, if it exists
parentAO = itemAO.parent;
itemAO.remove();
}
// Need the DOM nodes to actually link the actions
this.updateComplete.then(() =>
{
this.linkLeafActions(
parentAO ?? this.widget_object,
_item,
this._get_action_links(this.actions)
);
});
return results;
});
}
@ -1054,33 +1108,42 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
// Go over the widget & add links - this is where we decide which actions are
// 'allowed' for this widget at this time
var action_links = this._get_action_links(actions);
this.widget_object.updateActionLinks(action_links);
//Drop target enabeling
if (typeof this._selectOptions != 'undefined')
{
let self: Et2Tree = this
// 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.id == 'number' ? String(option.id) : option.id);
// @ts-ignore
let obj : EgwActionObject = treeObj.addObject(id, new EgwDragDropShoelaceTree(self, id));
obj.updateActionLinks(action_links);
const children = option.children ?? option.item ?? [];
for(let i = 0; i < children.length; i++)
{
apply_actions.call(this, treeObj, children[i]);
}
};
for (const selectOption of this._selectOptions)
{
apply_actions.call(this, this.widget_object, selectOption)
this.linkLeafActions(this.widget_object, selectOption, action_links)
}
}
}
/**
* Add actions on a leaf
*
* @param {EgwActionObject} parentActionObject
* @param {TreeItemData} option
* @param {string[]} action_links
* @protected
*/
protected linkLeafActions(parentActionObject : EgwActionObject, option : TreeItemData, action_links : string[])
{
// Add a new action object to the object manager
let id = option.value ?? (typeof option.id == 'number' ? String(option.id) : option.id);
this.widget_object.updateActionLinks(action_links);
// @ts-ignore
let obj : EgwActionObject = parentActionObject.addObject(id, new EgwDragDropShoelaceTree(this, id));
obj.findActionTargetHandler = this;
obj.updateActionLinks(action_links);
const children = <TreeItemData[]><unknown>(option.children ?? option.item) ?? [];
for(let i = 0; i < children.length; i++)
{
this.linkLeafActions(obj, children[i], action_links);
}
}
/**
@ -1182,11 +1245,9 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
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}
});
let action : EgwActionObject = this.widget_object.getObjectById(target.id);
return {target: target, action: action};
}
}