egroupware/api/js/egw_action/egw_action_popup.js

957 lines
25 KiB
JavaScript

/**
* eGroupWare egw_action framework - egw action framework
*
* @link http://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
* @version $Id$
*/
/*egw:uses
vendor.bower-asset.jquery.dist.jquery;
egw_menu;
*/
import {egwAction, egwActionImplementation, egwActionObject} from './egw_action.js';
import {egwFnct} from './egw_action_common.js';
import {egwMenu, _egw_active_menu} from "./egw_menu.js";
import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants.js";
import {tapAndSwipe} from "../tapandswipe";
if (typeof window._egwActionClasses == "undefined")
window._egwActionClasses = {};
_egwActionClasses["popup"] = {
"actionConstructor": egwPopupAction,
"implementation": getPopupImplementation
};
export function egwPopupAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple)
{
var action = new egwAction(_id, _handler, _caption, _icon, _onExecute, _allowOnMultiple);
action.type = "popup";
action.canHaveChildren = ["popup"];
action["default"] = false;
action.order = 0;
action.group = 0;
action.hint = false;
action.checkbox = false;
action.radioGroup = 0;
action.checked = false;
action.confirm_mass_selection = null;
action.shortcut = null;
action.singleClick = false;
action.set_singleClick = function(_value) {
action["singleClick"] = _value;
};
action.set_default = function(_value) {
action["default"] = _value;
};
action.set_order = function(_value) {
action.order = _value;
};
action.set_group = function(_value) {
action.group = _value;
};
action.set_hint = function(_value) {
action.hint = _value;
};
// If true, the action will be rendered as checkbox
action.set_checkbox = function(_value) {
action.checkbox = _value;
};
action.set_checked = function(_value) {
action.checked = _value;
};
/**
* Set either a confirmation prompt, or TRUE to indicate that this action
* cares about large selections and to ask the confirmation prompt(s)
*
* @param {String|Boolean} _value
*/
action.set_confirm_mass_selection = function(_value) {
action.confirm_mass_selection = _value;
};
// Allow checkbox to be set from context using the given function
action.set_isChecked = function(_value) {
action.isChecked = new egwFnct(this, null, []);
if(_value !== null)
{
action.isChecked.setValue(_value);
}
};
// If radioGroup is >0 and the element is a checkbox, radioGroup specifies
// the group of radio buttons this one belongs to
action.set_radioGroup = function(_value) {
action.radioGroup = _value;
};
action.set_shortcut = function(_value) {
if (_value)
{
var sc = {
"keyCode": -1,
"shift": false,
"ctrl": false,
"alt": false
};
if (typeof _value == "object" && typeof _value.keyCode != "undefined" &&
typeof _value.caption != "undefined")
{
sc.keyCode = _value.keyCode;
sc.caption = _value.caption;
sc.shift = (typeof _value.shift == "undefined") ? false : _value.shift;
sc.ctrl = (typeof _value.ctrl == "undefined") ? false : _value.ctrl;
sc.alt = (typeof _value.alt == "undefined") ? false : _value.alt;
}
this.shortcut = sc;
}
else
{
this.shortcut = false;
}
};
return action;
}
var
_popupActionImpl = null;
export function getPopupImplementation()
{
if (!_popupActionImpl)
{
_popupActionImpl = new egwPopupActionImplementation();
}
return _popupActionImpl;
}
export function egwPopupActionImplementation()
{
var ai = new egwActionImplementation();
ai.type = "popup";
ai.auto_paste = true;
/**
* Registers the handler for the default action
*
* @param {DOMNode} _node
* @param {function} _callback
* @param {object} _context
* @returns {boolean}
*/
ai._registerDefault = function(_node, _callback, _context)
{
var defaultHandler = function(e) {
// Prevent bubbling bound event on <a> tag, on touch devices
// a tag should be handled by default event
if (egwIsMobile() && e.target.tagName == "A") return true;
if (typeof document.selection != "undefined" && typeof document.selection.empty != "undefined")
{
document.selection.empty();
}
else if( typeof window.getSelection != "undefined")
{
var sel = window.getSelection();
sel.removeAllRanges();
}
if (!(_context.manager.getActionsByAttr('singleClick', true).length > 0 &&
e.originalEvent.target.classList.contains('et2_clickable')))
{
_callback.call(_context, "default", ai);
}
// Stop action from bubbling up to parents
e.stopPropagation();
e.cancelBubble = true;
// remove context menu if we are in mobile theme
// and intended to open the entry
if (_egw_active_menu && e.which == 1) _egw_active_menu.hide();
return false;
};
if (egwIsMobile() || _context.manager.getActionsByAttr('singleClick', true).length > 0) {
jQuery(_node).bind('click', defaultHandler);
} else {
_node.ondblclick = defaultHandler;
}
};
ai._getDefaultLink = function(_links) {
var defaultAction = null;
for (var k in _links)
{
if (_links[k].actionObj["default"] && _links[k].enabled)
{
defaultAction = _links[k].actionObj;
break;
}
}
return defaultAction;
};
ai._searchShortcut = function (_key, _objs, _links) {
for (var i = 0; i < _objs.length; i++)
{
var sc = _objs[i].shortcut;
if (sc && sc.keyCode == _key.keyCode && sc.shift == _key.shift &&
sc.ctrl == _key.ctrl && sc.alt == _key.alt &&
_objs[i].type == "popup" && (typeof _links[_objs[i].id] == "undefined" ||
_links[_objs[i].id].enabled))
{
return _objs[i];
}
var obj = this._searchShortcut(_key, _objs[i].children, _links);
if (obj) {
return obj;
}
}
};
ai._searchShortcutInLinks = function(_key, _links) {
var objs = [];
for (var k in _links)
{
if (_links[k].enabled)
{
objs.push(_links[k].actionObj);
}
}
return ai._searchShortcut(_key, objs, _links);
};
/**
* Handles a key press
*
* @param {object} _key
* @param {type} _selected
* @param {type} _links
* @param {type} _target
* @returns {Boolean}
*/
ai._handleKeyPress = function(_key, _selected, _links, _target) {
// Handle the default
if (_key.keyCode == EGW_KEY_ENTER && !_key.ctrl && !_key.shift && !_key.alt) {
var defaultAction = this._getDefaultLink(_links);
if (defaultAction)
{
defaultAction.execute(_selected);
return true;
}
}
// Menu button
if (_key.keyCode == EGW_KEY_MENU && !_key.ctrl)
{
return this.doExecuteImplementation({posx:0,posy:0}, _selected, _links, _target);
}
// Check whether the given shortcut exists
var obj = this._searchShortcutInLinks(_key, _links);
if (obj)
{
obj.execute(_selected);
return true;
}
return false;
};
ai._handleTapHold = function (_node, _callback)
{
//TODO (todo-jquery): ATM we need to convert the possible given jquery dom node object into DOM Element, this
// should be no longer neccessary after removing jQuery nodes.
if (_node instanceof jQuery)
{
_node = _node[0];
}
let tap = new tapAndSwipe(_node, {
// this threshold must be the same as the one set in et2_dataview_view_aoi
tapHoldThreshold: 600,
allowScrolling: "both",
tapAndHold: function(event, fingercount)
{
if (fingercount >= 2) return;
// don't trigger contextmenu if sorting is happening
if (document.querySelector('.sortable-drag')) return;
_callback(event);
}
});
}
/**
* Registers the handler for the context menu
*
* @param {DOMNode} _node
* @param {function} _callback
* @param {object} _context
* @returns {boolean}
*/
ai._registerContext = function(_node, _callback, _context)
{
var contextHandler = function(e) {
//Obtain the event object
if (!e)
{
e = window.event;
}
if (_egw_active_menu)
{
_egw_active_menu.hide();
}
else if (!e.ctrlKey && e.which == 3 || e.which === 0) // tap event indicates by 0
{
var _xy = ai._getPageXY(e);
var _implContext = {event:e, posx:_xy.posx, posy: _xy.posy};
_callback.call(_context, _implContext, ai);
}
e.cancelBubble = !e.ctrlKey || e.which == 1;
if (e.stopPropagation && e.cancelBubble)
{
e.stopPropagation();
}
return !e.cancelBubble;
};
// Safari still needs the taphold to trigger contextmenu
// Chrome has default event on touch and hold which acts like right click
this._handleTapHold(_node, contextHandler);
if (!egwIsMobile()) jQuery(_node).on('contextmenu', contextHandler);
};
ai.doRegisterAction = function(_aoi, _callback, _context)
{
var node = _aoi.getDOMNode();
if (node)
{
this._registerDefault(node, _callback, _context);
this._registerContext(node, _callback, _context);
return true;
}
return false;
};
ai.doUnregisterAction = function(_aoi)
{
var node = _aoi.getDOMNode();
jQuery(node).off();
};
/**
* Builds the context menu and shows it at the given position/DOM-Node.
*
* @param {object} _context
* @param {type} _selected
* @param {type} _links
* @param {type} _target
* @returns {Boolean}
*/
ai.doExecuteImplementation = function(_context, _selected, _links, _target)
{
if (typeof _target == "undefined")
{
_target = null;
}
ai._context = _context;
if (typeof _context == "object" && typeof _context.keyEvent == "object")
{
return ai._handleKeyPress(_context.keyEvent, _selected, _links, _target);
}
else if (_context != "default")
{
//Check whether the context has the posx and posy parameters
if ((typeof _context.posx != "number" || typeof _context.posy != "number") &&
typeof _context.id != "undefined")
{
// Calculate context menu position from the given DOM-Node
var node = _context;
x = jQuery(node).offset().left;
y = jQuery(node).offset().top;
_context = {"posx": x, "posy": y};
}
var menu = ai._buildMenu(_links, _selected, _target);
menu.showAt(_context.posx, _context.posy);
return true;
}
else
{
var defaultAction = ai._getDefaultLink(_links);
if (defaultAction)
{
defaultAction.execute(_selected);
}
}
return false;
};
/**
* Groups and sorts the given action tree layer
*
* @param {type} _layer
* @param {type} _links
* @param {type} _parentGroup
*/
ai._groupLayers = function(_layer, _links, _parentGroup)
{
// Seperate the multiple groups out of the layer
var link_groups = {};
for (var i = 0; i < _layer.children.length; i++)
{
var actionObj = _layer.children[i].action;
// Check whether the link group of the current element already exists,
// if not, create the group
var grp = actionObj.group;
if (typeof link_groups[grp] == "undefined")
{
link_groups[grp] = [];
}
// Search the link data for this action object if none is found,
// visible and enabled = true is assumed
var visible = true;
var enabled = true;
if (typeof _links[actionObj.id] != "undefined")
{
visible = _links[actionObj.id].visible;
enabled = _links[actionObj.id].enabled;
}
// Insert the element in order
var inserted = false;
var groupObj = {
"actionObj": actionObj,
"visible": visible,
"enabled": enabled,
"groups": []
};
for (var j = 0; j < link_groups[grp].length; j++)
{
var elem = link_groups[grp][j].actionObj;
if (elem.order > actionObj.order)
{
inserted = true;
link_groups[grp].splice(j, 0, groupObj);
break;
}
}
// If the object hasn't been inserted, add it to the end of the list
if (!inserted)
{
link_groups[grp].push(groupObj);
}
// If this child itself has children, group those elements too
if (_layer.children[i].children.length > 0)
{
this._groupLayers(_layer.children[i], _links, groupObj);
}
}
// Transform the link_groups object into an sorted array
var groups = [];
for (var k in link_groups)
{
groups.push({"grp": k, "links": link_groups[k]});
}
groups.sort(function(a, b) {
var ia = parseInt(a.grp);
var ib = parseInt(b.grp);
return (ia > ib) ? 1 : ((ia < ib) ? -1 : 0);
});
// Append the groups to the groups2 array
var groups2 = [];
for (var i = 0; i < groups.length; i++)
{
groups2.push(groups[i].links);
}
_parentGroup.groups = groups2;
};
/**
* Build the menu layers
*
* @param {type} _menu
* @param {type} _groups
* @param {type} _selected
* @param {type} _enabled
* @param {type} _target
*/
ai._buildMenuLayer = function(_menu, _groups, _selected, _enabled, _target)
{
var firstGroup = true;
for (var i = 0; i < _groups.length; i++)
{
var firstElem = true;
// Go through the elements of each group
for (var j = 0; j < _groups[i].length; j++)
{
var link = _groups[i][j];
if (link.visible)
{
// Add an seperator after each group
if (!firstGroup && firstElem)
{
_menu.addItem("", "-");
}
firstElem = false;
var item = _menu.addItem(link.actionObj.id, link.actionObj.caption,
link.actionObj.iconUrl);
item["default"] = link.actionObj["default"];
// As this code is also used when a drag-drop popup menu is built,
// we have to perform this check
if (link.actionObj.type == "popup")
{
item.set_hint(link.actionObj.hint);
item.set_checkbox(link.actionObj.checkbox);
item.set_checked(link.actionObj.checked);
if(link.actionObj.checkbox && link.actionObj.isChecked)
{
item.set_checked(link.actionObj.isChecked.exec(link.actionObj, _selected));
}
item.set_groupIndex(link.actionObj.radioGroup);
if (link.actionObj.shortcut && !egwIsMobile())
{
var sc = link.actionObj.shortcut;
item.set_shortcutCaption(sc.caption);
}
}
item.set_data(link.actionObj);
if (link.enabled && _enabled)
{
item.set_onClick(function(elem) {
// Pass the context
elem.data.menu_context = ai._context;
// Copy the "checked" state
if (typeof elem.data.checked != "undefined")
{
elem.data.checked = elem.checked;
}
elem.data.execute(_selected, _target);
if (typeof elem.data.checkbox != "undefined" && elem.data.checkbox)
{
return elem.data.checked;
}
});
}
else
{
item.set_enabled(false);
}
// Append the parent groups
if (link.groups)
{
this._buildMenuLayer(item, link.groups, _selected, link.enabled, _target);
}
}
}
firstGroup = firstGroup && firstElem;
}
};
/**
* Builds the context menu from the given action links
*
* @param {type} _links
* @param {type} _selected
* @param {type} _target
* @returns {egwMenu|egwActionImplementation._buildMenu.menu}
*/
ai._buildMenu = function(_links, _selected, _target)
{
// Build a tree containing all actions
var tree = {"root": []};
// Automatically add in Drag & Drop actions
if(this.auto_paste && !egwIsMobile())
{
this._addCopyPaste(_links,_selected);
}
for (var k in _links)
{
_links[k].actionObj.appendToTree(tree);
}
// We need the dummy object container in order to pass the array by
// reference
var groups = {
"groups": []
};
if (tree.root.length > 0)
{
// Sort every action object layer by the given sort position and grouping
this._groupLayers(tree.root[0], _links, groups);
}
var menu = new egwMenu();
// Build the menu layers
this._buildMenuLayer(menu, groups.groups, _selected, true, _target);
return menu;
};
ai._getPageXY = function getPageXY(event)
{
// document.body.scrollTop does not work in IE
var scrollTop = document.body.scrollTop ? document.body.scrollTop :
document.documentElement.scrollTop;
var scrollLeft = document.body.scrollLeft ? document.body.scrollLeft :
document.documentElement.scrollLeft;
return {'posx': (event.clientX + scrollLeft), 'posy': (event.clientY + scrollTop)};
};
/**
* Automagically add in context menu items for copy and paste from
* drag and drop actions, based on current clipboard and the accepted types
*
* @param {object[]} _links Actions for inclusion in the menu
* @param {egwActionObject[]} _selected Currently selected entries
*/
ai._addCopyPaste = function (_links, _selected)
{
// Get a list of drag & drop actions
var drag = _selected[0].getSelectedLinks('drag').links;
var drop = _selected[0].getSelectedLinks('drop').links;
// No drags & no drops means early exit (only by default added egw_cancel_drop does NOT count!)
if ((!drag || jQuery.isEmptyObject(drag)) &&
(!drop || jQuery.isEmptyObject(drop) ||
Object.keys(drop).length === 1 && typeof drop.egw_cancel_drop !== 'undefined'))
{
return;
}
// Find existing actions so we don't get copies
var mgr = _selected[0].manager;
var copy_action = mgr.getActionById('egw_copy');
var add_action = mgr.getActionById('egw_copy_add');
var clipboard_action = mgr.getActionById('egw_os_clipboard');
var paste_action = mgr.getActionById('egw_paste');
// Fake UI so we can simulate the position of the drop
var ui = {
position: {top: 0, left: 0},
offset: {top: 0, left: 0}
};
if(this._context.event)
{
var event = this._context.event.originalEvent;
ui.position = {top: event.pageY, left: event.pageX};
ui.offset = {top: event.offsetY, left: event.offsetX};
}
// Create default copy menu action
if(drag && !jQuery.isEmptyObject(drag))
{
// Don't re-add if it's there
if(copy_action == null)
{
// Create a drag action that allows linking
copy_action = mgr.addAction('popup', 'egw_copy', egw.lang('Copy to clipboard'), egw.image('copy'), function(action, selected) {
// Copied, now add to clipboard
var clipboard = {
type:[],
selected:[]
};
// When pasting we need to know the type of drag
for(var k in drag)
{
if(drag[k].enabled && drag[k].actionObj.dragType.length > 0)
{
clipboard.type = clipboard.type.concat(drag[k].actionObj.dragType);
}
}
clipboard.type = jQuery.unique(clipboard.type);
// egwAction is a circular structure and can't be stringified so just take what we want
// Hopefully that's enough for the action handlers
for(var k in selected)
{
if(selected[k].id) clipboard.selected.push({id:selected[k].id, data:selected[k].data});
}
// Save it in session
egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify(clipboard));
},true);
copy_action.group = 2.5;
}
if(add_action == null)
{
// Create an action to add selected to clipboard
add_action = mgr.addAction('popup', 'egw_copy_add', egw.lang('Add to clipboard'), egw.image('copy'), function(action, selected) {
// Copied, now add to clipboard
var clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || {
type:[],
selected:[]
};
// When pasting we need to know the type of drag
for(var k in drag)
{
if(drag[k].enabled && drag[k].actionObj.dragType.length > 0)
{
clipboard.type = clipboard.type.concat(drag[k].actionObj.dragType);
}
}
clipboard.type = jQuery.unique(clipboard.type);
// egwAction is a circular structure and can't be stringified so just take what we want
// Hopefully that's enough for the action handlers
for(var k in selected)
{
if(selected[k].id) clipboard.selected.push({id:selected[k].id, data:selected[k].data});
}
// Save it in session
egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify(clipboard));
},true);
add_action.group = 2.5;
}
if(clipboard_action == null)
{
// Create an action to add selected to clipboard
clipboard_action = mgr.addAction('popup', 'egw_os_clipboard', egw.lang('Copy to OS clipboard'), egw.image('copy'), function(action) {
if(document.queryCommandSupported('copy'))
{
jQuery(action.data.target).trigger('copy');
}
},true);
clipboard_action.group = 2.5;
}
let os_clipboard_caption = "";
if(this._context.event)
{
os_clipboard_caption = this._context.event.originalEvent.target.innerText.trim();
clipboard_action.set_caption(egw.lang('Copy "%1"', os_clipboard_caption.length > 20 ? os_clipboard_caption.substring(0, 20) + '...' : os_clipboard_caption));
clipboard_action.data.target = this._context.event.originalEvent.target;
}
jQuery(clipboard_action.data.target).off('copy').on('copy', function(event) {
// Cancel any no-select css
var target = jQuery(clipboard_action.data.target);
var old_select = target.css('user-select');
target.css('user-select','all');
var range = document.createRange();
range.selectNode(clipboard_action.data.target);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
target.css('user-select',old_select);
var successful = false;
try {
// detect we are in IE via checking setActive, since it's
// only supported in IE, and make sure there's clipboardData object
if (typeof event.target.setActive !='undefined' && window.clipboardData)
{
window.clipboardData.setData('Text', jQuery(clipboard_action.data.target).text().trim());
}
if(event.clipboardData)
{
event.clipboardData.setData('text/plain', jQuery(clipboard_action.data.target).text().trim());
event.clipboardData.setData('text/html', jQuery(clipboard_action.data.target).html());
}
// Show fail message, just in case
egw.message(egw.lang('Use Ctrl-C/Cmd-C to copy'));
successful = document.execCommand('copy');
} catch(err) {}
if(successful)
{
// Clear fail message
egw.message('');
window.getSelection().removeAllRanges();
target.css('user-select',old_select);
return false;
}
});
if(typeof _links[copy_action.id] == 'undefined')
{
_links[copy_action.id] = {
"actionObj": copy_action,
"enabled": true,
"visible": true,
"cnt": 0
};
}
if(typeof _links[add_action.id] == 'undefined')
{
_links[add_action.id] = {
"actionObj": add_action,
"enabled": true,
"visible": true,
"cnt": 0
};
}
if(typeof _links[clipboard_action.id] == 'undefined')
{
_links[clipboard_action.id] = {
"actionObj": clipboard_action,
"enabled": os_clipboard_caption.length > 0,
"visible": os_clipboard_caption.length > 0,
"cnt": 0
};
}
}
// Create default paste menu item
if(drop && !jQuery.isEmptyObject(drop))
{
// Create paste action
// This injects the clipboard data and calls the original handler
var paste_exec = function(action, selected) {
// Add in clipboard as a sender
var clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard'));
// Fake drop position
drop[action.id].actionObj.ui = ui;
// Set a flag so apps can tell the difference, if they need to
drop[action.id].actionObj.paste = true;
drop[action.id].actionObj.execute(clipboard.selected,selected[0]);
drop[action.id].actionObj.paste = false;
};
var clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || {
type:[],
selected:[]
};
// Don't re-add if action already exists
if(paste_action == null)
{
paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), paste_exec,true);
paste_action.group = 2.5;
paste_action.order = 9;
paste_action.canHaveChildren.push('drop');
}
// Set hint to something resembling current clipboard
var hint = egw.lang('Clipboard') + ":\n";
paste_action.set_hint(hint);
// Add titles of entries
for(var i = 0; i < clipboard.selected.length; i++)
{
var id = clipboard.selected[i].id.split('::');
egw.link_title(id[0],id[1],function(title) {if(title)this.hint += title+"\n";},paste_action);
}
// Add into links so it's included in menu
if(paste_action && paste_action.enabled.exec(paste_action, clipboard.selected, _selected[0]))
{
if(typeof _links[paste_action.id] == 'undefined')
{
_links[paste_action.id] = {
"actionObj": paste_action,
"enabled": false,
"visible": clipboard != null,
"cnt": 0
};
}
while(paste_action.children.length > 0)
{
paste_action.children[0].remove();
}
// If nothing [valid] in the clipboard, don't bother with children
if(clipboard == null || typeof clipboard.type != 'object')
{
return;
}
// Add in actual actions as children
for(var k in drop)
{
// Add some choices - need to be a copy, or they interfere with
// the original
var drop_clone = jQuery.extend({},drop[k].actionObj);
var parent = paste_action.parent === drop_clone.parent ? paste_action : (paste_action.getActionById(drop_clone.parent.id) || paste_action);
drop_clone.parent = parent;
drop_clone.onExecute = new egwFnct(this, null, []);
drop_clone.children = [];
drop_clone.set_onExecute(paste_exec);
parent.children.push(drop_clone);
parent.allowOnMultiple = paste_action.allowOnMultiple && drop_clone.allowOnMultiple;
_links[k] = jQuery.extend({},drop[k]);
_links[k].actionObj = drop_clone;
// Drop is allowed if clipboard types intersect drop types
_links[k].enabled = false;
_links[k].visible = false;
for (var i = 0; i < drop_clone.acceptedTypes.length; i++)
{
if (clipboard.type.indexOf(drop_clone.acceptedTypes[i]) != -1)
{
_links[paste_action.id].enabled = true;
_links[k].enabled = true;
_links[k].visible = true;
break;
}
}
}
}
}
};
return ai;
}