mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 16:44:20 +01:00
407 lines
14 KiB
TypeScript
407 lines
14 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} from "../jsapi/egw_global";
|
|
import {EgwActionObjectInterface} from "./EgwActionObjectInterface";
|
|
import {EGW_AO_STATE_DRAGGING} from "./egw_action_constants";
|
|
|
|
export class EgwDragActionImplementation implements EgwActionImplementation {
|
|
type = "drag";
|
|
helper: HTMLDivElement = null;
|
|
ddTypes: any[] = [];
|
|
selected: any[] = [];
|
|
defaultDDHelper: (_selected) => HTMLDivElement = (_selected) => {
|
|
// Table containing clone of rows
|
|
const table: HTMLTableElement = (document.createElement("table"));
|
|
table.classList.add('egwGridView_grid', 'et2_egw_action_ddHelper_row');
|
|
// tr element to use as last row to show 'more ...' label
|
|
const moreRow: HTMLTableRowElement = (document.createElement('tr'))
|
|
moreRow.classList.add('et2_egw_action_ddHelper_moreRow');
|
|
// Main div helper container
|
|
const div: HTMLDivElement = (document.createElement("div"));
|
|
div.append(table);
|
|
|
|
let rows = [];
|
|
// Maximum number of rows to show
|
|
let maxRows = 3;
|
|
// item label
|
|
const itemLabel = egw.lang(
|
|
(
|
|
egw.link_get_registry(egw.app_name(), _selected.length > 1 ? 'entries' : 'entry') || egw.app_name()
|
|
) as string
|
|
);
|
|
let index = 0;
|
|
|
|
// Take select all into account when counting number of rows, because they may not be
|
|
// in _selected object
|
|
const pseudoNumRows = (_selected[0]?._context?._selectionMgr?._selectAll) ?
|
|
_selected[0]._context?._selectionMgr?._total : _selected.length;
|
|
|
|
// Clone nodes but use copy webComponent properties
|
|
const carefulClone = (node, skip_text = false) =>
|
|
{
|
|
// Don't clone text nodes, it causes duplication in et2-description
|
|
if(skip_text && node.nodeType == node.TEXT_NODE)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let clone = node.cloneNode();
|
|
|
|
let widget_class = window.customElements.get(clone.localName);
|
|
let properties = widget_class ? widget_class.properties : [];
|
|
for(let key in properties)
|
|
{
|
|
clone[key] = node[key];
|
|
}
|
|
// Children
|
|
node.childNodes.forEach(c =>
|
|
{
|
|
// Don't clone text in et2-description, it causes duplication
|
|
const child = carefulClone(c, skip_text || ["ET2-DESCRIPTION"].indexOf(c.tagName) != -1)
|
|
if(child)
|
|
{
|
|
clone.appendChild(child);
|
|
}
|
|
})
|
|
if(widget_class)
|
|
{
|
|
clone.requestUpdate();
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
for(const egwActionObject of _selected)
|
|
{
|
|
let rowNode = egwActionObject.iface.getDOMNode();
|
|
if(egwActionObject._context && egwActionObject._context instanceof HTMLElement)
|
|
{
|
|
rowNode = egwActionObject._context;
|
|
}
|
|
const row : Node = carefulClone(rowNode);
|
|
if(row)
|
|
{
|
|
rows.push(row);
|
|
table.append(row);
|
|
}
|
|
index++;
|
|
if (index == maxRows) {
|
|
// Label to show number of items
|
|
const spanCnt = (document.createElement('span'))
|
|
spanCnt.classList.add('et2_egw_action_ddHelper_itemsCnt')
|
|
div.append(spanCnt);
|
|
|
|
spanCnt.textContent = (pseudoNumRows + ' ' + itemLabel);
|
|
// Number of not shown rows
|
|
const restRows = pseudoNumRows - maxRows;
|
|
if (restRows > 0) {
|
|
moreRow.textContent = egw.lang(`${pseudoNumRows - maxRows} more ${itemLabel} selected ...`);
|
|
}
|
|
table.append(moreRow);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const text = (document.createElement('div'))
|
|
text.classList.add('et2_egw_action_ddHelper_tip');
|
|
div.append(text);
|
|
|
|
// Add notice of Ctrl key, if supported
|
|
if ('draggable' in document.createElement('span') &&
|
|
navigator && navigator.userAgent.indexOf('Chrome') >= 0 && egw.app_name() == 'filemanager') // currently only filemanager supports drag out
|
|
{
|
|
if (rows.length == 1)
|
|
{
|
|
text.textContent=(egw.lang('You may drag file out to your desktop', itemLabel));
|
|
}
|
|
else
|
|
{
|
|
text.textContent=(egw.lang('Note: If you drag out these selected rows to desktop only the first selected row will be downloaded.', itemLabel));
|
|
}
|
|
}
|
|
// Final html DOM return as helper structure
|
|
return div;
|
|
};
|
|
|
|
registerAction: (_actionObjectInterface: EgwActionObjectInterface, _triggerCallback: Function, _context: any) => boolean = (_aoi, _callback, _context) => {
|
|
const node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();
|
|
|
|
if (node) {
|
|
if(typeof _aoi.handlers == "undefined")
|
|
{
|
|
_aoi.handlers = {};
|
|
}
|
|
if(typeof _aoi.handlers[this.type] == "undefined")
|
|
{
|
|
_aoi.handlers[this.type] = [];
|
|
}
|
|
|
|
// Prevent selection
|
|
node.onselectstart = function () {
|
|
return false;
|
|
};
|
|
if (!(window.FileReader && 'draggable' in document.createElement('span'))) {
|
|
// No DnD support
|
|
return;
|
|
}
|
|
|
|
// It shouldn't be so hard to get the action...
|
|
let action = null;
|
|
const groups = _context.getActionImplementationGroups();
|
|
if (!groups.drag) {
|
|
return;
|
|
}
|
|
|
|
if(_aoi.handlers[this.type].length !== 0)
|
|
{
|
|
// Already bound
|
|
return;
|
|
}
|
|
|
|
// Bind mouse handlers
|
|
//et2_dataview_view_aoi binds mousedown event in et2_dataview_rowAOI to "egwPreventSelect" function from egw_action_common via jQuery.mousedown
|
|
//jQuery(node).off("mousedown",egwPreventSelect)
|
|
//et2_dataview_view_aoi binds mousedown event in et2_dataview_rowAOI to "egwPreventSelect" function from egw_action_common via addEventListener
|
|
//node.removeEventListener("mousedown",egwPreventSelect)
|
|
const mousedown = (event) =>
|
|
{
|
|
if (_context.isSelection(event)) {
|
|
node.setAttribute("draggable", false);
|
|
} else if (event.which != 3) {
|
|
document.getSelection().removeAllRanges();
|
|
}
|
|
};
|
|
node.addEventListener("mousedown", mousedown)
|
|
_aoi.handlers[this.type].push({type: 'mousedown', listener: mousedown});
|
|
|
|
const mouseup = (event) =>
|
|
{
|
|
node.setAttribute("draggable", true);
|
|
|
|
// Set cursor back to auto. Seems FF can't handle cursor reversion
|
|
document.body.style.cursor = 'auto'
|
|
};
|
|
node.addEventListener("mouseup", mouseup)
|
|
_aoi.handlers[this.type].push({type: 'mousedown', listener: mousedown});
|
|
|
|
|
|
node.setAttribute('draggable', true);
|
|
const ai = this
|
|
const dragstart = function (event) {
|
|
|
|
let dragActionObject = _context;
|
|
if(this.findActionTarget)
|
|
{
|
|
dragActionObject = this.findActionTarget(event).action ?? _context;
|
|
}
|
|
|
|
// The helper function is called before the start function
|
|
// is evoked. Call the given callback function. The callback
|
|
// function will gather the selected elements and action links
|
|
// and call the doExecuteImplementation function. This
|
|
// will call the onExecute function of the first action
|
|
// in order to obtain the helper object (stored in ai.helper)
|
|
// and the multiple dragDropTypes (ai.ddTypes)
|
|
_callback.call(dragActionObject, false, ai);
|
|
|
|
// Stop parent elements from also starting to drag if we're nested
|
|
if(ai.selected.length)
|
|
{
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if(action && egw.app_name() == 'filemanager')
|
|
{
|
|
if(dragActionObject.isSelection(event))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get all selected
|
|
const selected = ai.selected;
|
|
|
|
// Set file data
|
|
for (let i = 0; i < 1; i++) {
|
|
let d = selected[i].data || egw.dataGetUIDdata(selected[i].id).data || {};
|
|
if (d && d.mime && d.download_url) {
|
|
let url = d.download_url;
|
|
|
|
// NEED an absolute URL
|
|
if (url[0] == '/') url = egw.link(url);
|
|
// egw.link adds the webserver, but that might not be an absolute URL - try again
|
|
if (url[0] == '/') url = window.location.origin + url;
|
|
event.dataTransfer.setData("DownloadURL", d.mime + ':' + d.name + ':' + url);
|
|
}
|
|
}
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
if (event.dataTransfer.types.length == 0) {
|
|
// No file data? Abort: drag does nothing
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
} else {
|
|
event.dataTransfer.effectAllowed = 'linkMove';
|
|
}
|
|
|
|
|
|
const data = {
|
|
ddTypes: ai.ddTypes,
|
|
selected: ai.selected.map((item) => {
|
|
return {id: item.id}
|
|
})
|
|
};
|
|
|
|
if (!ai.helper) {
|
|
ai.helper = ai.defaultDDHelper(ai.selected);
|
|
}
|
|
// Add a basic class to the helper in order to standardize the background layout
|
|
ai.helper.classList.add('et2_egw_action_ddHelper', 'ui-draggable-dragging');
|
|
document.body.append(ai.helper);
|
|
|
|
// Set a dragging state
|
|
ai.selected.forEach((item) =>
|
|
{
|
|
item.iface?.setState(ai.selected[0].iface.getState() | EGW_AO_STATE_DRAGGING);
|
|
});
|
|
this.classList.add('drag--moving');
|
|
|
|
event.dataTransfer.setData('application/json', JSON.stringify(data))
|
|
|
|
// Wait for any webComponents to finish
|
|
let wait = [];
|
|
const webComponents = [];
|
|
const check = (element) =>
|
|
{
|
|
if(typeof element.updateComplete !== "undefined")
|
|
{
|
|
webComponents.push(element)
|
|
element.requestUpdate();
|
|
wait.push(element.updateComplete);
|
|
}
|
|
element.childNodes.forEach(child => check(child));
|
|
}
|
|
check(ai.helper);
|
|
// Clumsily force widget update, since we can't do it async
|
|
Promise.all(wait).then(() =>
|
|
{
|
|
wait = [];
|
|
webComponents.forEach(e => wait.push(e.updateComplete));
|
|
Promise.all(wait).then(() =>
|
|
{
|
|
event.dataTransfer.setDragImage(ai.helper, 12, 12);
|
|
});
|
|
});
|
|
|
|
this.setAttribute('data-egwActionObjID', JSON.stringify(data.selected));
|
|
};
|
|
|
|
const dragend = (_) => {
|
|
const helper = document.querySelector('.et2_egw_action_ddHelper');
|
|
if (helper) helper.remove();
|
|
const draggable = document.querySelector('.drag--moving');
|
|
if (draggable) draggable.classList.remove('drag--moving');
|
|
// 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')})
|
|
// Clean up selected
|
|
ai.selected = [];
|
|
};
|
|
|
|
// Drag Event listeners
|
|
node.addEventListener('dragstart', dragstart, false);
|
|
_aoi.handlers[this.type].push({type: 'dragstart', listener: dragstart});
|
|
node.addEventListener('dragend', dragend, false);
|
|
_aoi.handlers[this.type].push({type: 'dragend', listener: dragend});
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
unregisterAction: (_actionObjectInterface: EgwActionObjectInterface) => boolean =(_aoi) => {
|
|
const node = _aoi.getDOMNode();
|
|
|
|
if (node) {
|
|
node.setAttribute('draggable', "false");
|
|
}
|
|
// 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 = (_context, _selected, _links) => {
|
|
// Reset the helper object of the action implementation
|
|
this.helper = null;
|
|
let hasLink = false;
|
|
|
|
// Store the drag-drop types
|
|
this.ddTypes = [];
|
|
this.selected = _selected;
|
|
|
|
// Call the onExecute event of the first actionObject
|
|
for (const k in _links) {
|
|
if (_links[k].visible) {
|
|
hasLink = true;
|
|
|
|
// Only execute the following code if a JS function is registered
|
|
// for the action and this is the first action link
|
|
if (!this.helper && _links[k].actionObj.onExecute.hasHandler()) {
|
|
this.helper = _links[k].actionObj.execute(_selected);
|
|
}
|
|
|
|
// Push the dragType of the associated action object onto the
|
|
// drag type list - this allows an element to support multiple
|
|
// drag/drop types.
|
|
const type: string[] = Array.isArray(_links[k].actionObj.dragType)
|
|
? _links[k].actionObj.dragType
|
|
: [_links[k].actionObj.dragType];
|
|
for (const i of type) {
|
|
if (this.ddTypes.indexOf(i) === -1) {
|
|
this.ddTypes.push(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If no helper has been defined, create a default one
|
|
if (!this.helper && hasLink) {
|
|
this.helper = this.defaultDDHelper(_selected);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @deprecated use upper case class
|
|
*/
|
|
export class egwDragActionImplementation extends EgwDragActionImplementation {
|
|
}
|
|
|
|
let _dragActionImpl = null
|
|
|
|
export function getDragImplementation():EgwDragActionImplementation {
|
|
if (!_dragActionImpl) {
|
|
_dragActionImpl = new EgwDragActionImplementation();
|
|
}
|
|
return _dragActionImpl
|
|
}
|