mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-05 14:20:40 +01:00
0425157270
- tree drop wouldn't work on newly added folders - tree drop actions sometimes targeted a parent leaf - flickering on drop hover
374 lines
13 KiB
TypeScript
374 lines
13 KiB
TypeScript
/**
|
|
* EGroupware egw_action framework - egw action framework
|
|
*
|
|
* @link https://www.egroupware.org
|
|
* @author Andreas Stöckel <as@stylite.de>
|
|
* @copyright 2011 by Andreas Stöckel
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package egw_action
|
|
*/
|
|
|
|
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";
|
|
//keeps track of current drop element where dragged item's entered.
|
|
// it's necessary for dragenter/dragleave issue correction.
|
|
private currentDropEl = null
|
|
|
|
|
|
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 = {};
|
|
}
|
|
_aoi.handlers[this.type] = [];
|
|
node.classList.add('et2dropzone');
|
|
const dragover = (event)=> {
|
|
if (event.preventDefault) {
|
|
event.preventDefault();
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const dragenter = function (event) {
|
|
// don't trigger dragenter if we are entering the drag element
|
|
// 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) || this == self.currentDropEl) return;
|
|
|
|
// stop the event from being fired for its children
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
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 = {
|
|
event: event,
|
|
ui: self.getTheDraggedData()
|
|
};
|
|
|
|
_aoi.triggerEvent(EGW_AI_DRAG_ENTER, data);
|
|
|
|
// cleanup drop hover class from all other DOMs if there's still anything left
|
|
Array.from(document.getElementsByClassName('et2dropzone drop-hover')).forEach(_i => {
|
|
_i.classList.remove('drop-hover')
|
|
})
|
|
|
|
this.classList.add('drop-hover');
|
|
|
|
return false;
|
|
};
|
|
|
|
const drop = function (event) {
|
|
event.preventDefault();
|
|
// 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};
|
|
ui.offset = {top: event.offsetY, left: event.offsetX};
|
|
|
|
|
|
let data = JSON.parse(event.dataTransfer.getData('application/json'));
|
|
|
|
if(!self.isAccepted(data, dropActionObject, _callback, undefined) || self.isTheDraggedDOM(this))
|
|
{
|
|
// clean up the helper dom
|
|
if (helper) helper.remove();
|
|
return;
|
|
}
|
|
|
|
let selected = data.selected.map((item) => {
|
|
return egw_getObjectManager(item.id, false)
|
|
});
|
|
|
|
//links is an Object of DropActions bound to their names
|
|
const links = _callback.call(dropActionObject, "links", self, EGW_AO_EXEC_THIS);
|
|
|
|
// Disable all links which only accept types which are not
|
|
// inside ddTypes
|
|
for (const k in links) {
|
|
const accepted = links[k].actionObj.acceptedTypes;
|
|
|
|
let enabled = false;
|
|
for (let i = 0; i < data.ddTypes.length; i++) {
|
|
if (accepted.indexOf(data.ddTypes[i]) != -1) {
|
|
enabled = true;
|
|
break;
|
|
}
|
|
}
|
|
// Check for allowing multiple selected
|
|
if (!links[k].actionObj.allowOnMultiple && selected.length > 1) {
|
|
enabled = false;
|
|
}
|
|
if (!enabled) {
|
|
links[k].enabled = false;
|
|
links[k].visible = !links[k].actionObj.hideOnDisabled;
|
|
}
|
|
}
|
|
|
|
// Check whether there is only one link
|
|
let cnt = 0;
|
|
let lnk = null;
|
|
for (const k in links) {
|
|
if (links[k].enabled && links[k].visible) {
|
|
lnk = links[k];
|
|
cnt += 1 + links[k].actionObj.children.length;
|
|
|
|
// Add ui, so you know what happened where
|
|
lnk.actionObj.ui = ui;
|
|
|
|
}
|
|
}
|
|
|
|
if (cnt == 1) {
|
|
window.setTimeout(function () {
|
|
lnk.actionObj.execute(selected, dropActionObject);
|
|
}, 0);
|
|
}
|
|
|
|
if (cnt > 1) {
|
|
// More than one drop action link is associated
|
|
// to the drop event - show those as a popup menu
|
|
// and let the user decide which one to use.
|
|
// This is possible as the popup and the popup action
|
|
// object and the drop action object share same
|
|
// set of properties.
|
|
const popup = getPopupImplementation();
|
|
const pos = popup._getPageXY(event);
|
|
|
|
// Don't add paste actions, this is a drop
|
|
popup.auto_paste = false;
|
|
|
|
window.setTimeout(function () {
|
|
popup.executeImplementation(pos, selected, links,
|
|
dropActionObject);
|
|
// Reset, popup is reused
|
|
popup.auto_paste = true;
|
|
}, 0); // Timeout is needed to have it working in IE
|
|
}
|
|
// Set cursor back to auto. Seems FF can't handle cursor reversion
|
|
jQuery('body').css({cursor: 'auto'});
|
|
|
|
_aoi.triggerEvent(EGW_AI_DRAG_OUT, {event: event, ui: self.getTheDraggedData()});
|
|
|
|
// clean up the helper dom
|
|
if (helper) helper.remove();
|
|
self.getTheDraggedDOM().classList.remove('drag--moving');
|
|
};
|
|
|
|
const dragleave = function (event) {
|
|
event.stopImmediatePropagation();
|
|
|
|
// don't trigger dragleave if we are leaving the drag element
|
|
// 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;
|
|
|
|
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()
|
|
};
|
|
|
|
_aoi.triggerEvent(EGW_AI_DRAG_OUT, data);
|
|
|
|
this.classList.remove('drop-hover');
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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('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;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
unregisterAction : (_actionObjectInterface : any) => boolean = function(_aoi)
|
|
{
|
|
const node = _aoi.getDOMNode();
|
|
|
|
if(node)
|
|
{
|
|
node.classList.remove('et2dropzone');
|
|
}
|
|
// Unregister handlers
|
|
if(_aoi.handlers)
|
|
{
|
|
_aoi.handlers[this.type]?.forEach(h => node.removeEventListener(h.type, h.listener));
|
|
delete _aoi.handlers[this.type];
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Builds the context menu and shows it at the given position/DOM-Node.
|
|
*
|
|
* @param {string} _context
|
|
* @param {array} _selected
|
|
* @param {object} _links
|
|
*/
|
|
executeImplementation: (_context: any, _selected: any, _links: any) => any = function (_context, _selected, _links) {
|
|
if (_context == "links") {
|
|
return _links;
|
|
}
|
|
};
|
|
|
|
|
|
isTheDraggedDOM = function (_dom) {
|
|
return _dom.classList.contains('drag--moving');
|
|
}
|
|
|
|
getTheDraggedDOM = function () {
|
|
return document.querySelector('.drag--moving');
|
|
}
|
|
|
|
getHelperDOM = function () {
|
|
return document.querySelector('.et2_egw_action_ddHelper');
|
|
}
|
|
|
|
getTheDraggedData = ()=> {
|
|
// @ts-ignore // in our case dataset will be present
|
|
let data = this.getTheDraggedDOM().dataset.egwactionobjid;
|
|
let selected = [];
|
|
if (data) {
|
|
data = JSON.parse(data);
|
|
selected = data.map((item) => {
|
|
return egw_getObjectManager(item.id, false)
|
|
});
|
|
}
|
|
return {
|
|
draggable: this.getTheDraggedDOM(),
|
|
helper: this.getHelperDOM(),
|
|
selected: selected,
|
|
position: undefined,
|
|
offset: undefined
|
|
|
|
}
|
|
}
|
|
|
|
// check if given draggable is accepted for drop
|
|
isAccepted = (_data, _context, _callback, _node)=> {
|
|
if (_node && !_node.classList.contains('et2dropzone')) return false;
|
|
if (typeof _data.ddTypes != "undefined") {
|
|
const accepted = this._fetchAccepted(
|
|
_callback.call(_context, "links", this, EGW_AO_EXEC_THIS));
|
|
|
|
// Check whether all drag types of the selected objects
|
|
// are accepted
|
|
const ddTypes = _data.ddTypes;
|
|
|
|
for (let i = 0; i < ddTypes.length; i++) {
|
|
if (accepted.indexOf(ddTypes[i]) != -1) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
private _fetchAccepted = (_links) =>{
|
|
// Accumulate the accepted types
|
|
const accepted = [];
|
|
for (let k in _links) {
|
|
for (let i = 0; i < _links[k].actionObj.acceptedTypes.length; i++) {
|
|
const type = _links[k].actionObj.acceptedTypes[i];
|
|
|
|
if (accepted.indexOf(type) == -1) {
|
|
accepted.push(type);
|
|
}
|
|
}
|
|
}
|
|
|
|
return accepted;
|
|
};
|
|
|
|
}
|
|
|
|
export class egwDropActionImplementation extends EgwDropActionImplementation {
|
|
} |