/** * EGroupware egw_action framework - egw action framework * * @link https://www.egroupware.org * @author Andreas Stöckel * @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 }