/** * eGroupWare egw_action framework - egw action framework * * @link http://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 * @version $Id$ */ /*egw:uses egw_action; egw_action_common; egw_action_popup; jquery.jquery; jquery.jquery-ui; */ /** * Register the drag and drop handlers */ if (typeof window._egwActionClasses == "undefined") window._egwActionClasses = {}; _egwActionClasses["drag"] = { "actionConstructor": egwDragAction, "implementation": getDragImplementation }; _egwActionClasses["drop"] = { "actionConstructor": egwDropAction, "implementation": getDropImplementation }; /** * The egwDragAction class overwrites the egwAction class and adds the new * "dragType" propery. The "onExecute" event of the drag action will be called * whenever dragging starts. The onExecute JS handler should return the * drag-drop helper object - otherwise an default helper will be generated. * * @param {egwAction} _id * @param {string} _handler * @param {string} _caption * @param {string} _icon * @param {(string|function)} _onExecute * @param {bool} _allowOnMultiple * @returns {egwDragAction} */ function egwDragAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple) { var action = new egwAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple); action.type = "drag"; action.dragType = "default"; action.hideOnDisabled = true; action.set_dragType = function(_value) { action.dragType = _value; }; return action; } var _dragActionImpl = null; function getDragImplementation() { if (!_dragActionImpl) { _dragActionImpl = new egwDragActionImplementation(); } return _dragActionImpl; } function egwDragActionImplementation() { var ai = new egwActionImplementation(); ai.type = "drag"; ai.helper = null; ai.ddTypes = []; ai.selected = []; ai.doRegisterAction = function(_aoi, _callback, _context) { var node = _aoi.getDOMNode(); if (node) { // 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... var action = null; var groups = _context.getActionImplementationGroups(); if(!groups.drag) return; for(var i = 0; i < groups.drag.length; i++) { // dragType 'file' says it can be dragged as a file if(groups.drag[i].link.actionObj.dragType == 'file') { action = groups.drag[i].link.actionObj; break; } } if(action) { /** * We found an action with dragType 'file', so by holding Ctrl * key & dragging, user can drag from browser to system. * The global data store must provide a full, absolute URL in 'download_url' * and a mime in 'mime'. * * Unfortunately, Native DnD to drag the file conflicts with jQueryUI draggable, * which handles all the other DnD actions. We get around this by: * 1. Require the user indicate a file drag with Ctrl key * 2. Disable jQueryUI draggable, then turn on native draggable attribute * This way we can at least toggle which one is operating, so they * both work alternately if not together. */ // Native DnD - Doesn't play nice with jQueryUI Sortable // Tell jQuery to include this property jQuery.event.props.push('dataTransfer'); $j(node).off("mousedown") .on("mousedown", function(event) { $j(node).draggable("option","disabled",event.ctrlKey || event.metaKey); $j(this).attr("draggable", event.ctrlKey || event.metaKey ? "true" : ""); // Disabling draggable adds some UI classes, but we don't care so remove them $j(node).removeClass("ui-draggable-disabled ui-state-disabled"); if(!(event.ctrlKey || event.metaKey) || !this.addEventListener) return; }) .on("dragstart", function(event) { if(event.dataTransfer == null) { return; } event.dataTransfer.effectAllowed="copy"; // Get all selected // Multiples aren't supported by event.dataTransfer, yet, so // select only the row they clicked on. // var selected = _context.getSelectedLinks('drag'); var selected = [_context]; _context.parent.setAllSelected(false); _context.setSelected(true); // Set file data for(var i = 0; i < selected.length; i++) { var data = egw.dataGetUIDdata(selected[i].id); if(data && data.data.mime && data.data.download_url) { var url = data.data.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; // Unfortunately, dragging files is currently only supported by Chrome if(navigator && navigator.userAgent.indexOf('Chrome')) { event.dataTransfer.setData("DownloadURL", data.data.mime+':'+data.data.name+':'+url); } else { // Include URL as a fallback event.dataTransfer.setData("text/uri-list", url); } } } if(event.dataTransfer.types.length == 0) { // No file data? Abort: drag does nothing event.preventDefault(); return; } // Create drag icon _callback.call(_context, _context, ai); // Drag icon must be visible for setDragImage() - we'll remove it on drag $j("body").append(ai.helper); event.dataTransfer.setDragImage(ai.helper[0],-12,-12); }) .on("drag", function(e) { // Remove the helper, it has been copied into the dataTransfer object now // Hopefully user didn't notice it... if(e.dataTransfer != null) { ai.helper.remove(); } }); } $j(node).draggable( { "distance": 20, "cursor": "move", "cursorAt": { top: -12, left: -12 }, "helper": function(e) { // 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(_context, false, ai); $j(node).data("ddTypes", ai.ddTypes); $j(node).data("selected", ai.selected); if (ai.helper) { // Append the helper object to the body element - this // fixes a bug in IE: If the element isn't inserted into // the DOM-tree jquery appends it to the parent node. // In case this is a table it doesn't work correctly $j("body").append(ai.helper); return ai.helper; } // Return an empty div if the helper dom node is not set return $j(document.createElement("div")); }, "start": function(e) { return ai.helper != null; }, // Solves problem with scroll position changing in the grid // component "refreshPositions": true, "scroll": false, "containment": "document", "iframeFix": true } ); return true; } return false; }; ai.doUnregisterAction = function(_aoi) { var node = _aoi.getDOMNode(); if (node && $j(node).data("uiDraggable")){ $j(node).draggable("destroy"); } }; /** * Builds the context menu and shows it at the given position/DOM-Node. * * @param {string} _context * @param {array} _selected * @param {object} _links */ ai.doExecuteImplementation = function(_context, _selected, _links) { // Reset the helper object of the action implementation this.helper = null; var hasLink = false; // Store the drag-drop types this.ddTypes = []; this.selected = _selected; // Call the onExecute event of the first actionObject for (var 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. var type = $j.isArray(_links[k].actionObj.dragType) ? _links[k].actionObj.dragType : [_links[k].actionObj.dragType]; for(var i = 0; i < type.length; i++) { if (this.ddTypes.indexOf(type[i]) == -1) { this.ddTypes.push(type[i]); } } } } // If no helper has been defined, create an default one if (!this.helper && hasLink) { this.helper = $j(document.createElement("div")); this.helper.addClass("egw_action_ddHelper"); this.helper.text("(" + _selected.length + ")"); } return true; }; return ai; } /** * The egwDropAction class overwrites the egwAction class and adds the "acceptedTypes" * property. This array should contain all "dragTypes" the drop action is allowed to * * @param {egwAction} _id * @param {string} _handler * @param {string} _caption * @param {string} _icon * @param {(string|function)} _onExecute * @param {bool} _allowOnMultiple * @returns {egwDropAction} */ function egwDropAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple) { var action = new egwAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple); action.type = "drop"; action.acceptedTypes = ["default"]; action.canHaveChildren = ["drag","popup"]; action["default"] = false; action.order = 0; action.group = 0; action.set_default = function(_value) { action["default"] = _value; }; action.set_order = function(_value) { action.order = _value; }; action.set_group = function(_value) { action.group = _value; }; /** * The acceptType property allows strings as well as arrays - strings are * automatically included in an array. * * @param {(string|array)} _value */ action.set_acceptedTypes = function(_value) { if (_value instanceof Array) { action.acceptedTypes = _value; } else { action.acceptedTypes = [_value]; } }; return action; } var _dropActionImpl = null; function getDropImplementation() { if (!_dropActionImpl) { _dropActionImpl = new egwDropActionImplementation(); } return _dropActionImpl; } var EGW_AI_DRAG = 0x0100; // Use the first byte as mask for event types - 01 is for events used with drag stuff var EGW_AI_DRAG_OUT = EGW_AI_DRAG | 0x01; var EGW_AI_DRAG_OVER = EGW_AI_DRAG | 0x02; function egwDropActionImplementation() { var ai = new egwActionImplementation(); ai.type = "drop"; ai.doRegisterAction = function(_aoi, _callback, _context) { var node = _aoi.getDOMNode(); var self = this; if (node) { $j(node).droppable( { "accept": function(_draggable) { if (typeof _draggable.data("ddTypes") != "undefined") { var accepted = self._fetchAccepted( _callback.call(_context, "links", self, EGW_AO_EXEC_THIS)); // Check whether all drag types of the selected objects // are accepted var ddTypes = _draggable.data("ddTypes"); for (var i = 0; i < ddTypes.length; i++) { if (accepted.indexOf(ddTypes[i]) != -1) { return true; } } return false; } }, "drop": function(event, ui) { var draggable = ui.draggable; var ddTypes = draggable.data("ddTypes"); var selected = draggable.data("selected"); var links = _callback.call(_context, "links", self, EGW_AO_EXEC_THIS); // Disable all links which only accept types which are not // inside ddTypes for (var k in links) { var accepted = links[k].actionObj.acceptedTypes; var enabled = false; for (var i = 0; i < ddTypes.length; i++) { if (accepted.indexOf(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 var cnt = 0; var lnk = null; for (var 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, _context); },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. var popup = getPopupImplementation(); var pos = popup._getPageXY(event.originalEvent); window.setTimeout(function() { popup.doExecuteImplementation(pos, selected, links, _context); }, 0); // Timeout is needed to have it working in IE } _aoi.triggerEvent(EGW_AI_DRAG_OUT); }, "over": function() { _aoi.triggerEvent(EGW_AI_DRAG_OVER); }, "out": function() { _aoi.triggerEvent(EGW_AI_DRAG_OUT); }, "tolerance": "pointer", activeClass: "ui-state-hover", hoverClass: "ui-state-active", // Greedy is for nested droppables - children consume the action greedy: true } ); return true; } return false; }; ai.doUnregisterAction = function(_aoi) { var node = _aoi.getDOMNode(); if (node && $j(node).data("uiDroppable")) { $j(node).droppable("destroy"); } }; ai._fetchAccepted = function(_links) { // Accumulate the accepted types var accepted = []; for (var k in _links) { for (var i = 0; i < _links[k].actionObj.acceptedTypes.length; i++) { var type = _links[k].actionObj.acceptedTypes[i]; if (accepted.indexOf(type) == -1) { accepted.push(type); } } } return accepted; }; /** * Builds the context menu and shows it at the given position/DOM-Node. * * @param {string} _context * @param {array} _selected * @param {object} _links */ ai.doExecuteImplementation = function(_context, _selected, _links) { if (_context == "links") { return _links; } }; return ai; }