RED.contextMenu = (function () { let menu; function disposeMenu() { $(document).off("mousedown.red-ui-workspace-context-menu"); if (menu) { menu.remove(); } menu = null; } function show(options) { if (menu) { menu.remove() } let menuItems = [] if (options.options) { menuItems = options.options } else if (options.type === 'workspace') { const selection = RED.view.selection() const noSelection = !selection || Object.keys(selection).length === 0 const hasSelection = (selection.nodes && selection.nodes.length > 0); const hasMultipleSelection = hasSelection && selection.nodes.length > 1; const virtulLinks = (selection.links && selection.links.filter(e => !!e.link)) || []; const wireLinks = (selection.links && selection.links.filter(e => !e.link)) || []; const hasLinks = wireLinks.length > 0; const isSingleLink = !hasSelection && hasLinks && wireLinks.length === 1 const isMultipleLinks = !hasSelection && hasLinks && wireLinks.length > 1 const canDelete = hasSelection || hasLinks const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group' const canEdit = !RED.workspaces.isLocked() const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g const isAllGroups = hasSelection && selection.nodes.filter(n => n.type !== 'group').length === 0 const hasGroup = hasSelection && selection.nodes.filter(n => n.type === 'group' ).length > 0 const offset = $("#red-ui-workspace-chart").offset() let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop() if (RED.view.snapGrid) { const gridSize = RED.view.gridSize() addX = gridSize * Math.floor(addX / gridSize) addY = gridSize * Math.floor(addY / gridSize) } menuItems.push( { onselect: 'core:show-action-list', onpostselect: function () { } } ) const insertOptions = [] menuItems.push({ label: RED._("contextMenu.insert"), options: insertOptions }) insertOptions.push( { label: RED._("contextMenu.node"), onselect: function () { RED.view.showQuickAddDialog({ position: [addX, addY], touchTrigger: true, splice: isSingleLink ? selection.links[0] : undefined, // spliceMultiple: isMultipleLinks }) }, disabled: !canEdit }, (hasLinks) ? { // has least 1 wire selected label: RED._("contextMenu.junction"), onselect: 'core:split-wires-with-junctions', disabled: !canEdit || !hasLinks } : { label: RED._("contextMenu.junction"), onselect: function () { const nn = { _def: { defaults: {} }, type: 'junction', z: RED.workspaces.active(), id: RED.nodes.id(), x: addX, y: addY, w: 0, h: 0, outputs: 1, inputs: 1, dirty: true, moved: true } const junction = RED.nodes.addJunction(nn); const historyEvent = { dirty: RED.nodes.dirty(), t: 'add', junctions: [junction] } RED.history.push(historyEvent); RED.nodes.dirty(true); RED.view.select({nodes: [junction] }); RED.view.redraw(true) }, disabled: !canEdit }, { label: RED._("contextMenu.linkNodes"), onselect: 'core:split-wire-with-link-nodes', disabled: !canEdit || !hasLinks }, null, { onselect: 'core:show-import-dialog', label: RED._('common.label.import')}, { onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') } ) if (hasSelection && canEdit) { const nodeOptions = [] if (!hasMultipleSelection && !isGroup) { nodeOptions.push( { onselect: 'core:show-node-help' }, null ) } nodeOptions.push( { onselect: 'core:enable-selected-nodes' }, { onselect: 'core:disable-selected-nodes' }, null, { onselect: 'core:show-selected-node-labels' }, { onselect: 'core:hide-selected-node-labels' } ) menuItems.push({ label: RED._('sidebar.info.node'), options: nodeOptions }) menuItems.push({ label: RED._('sidebar.info.group'), options: [ { onselect: 'core:group-selection' }, { onselect: 'core:ungroup-selection', disabled: !hasGroup }, ] }) if (hasGroup) { menuItems[menuItems.length - 1].options.push( { onselect: 'core:merge-selection-to-group', label: RED._("menu.label.groupMergeSelection") } ) } if (canRemoveFromGroup) { menuItems[menuItems.length - 1].options.push( { onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") } ) } menuItems[menuItems.length - 1].options.push( null, { onselect: 'core:copy-group-style', disabled: !hasGroup }, { onselect: 'core:paste-group-style', disabled: !hasGroup} ) } if (canEdit && hasMultipleSelection) { menuItems.push({ label: RED._('menu.label.arrange'), options: [ { label:RED._("menu.label.alignLeft"), onselect: "core:align-selection-to-left"}, { label:RED._("menu.label.alignCenter"), onselect: "core:align-selection-to-center"}, { label:RED._("menu.label.alignRight"), onselect: "core:align-selection-to-right"}, null, { label:RED._("menu.label.alignTop"), onselect: "core:align-selection-to-top"}, { label:RED._("menu.label.alignMiddle"), onselect: "core:align-selection-to-middle"}, { label:RED._("menu.label.alignBottom"), onselect: "core:align-selection-to-bottom"}, null, { label:RED._("menu.label.distributeHorizontally"), onselect: "core:distribute-selection-horizontally"}, { label:RED._("menu.label.distributeVertically"), onselect: "core:distribute-selection-vertically"} ] }) } menuItems.push( null, { onselect: 'core:undo', disabled: RED.history.list().length === 0 }, { onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 }, null, { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !canEdit || !hasSelection }, { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, { onselect: 'core:delete-selection', disabled: !canEdit || !canDelete }, { onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, { onselect: 'core:select-all-nodes' }, ) } var direction = "right"; var MENU_WIDTH = 500; // can not use menu width here if ((options.x -$(document).scrollLeft()) > ($(window).width() -MENU_WIDTH)) { direction = "left"; } menu = RED.menu.init({ direction: direction, onpreselect: function() { disposeMenu() }, onpostselect: function () { RED.view.focus() }, options: menuItems }); menu.attr("id", "red-ui-workspace-context-menu"); menu.css({ position: "absolute" }) menu.appendTo("body"); // TODO: prevent the menu from overflowing the window. var top = options.y var left = options.x if (top + menu.height() - $(document).scrollTop() > $(window).height()) { top -= (top + menu.height()) - $(window).height() + 22; } if (left + menu.width() - $(document).scrollLeft() > $(window).width()) { left -= (left + menu.width()) - $(window).width() + 18; } menu.css({ top: top + "px", left: left + "px" }) $(".red-ui-menu.red-ui-menu-dropdown").hide(); $(document).on("mousedown.red-ui-workspace-context-menu", function (evt) { if (menu && menu[0].contains(evt.target)) { return } disposeMenu() }); menu.show(); // set focus to first item so that pressing escape key closes the menu $("#red-ui-workspace-context-menu :first(ul) > a").trigger("focus") } // Allow escape key hook and other editor events to close context menu RED.keyboard.add("red-ui-workspace-context-menu", "escape", function () { RED.contextMenu.hide() }) RED.events.on("editor:open", function () { RED.contextMenu.hide() }); RED.events.on("search:open", function () { RED.contextMenu.hide() }); RED.events.on("type-search:open", function () { RED.contextMenu.hide() }); RED.events.on("actionList:open", function () { RED.contextMenu.hide() }); RED.events.on("view:selection-changed", function () { RED.contextMenu.hide() }); return { show: show, hide: disposeMenu } })()