/** * 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-ui-workspace-chart * \- "outer" * \- * \- .red-ui-workspace-chart-event-layer "eventLayer" * |- .red-ui-workspace-chart-background * |- .red-ui-workspace-chart-grid "gridLayer" * |- "groupLayer" * |- "linkLayer" * |- "dragGroupLayer" * \- "nodeLayer" */ RED.view = (function() { var DEBUG_EVENTS = false; 2 var space_width = 5000, space_height = 5000, lineCurveScale = 0.75, scaleFactor = 1, node_width = 100, node_height = 30; var touchLongPressTimeout = 1000, startTouchDistance = 0, startTouchCenter = [], moveTouchCenter = [], touchStartTime = 0; var workspaceScrollPositions = {}; var gridSize = 20; var snapGrid = false; var activeSpliceLink; var spliceActive = false; var spliceTimer; var groupHoverTimer; var activeSubflow = null; var activeNodes = []; var activeLinks = []; var activeFlowLinks = []; var activeLinkNodes = {}; var activeGroup = null; var activeHoverGroup = null; var activeGroups = []; var dirtyGroups = {}; var selected_link = null, mousedown_link = null, mousedown_node = null, mousedown_group = null, mousedown_port_type = null, mousedown_port_index = 0, mouseup_node = null, mouse_offset = [0,0], mouse_position = null, mouse_mode = 0, mousedown_group_handle = null; moving_set = [], lasso = null, ghostNode = null, showStatus = false, lastClickNode = null, dblClickPrimed = null, clickTime = 0, clickElapsed = 0, scroll_position = [], quickAddActive = false, quickAddLink = null, showAllLinkPorts = -1, groupNodeSelectPrimed = false; var selectNodesOptions; var clipboard = ""; // Note: these are the permitted status colour aliases. The actual RGB values // are set in the CSS - flow.scss/colors.scss var status_colours = { "red": "#c00", "green": "#5a8", "yellow": "#F9DF31", "blue": "#53A3F3", "grey": "#d3d3d3" } var PORT_TYPE_INPUT = 1; var PORT_TYPE_OUTPUT = 0; var chart; var outer; var eventLayer; var gridLayer; var linkLayer; var dragGroupLayer; var nodeLayer; var groupLayer; var drag_lines; function init() { chart = $("#red-ui-workspace-chart"); outer = d3.select("#red-ui-workspace-chart") .append("svg:svg") .attr("width", space_width) .attr("height", space_height) .attr("pointer-events", "all") .style("cursor","crosshair") .on("mousedown", function() { focusView(); }) .on("contextmenu", function(){ d3.event.preventDefault(); }); eventLayer = outer .append("svg:g") .on("dblclick.zoom", null) .append("svg:g") .attr('class','red-ui-workspace-chart-event-layer') .on("mousemove", canvasMouseMove) .on("mousedown", canvasMouseDown) .on("mouseup", canvasMouseUp) .on("mouseenter", function() { if (lasso) { if (d3.event.buttons !== 1) { lasso.remove(); lasso = null; } } else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) { resetMouseVars(); } }) .on("touchend", function() { clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { return; } canvasMouseUp.call(this); }) .on("touchcancel", canvasMouseUp) .on("touchstart", function() { var touch0; if (d3.event.touches.length>1) { clearTimeout(touchStartTime); touchStartTime = null; d3.event.preventDefault(); touch0 = d3.event.touches.item(0); var touch1 = d3.event.touches.item(1); var a = touch0["pageY"]-touch1["pageY"]; var b = touch0["pageX"]-touch1["pageX"]; var offset = chart.offset(); var scrollPos = [chart.scrollLeft(),chart.scrollTop()]; startTouchCenter = [ (touch1["pageX"]+(b/2)-offset.left+scrollPos[0])/scaleFactor, (touch1["pageY"]+(a/2)-offset.top+scrollPos[1])/scaleFactor ]; moveTouchCenter = [ touch1["pageX"]+(b/2), touch1["pageY"]+(a/2) ] startTouchDistance = Math.sqrt((a*a)+(b*b)); } else { var obj = d3.select(document.body); touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; startTouchCenter = [touch0.pageX,touch0.pageY]; startTouchDistance = 0; var point = d3.touches(this)[0]; touchStartTime = setTimeout(function() { touchStartTime = null; showTouchMenu(obj,pos); //lasso = eventLayer.append("rect") // .attr("ox",point[0]) // .attr("oy",point[1]) // .attr("rx",2) // .attr("ry",2) // .attr("x",point[0]) // .attr("y",point[1]) // .attr("width",0) // .attr("height",0) // .attr("class","nr-ui-view-lasso"); },touchLongPressTimeout); } }) .on("touchmove", function(){ if (RED.touch.radialMenu.active()) { d3.event.preventDefault(); return; } var touch0; if (d3.event.touches.length<2) { if (touchStartTime) { touch0 = d3.event.touches.item(0); var dx = (touch0.pageX-startTouchCenter[0]); var dy = (touch0.pageY-startTouchCenter[1]); var d = Math.abs(dx*dx+dy*dy); if (d > 64) { clearTimeout(touchStartTime); touchStartTime = null; } } else if (lasso) { d3.event.preventDefault(); } canvasMouseMove.call(this); } else { touch0 = d3.event.touches.item(0); var touch1 = d3.event.touches.item(1); var a = touch0["pageY"]-touch1["pageY"]; var b = touch0["pageX"]-touch1["pageX"]; var offset = chart.offset(); var scrollPos = [chart.scrollLeft(),chart.scrollTop()]; var moveTouchDistance = Math.sqrt((a*a)+(b*b)); var touchCenter = [ touch1["pageX"]+(b/2), touch1["pageY"]+(a/2) ]; if (!isNaN(moveTouchDistance)) { oldScaleFactor = scaleFactor; scaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000))); var deltaTouchCenter = [ // Try to pan whilst zooming - not 100% startTouchCenter[0]*(scaleFactor-oldScaleFactor),//-(touchCenter[0]-moveTouchCenter[0]), startTouchCenter[1]*(scaleFactor-oldScaleFactor) //-(touchCenter[1]-moveTouchCenter[1]) ]; startTouchDistance = moveTouchDistance; moveTouchCenter = touchCenter; chart.scrollLeft(scrollPos[0]+deltaTouchCenter[0]); chart.scrollTop(scrollPos[1]+deltaTouchCenter[1]); redraw(); } } }); // Workspace Background eventLayer.append("svg:rect") .attr("class","red-ui-workspace-chart-background") .attr("width", space_width) .attr("height", space_height); gridLayer = eventLayer.append("g").attr("class","red-ui-workspace-chart-grid"); updateGrid(); groupLayer = eventLayer.append("g"); linkLayer = eventLayer.append("g"); dragGroupLayer = eventLayer.append("g"); nodeLayer = eventLayer.append("g"); drag_lines = []; RED.events.on("workspace:change",function(event) { if (event.old !== 0) { workspaceScrollPositions[event.old] = { left:chart.scrollLeft(), top:chart.scrollTop() }; } var scrollStartLeft = chart.scrollLeft(); var scrollStartTop = chart.scrollTop(); activeSubflow = RED.nodes.subflow(event.workspace); RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow); RED.menu.setDisabled("menu-item-workspace-delete",RED.workspaces.count() == 1 || activeSubflow); if (workspaceScrollPositions[event.workspace]) { chart.scrollLeft(workspaceScrollPositions[event.workspace].left); chart.scrollTop(workspaceScrollPositions[event.workspace].top); } else { chart.scrollLeft(0); chart.scrollTop(0); } var scrollDeltaLeft = chart.scrollLeft() - scrollStartLeft; var scrollDeltaTop = chart.scrollTop() - scrollStartTop; if (mouse_position != null) { mouse_position[0] += scrollDeltaLeft; mouse_position[1] += scrollDeltaTop; } if (RED.workspaces.selection().length === 0) { clearSelection(); } RED.nodes.eachNode(function(n) { n.dirty = true; n.dirtyStatus = true; }); updateSelection(); updateActiveNodes(); redraw(); }); RED.statusBar.add({ id: "view-zoom-controls", align: "right", element: $(''+ ''+ ''+ ''+ '') }) $("#red-ui-view-zoom-out").on("click", zoomOut); RED.popover.tooltip($("#red-ui-view-zoom-out"),RED._('actions.zoom-out'),'core:zoom-out'); $("#red-ui-view-zoom-zero").on("click", zoomZero); RED.popover.tooltip($("#red-ui-view-zoom-zero"),RED._('actions.zoom-reset'),'core:zoom-reset'); $("#red-ui-view-zoom-in").on("click", zoomIn); RED.popover.tooltip($("#red-ui-view-zoom-in"),RED._('actions.zoom-in'),'core:zoom-in'); chart.on("DOMMouseScroll mousewheel", function (evt) { if ( evt.altKey ) { evt.preventDefault(); evt.stopPropagation(); var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta; if (move <= 0) { zoomOut(); } else { zoomIn(); } } }); // Handle nodes dragged from the palette chart.droppable({ accept:".red-ui-palette-node", drop: function( event, ui ) { d3.event = event; var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); var result = addNode(selected_tool); if (!result) { return; } var historyEvent = result.historyEvent; var nn = result.node; var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); if (showLabel !== undefined && !/^link (in|out)$/.test(nn._def.type) && !nn._def.defaults.hasOwnProperty("l")) { nn.l = showLabel; } var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); var mousePos = d3.touches(this)[0]||d3.mouse(this); mousePos[1] += this.scrollTop + ((nn.h/2)-helperOffset[1]); mousePos[0] += this.scrollLeft + ((nn.w/2)-helperOffset[0]); mousePos[1] /= scaleFactor; mousePos[0] /= scaleFactor; if (snapGrid) { mousePos[0] = gridSize*(Math.ceil(mousePos[0]/gridSize)); mousePos[1] = gridSize*(Math.ceil(mousePos[1]/gridSize)); } nn.x = mousePos[0]; nn.y = mousePos[1]; var spliceLink = $(ui.helper).data("splice"); if (spliceLink) { // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog RED.nodes.removeLink(spliceLink); var link1 = { source:spliceLink.source, sourcePort:spliceLink.sourcePort, target: nn }; var link2 = { source:nn, sourcePort:0, target: spliceLink.target }; RED.nodes.addLink(link1); RED.nodes.addLink(link2); historyEvent.links = [link1,link2]; historyEvent.removedLinks = [spliceLink]; } RED.history.push(historyEvent); RED.nodes.add(nn); RED.editor.validateNode(nn); RED.nodes.dirty(true); // auto select dropped node - so info shows (if visible) clearSelection(); nn.selected = true; moving_set.push({n:nn}); updateActiveNodes(); updateSelection(); redraw(); if (nn._def.autoedit) { RED.editor.edit(nn); } } }); chart.on("focus", function() { $("#red-ui-workspace-tabs").addClass("red-ui-workspace-focussed"); }); chart.on("blur", function() { $("#red-ui-workspace-tabs").removeClass("red-ui-workspace-focussed"); }); RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection); RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();}); RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard);}); RED.actions.add("core:delete-selection",deleteSelection); RED.actions.add("core:edit-selected-node",editSelection); RED.actions.add("core:undo",RED.history.pop); RED.actions.add("core:redo",RED.history.redo); RED.actions.add("core:select-all-nodes",selectAll); RED.actions.add("core:zoom-in",zoomIn); RED.actions.add("core:zoom-out",zoomOut); RED.actions.add("core:zoom-reset",zoomZero); RED.actions.add("core:enable-selected-nodes", function() { setSelectedNodeState(false)}); RED.actions.add("core:disable-selected-nodes", function() { setSelectedNodeState(true)}); RED.actions.add("core:toggle-show-grid",function(state) { if (state === undefined) { RED.userSettings.toggle("view-show-grid"); } else { toggleShowGrid(state); } }); RED.actions.add("core:toggle-snap-grid",function(state) { if (state === undefined) { RED.userSettings.toggle("view-snap-grid"); } else { toggleSnapGrid(state); } }); RED.actions.add("core:toggle-status",function(state) { if (state === undefined) { RED.userSettings.toggle("view-node-status"); } else { toggleStatus(state); } }); RED.view.navigator.init(); RED.view.tools.init(); } function updateGrid() { var gridTicks = []; for (var i=0;i 0) { if (delta < node_width) { scale = 0.75-0.75*((node_width-delta)/node_width); // scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width)); // if (Math.abs(dy) < 3*node_height) { // scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ; // } } } else { scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width)); } if (dx*sc > 0) { return "M "+origX+" "+origY+ " C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+ (destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+ destX+" "+destY } else { var midX = Math.floor(destX-dx/2); var midY = Math.floor(destY-dy/2); // if (dy === 0) { midY = destY + node_height; } var cp_height = node_height/2; var y1 = (destY + midY)/2 var topX =origX + sc*node_width*scale; var topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height); var bottomX = destX - sc*node_width*scale; var bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height); var x1 = (origX+topX)/2; var scy = dy>0?1:-1; var cp = [ // Orig -> Top [x1,origY], [topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)], // Top -> Mid // [Mirror previous cp] [x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)], // Mid -> Bottom // [Mirror previous cp] [bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)], // Bottom -> Dest // [Mirror previous cp] [(destX+bottomX)/2,destY] ]; if (cp[2][1] === topY+scy*cp_height) { if (Math.abs(dy) < cp_height*10) { cp[1][1] = topY-scy*cp_height/2; cp[3][1] = bottomY-scy*cp_height/2; } cp[2][0] = topX; } return "M "+origX+" "+origY+ " C "+ cp[0][0]+" "+cp[0][1]+" "+ cp[1][0]+" "+cp[1][1]+" "+ topX+" "+topY+ " S "+ cp[2][0]+" "+cp[2][1]+" "+ midX+" "+midY+ " S "+ cp[3][0]+" "+cp[3][1]+" "+ bottomX+" "+bottomY+ " S "+ cp[4][0]+" "+cp[4][1]+" "+ destX+" "+destY } } function addNode(type,x,y) { var m = /^subflow:(.+)$/.exec(type); if (activeSubflow && m) { var subflowId = m[1]; if (subflowId === activeSubflow.id) { RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddSubflowToItself")}),"error"); return; } if (RED.nodes.subflowContains(m[1],activeSubflow.id)) { RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddCircularReference")}),"error"); return; } } var nn = { id:RED.nodes.id(),z:RED.workspaces.active()}; nn.type = type; nn._def = RED.nodes.getType(nn.type); if (!m) { nn.inputs = nn._def.inputs || 0; nn.outputs = nn._def.outputs; for (var d in nn._def.defaults) { if (nn._def.defaults.hasOwnProperty(d)) { if (nn._def.defaults[d].value !== undefined) { nn[d] = JSON.parse(JSON.stringify(nn._def.defaults[d].value)); } } } if (nn._def.onadd) { try { nn._def.onadd.call(nn); } catch(err) { console.log("Definition error: "+nn.type+".onadd:",err); } } } else { var subflow = RED.nodes.subflow(m[1]); nn.name = ""; nn.inputs = subflow.in.length; nn.outputs = subflow.out.length; } nn.changed = true; nn.moved = true; nn.w = node_width; nn.h = Math.max(node_height,(nn.outputs||0) * 15); nn.resize = true; var historyEvent = { t:"add", nodes:[nn.id], dirty:RED.nodes.dirty() } if (activeSubflow) { var subflowRefresh = RED.subflow.refresh(true); if (subflowRefresh) { historyEvent.subflow = { id:activeSubflow.id, changed: activeSubflow.changed, instances: subflowRefresh.instances } } } return { node: nn, historyEvent: historyEvent } } function canvasMouseDown() { if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); } var point; if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } if (d3.event.button === 1) { // Middle Click pan mouse_mode = RED.state.PANNING; mouse_position = [d3.event.pageX,d3.event.pageY] scroll_position = [chart.scrollLeft(),chart.scrollTop()]; return; } if (!mousedown_node && !mousedown_link && !mousedown_group) { selected_link = null; updateSelection(); } if (mouse_mode === 0) { if (lasso) { lasso.remove(); lasso = null; } } if (mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) { if (d3.event.metaKey || d3.event.ctrlKey) { d3.event.stopPropagation(); clearSelection(); point = d3.mouse(this); var clickedGroup = getGroupAt(point[0],point[1]); if (drag_lines.length > 0) { clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g) } showQuickAddDialog(point, null, clickedGroup); } } if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) { if (!touchStartTime) { point = d3.mouse(this); lasso = eventLayer.append("rect") .attr("ox",point[0]) .attr("oy",point[1]) .attr("rx",1) .attr("ry",1) .attr("x",point[0]) .attr("y",point[1]) .attr("width",0) .attr("height",0) .attr("class","nr-ui-view-lasso"); d3.event.preventDefault(); } } } function showQuickAddDialog(point, spliceLink, targetGroup) { if (targetGroup && !targetGroup.active) { targetGroup.active = true; targetGroup.dirty = true; selectGroup(targetGroup,false); activeGroup = targetGroup; RED.view.redraw(); } var ox = point[0]; var oy = point[1]; if (RED.settings.get("editor").view['view-snap-grid']) { // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','red') point[0] = Math.round(point[0] / gridSize) * gridSize; point[1] = Math.round(point[1] / gridSize) * gridSize; // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','blue') } var mainPos = $("#red-ui-main-container").position(); if (mouse_mode !== RED.state.QUICK_JOINING) { mouse_mode = RED.state.QUICK_JOINING; $(window).on('keyup',disableQuickJoinEventHandler); } quickAddActive = true; if (ghostNode) { ghostNode.remove(); } ghostNode = eventLayer.append("g").attr('transform','translate('+(point[0] - node_width/2)+','+(point[1] - node_height/2)+')'); ghostNode.append("rect") .attr("class","red-ui-flow-node-placeholder") .attr("rx", 5) .attr("ry", 5) .attr("width",node_width) .attr("height",node_height) .attr("fill","none") // var ghostLink = ghostNode.append("svg:path") // .attr("class","red-ui-flow-link-link") // .attr("d","M 0 "+(node_height/2)+" H "+(gridSize * -2)) // .attr("opacity",0); var filter; if (drag_lines.length > 0) { if (drag_lines[0].virtualLink) { filter = {type:drag_lines[0].node.type === 'link in'?'link out':'link in'} } else if (drag_lines[0].portType === PORT_TYPE_OUTPUT) { filter = {input:true} } else { filter = {output:true} } quickAddLink = { node: drag_lines[0].node, port: drag_lines[0].port, portType: drag_lines[0].portType, } if (drag_lines[0].virtualLink) { quickAddLink.virtualLink = true; } hideDragLines(); } if (spliceLink) { filter = {input:true, output:true} } var rebuildQuickAddLink = function() { if (!quickAddLink) { return; } if (!quickAddLink.el) { quickAddLink.el = dragGroupLayer.append("svg:path").attr("class", "red-ui-flow-drag-line"); } var numOutputs = (quickAddLink.portType === PORT_TYPE_OUTPUT)?(quickAddLink.node.outputs || 1):1; var sourcePort = quickAddLink.port; var portY = -((numOutputs-1)/2)*13 +13*sourcePort; var sc = (quickAddLink.portType === PORT_TYPE_OUTPUT)?1:-1; quickAddLink.el.attr("d",generateLinkPath(quickAddLink.node.x+sc*quickAddLink.node.w/2,quickAddLink.node.y+portY,point[0]-sc*node_width/2,point[1],sc)); } if (quickAddLink) { rebuildQuickAddLink(); } var lastAddedX; var lastAddedWidth; RED.typeSearch.show({ x:d3.event.clientX-mainPos.left-node_width/2 - (ox-point[0]), y:d3.event.clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), filter: filter, move: function(dx,dy) { if (ghostNode) { var pos = d3.transform(ghostNode.attr("transform")).translate; ghostNode.attr("transform","translate("+(pos[0]+dx)+","+(pos[1]+dy)+")") point[0] += dx; point[1] += dy; rebuildQuickAddLink(); } }, cancel: function() { if (quickAddLink) { if (quickAddLink.el) { quickAddLink.el.remove(); } quickAddLink = null; } quickAddActive = false; if (ghostNode) { ghostNode.remove(); } resetMouseVars(); updateSelection(); hideDragLines(); redraw(); }, add: function(type,keepAdding) { var result = addNode(type); if (!result) { return; } if (keepAdding) { mouse_mode = RED.state.QUICK_JOINING; } var nn = result.node; var historyEvent = result.historyEvent; nn.x = point[0]; nn.y = point[1]; var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); if (showLabel !== undefined && !/^link (in|out)$/.test(nn._def.type) && !nn._def.defaults.hasOwnProperty("l")) { nn.l = showLabel; } if (quickAddLink) { var drag_line = quickAddLink; var src = null,dst,src_port; if (drag_line.portType === PORT_TYPE_OUTPUT && (nn.inputs > 0 || drag_line.virtualLink) ) { src = drag_line.node; src_port = drag_line.port; dst = nn; } else if (drag_line.portType === PORT_TYPE_INPUT && (nn.outputs > 0 || drag_line.virtualLink)) { src = nn; dst = drag_line.node; src_port = 0; } if (src !== null) { // Joining link nodes via virual wires. Need to update // the src and dst links property if (drag_line.virtualLink) { historyEvent = { t:'multi', events: [historyEvent] } var oldSrcLinks = $.extend(true,{},{v:src.links}).v var oldDstLinks = $.extend(true,{},{v:dst.links}).v src.links.push(dst.id); dst.links.push(src.id); src.dirty = true; dst.dirty = true; historyEvent.events.push({ t:'edit', node: src, dirty: RED.nodes.dirty(), changed: src.changed, changes: { links:oldSrcLinks } }); historyEvent.events.push({ t:'edit', node: dst, dirty: RED.nodes.dirty(), changed: dst.changed, changes: { links:oldDstLinks } }); src.changed = true; dst.changed = true; } else { var link = {source: src, sourcePort:src_port, target: dst}; RED.nodes.addLink(link); historyEvent.links = [link]; } if (!keepAdding) { quickAddLink.el.remove(); quickAddLink = null; if (mouse_mode === RED.state.QUICK_JOINING) { if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); } else { resetMouseVars(); } } } else { quickAddLink.node = nn; quickAddLink.port = 0; } } else { hideDragLines(); resetMouseVars(); } } else { if (!keepAdding) { if (mouse_mode === RED.state.QUICK_JOINING) { if (nn.outputs > 0) { showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); } else if (nn.inputs > 0) { showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); } else { resetMouseVars(); } } } else { if (nn.outputs > 0) { quickAddLink = { node: nn, port: 0, portType: PORT_TYPE_OUTPUT } } else if (nn.inputs > 0) { quickAddLink = { node: nn, port: 0, portType: PORT_TYPE_INPUT } } else { resetMouseVars(); } } } if (targetGroup) { RED.group.addToGroup(targetGroup, nn); } if (spliceLink) { resetMouseVars(); // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog RED.nodes.removeLink(spliceLink); var link1 = { source:spliceLink.source, sourcePort:spliceLink.sourcePort, target: nn }; var link2 = { source:nn, sourcePort:0, target: spliceLink.target }; RED.nodes.addLink(link1); RED.nodes.addLink(link2); historyEvent.links = (historyEvent.links || []).concat([link1,link2]); historyEvent.removedLinks = [spliceLink]; } RED.history.push(historyEvent); RED.nodes.add(nn); RED.editor.validateNode(nn); RED.nodes.dirty(true); // auto select dropped node - so info shows (if visible) clearSelection(); nn.selected = true; if (targetGroup) { selectGroup(targetGroup,false); targetGroup.active = true targetGroup.dirty = true; activeGroup = targetGroup; } moving_set.push({n:nn}); updateActiveNodes(); updateSelection(); redraw(); // At this point the newly added node will have a real width, // so check if the position needs nudging if (lastAddedX !== undefined) { var lastNodeRHEdge = lastAddedX + lastAddedWidth/2; var thisNodeLHEdge = nn.x - nn.w/2; var gap = thisNodeLHEdge - lastNodeRHEdge; if (gap != gridSize *2) { nn.x = nn.x + gridSize * 2 - gap; nn.dirty = true; nn.x = Math.ceil(nn.x / gridSize) * gridSize; redraw(); } } if (keepAdding) { if (lastAddedX === undefined) { // ghostLink.attr("opacity",1); setTimeout(function() { RED.typeSearch.refresh({filter:{input:true}}); },100); } lastAddedX = nn.x; lastAddedWidth = nn.w; point[0] = nn.x + nn.w/2 + node_width/2 + gridSize * 2; ghostNode.attr('transform','translate('+(point[0] - node_width/2)+','+(point[1] - node_height/2)+')'); rebuildQuickAddLink(); } else { quickAddActive = false; ghostNode.remove(); } } }); updateActiveNodes(); updateSelection(); redraw(); } function canvasMouseMove() { var i; var node; // Prevent touch scrolling... //if (d3.touches(this)[0]) { // d3.event.preventDefault(); //} // TODO: auto scroll the container //var point = d3.mouse(this); //if (point[0]-container.scrollLeft < 30 && container.scrollLeft > 0) { container.scrollLeft -= 15; } //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop); if (mouse_mode === RED.state.PANNING) { var pos = [d3.event.pageX,d3.event.pageY]; var deltaPos = [ mouse_position[0]-pos[0], mouse_position[1]-pos[1] ]; chart.scrollLeft(scroll_position[0]+deltaPos[0]) chart.scrollTop(scroll_position[1]+deltaPos[1]) return } mouse_position = d3.touches(this)[0]||d3.mouse(this); if (lasso) { var ox = parseInt(lasso.attr("ox")); var oy = parseInt(lasso.attr("oy")); var x = parseInt(lasso.attr("x")); var y = parseInt(lasso.attr("y")); var w; var h; if (mouse_position[0] < ox) { x = mouse_position[0]; w = ox-x; } else { w = mouse_position[0]-x; } if (mouse_position[1] < oy) { y = mouse_position[1]; h = oy-y; } else { h = mouse_position[1]-y; } lasso .attr("x",x) .attr("y",y) .attr("width",w) .attr("height",h) ; return; } if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) { return; } var mousePos; // if (mouse_mode === RED.state.GROUP_RESIZE) { // mousePos = mouse_position; // var nx = mousePos[0] + mousedown_group.dx; // var ny = mousePos[1] + mousedown_group.dy; // switch(mousedown_group.activeHandle) { // case 0: mousedown_group.pos.x0 = nx; mousedown_group.pos.y0 = ny; break; // case 1: mousedown_group.pos.x1 = nx; mousedown_group.pos.y0 = ny; break; // case 2: mousedown_group.pos.x1 = nx; mousedown_group.pos.y1 = ny; break; // case 3: mousedown_group.pos.x0 = nx; mousedown_group.pos.y1 = ny; break; // } // mousedown_group.dirty = true; // } if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { // update drag line if (drag_lines.length === 0 && mousedown_port_type !== null) { if (d3.event.shiftKey) { // Get all the wires we need to detach. var links = []; var existingLinks = []; if (selected_link && ((mousedown_port_type === PORT_TYPE_OUTPUT && selected_link.source === mousedown_node && selected_link.sourcePort === mousedown_port_index ) || (mousedown_port_type === PORT_TYPE_INPUT && selected_link.target === mousedown_node )) ) { existingLinks = [selected_link]; } else { var filter; if (mousedown_port_type === PORT_TYPE_OUTPUT) { filter = { source:mousedown_node, sourcePort: mousedown_port_index } } else { filter = { target: mousedown_node } } existingLinks = RED.nodes.filterLinks(filter); } for (i=0;i 3) { mouse_mode = RED.state.MOVING_ACTIVE; clickElapsed = 0; spliceActive = false; if (moving_set.length === 1) { node = moving_set[0]; spliceActive = node.n.hasOwnProperty("_def") && ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) && RED.nodes.filterLinks({ source: node.n }).length === 0 && RED.nodes.filterLinks({ target: node.n }).length === 0; } } } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) { mousePos = mouse_position; var minX = 0; var minY = 0; var maxX = space_width; var maxY = space_height; for (var n = 0; n 0) { var gridOffset = [0,0]; node = moving_set[0]; gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2); gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize)); if (gridOffset[0] !== 0 || gridOffset[1] !== 0) { for (i = 0; i 0) { historyEvent = { t:"delete", links: removedLinks, dirty:RED.nodes.dirty() }; RED.history.push(historyEvent); RED.nodes.dirty(true); } hideDragLines(); } if (lasso) { var x = parseInt(lasso.attr("x")); var y = parseInt(lasso.attr("y")); var x2 = x+parseInt(lasso.attr("width")); var y2 = y+parseInt(lasso.attr("height")); if (!d3.event.shiftKey) { clearSelection(); } activeNodes.forEach(function(n) { if (n.z == RED.workspaces.active() && !n.selected) { if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { if (n.g) { var group = RED.nodes.group(n.g); if (!group.selected) { selectGroup(RED.nodes.group(n.g),true); } } else { n.selected = true; n.dirty = true; moving_set.push({n:n}); } } } }); if (activeSubflow) { activeSubflow.in.forEach(function(n) { n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2); if (n.selected) { n.dirty = true; moving_set.push({n:n}); } }); activeSubflow.out.forEach(function(n) { n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2); if (n.selected) { n.dirty = true; moving_set.push({n:n}); } }); if (activeSubflow.status) { activeSubflow.status.selected = (activeSubflow.status.x > x && activeSubflow.status.x < x2 && activeSubflow.status.y > y && activeSubflow.status.y < y2); if (activeSubflow.status.selected) { activeSubflow.status.dirty = true; moving_set.push({n:activeSubflow.status}); } } } updateSelection(); lasso.remove(); lasso = null; } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey && !d3.event.metaKey ) { clearSelection(); updateSelection(); } if (mouse_mode == RED.state.MOVING_ACTIVE) { if (moving_set.length > 0) { if (activeHoverGroup) { for (var j=0;j 0) { historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}; if (activeSpliceLink) { // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp var spliceLink = d3.select(activeSpliceLink).data()[0]; RED.nodes.removeLink(spliceLink); var link1 = { source:spliceLink.source, sourcePort:spliceLink.sourcePort, target: moving_set[0].n }; var link2 = { source:moving_set[0].n, sourcePort:0, target: spliceLink.target }; RED.nodes.addLink(link1); RED.nodes.addLink(link2); historyEvent.links = [link1,link2]; historyEvent.removedLinks = [spliceLink]; updateActiveNodes(); } RED.nodes.dirty(true); RED.history.push(historyEvent); } } } // if (mouse_mode === RED.state.MOVING && mousedown_node && mousedown_node.g) { // if (mousedown_node.gSelected) { // delete mousedown_node.gSelected // } else { // if (!d3.event.ctrlKey && !d3.event.metaKey) { // clearSelection(); // } // RED.nodes.group(mousedown_node.g).selected = true; // mousedown_node.selected = true; // mousedown_node.dirty = true; // moving_set.push({n:mousedown_node}); // } // } if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) { // if (mousedown_node) { // delete mousedown_node.gSelected; // } for (i=0;i 0.3) { zoomView(scaleFactor-0.1); } } function zoomZero() { zoomView(1); } function zoomView(factor) { var screenSize = [chart.width(),chart.height()]; var scrollPos = [chart.scrollLeft(),chart.scrollTop()]; var center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor]; scaleFactor = factor; var newCenter = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor]; var delta = [(newCenter[0]-center[0])*scaleFactor,(newCenter[1]-center[1])*scaleFactor] chart.scrollLeft(scrollPos[0]-delta[0]); chart.scrollTop(scrollPos[1]-delta[1]); RED.view.navigator.resize(); redraw(); } function selectAll() { if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions.single) { return; } activeGroups.forEach(function(g) { selectGroup(g, true); if (!g.selected) { g.selected = true; g.dirty = true; } }) activeNodes.forEach(function(n) { if (mouse_mode === RED.state.SELECTING_NODE) { if (selectNodesOptions.filter && !selectNodesOptions.filter(n)) { return; } } if (!n.g && !n.selected) { n.selected = true; n.dirty = true; moving_set.push({n:n}); } }); if (mouse_mode !== RED.state.SELECTING_NODE && activeSubflow) { activeSubflow.in.forEach(function(n) { if (!n.selected) { n.selected = true; n.dirty = true; moving_set.push({n:n}); } }); activeSubflow.out.forEach(function(n) { if (!n.selected) { n.selected = true; n.dirty = true; moving_set.push({n:n}); } }); if (activeSubflow.status) { if (!activeSubflow.status.selected) { activeSubflow.status.selected = true; activeSubflow.status.dirty = true; moving_set.push({n:activeSubflow.status}); } } } selected_link = null; if (mouse_mode !== RED.state.SELECTING_NODE) { updateSelection(); } redraw(); } function clearSelection() { if (DEBUG_EVENTS) { console.warn("clearSelection", mouse_mode); } for (var i=0;i 0) { selection.nodes = moving_set.map(function(n) { return n.n;}); } if (selected_link != null) { selection.link = selected_link; } selection.groups = activeGroups.filter(function(g) { return g.selected }) activeLinks = RED.nodes.filterLinks({ source:{z:activeWorkspace}, target:{z:activeWorkspace} }); var tabOrder = RED.nodes.getWorkspaceOrder(); var currentLinks = activeLinks; var addedLinkLinks = {}; activeFlowLinks = []; var activeLinkNodeIds = Object.keys(activeLinkNodes); activeLinkNodeIds.forEach(function(n) { activeLinkNodes[n].dirty = true; }) activeLinkNodes = {}; for (var i=0;i 0) { activeFlowLinks.push({ refresh: Math.floor(Math.random()*10000), node: linkNode, links: offFlowLinks//offFlows.map(function(i) { return {id:i,links:offFlowLinks[i]};}) }); } } } if (activeFlowLinks.length === 0 && selected_link !== null && selected_link.link) { activeLinks.push(selected_link); activeLinkNodes[selected_link.source.id] = selected_link.source; selected_link.source.dirty = true; activeLinkNodes[selected_link.target.id] = selected_link.target; selected_link.target.dirty = true; } } else { selection.flows = workspaceSelection; } var selectionJSON = activeWorkspace+":"+JSON.stringify(selection,function(key,value) { if (key === 'nodes' || key === 'flows' || key === 'groups') { return value.map(function(n) { return n.id }) } else if (key === 'link') { return value.source.id+":"+value.sourcePort+":"+value.target.id; } return value; }); if (selectionJSON !== lastSelection) { lastSelection = selectionJSON; RED.events.emit("view:selection-changed",selection); } } function editSelection() { if (moving_set.length > 0) { var node = moving_set[0].n; if (node.type === "subflow") { RED.editor.editSubflow(activeSubflow); } else { RED.editor.edit(node); } } } function deleteSelection() { if (mouse_mode === RED.state.SELECTING_NODE) { return; } if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; } var workspaceSelection = RED.workspaces.selection(); if (workspaceSelection.length > 0) { var workspaceCount = 0; workspaceSelection.forEach(function(ws) { if (ws.type === 'tab') { workspaceCount++ } }); if (workspaceCount === RED.workspaces.count()) { // Cannot delete all workspaces return; } var historyEvent = { t: 'delete', dirty: RED.nodes.dirty(), nodes: [], links: [], workspaces: [], subflows: [] } var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0); for (var i=0;i 0 || selected_link != null) { var result; var removedNodes = []; var removedLinks = []; var removedSubflowOutputs = []; var removedSubflowInputs = []; var removedSubflowStatus; var subflowInstances = []; var startDirty = RED.nodes.dirty(); var startChanged = false; if (moving_set.length > 0) { for (var i=0;i 0) { result = RED.subflow.removeOutput(removedSubflowOutputs); if (result) { removedLinks = removedLinks.concat(result.links); } } // Assume 0/1 inputs if (removedSubflowInputs.length == 1) { result = RED.subflow.removeInput(); if (result) { removedLinks = removedLinks.concat(result.links); } } if (removedSubflowStatus) { result = RED.subflow.removeStatus(); if (result) { removedLinks = removedLinks.concat(result.links); } } var instances = RED.subflow.refresh(true); if (instances) { subflowInstances = instances.instances; } moving_set = []; if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus) { RED.nodes.dirty(true); } } var historyEvent; if (selected_link && selected_link.link) { var sourceId = selected_link.source.id; var targetId = selected_link.target.id; var sourceIdIndex = selected_link.target.links.indexOf(sourceId); var targetIdIndex = selected_link.source.links.indexOf(targetId); historyEvent = { t:"multi", events: [ { t: "edit", node: selected_link.source, changed: selected_link.source.changed, changes: { links: $.extend(true,{},{v:selected_link.source.links}).v } }, { t: "edit", node: selected_link.target, changed: selected_link.target.changed, changes: { links: $.extend(true,{},{v:selected_link.target.links}).v } } ], dirty:RED.nodes.dirty() } RED.nodes.dirty(true); selected_link.source.changed = true; selected_link.target.changed = true; selected_link.target.links.splice(sourceIdIndex,1); selected_link.source.links.splice(targetIdIndex,1); selected_link.source.dirty = true; selected_link.target.dirty = true; } else { if (selected_link) { RED.nodes.removeLink(selected_link); removedLinks.push(selected_link); } RED.nodes.dirty(true); historyEvent = { t:"delete", nodes:removedNodes, links:removedLinks, subflowOutputs:removedSubflowOutputs, subflowInputs:removedSubflowInputs, subflow: { id: activeSubflow?activeSubflow.id:undefined, instances: subflowInstances }, dirty:startDirty }; if (removedSubflowStatus) { historyEvent.subflow.status = removedSubflowStatus; } } RED.history.push(historyEvent); selected_link = null; updateActiveNodes(); updateSelection(); redraw(); } } function copySelection() { if (mouse_mode === RED.state.SELECTING_NODE) { return; } var nodes = []; var selection = RED.workspaces.selection(); if (selection.length > 0) { nodes = []; selection.forEach(function(n) { if (n.type === 'tab') { nodes.push(n); nodes = nodes.concat(RED.nodes.filterNodes({z:n.id})); } }); } else if (moving_set.length > 0) { nodes = moving_set.map(function(n) { return n.n }); } if (nodes.length > 0) { var nns = []; for (var n=0;n 0) { if (drag_lines[0].node === d) { // Cannot quick-join to self return } if (drag_lines[0].virtualLink && ( (drag_lines[0].node.type === 'link in' && d.type !== 'link out') || (drag_lines[0].node.type === 'link out' && d.type !== 'link in') ) ) { return } } document.body.style.cursor = ""; if (mouse_mode == RED.state.JOINING || mouse_mode == RED.state.QUICK_JOINING) { if (typeof TouchEvent != "undefined" && d3.event instanceof TouchEvent) { RED.nodes.eachNode(function(n) { if (n.z == RED.workspaces.active()) { var hw = n.w/2; var hh = n.h/2; if (n.x-hw mouse_position[0] && n.y-hhmouse_position[1]) { mouseup_node = n; portType = mouseup_node.inputs>0?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT; portIndex = 0; } } }); } else { mouseup_node = d; } var addedLinks = []; var removedLinks = []; var modifiedNodes = []; // joining link nodes var select_link = null; for (i=0;i 0 || removedLinks.length > 0 || modifiedNodes.length > 0) { // console.log(addedLinks); // console.log(removedLinks); // console.log(modifiedNodes); var historyEvent; if (modifiedNodes.length > 0) { historyEvent = { t:"multi", events: linkEditEvents, dirty:RED.nodes.dirty() }; } else { historyEvent = { t:"add", links:addedLinks, removedLinks: removedLinks, dirty:RED.nodes.dirty() }; } if (activeSubflow) { var subflowRefresh = RED.subflow.refresh(true); if (subflowRefresh) { historyEvent.subflow = { id:activeSubflow.id, changed: activeSubflow.changed, instances: subflowRefresh.instances } } } RED.history.push(historyEvent); updateActiveNodes(); RED.nodes.dirty(true); } if (mouse_mode === RED.state.QUICK_JOINING) { if (addedLinks.length > 0 || modifiedNodes.length > 0) { hideDragLines(); if (portType === PORT_TYPE_INPUT && d.outputs > 0) { showDragLines([{node:d,port:0,portType:PORT_TYPE_OUTPUT}]); } else if (portType === PORT_TYPE_OUTPUT && d.inputs > 0) { showDragLines([{node:d,port:0,portType:PORT_TYPE_INPUT}]); } else { resetMouseVars(); } selected_link = select_link; mousedown_link = select_link; if (select_link) { updateSelection(); } } redraw(); return; } resetMouseVars(); hideDragLines(); selected_link = select_link; mousedown_link = select_link; if (select_link) { updateSelection(); } redraw(); } } var portLabelHoverTimeout = null; var portLabelHover = null; function getElementPosition(node) { var d3Node = d3.select(node); if (d3Node.attr('class') === 'red-ui-workspace-chart-event-layer') { return [0,0]; } var result = []; var localPos = [0,0]; if (node.nodeName.toLowerCase() === 'g') { var transform = d3Node.attr("transform"); if (transform) { localPos = d3.transform(transform).translate; } } else { localPos = [d3Node.attr("x")||0,d3Node.attr("y")||0]; } var parentPos = getElementPosition(node.parentNode); return [localPos[0]+parentPos[0],localPos[1]+parentPos[1]] } function getPortLabel(node,portType,portIndex) { var result; var nodePortLabels = (portType === PORT_TYPE_INPUT)?node.inputLabels:node.outputLabels; if (nodePortLabels && nodePortLabels[portIndex]) { return nodePortLabels[portIndex]; } var portLabels = (portType === PORT_TYPE_INPUT)?node._def.inputLabels:node._def.outputLabels; if (typeof portLabels === 'string') { result = portLabels; } else if (typeof portLabels === 'function') { try { result = portLabels.call(node,portIndex); } catch(err) { console.log("Definition error: "+node.type+"."+((portType === PORT_TYPE_INPUT)?"inputLabels":"outputLabels"),err); result = null; } } else if ($.isArray(portLabels)) { result = portLabels[portIndex]; } return result; } function showTooltip(x,y,content,direction) { var tooltip = eventLayer.append("g") .attr("transform","translate("+x+","+y+")") .attr("class","red-ui-flow-port-tooltip"); var lines = content.split("\n"); var labelWidth = 6; var labelHeight = 12; var labelHeights = []; var lineHeight = 0; lines.forEach(function(l,i) { var labelDimensions = calculateTextDimensions(l||" ", "red-ui-flow-port-tooltip-label", 8,0); labelWidth = Math.max(labelWidth,labelDimensions[0] + 6); labelHeights.push(labelDimensions[1]); if (i === 0) { lineHeight = labelDimensions[1]; } labelHeight += labelDimensions[1]; }); var labelWidth1 = (labelWidth/2)-5-2; var labelWidth2 = labelWidth - 4; var labelHeight1 = (labelHeight/2)-5-2; var labelHeight2 = labelHeight - 4; var path; var lx; var ly = -labelHeight/2; var anchor; if (direction === "left") { path = "M0 0 l -5 -5 v -"+(labelHeight1)+" q 0 -2 -2 -2 h -"+labelWidth+" q -2 0 -2 2 v "+(labelHeight2)+" q 0 2 2 2 h "+labelWidth+" q 2 0 2 -2 v -"+(labelHeight1)+" l 5 -5"; lx = -14; anchor = "end"; } else if (direction === "right") { path = "M0 0 l 5 -5 v -"+(labelHeight1)+" q 0 -2 2 -2 h "+labelWidth+" q 2 0 2 2 v "+(labelHeight2)+" q 0 2 -2 2 h -"+labelWidth+" q -2 0 -2 -2 v -"+(labelHeight1)+" l -5 -5" lx = 14; anchor = "start"; } else if (direction === "top") { path = "M0 0 l 5 -5 h "+(labelWidth1)+" q 2 0 2 -2 v -"+labelHeight+" q 0 -2 -2 -2 h -"+(labelWidth2)+" q -2 0 -2 2 v "+labelHeight+" q 0 2 2 2 h "+(labelWidth1)+" l 5 5" lx = -labelWidth/2 + 6; ly = -labelHeight-lineHeight+12; anchor = "start"; } tooltip.append("path").attr("d",path); lines.forEach(function(l,i) { ly += labelHeights[i]; // tooltip.append("path").attr("d","M "+(lx-10)+" "+ly+" l 20 0 m -10 -5 l 0 10 ").attr('r',2).attr("stroke","#f00").attr("stroke-width","1").attr("fill","none") tooltip.append("svg:text").attr("class","red-ui-flow-port-tooltip-label") .attr("x", lx) .attr("y", ly) .attr("text-anchor",anchor) .text(l||" ") }); return tooltip; } function portMouseOver(port,d,portType,portIndex) { if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } clearTimeout(portLabelHoverTimeout); var active = (mouse_mode!=RED.state.JOINING && mouse_mode != RED.state.QUICK_JOINING) || // Not currently joining - all ports active ( drag_lines.length > 0 && // Currently joining drag_lines[0].portType !== portType && // INPUT->OUTPUT OUTPUT->INPUT ( !drag_lines[0].virtualLink || // Not a link wire (drag_lines[0].node.type === 'link in' && d.type === 'link out') || (drag_lines[0].node.type === 'link out' && d.type === 'link in') ) ) if (active && ((portType === PORT_TYPE_INPUT && ((d._def && d._def.inputLabels)||d.inputLabels)) || (portType === PORT_TYPE_OUTPUT && ((d._def && d._def.outputLabels)||d.outputLabels)))) { portLabelHoverTimeout = setTimeout(function() { var tooltip = getPortLabel(d,portType,portIndex); if (!tooltip) { return; } var pos = getElementPosition(port.node()); portLabelHoverTimeout = null; portLabelHover = showTooltip( (pos[0]+(portType===PORT_TYPE_INPUT?-2:12)), (pos[1]+5), tooltip, portType===PORT_TYPE_INPUT?"left":"right" ); },500); } port.classed("red-ui-flow-port-hovered",active); } function portMouseOut(port,d,portType,portIndex) { if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } clearTimeout(portLabelHoverTimeout); if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; } port.classed("red-ui-flow-port-hovered",false); } function nodeMouseUp(d) { if (DEBUG_EVENTS) { console.warn("nodeMouseUp", mouse_mode,d); } if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) { mouse_mode = RED.state.DEFAULT; if (d.type != "subflow") { RED.editor.edit(d); } else { RED.editor.editSubflow(activeSubflow); } clickElapsed = 0; d3.event.stopPropagation(); return; } if (mouse_mode === RED.state.MOVING) { // Moving primed, but not active. if (!groupNodeSelectPrimed && !d.selected && d.g && RED.nodes.group(d.g).selected) { clearSelection(); activeGroup = RED.nodes.group(d.g); activeGroup.active = true; activeGroup.dirty = true; selectGroup(RED.nodes.group(d.g), false); mousedown_node.selected = true; moving_set.push({n:mousedown_node}); var mouse = d3.touches(this)[0]||d3.mouse(this); mouse[0] += d.x-d.w/2; mouse[1] += d.y-d.h/2; prepareDrag(mouse); return; } } groupNodeSelectPrimed = false; var direction = d._def? (d.inputs > 0 ? 1: 0) : (d.direction == "in" ? 0: 1) var wasJoining = false; if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { wasJoining = true; if (drag_lines.length > 0) { if (drag_lines[0].virtualLink) { if (d.type === 'link in') { direction = 1; } else if (d.type === 'link out') { direction = 0; } } } } portMouseUp(d, direction, 0); if (wasJoining) { d3.selectAll(".red-ui-flow-port-hovered").classed("red-ui-flow-port-hovered",false); } } function prepareDrag(mouse) { mouse_mode = RED.state.MOVING; // Called when moving_set should be prepared to be dragged for (i=0;i 0 && clickElapsed < 750) { mouse_mode = RED.state.DEFAULT; RED.editor.editGroup(g); d3.event.stopPropagation(); return; } } function groupMouseDown(g) { var mouse = d3.touches(this.parentNode)[0]||d3.mouse(this.parentNode); // if (! (mouse[0] < g.x+10 || mouse[0] > g.x+g.w-10 || mouse[1] < g.y+10 || mouse[1] > g.y+g.h-10) ) { // return // } focusView(); if (d3.event.button === 1) { return; } if (mouse_mode == RED.state.IMPORT_DRAGGING) { RED.keyboard.remove("escape"); console.log("Dragged a node into the group") } else if (mouse_mode == RED.state.QUICK_JOINING) { d3.event.stopPropagation(); return; } else if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } mousedown_group = g; var now = Date.now(); clickElapsed = now-clickTime; clickTime = now; dblClickPrimed = ( lastClickNode == g && d3.event.button === 0 && !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey ); lastClickNode = g; if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) { if (g === activeGroup) { exitActiveGroup(); } deselectGroup(g); d3.event.stopPropagation(); } else { if (!g.selected) { if (!d3.event.ctrlKey && !d3.event.metaKey) { clearSelection(); } selectGroup(g,true);//!wasSelected); } else { exitActiveGroup(); } if (d3.event.button != 2) { var d = g.nodes[0]; prepareDrag(mouse); mousedown_group.dx = mousedown_group.x - mouse[0]; mousedown_group.dy = mousedown_group.y - mouse[1]; } } updateSelection(); redraw(); d3.event.stopPropagation(); } function selectGroup(g, includeNodes) { if (!g.selected) { g.selected = true; g.dirty = true; } if (includeNodes) { var currentSet = new Set(moving_set.map(function(n) { return n.n })); var allNodes = RED.group.getNodes(g,true); allNodes.forEach(function(n) { if (!currentSet.has(n)) { moving_set.push({n:n}) // n.selected = true; } n.dirty = true; }) } } function exitActiveGroup() { if (activeGroup) { activeGroup.active = false; activeGroup.dirty = true; deselectGroup(activeGroup); selectGroup(activeGroup,true); activeGroup = null; } } function deselectGroup(g) { if (g.selected) { g.selected = false; g.dirty = true; } var nodeSet = new Set(g.nodes); for (var i = moving_set.length-1; i >= 0; i -= 1) { if (nodeSet.has(moving_set[i].n)) { moving_set[i].n.selected = false; moving_set[i].n.dirty = true; moving_set.splice(i,1); } } } function getGroupAt(x,y) { var candidateGroups = {}; for (var i=0;i= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) { candidateGroups[g.id] = g; } } var ids = Object.keys(candidateGroups); if (ids.length > 1) { ids.forEach(function(id) { if (candidateGroups[id] && candidateGroups[id].g) { delete candidateGroups[candidateGroups[id].g] } }) ids = Object.keys(candidateGroups); } if (ids.length === 0) { return null; } else { return candidateGroups[ids[0]] } } // function groupHandleMouseDown(group, groupEl, handle,handleIndex) { // d3.event.stopPropagation(); // console.log("GHANDLE MD"); // if (d3.event.button != 2) { // mousedown_group = group; // group.activeHandle = handleIndex; // var mouse = d3.touches(handle.parentNode.parentNode)[0]||d3.mouse(handle.parentNode.parentNode); // switch(handleIndex) { // case 0: group.ox = group.pos.x0; group.oy = group.pos.y0; break; // case 1: group.ox = group.pos.x1; group.oy = group.pos.y0; break; // case 2: group.ox = group.pos.x1; group.oy = group.pos.y1; break; // case 3: group.ox = group.pos.x0; group.oy = group.pos.y1; break; // } // group.dx = group.ox - mouse[0]; // group.dy = group.oy - mouse[1]; // console.log("START",group.ox, group.oy); // mouse_offset = d3.mouse(document.body); // if (isNaN(mouse_offset[0])) { // mouse_offset = d3.touches(document.body)[0]; // } // mouse_mode = RED.state.GROUP_RESIZE; // } // } // function groupHandleMouseUp(group,groupEl,handle,handleIndex) { // console.log("GHANDLE MU"); // d3.event.stopPropagation(); // delete group.ox; // delete group.oy; // delete group.dx; // delete group.dy; // resetMouseVars(); // mouse_mode = RED.state.DEFAULT; // } function isButtonEnabled(d) { var buttonEnabled = true; var ws = RED.nodes.workspace(RED.workspaces.active()); if (ws && !ws.disabled && !d.d) { if (d._def.button.hasOwnProperty('enabled')) { if (typeof d._def.button.enabled === "function") { buttonEnabled = d._def.button.enabled.call(d); } else { buttonEnabled = d._def.button.enabled; } } } else { buttonEnabled = false; } return buttonEnabled; } function nodeButtonClicked(d) { if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } var activeWorkspace = RED.workspaces.active(); var ws = RED.nodes.workspace(activeWorkspace); if (ws && !ws.disabled && !d.d) { if (d._def.button.toggle) { d[d._def.button.toggle] = !d[d._def.button.toggle]; d.dirty = true; } if (d._def.button.onclick) { try { d._def.button.onclick.call(d); } catch(err) { console.log("Definition error: "+d.type+".onclick",err); } } if (d.dirty) { redraw(); } } else { if (activeSubflow) { RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabledSubflow")}),"warning"); } else { RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabled")}),"warning"); } } d3.event.preventDefault(); } function showTouchMenu(obj,pos) { var mdn = mousedown_node; var options = []; options.push({name:"delete",disabled:(moving_set.length===0 && selected_link === null),onselect:function() {deleteSelection();}}); options.push({name:"cut",disabled:(moving_set.length===0),onselect:function() {copySelection();deleteSelection();}}); options.push({name:"copy",disabled:(moving_set.length===0),onselect:function() {copySelection();}}); options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard,false,true);}}); options.push({name:"edit",disabled:(moving_set.length != 1),onselect:function() { RED.editor.edit(mdn);}}); options.push({name:"select",onselect:function() {selectAll();}}); options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}}); RED.touch.radialMenu.show(obj,pos,options); resetMouseVars(); } function createIconAttributes(iconUrl, icon_group, d) { var fontAwesomeUnicode = null; if (iconUrl.indexOf("font-awesome/") === 0) { var iconName = iconUrl.substr(13); var fontAwesomeUnicode = RED.nodes.fontAwesome.getIconUnicode(iconName); if (!fontAwesomeUnicode) { var iconPath = RED.utils.getDefaultNodeIcon(d._def, d); iconUrl = RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file; } } if (fontAwesomeUnicode) { // Since Node-RED workspace uses SVG, i tag cannot be used for font-awesome icon. // On SVG, use text tag as an alternative. icon_group.append("text") .attr("xlink:href",iconUrl) .attr("class","fa-lg") .attr("x",15) .text(fontAwesomeUnicode); } else { var icon = icon_group.append("image") .attr("xlink:href",iconUrl) .attr("class","red-ui-flow-node-icon") .attr("x",0) .attr("width","30") .attr("height","30") .style("display","none"); var img = new Image(); img.src = iconUrl; img.onload = function() { if (!iconUrl.match(/\.svg$/)) { var largestEdge = Math.max(img.width,img.height); var scaleFactor = 1; if (largestEdge > 30) { scaleFactor = 30/largestEdge; } var width = img.width * scaleFactor; var height = img.height * scaleFactor; icon.attr("width",width); icon.attr("height",height); icon.attr("x",15-width/2); } icon.attr("xlink:href",iconUrl); icon.style("display",null); //if ("right" == d._def.align) { // icon.attr("x",function(d){return d.w-img.width-1-(d.outputs>0?5:0);}); // icon_shade.attr("x",function(d){return d.w-30}); // icon_shade_border.attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2);}); //} } } } function redraw() { eventLayer.attr("transform","scale("+scaleFactor+")"); outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor); // Don't bother redrawing nodes if we're drawing links if (showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) { var dirtyNodes = {}; if (activeSubflow) { var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;}); subflowOutputs.exit().remove(); var outGroup = subflowOutputs.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-output").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"}); outGroup.each(function(d,i) { d.w=40; d.h=40; }); outGroup.append("rect").attr("class","red-ui-flow-subflow-port").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40) // TODO: This is exactly the same set of handlers used for regular nodes - DRY .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",function(d) { var obj = d3.select(this); var touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; startTouchCenter = [touch0.pageX,touch0.pageY]; startTouchDistance = 0; touchStartTime = setTimeout(function() { showTouchMenu(obj,pos); },touchLongPressTimeout); nodeMouseDown.call(this,d) }) .on("touchend", function(d) { clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { d3.event.stopPropagation(); return; } nodeMouseUp.call(this,d); }); outGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);} ) .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); outGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",20).attr("y",12).style("font-size","10px").text("output"); outGroup.append("svg:text").attr("class","red-ui-flow-port-label red-ui-flow-port-index").attr("x",20).attr("y",28).text(function(d,i){ return i+1}); var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;}); subflowInputs.exit().remove(); var inGroup = subflowInputs.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-input").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"}); inGroup.each(function(d,i) { d.w=40; d.h=40; }); inGroup.append("rect").attr("class","red-ui-flow-subflow-port").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40) // TODO: This is exactly the same set of handlers used for regular nodes - DRY .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",function(d) { var obj = d3.select(this); var touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; startTouchCenter = [touch0.pageX,touch0.pageY]; startTouchDistance = 0; touchStartTime = setTimeout(function() { showTouchMenu(obj,pos); },touchLongPressTimeout); nodeMouseDown.call(this,d) }) .on("touchend", function(d) { clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { d3.event.stopPropagation(); return; } nodeMouseUp.call(this,d); }); inGroup.append("g").attr('transform','translate(35,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} ) .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);} ) .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_OUTPUT,0);}) .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_OUTPUT,0);}); inGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",18).attr("y",20).style("font-size","10px").text("input"); var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;}); subflowStatus.exit().remove(); var statusGroup = subflowStatus.enter().insert("svg:g").attr("class","red-ui-flow-node red-ui-flow-subflow-port-status").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"}); statusGroup.each(function(d,i) { d.w=40; d.h=40; }); statusGroup.append("rect").attr("class","red-ui-flow-subflow-port").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40) // TODO: This is exactly the same set of handlers used for regular nodes - DRY .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",function(d) { var obj = d3.select(this); var touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; startTouchCenter = [touch0.pageX,touch0.pageY]; startTouchDistance = 0; touchStartTime = setTimeout(function() { showTouchMenu(obj,pos); },touchLongPressTimeout); nodeMouseDown.call(this,d) }) .on("touchend", function(d) { clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { d3.event.stopPropagation(); return; } nodeMouseUp.call(this,d); }); statusGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);} ) .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); statusGroup.append("svg:text").attr("class","red-ui-flow-port-label").attr("x",22).attr("y",20).style("font-size","10px").text("status"); subflowOutputs.each(function(d,i) { if (d.dirty) { var output = d3.select(this); output.classed("red-ui-flow-node-selected",function(d) { return d.selected; }) output.selectAll(".red-ui-flow-port-index").text(function(d){ return d.i+1}); output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); dirtyNodes[d.id] = d; d.dirty = false; } }); subflowInputs.each(function(d,i) { if (d.dirty) { var input = d3.select(this); input.classed("red-ui-flow-node-selected",function(d) { return d.selected; }) input.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); dirtyNodes[d.id] = d; d.dirty = false; } }); subflowStatus.each(function(d,i) { if (d.dirty) { var output = d3.select(this); output.classed("red-ui-flow-node-selected",function(d) { return d.selected; }) output.selectAll(".red-ui-flow-port-index").text(function(d){ return d.i+1}); output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); dirtyNodes[d.id] = d; d.dirty = false; } }); } else { nodeLayer.selectAll(".red-ui-flow-subflow-port-output").remove(); nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove(); nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove(); } var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id}); node.exit().remove(); var nodeEnter = node.enter().insert("svg:g") .attr("class", "red-ui-flow-node red-ui-flow-node-group") .classed("red-ui-flow-subflow",function(d) { return activeSubflow != null; }); nodeEnter.each(function(d,i) { var node = d3.select(this); var isLink = (d.type === "link in" || d.type === "link out") var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; node.attr("id",d.id); var labelWidth = calculateTextWidth(RED.utils.getNodeLabel(d), "red-ui-flow-node-label", 50); if (d.resize || d.w === undefined) { if (hideLabel) { d.w = node_height; } else { d.w = Math.max(node_width,20*(Math.ceil((labelWidth+(d._def.inputs>0?7:0))/20)) ); } } if (hideLabel) { d.h = Math.max(node_height,(d.outputs || 0) * 15); } else { d.h = Math.max(6+24*separateTextByLineBreak.length, (d.outputs || 0) * 15, 30); } // if (d._def.badge) { // var badge = node.append("svg:g").attr("class","node_badge_group"); // var badgeRect = badge.append("rect").attr("class","node_badge").attr("rx",5).attr("ry",5).attr("width",40).attr("height",15); // badge.append("svg:text").attr("class","node_badge_label").attr("x",35).attr("y",11).attr("text-anchor","end").text(d._def.badge()); // if (d._def.onbadgeclick) { // badgeRect.attr("cursor","pointer") // .on("click",function(d) { d._def.onbadgeclick.call(d);d3.event.preventDefault();}); // } // } if (d._def.button) { var nodeButtonGroup = node.append("svg:g") .attr("transform",function(d) { return "translate("+((d._def.align == "right") ? 94 : -25)+",2)"; }) .attr("class","red-ui-flow-node-button"); nodeButtonGroup.append("rect") .attr("class","red-ui-flow-node-button-background") .attr("rx",5) .attr("ry",5) .attr("width",32) .attr("height",node_height-4); nodeButtonGroup.append("rect") .attr("class","red-ui-flow-node-button-button") .attr("x",function(d) { return d._def.align == "right"? 11:5}) .attr("y",4) .attr("rx",4) .attr("ry",4) .attr("width",16) .attr("height",node_height-12) .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/}) .attr("cursor","pointer") .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}}) .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}}) .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) { var op = 1; if (d._def.button.toggle) { op = d[d._def.button.toggle]?1:0.2; } d3.select(this).attr("fill-opacity",op); }}) .on("click",nodeButtonClicked) .on("touchstart",nodeButtonClicked) } var mainRect = node.append("rect") .attr("class", "red-ui-flow-node") .classed("red-ui-flow-node-unknown",function(d) { return d.type == "unknown"; }) .attr("rx", 5) .attr("ry", 5) .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/}) .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",function(d) { var obj = d3.select(this); var touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; startTouchCenter = [touch0.pageX,touch0.pageY]; startTouchDistance = 0; touchStartTime = setTimeout(function() { showTouchMenu(obj,pos); },touchLongPressTimeout); nodeMouseDown.call(this,d) }) .on("touchend", function(d) { clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { d3.event.stopPropagation(); return; } nodeMouseUp.call(this,d); }) .on("mouseover",function(d) { if (mouse_mode === 0 || mouse_mode === RED.state.SELECTING_NODE) { if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions && selectNodesOptions.filter) { if (selectNodesOptions.filter(d)) { node.classed("red-ui-flow-node-hovered",true); } } else { node.classed("red-ui-flow-node-hovered",true); } clearTimeout(portLabelHoverTimeout); if (d.hasOwnProperty('l')?!d.l : (d.type === "link in" || d.type === "link out")) { portLabelHoverTimeout = setTimeout(function() { var tooltip; if (d._def.label) { tooltip = d._def.label; try { tooltip = (typeof tooltip === "function" ? tooltip.call(d) : tooltip)||""; } catch(err) { console.log("Definition error: "+d.type+".label",err); tooltip = d.type; } } if (tooltip !== "") { var pos = getElementPosition(node.node()); portLabelHoverTimeout = null; portLabelHover = showTooltip( (pos[0] + d.w/2), (pos[1]-1), tooltip, "top" ); } },500); } } else if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { if (drag_lines.length > 0) { var selectClass; var portType; if ((drag_lines[0].virtualLink && drag_lines[0].portType === PORT_TYPE_INPUT) || drag_lines[0].portType === PORT_TYPE_OUTPUT) { selectClass = ".red-ui-flow-port-input .red-ui-flow-port"; portType = PORT_TYPE_INPUT; } else { selectClass = ".red-ui-flow-port-output .red-ui-flow-port"; portType = PORT_TYPE_OUTPUT; } portMouseOver(d3.select(this.parentNode).selectAll(selectClass),d,portType,0); } } }) .on("mouseout",function(d) { node.classed("red-ui-flow-node-hovered",false); clearTimeout(portLabelHoverTimeout); if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; } if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { if (drag_lines.length > 0) { var selectClass; var portType; if ((drag_lines[0].virtualLink && drag_lines[0].portType === PORT_TYPE_INPUT) || drag_lines[0].portType === PORT_TYPE_OUTPUT) { selectClass = ".red-ui-flow-port-input .red-ui-flow-port"; portType = PORT_TYPE_INPUT; } else { selectClass = ".red-ui-flow-port-output .red-ui-flow-port"; portType = PORT_TYPE_OUTPUT; } portMouseOut(d3.select(this.parentNode).selectAll(selectClass),d,portType,0); } } }); //node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none"); //node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none"); if (d._def.icon) { var icon_url = RED.utils.getNodeIcon(d._def,d); var icon_group = node.append("g") .attr("class","red-ui-flow-node-icon-group") .attr("x",0).attr("y",0); var icon_shade = icon_group.append("rect") .attr("x",0).attr("y",0) .attr("class","red-ui-flow-node-icon-shade") .attr("width","30") .attr("height",function(d){return Math.min(50,d.h-4);}); createIconAttributes(icon_url, icon_group, d); var icon_shade_border = icon_group.append("path") .attr("d",function(d) { return "M 30 1 l 0 "+(d.h-2)}) .attr("class","red-ui-flow-node-icon-shade-border"); if ("right" == d._def.align) { icon_group.attr("class","red-ui-flow-node-icon-group red-ui-flow-node-icon-group-"+d._def.align); icon_shade_border.attr("d",function(d) { return "M 0 1 l 0 "+(d.h-2)}) //icon.attr("class","red-ui-flow-node-icon red-ui-flow-node-icon-"+d._def.align); //icon.attr("class","red-ui-flow-node-icon-shade red-ui-flow-node-icon-shade-"+d._def.align); //icon.attr("class","red-ui-flow-node-icon-shade-border red-ui-flow-node-icon-shade-border-"+d._def.align); } //if (d.inputs > 0 && d._def.align == null) { // icon_shade.attr("width",35); // icon.attr("transform","translate(5,0)"); // icon_shade_border.attr("transform","translate(5,0)"); //} //if (d._def.outputs > 0 && "right" == d._def.align) { // icon_shade.attr("width",35); //icon.attr("x",5); //} //icon.style("pointer-events","none"); icon_group.style("pointer-events","none"); } var labelLineNumber = (separateTextByLineBreak.length == 0)? 1:separateTextByLineBreak.length; var labelId = d.id.replace(".","-"); for(var i=0;i 0) { clearTimeout(portLabelHoverTimeout); portLabelHoverTimeout = setTimeout(function() { var pos = getElementPosition(nodeErrorButton.node()); portLabelHoverTimeout = null; portLabelHover = showTooltip( (pos[0]), (pos[1]), RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - "), "top" ); },500); } }).on("mouseleave", function() { clearTimeout(portLabelHoverTimeout); if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; } }); }); node.each(function(d,i) { if (d.dirty) { var isLink = (d.type === "link in" || d.type === "link out") var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; dirtyNodes[d.id] = d; //if (d.x < -50) deleteSelection(); // Delete nodes if dragged back to palette var labelLineNumber = (d.h-6)/24; var labelWidth = calculateTextWidth(RED.utils.getNodeLabel(d), "red-ui-flow-node-label", 50); if (d.resize) { var ow = d.w; if (hideLabel) { d.w = node_height; } else { d.w = Math.max(node_width,20*(Math.ceil((labelWidth+(d._def.inputs>0?7:0))/20)) ); } // d.w = Math.max(node_width,20*(Math.ceil((calculateTextWidth(l, "red-ui-flow-node-label", 50)+(d._def.inputs>0?7:0))/20)) ); d.x += (d.w-ow)/2; d.resize = false; } if (hideLabel) { d.h = Math.max(node_height,(d.outputs || 0) * 15); } else { d.h = Math.max(6+24*separateTextByLineBreak.length,(d.outputs || 0) * 15, 30); } var thisNode = d3.select(this); thisNode.classed("red-ui-flow-node-disabled", function(d) { return d.d === true}); thisNode.classed("red-ui-flow-subflow",function(d) { return activeSubflow != null; }) //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}}); thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); if (mouse_mode != RED.state.MOVING_ACTIVE) { thisNode.classed("red-ui-flow-node-selected",function(d) { return d.selected }) thisNode.selectAll(".red-ui-flow-node") .attr("width",function(d){return d.w}) .attr("height",function(d){return d.h}) .classed("red-ui-flow-node-highlighted",function(d) { return d.highlighted; }) ; var l = ""; if (d._def.label) { l = d._def.label; try { l = (typeof l === "function" ? l.call(d) : l)||""; l = RED.text.bidi.enforceTextDirectionWithUCC(l); } catch(err) { console.log("Definition error: "+d.type+".label",err); l = d.type; } } var sa = convertLineBreakCharacter(l); var sn = sa.length; var st = ""; var yp = d.h / 2 - (sn / 2) * 24 + 16 var labelId = d.id.replace(".","-"); if(labelLineNumber0?5:0);}); //thisNode.selectAll(".red-ui-flow-node-icon-shade-right").attr("x",function(d){return d.w-30;}); //thisNode.selectAll(".red-ui-flow-node-icon-shade-border-right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)}); var inputPorts = thisNode.selectAll(".red-ui-flow-port-input"); if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) { inputPorts.remove(); } else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) { var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input"); var inputGroupPorts; if (d.type === "link in") { inputGroupPorts = inputGroup.append("circle") .attr("cx",-1).attr("cy",5) .attr("r",5) .attr("class","red-ui-flow-port red-ui-flow-link-port") } else { inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) } inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); } var numOutputs = d.outputs; if (isLink && d.type === "link out") { if (showAllLinkPorts===PORT_TYPE_OUTPUT || activeLinkNodes[d.id]) { d.ports = [0]; numOutputs = 1; } else { d.ports = []; numOutputs = 0; } } var y = (d.h/2)-((numOutputs-1)/2)*13; d.ports = d.ports || d3.range(numOutputs); d._ports = thisNode.selectAll(".red-ui-flow-port-output").data(d.ports); var output_group = d._ports.enter().append("g").attr("class","red-ui-flow-port-output"); var output_group_ports; if (d.type === "link out") { output_group_ports = output_group.append("circle") .attr("cx",11).attr("cy",5) .attr("r",5) .attr("class","red-ui-flow-port red-ui-flow-link-port") } else { output_group_ports = output_group.append("rect") .attr("class","red-ui-flow-port") .attr("rx",3).attr("ry",3) .attr("width",10) .attr("height",10) } output_group_ports.on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,PORT_TYPE_OUTPUT,i);}})() ) .on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,PORT_TYPE_OUTPUT,i);}})() ) .on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,PORT_TYPE_OUTPUT,i);}})() ) .on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,PORT_TYPE_OUTPUT,i);}})() ) .on("mouseover",(function(){var node = d; return function(d,i){portMouseOver(d3.select(this),node,PORT_TYPE_OUTPUT,i);}})()) .on("mouseout",(function(){var node = d; return function(d,i) {portMouseOut(d3.select(this),node,PORT_TYPE_OUTPUT,i);}})()); d._ports.exit().remove(); if (d._ports) { numOutputs = d.outputs || 1; y = (d.h/2)-((numOutputs-1)/2)*13; var x = d.w - 5; d._ports.each(function(d,i) { var port = d3.select(this); port.attr("transform", function(d) { return "translate("+x+","+((y+13*i)-5)+")";}); }); } if (d._def.icon) { var icon = thisNode.select(".red-ui-flow-node-icon"); var faIcon = thisNode.select(".fa-lg"); var current_url; if (!icon.empty()) { current_url = icon.attr("xlink:href"); } else { current_url = faIcon.attr("xlink:href"); } var new_url = RED.utils.getNodeIcon(d._def,d); if (new_url !== current_url) { if (!icon.empty()) { icon.remove(); } else { faIcon.remove(); } var iconGroup = thisNode.select(".red-ui-flow-node-icon-group"); createIconAttributes(new_url, iconGroup, d); } } thisNode.selectAll(".red-ui-flow-node-changed") .attr("transform",function(d){return "translate("+(d.w-10)+", -2)"}) .classed("hide",function(d) { return !(d.changed||d.moved); }); thisNode.selectAll(".red-ui-flow-node-error") .attr("transform",function(d){ return "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)"}) .classed("hide",function(d) { return d.valid; }); thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) { var port = d3.select(this); port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";}) }); thisNode.selectAll(".red-ui-flow-node-icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;}); thisNode.selectAll(".red-ui-flow-node-icon-shade").attr("height",function(d){return d.h;}); thisNode.selectAll(".red-ui-flow-node-icon-shade-border").attr("d", function (d) { return "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0 : 30) + " 1 l 0 " + (d.h - 2); }); thisNode.selectAll(".fa-lg").attr("y",function(d){return (d.h+13)/2;}); thisNode.selectAll(".red-ui-flow-node-button").attr("opacity",function(d) { return (!isButtonEnabled(d))?0.4:1 }); thisNode.selectAll(".red-ui-flow-node-button-button").attr("cursor",function(d) { return (!isButtonEnabled(d))?"":"pointer"; }); thisNode.selectAll(".red-ui-flow-node-button").attr("transform",function(d){ var x = d._def.align == "right"?d.w-6:-25; if (d._def.button.toggle && !d[d._def.button.toggle]) { x = x - (d._def.align == "right"?8:-8); } return "translate("+x+",2)"; }); thisNode.selectAll(".red-ui-flow-node-button rect").attr("fill-opacity",function(d){ if (d._def.button.toggle) { return d[d._def.button.toggle]?1:0.2; } return 1; }); if (d._def.button && (typeof d._def.button.visible === "function")) { // is defined and a function... if (d._def.button.visible.call(d) === false) { thisNode.selectAll(".red-ui-flow-node-button").style("display","none"); } else { thisNode.selectAll(".red-ui-flow-node-button").style("display","inherit"); } } // thisNode.selectAll(".node_badge_group").attr("transform",function(d){return "translate("+(d.w-40)+","+(d.h+3)+")";}); // thisNode.selectAll("text.node_badge_label").text(function(d,i) { // if (d._def.badge) { // if (typeof d._def.badge == "function") { // try { // return d._def.badge.call(d); // } catch(err) { // console.log("Definition error: "+d.type+".badge",err); // return ""; // } // } else { // return d._def.badge; // } // } // return ""; // }); } if (d.dirtyStatus) { if (!showStatus || !d.status) { thisNode.selectAll(".red-ui-flow-node-status-group").style("display","none"); } else { thisNode.selectAll(".red-ui-flow-node-status-group").style("display","inline"); var fill = status_colours[d.status.fill]; // Only allow our colours for now if (d.status.shape == null && fill == null) { thisNode.selectAll(".red-ui-flow-node-status").style("display","none"); thisNode.selectAll(".red-ui-flow-node-status-group").attr("transform","translate(-14,"+(d.h+3)+")"); } else { thisNode.selectAll(".red-ui-flow-node-status-group").attr("transform","translate(3,"+(d.h+3)+")"); var statusClass = "red-ui-flow-node-status-"+(d.status.shape||"dot")+"-"+d.status.fill; thisNode.selectAll(".red-ui-flow-node-status").style("display","inline").attr("class","red-ui-flow-node-status "+statusClass); } if (d.status.hasOwnProperty('text')) { thisNode.selectAll(".red-ui-flow-node-status-label").text(d.status.text); } else { thisNode.selectAll(".red-ui-flow-node-status-label").text(""); } } delete d.dirtyStatus; } d.dirty = false; if (d.g) { if (!dirtyGroups[d.g]) { var gg = d.g; while (gg && !dirtyGroups[gg]) { dirtyGroups[gg] = RED.nodes.group(gg); gg = dirtyGroups[gg].g; } } // var group = dirtyGroups[d.g]; // group.x = Math.min(group.x,d.x-d.w/2-25-((d._def.button && d._def.align!=="right")?20:0)); // group.y = Math.min(group.y,d.y-d.h/2-25), // group.w = Math.max(group.w, d.x+d.w/2+25+((d._def.button && d._def.align=="right")?20:0) - group.x), // group.h = Math.max(group.h, d.y+d.h/2+25 - group.y) } } }); var link = linkLayer.selectAll(".red-ui-flow-link").data( activeLinks, function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i; } ); var linkEnter = link.enter().insert("g",".red-ui-flow-node").attr("class","red-ui-flow-link"); linkEnter.each(function(d,i) { var l = d3.select(this); d.added = true; l.append("svg:path").attr("class","red-ui-flow-link-background red-ui-flow-link-path") .classed("red-ui-flow-link-link", function(d) { return d.link }) .on("mousedown",function(d) { if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } mousedown_link = d; clearSelection(); selected_link = mousedown_link; updateSelection(); redraw(); focusView(); d3.event.stopPropagation(); if (d3.event.metaKey || d3.event.ctrlKey) { l.classed("red-ui-flow-link-splice",true); var point = d3.mouse(this); var clickedGroup = getGroupAt(point[0],point[1]); showQuickAddDialog(point, selected_link, clickedGroup); } }) .on("touchstart",function(d) { if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); return; } mousedown_link = d; clearSelection(); selected_link = mousedown_link; updateSelection(); redraw(); focusView(); d3.event.stopPropagation(); var obj = d3.select(document.body); var touch0 = d3.event.touches.item(0); var pos = [touch0.pageX,touch0.pageY]; touchStartTime = setTimeout(function() { touchStartTime = null; showTouchMenu(obj,pos); },touchLongPressTimeout); }) l.append("svg:path").attr("class","red-ui-flow-link-outline red-ui-flow-link-path"); l.append("svg:path").attr("class","red-ui-flow-link-line red-ui-flow-link-path") .classed("red-ui-flow-link-link", function(d) { return d.link }) .classed("red-ui-flow-subflow-link", function(d) { return !d.link && activeSubflow }); }); link.exit().remove(); var links = linkLayer.selectAll(".red-ui-flow-link-path"); links.each(function(d) { var link = d3.select(this); if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { if (/red-ui-flow-link-line/.test(link.attr('class'))) { link.classed("red-ui-flow-subflow-link", function(d) { return !d.link && activeSubflow }); } link.attr("d",function(d){ var numOutputs = d.source.outputs || 1; var sourcePort = d.sourcePort || 0; var y = -((numOutputs-1)/2)*13 +13*sourcePort; d.x1 = d.source.x+d.source.w/2; d.y1 = d.source.y+y; d.x2 = d.target.x-d.target.w/2; d.y2 = d.target.y; // return "M "+d.x1+" "+d.y1+ // " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+ // (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+ // d.x2+" "+d.y2; var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1); if (/NaN/.test(path)) { return "" } return path; }); link.classed("red-ui-flow-node-disabled", function(d) { return d.source.d || d.target.d; }); } }) link.classed("red-ui-flow-link-selected", function(d) { return d === selected_link || d.selected; }); link.classed("red-ui-flow-link-unknown",function(d) { delete d.added; return d.target.type == "unknown" || d.source.type == "unknown" }); var offLinks = linkLayer.selectAll(".red-ui-flow-link-off-flow").data( activeFlowLinks, function(d) { return d.node.id+":"+d.refresh } ); var offLinksEnter = offLinks.enter().insert("g",".red-ui-flow-node").attr("class","red-ui-flow-link-off-flow"); offLinksEnter.each(function(d,i) { var g = d3.select(this); var s = 1; var labelAnchor = "start"; if (d.node.type === "link in") { s = -1; labelAnchor = "end"; } var stemLength = s*30; var branchLength = s*20; var l = g.append("svg:path").attr("class","red-ui-flow-link-link").attr("d","M 0 0 h "+stemLength); var links = d.links; var flows = Object.keys(links); var tabOrder = RED.nodes.getWorkspaceOrder(); flows.sort(function(A,B) { return tabOrder.indexOf(A) - tabOrder.indexOf(B); }); var linkWidth = 10; var h = node_height; var y = -(flows.length-1)*h/2; var linkGroups = g.selectAll(".red-ui-flow-link-group").data(flows); var enterLinkGroups = linkGroups.enter().append("g").attr("class","red-ui-flow-link-group") .on('mouseover', function() { if (mouse_mode !== 0) { return } d3.select(this).classed('red-ui-flow-link-group-active',true)}) .on('mouseout', function() {if (mouse_mode !== 0) { return } d3.select(this).classed('red-ui-flow-link-group-active',false)}) .on('mousedown', function() { d3.event.preventDefault(); d3.event.stopPropagation(); }) .on('mouseup', function(f) { if (mouse_mode !== 0) { return } d3.event.stopPropagation(); var targets = d.links[f]; RED.workspaces.show(f); targets.forEach(function(n) { n.selected = true; n.dirty = true; moving_set.push({n:n}); }); updateSelection(); redraw(); }); enterLinkGroups.each(function(f) { var linkG = d3.select(this); linkG.append("svg:path") .attr("class","red-ui-flow-link-link") .attr("d", "M "+stemLength+" 0 "+ "C "+(stemLength+(1.7*branchLength))+" "+0+ " "+(stemLength+(0.1*branchLength))+" "+y+" "+ (stemLength+branchLength*1.5)+" "+y+" " ); linkG.append("svg:path") .attr("class","red-ui-flow-link-port") .attr("d", "M "+(stemLength+branchLength*1.5+s*(linkWidth+7))+" "+(y-12)+" "+ "h "+(-s*linkWidth)+" "+ "a 3 3 45 0 "+(s===1?"0":"1")+" "+(s*-3)+" 3 "+ "v 18 "+ "a 3 3 45 0 "+(s===1?"0":"1")+" "+(s*3)+" 3 "+ "h "+(s*linkWidth) ); linkG.append("svg:path") .attr("class","red-ui-flow-link-port") .attr("d", "M "+(stemLength+branchLength*1.5+s*(linkWidth+10))+" "+(y-12)+" "+ "h "+(s*(linkWidth*3))+" "+ "M "+(stemLength+branchLength*1.5+s*(linkWidth+10))+" "+(y+12)+" "+ "h "+(s*(linkWidth*3)) ).style("stroke-dasharray","12 3 8 4 3"); linkG.append("rect").attr("class","red-ui-flow-port red-ui-flow-link-port") .attr("x",stemLength+branchLength*1.5-4+(s*4)) .attr("y",y-4) .attr("rx",2) .attr("ry",2) .attr("width",8) .attr("height",8); linkG.append("rect") .attr("x",stemLength+branchLength*1.5-(s===-1?node_width:0)) .attr("y",y-12) .attr("width",node_width) .attr("height",24) .style("stroke","none") .style("fill","transparent") var tab = RED.nodes.workspace(f); var label; if (tab) { label = tab.label || tab.id; } linkG.append("svg:text") .attr("class","red-ui-flow-port-label") .attr("x",stemLength+branchLength*1.5+(s*15)) .attr("y",y+1) .style("font-size","10px") .style("text-anchor",labelAnchor) .text(label); y += h; }); linkGroups.exit().remove(); }); offLinks.exit().remove(); offLinks = linkLayer.selectAll(".red-ui-flow-link-off-flow"); offLinks.each(function(d) { var s = 1; if (d.node.type === "link in") { s = -1; } var link = d3.select(this); link.attr("transform", function(d) { return "translate(" + (d.node.x+(s*d.node.w/2)) + "," + (d.node.y) + ")"; }); }) var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id }); group.exit().remove(); var groupEnter = group.enter().insert("svg:g") .attr("class", "red-ui-flow-group") groupEnter.each(function(d,i) { var g = d3.select(this); // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-0",true).style({ // "fill":"white", // "stroke": "#ff7f0e", // "stroke-width": 2 // }).attr("d","m -5 6 v -2 q 0 -9 9 -9 h 2 v 11 z") // // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-1",true).style({ // "fill": "white", // "stroke": "#ff7f0e", // "stroke-width": 2 // }).attr("d","m -6 -5 h 2 q 9 0 9 9 v 2 h -11 z") // // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-2",true).style({ // "fill": "white", // "stroke": "#ff7f0e", // "stroke-width": 2 // }).attr("d","m 5 -6 v 2 q 0 9 -9 9 h -2 v -11 z") // // g.append('path').classed("red-ui-flow-group-handle red-ui-flow-group-handle-3",true).style({ // "fill": "white", // "stroke": "#ff7f0e", // "stroke-width": 2 // }).attr("d","m 6 5 h -2 q -9 0 -9 -9 v -2 h 11 z") g.append('rect').classed("red-ui-flow-group-outline",true) .attr('rx',1).attr('ry',1).style({ "fill":"none", "stroke": "#ff7f0e", "pointer-events": "stroke", "stroke-opacity": 0, "stroke-width": 15 }) g.append('rect').classed("red-ui-flow-group-body",true) .attr('rx',1).attr('ry',1).style({ "pointer-events": "none", "fill":d.fill||"none", "stroke": d.stroke||"none", "stroke-width": 2 }) g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp) d.dirty = true; }); group.each(function(d,i) { if (d.dirty || dirtyGroups[d.id]) { var g = d3.select(this); var minX = Number.POSITIVE_INFINITY; var minY = Number.POSITIVE_INFINITY; var maxX = 0; var maxY = 0; d.nodes.forEach(function(n) { if (n.type !== "group") { minX = Math.min(minX,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0)); minY = Math.min(minY,n.y-n.h/2-25); maxX = Math.max(maxX,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0)); maxY = Math.max(maxY,n.y+n.h/2+25); } else { minX = Math.min(minX,n.x-25) minY = Math.min(minY,n.y-25) maxX = Math.max(maxX,n.x+n.w+25) maxY = Math.max(maxY,n.y+n.h+25) } }); d.x = minX; d.y = minY; d.w = maxX - minX; d.h = maxY - minY; g.attr("transform","translate("+d.x+","+d.y+")"); g.selectAll(".red-ui-flow-group-outline") .attr("width",d.w) .attr("height",d.h) .style("stroke-opacity",function(d) { if (d.selected) { return 0.3 } return 0}); g.selectAll(".red-ui-flow-group-body") .attr("width",d.w) .attr("height",d.h) .style("stroke",function(d) { /*if (d.selected) { return "#ff7f0e" } */return d.style.stroke || "none"}) .style("stroke-dasharray", function(d) { return (d.active||d.hovered)?"10 4":"none"}) .style("fill", function(d) { return d.style.fill || "none"}) .style("fill-opacity", 0.1) // g.selectAll(".red-ui-flow-group-handle-0") // .style("opacity",function(d) { return d.selected?1:0}) // g.selectAll(".red-ui-flow-group-handle-1") // .attr("transform","translate("+(maxX-minX)+",0)") // .style("opacity",function(d) { return d.selected?1:0}) // g.selectAll(".red-ui-flow-group-handle-2") // .attr("transform","translate("+(maxX-minX)+","+(maxY-minY)+")") // .style("opacity",function(d) { return d.selected?1:0}) // g.selectAll(".red-ui-flow-group-handle-3") // .attr("transform","translate(0,"+(maxY-minY)+")") // .style("opacity",function(d) { return d.selected?1:0}) delete dirtyGroups[d.id]; delete d.dirty; } }) } else { // JOINING - unselect any selected links linkLayer.selectAll(".red-ui-flow-link-selected").data( activeLinks, function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i; } ).classed("red-ui-flow-link-selected", false); } RED.view.navigator.refresh(); if (d3.event) { d3.event.preventDefault(); } } function focusView() { try { // Workaround for browser unexpectedly scrolling iframe into full // view - record the parent scroll position and restore it after // setting the focus var scrollX = window.parent.window.scrollX; var scrollY = window.parent.window.scrollY; chart.trigger("focus"); window.parent.window.scrollTo(scrollX,scrollY); } catch(err) { // In case we're iframed into a page of a different origin, just focus // the view following the inevitable DOMException chart.trigger("focus"); } } /** * Imports a new collection of nodes from a JSON String. * - all get new IDs assigned * - all "selected" * - attached to mouse for placing - "IMPORT_DRAGGING" */ function importNodes(newNodesStr,addNewFlow,touchImport) { if (mouse_mode === RED.state.SELECTING_NODE) { return; } try { var activeSubflowChanged; if (activeSubflow) { activeSubflowChanged = activeSubflow.changed; } var result = RED.nodes.import(newNodesStr,true,addNewFlow); if (result) { var new_nodes = result[0]; var new_links = result[1]; var new_workspaces = result[2]; var new_subflows = result[3]; var new_default_workspace = result[4]; if (addNewFlow && new_default_workspace) { RED.workspaces.show(new_default_workspace.id); } var new_ms = new_nodes.filter(function(n) { return n.hasOwnProperty("x") && n.hasOwnProperty("y") && n.z == RED.workspaces.active() }).map(function(n) { return {n:n};}); var new_node_ids = new_nodes.map(function(n){ n.changed = true; return n.id; }); // TODO: pick a more sensible root node if (new_ms.length > 0) { var root_node = new_ms[0].n; var dx = root_node.x; var dy = root_node.y; if (mouse_position == null) { mouse_position = [0,0]; } var minX = 0; var minY = 0; var i; var node; for (i=0;i 0 && node.n._def.outputs > 0; } } RED.keyboard.add("*","escape",function(){ RED.keyboard.remove("escape"); clearSelection(); RED.history.pop(); mouse_mode = 0; }); clearSelection(); moving_set = new_ms; } var historyEvent = { t:"add", nodes:new_node_ids, links:new_links, workspaces:new_workspaces, subflows:new_subflows, dirty:RED.nodes.dirty() }; if (new_ms.length === 0) { RED.nodes.dirty(true); } if (activeSubflow) { var subflowRefresh = RED.subflow.refresh(true); if (subflowRefresh) { historyEvent.subflow = { id:activeSubflow.id, changed: activeSubflowChanged, instances: subflowRefresh.instances } } } RED.history.push(historyEvent); updateActiveNodes(); redraw(); var counts = []; var newNodeCount = 0; var newConfigNodeCount = 0; new_nodes.forEach(function(n) { if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) { newNodeCount++; } else { newConfigNodeCount++; } }) if (new_workspaces.length > 0) { counts.push(RED._("clipboard.flow",{count:new_workspaces.length})); } if (newNodeCount > 0) { counts.push(RED._("clipboard.node",{count:newNodeCount})); } if (newConfigNodeCount > 0) { counts.push(RED._("clipboard.configNode",{count:newNodeCount})); } if (new_subflows.length > 0) { counts.push(RED._("clipboard.subflow",{count:new_subflows.length})); } if (counts.length > 0) { var countList = "
  • "+counts.join("
  • ")+"
"; RED.notify("

"+RED._("clipboard.nodesImported")+"

"+countList,{id:"clipboard"}); } } } catch(error) { if (error.code != "NODE_RED") { console.log(error.stack); RED.notify(RED._("notification.error",{message:error.toString()}),"error"); } else { RED.notify(RED._("notification.error",{message:error.message}),"error"); } } } function toggleShowGrid(state) { if (state) { gridLayer.style("visibility","visible"); } else { gridLayer.style("visibility","hidden"); } } function toggleSnapGrid(state) { snapGrid = state; redraw(); } function toggleStatus(s) { showStatus = s; RED.nodes.eachNode(function(n) { n.dirtyStatus = true; n.dirty = true;}); //TODO: subscribe/unsubscribe here redraw(); } function setSelectedNodeState(isDisabled) { if (mouse_mode === RED.state.SELECTING_NODE) { return; } var workspaceSelection = RED.workspaces.selection(); var changed = false; if (workspaceSelection.length > 0) { // TODO: toggle workspace state } else if (moving_set.length > 0) { var historyEvents = []; for (var i=0;i 0) { RED.history.push({ t:"multi", events: historyEvents, dirty:RED.nodes.dirty() }) RED.nodes.dirty(true) } } RED.view.redraw(); } return { init: init, state:function(state) { if (state == null) { return mouse_mode } else { mouse_mode = state; } }, redraw: function(updateActive) { if (updateActive) { updateActiveNodes(); updateSelection(); } redraw(); }, focus: focusView, importNodes: importNodes, calculateTextWidth: calculateTextWidth, select: function(selection) { if (typeof selection !== "undefined") { clearSelection(); if (typeof selection == "string") { var selectedNode = RED.nodes.node(selection); if (selectedNode) { selectedNode.selected = true; selectedNode.dirty = true; moving_set = [{n:selectedNode}]; } } else if (selection) { if (selection.groups) { updateActiveNodes(); selection.groups.forEach(function(g) { selectGroup(g,true); }) } } } updateSelection(); redraw(true); }, selection: function() { var selection = {}; var allNodes = new Set(); if (moving_set.length > 0) { moving_set.forEach(function(n) { allNodes.add(n.n); }); } var selectedGroups = activeGroups.filter(function(g) { return g.selected }); if (selectedGroups.length > 0) { if (selectedGroups.length === 1 && selectedGroups[0].active) { // Let nodes be nodes } else { selectedGroups.forEach(function(g) { var groupNodes = RED.group.getNodes(g,true); groupNodes.forEach(function(n) { allNodes.delete(n); }); allNodes.add(g); }); } } if (allNodes.size > 0) { selection.nodes = Array.from(allNodes); } if (selected_link != null) { selection.link = selected_link; } return selection; }, scale: function() { return scaleFactor; }, getLinksAtPoint: function(x,y) { var result = []; var links = outer.selectAll(".red-ui-flow-link-background")[0]; for (var i=0;i= bb.x && y >= bb.y && x <= bb.x+bb.width && y <= bb.y+bb.height) { result.push(links[i]) } } return result; }, reveal: function(id) { if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) { RED.workspaces.show(id); } else { var node = RED.nodes.node(id); if (node._def.category !== 'config' && node.z) { node.highlighted = true; node.dirty = true; RED.workspaces.show(node.z); var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor]; var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor]; if (node.x < scrollPos[0] || node.y < scrollPos[1] || node.x > screenSize[0]+scrollPos[0] || node.y > screenSize[1]+scrollPos[1]) { var deltaX = '-='+(((scrollPos[0] - node.x) + screenSize[0]/2)*scaleFactor); var deltaY = '-='+(((scrollPos[1] - node.y) + screenSize[1]/2)*scaleFactor); chart.animate({ scrollLeft: deltaX, scrollTop: deltaY },200); } if (!node._flashing) { node._flashing = true; var flash = 22; var flashFunc = function() { flash--; node.dirty = true; if (flash >= 0) { node.highlighted = !node.highlighted; setTimeout(flashFunc,100); } else { node.highlighted = false; delete node._flashing; } RED.view.redraw(); } flashFunc(); } } else if (node._def.category === 'config') { RED.sidebar.config.show(id); } } }, gridSize: function(v) { if (v === undefined) { return gridSize; } else { gridSize = Math.max(5,v); updateGrid(); } }, getActiveNodes: function() { return activeNodes; }, selectNodes: function(options) { $("#red-ui-workspace-tabs-shade").show(); $("#red-ui-palette-shade").show(); $("#red-ui-sidebar-shade").show(); $("#red-ui-header-shade").show(); $("#red-ui-workspace").addClass("red-ui-workspace-select-mode"); mouse_mode = RED.state.SELECTING_NODE; clearSelection(); if (options.selected) { console.log(options.selected); options.selected.forEach(function(id) { var n = RED.nodes.node(id); if (n) { n.selected = true; n.dirty = true; moving_set.push({n:n}); } }) } redraw(); selectNodesOptions = options||{}; var closeNotification = function() { clearSelection(); $("#red-ui-workspace-tabs-shade").hide(); $("#red-ui-palette-shade").hide(); $("#red-ui-sidebar-shade").hide(); $("#red-ui-header-shade").hide(); $("#red-ui-workspace").removeClass("red-ui-workspace-select-mode"); resetMouseVars(); notification.close(); } selectNodesOptions.done = function(selection) { closeNotification(); if (selectNodesOptions.onselect) { selectNodesOptions.onselect(selection); } } var buttons = [{ text: RED._("common.label.cancel"), click: function(e) { closeNotification(); if (selectNodesOptions.oncancel) { selectNodesOptions.oncancel(); } } }]; if (!selectNodesOptions.single) { buttons.push({ text: RED._("common.label.done"), class: "primary", click: function(e) { var selection = moving_set.map(function(n) { return n.n;}); selectNodesOptions.done(selection); } }); } var notification = RED.notify(selectNodesOptions.prompt || RED._("workspace.selectNodes"),{ modal: false, fixed: true, type: "compact", buttons: buttons }) }, scroll: function(x,y) { chart.scrollLeft(chart.scrollLeft()+x); chart.scrollTop(chart.scrollTop()+y) } }; })();