/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ RED.group = (function() { var _groupEditTemplate = ''; var colorPalette = [ "#ff0000", "#ffC000", "#ffff00", "#92d04f", "#0070c0", "#001f60", "#6f2fa0", "#000000", "#777777" ] var colorSteps = 3; var colorCount = colorPalette.length; for (var i=0,len=colorPalette.length*colorSteps;i 1); } } RED.menu.setDisabled("menu-item-group-group", locked || !activateGroup); RED.menu.setDisabled("menu-item-group-ungroup", locked || !activateUngroup); RED.menu.setDisabled("menu-item-group-merge", locked || !activateMerge); RED.menu.setDisabled("menu-item-group-remove", locked || !activateRemove); RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected); RED.menu.setDisabled("menu-item-edit-paste-group-style", locked || !activateUngroup); }); RED.actions.add("core:group-selection", function() { groupSelection() }) RED.actions.add("core:ungroup-selection", function() { ungroupSelection() }) RED.actions.add("core:merge-selection-to-group", function() { mergeSelection() }) RED.actions.add("core:remove-selection-from-group", function() { removeSelection() }) RED.actions.add("core:copy-group-style", function() { copyGroupStyle() }); RED.actions.add("core:paste-group-style", function() { pasteGroupStyle() }); $(_groupEditTemplate).appendTo("#red-ui-editor-node-configs"); var groupStyleDiv = $("
",{ class:"red-ui-flow-group-body", style: "position: absolute; top: -1000px;" }).appendTo(document.body); var groupStyle = getComputedStyle(groupStyleDiv[0]); defaultGroupStyle = { stroke: convertColorToHex(groupStyle.stroke), "stroke-opacity": groupStyle.strokeOpacity, fill: convertColorToHex(groupStyle.fill), "fill-opacity": groupStyle.fillOpacity, label: true, "label-position": "nw" } groupStyleDiv.remove(); groupStyleDiv = $("
",{ class:"red-ui-flow-group-label", style: "position: absolute; top: -1000px;" }).appendTo(document.body); groupStyle = getComputedStyle(groupStyleDiv[0]); defaultGroupStyle.color = convertColorToHex(groupStyle.fill); groupStyleDiv.remove(); } function convertColorToHex(c) { var m = /^rgb\((\d+), (\d+), (\d+)\)$/.exec(c); if (m) { var s = ((parseInt(m[1])<<16) + (parseInt(m[2])<<8) + parseInt(m[3])).toString(16) return '#'+'000000'.slice(0, 6-s.length)+s; } return c; } var groupStyleClipboard; function copyGroupStyle() { if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].type === 'group') { groupStyleClipboard = JSON.parse(JSON.stringify(selection.nodes[0].style)); RED.notify(RED._("clipboard.groupStyleCopied"),{id:"clipboard"}) RED.menu.setDisabled("menu-item-edit-paste-group-style", false) } } function pasteGroupStyle() { if (RED.workspaces.isLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } if (groupStyleClipboard) { var selection = RED.view.selection(); if (selection.nodes) { var historyEvent = { t:'multi', events:[], dirty: RED.nodes.dirty() } selection.nodes.forEach(function(n) { if (n.type === 'group') { historyEvent.events.push({ t: "edit", node: n, changes: { style: JSON.parse(JSON.stringify(n.style)) }, dirty: RED.nodes.dirty() }); n.style = JSON.parse(JSON.stringify(groupStyleClipboard)); n.dirty = true; } }) if (historyEvent.events.length > 0) { RED.history.push(historyEvent); RED.nodes.dirty(true); RED.view.redraw(); } } } } function groupSelection() { if (RED.workspaces.isLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { var group = createGroup(selection.nodes); if (group) { var historyEvent = { t:"createGroup", groups: [ group ], dirty: RED.nodes.dirty() } RED.history.push(historyEvent); RED.view.select({nodes:[group]}); RED.nodes.dirty(true); RED.view.focus(); } } } function ungroupSelection() { if (RED.workspaces.isLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { var newSelection = []; let groups = selection.nodes.filter(function(n) { return n.type === "group" }); var historyEvent = { t:"ungroup", groups: [ ], dirty: RED.nodes.dirty() } groups.forEach(function(g) { newSelection = newSelection.concat(ungroup(g)) historyEvent.groups.push(g); }) RED.history.push(historyEvent); RED.view.select({nodes:newSelection}) RED.nodes.dirty(true); RED.view.focus(); } } function ungroup(g) { if (RED.workspaces.isLocked()) { return } var nodes = []; var parentGroup = RED.nodes.group(g.g); g.nodes.forEach(function(n) { nodes.push(n); if (parentGroup) { // Move nodes to parent group n.g = parentGroup.id; parentGroup.nodes.push(n); parentGroup.dirty = true; n.dirty = true; } else { delete n.g; } if (n.type === 'group') { RED.events.emit("groups:change",n) } else if (n.type !== 'junction') { RED.events.emit("nodes:change",n) } else { RED.events.emit("junctions:change",n) } }) RED.nodes.removeGroup(g); return nodes; } function mergeSelection() { if (RED.workspaces.isLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { var nodes = []; var historyEvent = { t: "multi", events: [] } var ungroupHistoryEvent = { t: "ungroup", groups: [] } var n; var parentGroup; // First pass, check they are all in the same parent // TODO: DRY mergeSelection,removeSelection,... for (var i=0; i 0) { n.env.forEach(env => { mergedEnv[env.name] = env }) } ungroupHistoryEvent.groups.push(n); nodes = nodes.concat(ungroup(n)); } else { nodes.push(n); } n.dirty = true; } if (ungroupHistoryEvent.groups.length > 0) { historyEvent.events.push(ungroupHistoryEvent); } // Finally, create the new group var group = createGroup(nodes); if (group) { if (existingGroup) { group.style = existingGroup.style; group.name = existingGroup.name; } group.env = Object.values(mergedEnv) RED.view.select({nodes:[group]}) } historyEvent.events.push({ t:"createGroup", groups: [ group ], dirty: RED.nodes.dirty() }); RED.history.push(historyEvent); RED.nodes.dirty(true); RED.view.focus(); } } function removeSelection() { if (RED.workspaces.isLocked()) { return } if (RED.view.state() !== RED.state.DEFAULT) { return } var selection = RED.view.selection(); if (selection.nodes) { var nodes = []; var n; var parentGroup = RED.nodes.group(selection.nodes[0].g); if (parentGroup) { try { removeFromGroup(parentGroup,selection.nodes,true); var historyEvent = { t: "removeFromGroup", dirty: RED.nodes.dirty(), group: parentGroup, nodes: selection.nodes } RED.history.push(historyEvent); RED.nodes.dirty(true); } catch(err) { RED.notify(err,"error"); return; } } RED.view.select({nodes:selection.nodes}) RED.view.focus(); } } function createGroup(nodes) { if (RED.workspaces.isLocked()) { return } if (nodes.length === 0) { return; } const existingGroup = nodes[0].g for (let i = 0; i < nodes.length; i++) { const n = nodes[i] if (n.type === 'subflow') { RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error"); return; } if (n.g !== existingGroup) { console.warn("Cannot add nooes with different z properties") return } } // nodes is an array // each node must be on the same tab (z) var group = { id: RED.nodes.id(), type: 'group', nodes: [], style: JSON.parse(JSON.stringify(defaultGroupStyle)), x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY, w: 0, h: 0, _def: RED.group.def, changed: true } group.z = nodes[0].z; group = RED.nodes.addGroup(group); if (existingGroup) { addToGroup(RED.nodes.group(existingGroup), group) } try { addToGroup(group,nodes); } catch(err) { RED.notify(err,"error"); return; } return group; } function addToGroup(group,nodes) { if (!Array.isArray(nodes)) { nodes = [nodes]; } var i,n,z; var g; // First pass - validate we can safely add these nodes to the group for (i=0;i -1) { g.nodes.splice(ni,1) } } n.g = group.id; n.dirty = true; group.nodes.push(n); group.x = Math.min(group.x,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0)); group.y = Math.min(group.y,n.y-n.h/2-25); group.w = Math.max(group.w,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0) - group.x); group.h = Math.max(group.h,n.y+n.h/2+25-group.y); if (n.type === 'group') { RED.events.emit("groups:change",n) } else if (n.type !== 'junction') { RED.events.emit("nodes:change",n) } else { RED.events.emit("junctions:change",n) } } } if (g) { RED.events.emit("groups:change",group) } markDirty(group); } function removeFromGroup(group, nodes, reparent) { if (RED.workspaces.isLocked()) { return } if (!Array.isArray(nodes)) { nodes = [nodes]; } var n; // First pass, check they are all in the same parent // TODO: DRY mergeSelection,removeSelection,... for (var i=0; i",{style:"display:inline-block"}); var layoutHiddenInput = $("", { id: options.id, type: "hidden", value: options.value }).appendTo(container); var layoutButton = $('