mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 01:13:25 +01:00
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:
parent
aadaa28f86
commit
0c2f211ada
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user