mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-15 11:11:49 +01:00
846 lines
32 KiB
TypeScript
846 lines
32 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 {_egw_active_menu, egwMenu, egwMenuItem} from "./egw_menu";
|
|
import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants";
|
|
import {tapAndSwipe} from "../tapandswipe";
|
|
import {EgwFnct} from "./egw_action_common";
|
|
import "./egwGlobal"
|
|
import {EgwActionImplementation} from "./EgwActionImplementation";
|
|
import {EgwActionObject} from "./EgwActionObject";
|
|
import {EgwPopupAction} from "./EgwPopupAction";
|
|
import {egw} from "../jsapi/egw_global";
|
|
import {FindActionTarget} from "../etemplate/FindActionTarget";
|
|
|
|
export class EgwPopupActionImplementation implements EgwActionImplementation {
|
|
type = "popup";
|
|
auto_paste = true;
|
|
parent?: FindActionTarget //currently only implemented by Et2Tree
|
|
|
|
registerAction = (_aoi, _callback, _context) => {
|
|
const node = _aoi.getDOMNode();
|
|
let parentNode = null;
|
|
let parentAO = null;
|
|
let isNew = false;
|
|
|
|
// 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;
|
|
isNew = true;
|
|
}
|
|
if(typeof _aoi.handlers == "undefined")
|
|
{
|
|
_aoi.handlers = {};
|
|
}
|
|
if(typeof _aoi.handlers[this.type] == "undefined")
|
|
{
|
|
_aoi.handlers[this.type] = [];
|
|
}
|
|
|
|
if(_aoi.handlers[this.type].length == 0)
|
|
{
|
|
_aoi.handlers[this.type].push({type: 'contextmenu', listener: _callback});
|
|
if(isNew)
|
|
{
|
|
//if a parent is available the context menu Event-listener will only be bound once on the parent
|
|
this._registerDefault(parentNode, _callback, parentAO);
|
|
this._registerContext(parentNode, _callback, parentAO);
|
|
|
|
return true;
|
|
}
|
|
else if(node && !parentNode)
|
|
{
|
|
this._registerDefault(node, _callback, _context);
|
|
this._registerContext(node, _callback, _context);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
|
|
};
|
|
|
|
unregisterAction = function (_aoi) {
|
|
const node = _aoi.getDOMNode();
|
|
//TODO jQuery replacement
|
|
jQuery(node).off();
|
|
|
|
// 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 {object} _context
|
|
* @param {type} _selected
|
|
* @param {type} _links
|
|
* @param {type} _target
|
|
* @returns {Boolean}
|
|
*/
|
|
executeImplementation = (_context, _selected, _links, _target)=> {
|
|
if (typeof _target == "undefined") {
|
|
_target = null;
|
|
}
|
|
|
|
this._context = _context;
|
|
if (typeof _context == "object" && typeof _context.keyEvent == "object") {
|
|
return this._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
|
|
let node = _context;
|
|
|
|
const x = jQuery(node).offset().left;
|
|
const y = jQuery(node).offset().top;
|
|
|
|
_context = {"posx": x, "posy": y};
|
|
}
|
|
|
|
const menu = this._buildMenu(_links, _selected, _target);
|
|
menu.showAt(_context.posx, _context.posy);
|
|
|
|
return true;
|
|
} else {
|
|
const defaultAction = this._getDefaultLink(_links);
|
|
if (defaultAction) {
|
|
defaultAction.execute(_selected);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Registers the handler for the default action
|
|
*
|
|
* @param {any} _node
|
|
* @param {function} _callback
|
|
* @param {object} _context
|
|
* @returns {boolean}
|
|
*/
|
|
private _registerDefault = (_node, _callback, _context)=> {
|
|
const defaultHandler = (e)=> {
|
|
const x = _node
|
|
//use different node and context for callback if event happens on parent
|
|
let nodeToUse;
|
|
let contextToUse;
|
|
if (x.findActionTarget)
|
|
{
|
|
const y = x.findActionTarget(e);
|
|
nodeToUse = y?.target;
|
|
contextToUse = y?.action;
|
|
e.originalEvent = e;
|
|
|
|
}
|
|
//allow bubbling of the expand folder event
|
|
//do not stop bubbling of events if the event is supposed to be handled by the et2-tree
|
|
if (window.egwIsMobile() && (nodeToUse || e.currentTarget).tagName == "SL-TREE-ITEM") return true;
|
|
// a tag should be handled by default event
|
|
// Prevent bubbling bound event on <a> tag, on touch devices
|
|
if (window.egwIsMobile() && (nodeToUse || 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") {
|
|
const sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
}
|
|
|
|
if (!
|
|
((contextToUse || _context).manager.getActionsByAttr('singleClick', true).length > 0 &&
|
|
(nodeToUse || e.target).classList.contains('et2_clickable')
|
|
)
|
|
)
|
|
{
|
|
_callback.call(contextToUse || _context, "default", this);
|
|
}
|
|
|
|
// 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 (window.egwIsMobile() || _context.manager.getActionsByAttr('singleClick', true).length > 0) {
|
|
_node.addEventListener('click',defaultHandler)//jQuery(_node).on('click', defaultHandler);
|
|
} else {
|
|
_node.ondblclick = defaultHandler;
|
|
}
|
|
};
|
|
|
|
private _getDefaultLink = function (_links) {
|
|
let defaultAction = null;
|
|
for (const k in _links) {
|
|
if (_links[k].actionObj["default"] && _links[k].enabled) {
|
|
defaultAction = _links[k].actionObj;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return defaultAction;
|
|
};
|
|
|
|
private _searchShortcut = (_key, _objs, _links) => {
|
|
for (const item of _objs) {
|
|
const shortcut = item.shortcut;
|
|
if (shortcut && shortcut.keyCode == _key.keyCode && shortcut.shift == _key.shift &&
|
|
shortcut.ctrl == _key.ctrl && shortcut.alt == _key.alt &&
|
|
item.type == "popup" && (typeof _links[item.id] == "undefined" ||
|
|
_links[item.id].enabled)) {
|
|
return item;
|
|
}
|
|
|
|
const obj = this._searchShortcut(_key, item.children, _links);
|
|
if (obj) {
|
|
return obj;
|
|
}
|
|
}
|
|
};
|
|
|
|
private _searchShortcutInLinks = (_key, _links)=> {
|
|
const objs = [];
|
|
for (const k in _links) {
|
|
if (_links[k].enabled) {
|
|
objs.push(_links[k].actionObj);
|
|
}
|
|
}
|
|
|
|
return this._searchShortcut(_key, objs, _links);
|
|
};
|
|
|
|
/**
|
|
* Handles a key press
|
|
*
|
|
* @param {object} _key
|
|
* @param {type} _selected
|
|
* @param {type} _links
|
|
* @param {type} _target
|
|
* @returns {Boolean}
|
|
*/
|
|
private _handleKeyPress = (_key, _selected, _links, _target) => {
|
|
// Handle the default
|
|
if (_key.keyCode == EGW_KEY_ENTER && !_key.ctrl && !_key.shift && !_key.alt) {
|
|
const defaultAction = this._getDefaultLink(_links);
|
|
if (defaultAction) {
|
|
defaultAction.execute(_selected);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Menu button
|
|
if (_key.keyCode == EGW_KEY_MENU && !_key.ctrl) {
|
|
return this.executeImplementation({posx: 0, posy: 0}, _selected, _links, _target);
|
|
}
|
|
|
|
|
|
// Check whether the given shortcut exists
|
|
const obj = this._searchShortcutInLinks(_key, _links);
|
|
if (obj) {
|
|
obj.execute(_selected);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
private _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 necessary 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: 1000,
|
|
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);
|
|
}
|
|
});
|
|
// bind a custom event tapandhold to be able to call it from nm action button
|
|
_node.addEventListener('tapandhold', _event => {
|
|
_callback(_event)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Registers the handler for the context menu
|
|
*
|
|
* @param {any} _node
|
|
* @param {function} _callback
|
|
* @param {object} _context
|
|
* @returns {boolean}
|
|
*/
|
|
private _registerContext = (_node, _callback, _context) => {
|
|
|
|
const contextHandler = (e) => {
|
|
const x = _node
|
|
//use different node and context for callback if event happens on parent
|
|
let nodeToUse;
|
|
let contextToUse;
|
|
if (x.findActionTarget)
|
|
{
|
|
const y = x.findActionTarget(e);
|
|
nodeToUse = y?.target;
|
|
contextToUse = y?.action;
|
|
e.originalEvent = e;
|
|
}
|
|
//Obtain the event object, this should not happen at any point
|
|
if (!e) {
|
|
e = window.event;
|
|
}
|
|
|
|
// Close any open tooltip so they don't get in the way
|
|
egw(window).tooltipCancel();
|
|
|
|
if (_egw_active_menu)
|
|
{
|
|
_egw_active_menu.hide();
|
|
} else if (!e.ctrlKey && e.which == 3 || e.which === 0 || e.type === 'tapandhold') // tap event indicates by 0
|
|
{
|
|
const _xy = this._getPageXY(e);
|
|
const _implContext = {
|
|
event: e, posx: _xy.posx,
|
|
posy: _xy.posy,
|
|
innerText: nodeToUse?.title || _node.innerText,//nodeToUse only exists on widgets that define findActionTarget
|
|
target: nodeToUse || _node,
|
|
};
|
|
_callback.call(contextToUse || _context, _implContext, this);
|
|
}
|
|
|
|
e.cancelBubble = !e.ctrlKey || e.which == 1;
|
|
if (e.stopPropagation && e.cancelBubble) {
|
|
e.stopPropagation();
|
|
e.preventDefault()
|
|
}
|
|
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 (!window.egwIsMobile())
|
|
{
|
|
_node.addEventListener('contextmenu', contextHandler);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Groups and sorts the given action tree layer
|
|
*
|
|
* @param {type} _layer
|
|
* @param {type} _links
|
|
* @param {type} _parentGroup
|
|
*/
|
|
private _groupLayers = (_layer, _links, _parentGroup) => {
|
|
// Separate the multiple groups out of the layer
|
|
const link_groups = {};
|
|
|
|
for (let i = 0; i < _layer.children.length; i++) {
|
|
const popupAction:EgwPopupAction = _layer.children[i].action;
|
|
|
|
// Check whether the link group of the current element already exists,
|
|
// if not, create the group
|
|
const grp = popupAction.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
|
|
let visible = true;
|
|
let enabled = true;
|
|
|
|
if (typeof _links[popupAction.id] != "undefined") {
|
|
visible = _links[popupAction.id].visible;
|
|
enabled = _links[popupAction.id].enabled;
|
|
}
|
|
|
|
// Insert the element in order
|
|
let inserted = false;
|
|
const groupObj = {
|
|
"actionObj": popupAction,
|
|
"visible": visible,
|
|
"enabled": enabled,
|
|
"groups": []
|
|
};
|
|
|
|
for (let j = 0; j < link_groups[grp].length; j++) {
|
|
const elem:EgwPopupAction = link_groups[grp][j].actionObj;
|
|
if (elem.order > popupAction.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 a sorted array
|
|
const groups = [];
|
|
|
|
for (const k in link_groups) {
|
|
groups.push({"grp": k, "links": link_groups[k]});
|
|
}
|
|
|
|
groups.sort(function (a, b) {
|
|
const ia = parseInt(a.grp);
|
|
const ib = parseInt(b.grp);
|
|
return (ia > ib) ? 1 : ((ia < ib) ? -1 : 0);
|
|
});
|
|
|
|
// Append the groups to the groups2 array
|
|
const groups2 = [];
|
|
for (const item of groups) {
|
|
groups2.push(item.links);
|
|
}
|
|
|
|
_parentGroup.groups = groups2;
|
|
};
|
|
|
|
/**
|
|
* Build the menu layers
|
|
*
|
|
* @param {type} _menu
|
|
* @param {type} _groups
|
|
* @param {type} _selected
|
|
* @param {type} _enabled
|
|
* @param {type} _target
|
|
*/
|
|
private _buildMenuLayer = (_menu, _groups, _selected, _enabled, _target) => {
|
|
let firstGroup = true;
|
|
|
|
for (const item1 of _groups) {
|
|
let firstElem = true;
|
|
|
|
// Go through the elements of each group
|
|
for (const link of item1) {
|
|
if (link.visible) {
|
|
// Add a separator after each group
|
|
if (!firstGroup && firstElem) {
|
|
_menu.addItem("", "-");
|
|
}
|
|
firstElem = false;
|
|
|
|
const item:egwMenuItem = _menu.addItem(link.actionObj.id, link.actionObj.caption,
|
|
link.actionObj.iconUrl, undefined, link.actionObj.color);
|
|
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 && !window.egwIsMobile()) {
|
|
const shortcut = link.actionObj.shortcut;
|
|
item.set_shortcutCaption(shortcut.caption);
|
|
}
|
|
}
|
|
|
|
item.set_data(link.actionObj);
|
|
if (link.enabled && _enabled) {
|
|
item.set_onClick( (elem) => {
|
|
// Pass the context
|
|
elem.data.menu_context = this._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}
|
|
*/
|
|
private _buildMenu = (_links, _selected, _target) => {
|
|
// Build a tree containing all actions
|
|
const tree = {"root": []};
|
|
|
|
// Automatically add in Drag & Drop actions
|
|
if(this.auto_paste && !window.egwIsMobile() && !this._context.event?.type.match(/touch/))
|
|
{
|
|
this._addCopyPaste(_links, _selected);
|
|
}
|
|
|
|
for (const k in _links) {
|
|
_links[k].actionObj.appendToTree(tree);
|
|
}
|
|
|
|
// We need the dummy object container in order to pass the array by
|
|
// reference
|
|
const 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);
|
|
}
|
|
|
|
const menu = new egwMenu();
|
|
|
|
// Build the menu layers
|
|
this._buildMenuLayer(menu, groups.groups, _selected, true, _target);
|
|
|
|
return menu;
|
|
};
|
|
|
|
_getPageXY = function getPageXY(event) {
|
|
// document.body.scrollTop does not work in IE
|
|
const scrollTop = document.body.scrollTop ? document.body.scrollTop :
|
|
document.documentElement.scrollTop;
|
|
const 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
|
|
*/
|
|
private _addCopyPaste = (_links, _selected:EgwActionObject[])=> {
|
|
// Get a list of drag & drop actions
|
|
const drag = _selected[0].getSelectedLinks('drag').links;
|
|
const 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
|
|
const mgr = _selected[0].manager;
|
|
let copy_action = mgr.getActionById('egw_copy');
|
|
let add_action = mgr.getActionById('egw_copy_add');
|
|
let clipboard_action = mgr.getActionById('egw_os_clipboard');
|
|
let paste_action = mgr.getActionById('egw_paste');
|
|
|
|
// Fake UI so we can simulate the position of the drop
|
|
const ui = {
|
|
position: {top: 0, left: 0},
|
|
offset: {top: 0, left: 0}
|
|
};
|
|
if (this._context.event) {
|
|
const event = this._context.event.originalEvent || this._context.event;
|
|
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', window.egw.lang('Copy to clipboard'), window.egw.image('copy'), function (action, selected) {
|
|
// Copied, now add to clipboard
|
|
const clipboard = {
|
|
type: [],
|
|
selected: []
|
|
};
|
|
|
|
// When pasting we need to know the type of drag
|
|
for (const 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.uniqueSort(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 (const k in selected) {
|
|
if(selected[k].id)
|
|
{
|
|
clipboard.selected.push({
|
|
id: selected[k].id,
|
|
data: {...(window.egw.dataGetUIDdata(selected[k].id)?.data ?? {}), ...selected[k].data}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Save it in session
|
|
window.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', window.egw.lang('Add to clipboard'), window.egw.image('copy'), function (action, selected) {
|
|
// Copied, now add to clipboard
|
|
const clipboard = JSON.parse(window.egw.getSessionItem('phpgwapi', 'egw_clipboard')) || {
|
|
type: [],
|
|
selected: []
|
|
};
|
|
|
|
// When pasting we need to know the type of drag
|
|
for (const k in drag) {
|
|
if (drag[k].enabled && drag[k].actionObj.dragType.length > 0) {
|
|
clipboard.type = clipboard.type.concat(drag[k].actionObj.dragType);
|
|
}
|
|
}
|
|
clipboard.type = [...new Set(clipboard.type)].sort();
|
|
// 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 (const k in selected) {
|
|
if(selected[k].id)
|
|
{
|
|
clipboard.selected.push({
|
|
id: selected[k].id,
|
|
data: {...(window.egw.dataGetUIDdata(selected[k].id)?.data ?? {}), ...selected[k].data}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Save it in session
|
|
window.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', window.egw.lang('Copy to OS clipboard'), window.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.target.innerText.trim()
|
|
clipboard_action.set_caption(window.egw.lang('Copy "%1"', os_clipboard_caption.length > 20 ? os_clipboard_caption.substring(0, 20) + '...' : os_clipboard_caption));
|
|
clipboard_action.data.target = this._context.target;
|
|
}
|
|
jQuery(clipboard_action.data.target).off('copy').on('copy', function (event) {
|
|
try {
|
|
window.egw.copyTextToClipboard(os_clipboard_caption, clipboard_action.data.target, event).then((successful) => {
|
|
// Fallback
|
|
if (typeof successful == "undefined") {
|
|
// Clear message
|
|
window.egw.message(window.egw.lang("'%1' copied to clipboard", os_clipboard_caption.length > 20 ? os_clipboard_caption.substring(0, 20) + '...' : os_clipboard_caption));
|
|
window.getSelection().removeAllRanges();
|
|
return false;
|
|
} else {
|
|
// Show fail message
|
|
window.egw.message(window.egw.lang('Use Ctrl-C/Cmd-C to copy'));
|
|
}
|
|
});
|
|
|
|
} catch (err) {
|
|
}
|
|
});
|
|
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
|
|
let paste_exec = function (action, selected) {
|
|
// Add in clipboard as a sender
|
|
let clipboard = JSON.parse(window.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;
|
|
};
|
|
|
|
let clipboard = JSON.parse(window.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', window.egw.lang('Paste'), window.egw.image('editpaste'), paste_exec, true);
|
|
paste_action.group = 2.5;
|
|
paste_action.order = 9;
|
|
if (typeof paste_action.canHaveChildren !== "boolean") {
|
|
paste_action.canHaveChildren.push('drop');
|
|
}
|
|
}
|
|
|
|
// Set hint to something resembling current clipboard
|
|
let hint = window.egw.lang('Clipboard') + ":\n";
|
|
paste_action.set_hint(hint);
|
|
// Add titles of entries
|
|
for (let i = 0; i < clipboard.selected.length; i++) {
|
|
let id = clipboard.selected[i].id.split('::');
|
|
window.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
|
|
// @ts-ignore exec uses arguments:IArguments and therefor can consume them even if ts does not know it
|
|
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 (let k in drop) {
|
|
// Add some choices - need to be a copy, or they interfere with
|
|
// the original
|
|
//replace jQuery with spread operator
|
|
// set the Prototype of the copy set_onExecute is not available otherwise
|
|
let drop_clone = drop[k].actionObj.clone()//Object.assign(Object.create(Object.getPrototypeOf(drop[k].actionObj)), drop[k].actionObj) //{...drop[k].actionObj};
|
|
//warning This method is really slow
|
|
//Object.setPrototypeOf(drop_clone, EgwAction.prototype)
|
|
let 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 (let 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
private _context: any;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @deprecated use uppercase class
|
|
*/
|
|
export class egwPopupActionImplementation extends EgwPopupActionImplementation{}
|
|
let _popupActionImpl = null;
|
|
|
|
export function getPopupImplementation(): EgwPopupActionImplementation {
|
|
if (!_popupActionImpl) {
|
|
_popupActionImpl = new EgwPopupActionImplementation();
|
|
}
|
|
return _popupActionImpl;
|
|
} |