/** * 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" * |- "groupSelectLayer" * |- "linkLayer" * |- "dragGroupLayer" * |- "nodeLayer" */ RED.view = (function() { var space_width = 5000, space_height = 5000, lineCurveScale = 0.75, scaleFactor = 1, node_width = 100, node_height = 30, dblClickInterval = 650; 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; var mousedown_link = null; var mousedown_node = null; var mousedown_group = null; var mousedown_port_type = null; var mousedown_port_index = 0; var mouseup_node = null; var mouse_offset = [0,0]; var mouse_position = null; var mouse_mode = 0; var mousedown_group_handle = null; var moving_set = []; var lasso = null; var ghostNode = null; var showStatus = false; var lastClickNode = null; var dblClickPrimed = null; var clickTime = 0; var clickElapsed = 0; var scroll_position = []; var quickAddActive = false; var quickAddLink = null; var showAllLinkPorts = -1; var 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 groupSelectLayer; 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") .style("touch-action","none") .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() { d3.event.preventDefault(); clearTimeout(touchStartTime); touchStartTime = null; if (RED.touch.radialMenu.active()) { return; } canvasMouseUp.call(this); }) .on("touchcancel", function() { if (RED.view.DEBUG) { console.warn("eventLayer.touchcancel", mouse_mode); } d3.event.preventDefault(); canvasMouseUp.call(this); }) .on("touchstart", function() { if (RED.view.DEBUG) { console.warn("eventLayer.touchstart", mouse_mode); } 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); } d3.event.preventDefault(); }) .on("touchmove", function(){ if (RED.touch.radialMenu.active()) { d3.event.preventDefault(); return; } if (RED.view.DEBUG) { console.warn("eventLayer.touchmove", mouse_mode, mousedown_node); } 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; if (!mousedown_node && !mousedown_group) { mouse_mode = RED.state.PANNING; mouse_position = [touch0.pageX,touch0.pageY] scroll_position = [chart.scrollLeft(),chart.scrollTop()]; } } } 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(); } } d3.event.preventDefault(); }); // 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"); groupSelectLayer = 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.nodes.add(nn); var group = $(ui.helper).data("group"); if (group) { RED.group.addToGroup(group, nn); historyEvent = { t: 'multi', events: [historyEvent], } historyEvent.events.push({ t: "addToGroup", group: group, nodes: nn }) } RED.history.push(historyEvent); RED.editor.validateNode(nn); RED.nodes.dirty(true); // auto select dropped node - so info shows (if visible) exitActiveGroup(); clearSelection(); nn.selected = true; moving_set.push({n:nn}); if (group) { selectGroup(group,false); enterActiveGroup(group); activeGroup = group; } 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 (RED.view.DEBUG) { 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, touchTrigger) { if (targetGroup && !targetGroup.active) { selectGroup(targetGroup,false); enterActiveGroup(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]), disableFocus: touchTrigger, 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) { if (touchTrigger) { keepAdding = false; resetMouseVars(); } 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(); } } } RED.nodes.add(nn); RED.editor.validateNode(nn); if (targetGroup) { RED.group.addToGroup(targetGroup, nn); if (historyEvent.t !== "multi") { historyEvent = { t:'multi', events: [historyEvent] } } historyEvent.events.push({ t: "addToGroup", group: targetGroup, nodes: 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.dirty(true); // auto select dropped node - so info shows (if visible) clearSelection(); nn.selected = true; if (targetGroup) { selectGroup(targetGroup,false); enterActiveGroup(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]; if (d3.event.touches) { var touch0 = d3.event.touches.item(0); pos = [touch0.pageX, touch0.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 && !dblClickPrimed) || (dblClickPrimed && d > 10)) { 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 i = 0; // Prefer to snap nodes to the grid if there is one in the selection do { node = moving_set[i++]; } while(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(); } activeGroups.forEach(function(g) { if (!g.selected) { if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) { while (g.g) { g = RED.nodes.group(g.g); } if (!g.selected) { selectGroup(g,true); } } } }) activeNodes.forEach(function(n) { if (!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); while (group.g) { group = RED.nodes.group(group.g); } if (!group.selected) { selectGroup(group,true); } } else { n.selected = true; n.dirty = true; moving_set.push({n:n}); } } } }); // var selectionChanged = false; // do { // selectionChanged = false; // selectedGroups.forEach(function(g) { // if (g.g && g.selected && RED.nodes.group(g.g).selected) { // g.selected = false; // selectionChanged = true; // } // }) // } while(selectionChanged); 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) { var addedToGroup = null; 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(); } if (addedToGroup) { historyEvent.addToGroup = addedToGroup; } 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; } exitActiveGroup(); activeGroups.forEach(function(g) { if (!g.g) { selectGroup(g, true); if (!g.selected) { g.selected = true; g.dirty = true; } } else { g.selected = false; 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 (RED.view.DEBUG) { console.warn("clearSelection", mouse_mode,"moving_set.length:",moving_set.length); } 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') { 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 if (node.type === "group") { RED.editor.editGroup(node); } 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: [], groups: [], workspaces: [], subflows: [] } var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0); for (var i=0;i 0 || selected_link != null) { var result; var node; var removedNodes = []; var removedLinks = []; var removedGroups = []; var removedSubflowOutputs = []; var removedSubflowInputs = []; var removedSubflowStatus; var subflowInstances = []; var startDirty = RED.nodes.dirty(); var startChanged = false; var selectedGroups = []; if (moving_set.length > 0) { for (var i=0;i=0; i--) { var g = selectedGroups[i]; removedGroups.push(g); RED.nodes.removeGroup(g); } if (removedSubflowOutputs.length > 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 || removedGroups.length > 0) { 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, groups: removedGroups, 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.groups(n.id)); nodes = nodes.concat(RED.nodes.filterNodes({z:n.id})); } }); } else { selection = RED.view.selection(); if (selection.nodes) { selection.nodes.forEach(function(n) { nodes.push(n); if (n.type === 'group') { nodes = nodes.concat(RED.group.getNodes(n,true)); } }) } } if (nodes.length > 0) { var nns = []; var nodeCount = 0; var groupCount = 0; var handled = {}; for (var n=0;n 0) { RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"}); } else if (groupCount > 0) { RED.notify(RED._("clipboard.groupCopied",{count:groupCount}),{id:"clipboard"}); } } } function calculateTextWidth(str, className) { var result=convertLineBreakCharacter(str); var width = 0; for (var i=0;i 1) { var i=0; for (i=0;i 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" && evt instanceof TouchEvent) { var found = false; 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]) { found = true; mouseup_node = n; portType = mouseup_node.inputs>0?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT; portIndex = 0; } } }); if (!found && activeSubflow) { var subflowPorts = []; if (activeSubflow.status) { subflowPorts.push(activeSubflow.status) } if (activeSubflow.in) { subflowPorts = subflowPorts.concat(activeSubflow.in) } if (activeSubflow.out) { subflowPorts = subflowPorts.concat(activeSubflow.out) } for (var i=0;i mouse_position[0] && n.y-hhmouse_position[1]) { found = true; mouseup_node = n; portType = mouseup_node.direction === "in"?PORT_TYPE_OUTPUT:PORT_TYPE_INPUT; portIndex = 0; break; } } } } 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"); // First check for a user-provided newline - "\\n " var newlineIndex = content.indexOf("\\n "); if (newlineIndex > -1 && content[newlineIndex-1] !== '\\') { content = content.substring(0,newlineIndex)+"..."; } 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"); labelWidth = Math.max(labelWidth,labelDimensions[0] + 14); 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 prepareDrag(mouse) { mouse_mode = RED.state.MOVING; // Called when moving_set should be prepared to be dragged for (i=0;i 0 && clickElapsed < dblClickInterval) { 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(); selectGroup(RED.nodes.group(d.g), false); enterActiveGroup(RED.nodes.group(d.g)) 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); updateSelection(); 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; } } else { if (drag_lines[0].portType === 1) { direction = PORT_TYPE_OUTPUT; } else { direction = PORT_TYPE_INPUT; } } } } portMouseUp(d, direction, 0); if (wasJoining) { d3.selectAll(".red-ui-flow-port-hovered").classed("red-ui-flow-port-hovered",false); } } function nodeMouseDown(d) { if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); } focusView(); if (d3.event.button === 1) { return; } //var touch0 = d3.event; //var pos = [touch0.pageX,touch0.pageY]; //RED.touch.radialMenu.show(d3.select(this),pos); if (mouse_mode == RED.state.IMPORT_DRAGGING) { RED.keyboard.remove("escape"); 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); var historyEvent = RED.history.peek(); historyEvent.links = [link1,link2]; historyEvent.removedLinks = [spliceLink]; updateActiveNodes(); } updateSelection(); RED.nodes.dirty(true); redraw(); resetMouseVars(); d3.event.stopPropagation(); return; } else if (mouse_mode == RED.state.QUICK_JOINING) { d3.event.stopPropagation(); return; } else if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); if (selectNodesOptions.single) { selectNodesOptions.done(d); return; } if (d.selected) { d.selected = false; for (i=0;i 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); } } } function nodeMouseOut(d) { if (RED.view.DEBUG) { console.warn("nodeMouseOut", mouse_mode,d); } this.parentNode.classList.remove("red-ui-flow-node-hovered"); 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); } } } function portMouseDownProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); } function portTouchStartProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() } function portMouseUpProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); } function portTouchEndProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() } function portMouseOverProxy(e) { portMouseOver(d3.select(this), this.__data__,this.__portType__,this.__portIndex__, e); } function portMouseOutProxy(e) { portMouseOut(d3.select(this), this.__data__,this.__portType__,this.__portIndex__, e); } function linkMouseDown(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) { d3.select(this).classed("red-ui-flow-link-splice",true); var point = d3.mouse(this); var clickedGroup = getGroupAt(point[0],point[1]); showQuickAddDialog(point, selected_link, clickedGroup); } } function linkTouchStart(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); d3.event.preventDefault(); } function groupMouseUp(g) { if (dblClickPrimed && mousedown_group == g && clickElapsed > 0 && clickElapsed < dblClickInterval) { 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"); } 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.touches || d3.event.button === 0) && !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey && clickElapsed < dblClickInterval ); 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(); } if (activeGroup) { if (!RED.group.contains(activeGroup,g)) { // Clicked on a group that is outside the activeGroup exitActiveGroup(); } else { } } 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, addToMovingSet) { if (!g.selected) { g.selected = true; g.dirty = true; } if (addToMovingSet !== false) { moving_set.push({n:g}); } 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 enterActiveGroup(group) { if (activeGroup) { exitActiveGroup(); } group.active = true; group.dirty = true; activeGroup = group; for (var i = moving_set.length-1; i >= 0; i -= 1) { if (moving_set[i].n === group) { moving_set.splice(i,1); break; } } } 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); nodeSet.add(g); for (var i = moving_set.length-1; i >= 0; i -= 1) { if (nodeSet.has(moving_set[i].n) || moving_set[i].n === g) { 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[ids.length-1]] } } 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) { if (d3.event) { 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"); } } if (d3.event) { 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();}}); options.push({name:"add",onselect:function() { chartPos = chart.offset(); showQuickAddDialog([pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()],undefined,undefined,true) }}); 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") .style("display","none") .attr("xlink:href",iconUrl) .attr("class","red-ui-flow-node-icon") .attr("x",0) .attr("width","30") .attr("height","30"); 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 errorBadgeMouseEnter(e) { var d = this.__data__; if (d.validationErrors && d.validationErrors.length > 0) { clearTimeout(portLabelHoverTimeout); var node = this; portLabelHoverTimeout = setTimeout(function() { var pos = getElementPosition(node); portLabelHoverTimeout = null; portLabelHover = showTooltip( (pos[0]), (pos[1]), RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - "), "top" ); },500); } } function errorBadgeMouseLeave() { clearTimeout(portLabelHoverTimeout); if (portLabelHover) { portLabelHover.remove(); portLabelHover = null; } } function redrawStatus(d,nodeEl) { if (d.z !== RED.workspaces.active()) { return; } if (!nodeEl) { nodeEl = document.getElementById(d.id); } if (nodeEl) { if (!showStatus || !d.status) { nodeEl.__statusGroup__.style.display = "none"; } else { nodeEl.__statusGroup__.style.display = "inline"; var fill = status_colours[d.status.fill]; // Only allow our colours for now if (d.status.shape == null && fill == null) { nodeEl.__statusShape__.style.display = "none"; nodeEl.__statusGroup__.setAttribute("transform","translate(-14,"+(d.h+3)+")"); } else { nodeEl.__statusGroup__.setAttribute("transform","translate(3,"+(d.h+3)+")"); var statusClass = "red-ui-flow-node-status-"+(d.status.shape||"dot")+"-"+d.status.fill; nodeEl.__statusShape__.style.display = "inline"; nodeEl.__statusShape__.setAttribute("class","red-ui-flow-node-status "+statusClass); } if (d.status.hasOwnProperty('text')) { nodeEl.__statusLabel__.textContent = d.status.text; } else { nodeEl.__statusLabel__.textContent = ""; } } delete d.dirtyStatus; } } var pendingRedraw; function redraw() { if (RED.view.DEBUG_SYNC_REDRAW) { _redraw(); } else { if (pendingRedraw) { cancelAnimationFrame(pendingRedraw); } pendingRedraw = requestAnimationFrame(_redraw); } } 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",nodeTouchStart) .on("touchend",nodeTouchEnd) 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);d3.event.preventDefault();} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) .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",nodeTouchStart) .on("touchend", nodeTouchEnd); 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);d3.event.preventDefault();} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);d3.event.preventDefault();} ) .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",nodeTouchStart) .on("touchend", nodeTouchEnd); 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);d3.event.preventDefault();} ) .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);}) .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) .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", activeSubflow != null); nodeEnter.each(function(d,i) { this.__outputs__ = []; this.__inputs__ = []; var node = d3.select(this); var nodeContents = document.createDocumentFragment(); var isLink = (d.type === "link in" || d.type === "link out") var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; node.attr("id",d.id); d.h = node_height; d.resize = true; if (d._def.button) { var buttonGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); buttonGroup.__data__ = d; buttonGroup.setAttribute("transform", "translate("+((d._def.align == "right") ? 94 : -25)+",2)"); buttonGroup.setAttribute("class","red-ui-flow-node-button"); node[0][0].__buttonGroup__ = buttonGroup; var bgBackground = document.createElementNS("http://www.w3.org/2000/svg","rect"); bgBackground.__data__ = d; bgBackground.setAttribute("class","red-ui-flow-node-button-background"); bgBackground.setAttribute("rx",5); bgBackground.setAttribute("ry",5); bgBackground.setAttribute("width",32); bgBackground.setAttribute("height",node_height-4); buttonGroup.appendChild(bgBackground); node[0][0].__buttonGroupBackground__ = bgBackground; var bgButton = document.createElementNS("http://www.w3.org/2000/svg","rect"); bgButton.__data__ = d; bgButton.setAttribute("class","red-ui-flow-node-button-button"); bgButton.setAttribute("x", d._def.align == "right"? 11:5); bgButton.setAttribute("y",4); bgButton.setAttribute("rx",4); bgButton.setAttribute("ry",4); bgButton.setAttribute("width",16); bgButton.setAttribute("height",node_height-12); bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); d3.select(bgButton) .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",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();}) buttonGroup.appendChild(bgButton); node[0][0].__buttonGroupButton__ = bgButton; nodeContents.appendChild(buttonGroup); } var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); mainRect.__data__ = d; mainRect.setAttribute("class", "red-ui-flow-node "+(d.type == "unknown"?"red-ui-flow-node-unknown":"")); mainRect.setAttribute("rx", 5); mainRect.setAttribute("ry", 5); mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); node[0][0].__mainRect__ = mainRect; d3.select(mainRect) .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",nodeTouchStart) .on("touchend",nodeTouchEnd) .on("mouseover",nodeMouseOver) .on("mouseout",nodeMouseOut); nodeContents.appendChild(mainRect); //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_groupEl = document.createElementNS("http://www.w3.org/2000/svg","g"); icon_groupEl.__data__ = d; icon_groupEl.setAttribute("class","red-ui-flow-node-icon-group"+("right" == d._def.align?" red-ui-flow-node-icon-group-right":"")); icon_groupEl.setAttribute("x",0); icon_groupEl.setAttribute("y",0); icon_groupEl.style["pointer-events"] = "none"; node[0][0].__iconGroup__ = icon_groupEl; var icon_shade = document.createElementNS("http://www.w3.org/2000/svg","rect"); icon_shade.setAttribute("x",0); icon_shade.setAttribute("y",0); icon_shade.setAttribute("class","red-ui-flow-node-icon-shade") icon_shade.setAttribute("width",30); icon_shade.setAttribute("height",Math.min(50,d.h-4)); icon_groupEl.appendChild(icon_shade); node[0][0].__iconShade__ = icon_shade; var icon_group = d3.select(icon_groupEl) createIconAttributes(icon_url, icon_group, d); var icon_shade_border = document.createElementNS("http://www.w3.org/2000/svg","path"); icon_shade_border.setAttribute("d","right" != d._def.align ? "M 30 1 l 0 "+(d.h-2) : "M 0 1 l 0 "+(d.h-2) ) icon_shade_border.setAttribute("class", "red-ui-flow-node-icon-shade-border") icon_groupEl.appendChild(icon_shade_border); node[0][0].__iconShadeBorder__ = icon_shade_border; nodeContents.appendChild(icon_groupEl); } var text = document.createElementNS("http://www.w3.org/2000/svg","g"); text.setAttribute("class","red-ui-flow-node-label"+(hideLabel?" hide":"")+(d._def.align?" red-ui-flow-node-label-"+d._def.align:"")); text.setAttribute("transform","translate(38,0)"); // text.setAttribute("dy", ".3px"); // text.setAttribute("text-anchor",d._def.align !== "right" ? "start":"end"); nodeContents.appendChild(text); node[0][0].__textGroup__ = text; var statusEl = document.createElementNS("http://www.w3.org/2000/svg","g"); // statusEl.__data__ = d; statusEl.setAttribute("class","red-ui-flow-node-status-group"); statusEl.style.display = "none"; node[0][0].__statusGroup__ = statusEl; var statusRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); statusRect.setAttribute("class","red-ui-flow-node-status"); statusRect.setAttribute("x",6); statusRect.setAttribute("y",1); statusRect.setAttribute("width",9); statusRect.setAttribute("height",9); statusRect.setAttribute("rx",2); statusRect.setAttribute("ry",2); statusRect.setAttribute("stroke-width","3"); statusEl.appendChild(statusRect); node[0][0].__statusShape__ = statusRect; var statusLabel = document.createElementNS("http://www.w3.org/2000/svg","text"); statusLabel.setAttribute("class","red-ui-flow-node-status-label"); statusLabel.setAttribute("x",20); statusLabel.setAttribute("y",10); statusEl.appendChild(statusLabel); node[0][0].__statusLabel__ = statusLabel; nodeContents.appendChild(statusEl); var changeBadgeG = document.createElementNS("http://www.w3.org/2000/svg","g"); changeBadgeG.setAttribute("class","red-ui-flow-node-changed hide"); changeBadgeG.setAttribute("transform","translate(20, -2)"); node[0][0].__changeBadge__ = changeBadgeG; var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle"); changeBadge.setAttribute("r",5); changeBadgeG.appendChild(changeBadge); nodeContents.appendChild(changeBadgeG); var errorBadgeG = document.createElementNS("http://www.w3.org/2000/svg","g"); errorBadgeG.setAttribute("class","red-ui-flow-node-error hide"); errorBadgeG.setAttribute("transform","translate(0, -2)"); node[0][0].__errorBadge__ = errorBadgeG; var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path"); errorBadge.setAttribute("d","M -5,4 l 10,0 -5,-8 z"); errorBadgeG.appendChild(errorBadge); errorBadge.__data__ = d; errorBadge.addEventListener("mouseenter", errorBadgeMouseEnter); errorBadge.addEventListener("mouseleave", errorBadgeMouseLeave); nodeContents.appendChild(errorBadgeG); node[0][0].appendChild(nodeContents); }); node.each(function(d,i) { if (d.dirty) { var thisNode = d3.select(this); 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 label = RED.utils.getNodeLabel(d, d.type); var labelParts; if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label || this.__outputs__.length !== d.outputs) { labelParts = getLabelParts(label, "red-ui-flow-node-label"); this.__label__ = label; if (labelParts.lines.length !== this.__labelLineCount__) { d.resize = true; } this.__labelLineCount__ = labelParts.lines.length; if (hideLabel) { d.h = Math.max(node_height,(d.outputs || 0) * 15); } else { d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, 30); } this.__hideLabel__ = hideLabel; } if (d.resize) { var ow = d.w; if (hideLabel) { d.w = node_height; } else { d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)) ); } if (ow !== undefined) { d.x += (d.w-ow)/2; } d.resize = false; } //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}}); this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"); // This might be the first redraw after a node has been click-dragged to start a move. // So its selected state might have changed since the last redraw. this.classList.toggle("red-ui-flow-node-selected", !!d.selected ) if (mouse_mode != RED.state.MOVING_ACTIVE) { this.classList.toggle("red-ui-flow-node-disabled", d.d === true); this.__mainRect__.setAttribute("width", d.w) this.__mainRect__.setAttribute("height", d.h) this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted ); if (labelParts) { // The label has changed var sa = labelParts.lines; var sn = labelParts.lines.length; var textLines = this.__textGroup__.childNodes; while(textLines.length > sn) { textLines[textLines.length-1].remove(); } for (var i=0; i numOutputs) { var port = this.__outputs__.pop(); port.remove(); } for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) { var portGroup; if (portIndex === this.__outputs__.length) { portGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); portGroup.setAttribute("class","red-ui-flow-port-output"); var portPort; if (d.type === "link out") { portPort = document.createElementNS("http://www.w3.org/2000/svg","circle"); portPort.setAttribute("cx",11); portPort.setAttribute("cy",5); portPort.setAttribute("r",5); portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port"); } else { portPort = document.createElementNS("http://www.w3.org/2000/svg","rect"); portPort.setAttribute("rx",3); portPort.setAttribute("ry",3); portPort.setAttribute("width",10); portPort.setAttribute("height",10); portPort.setAttribute("class","red-ui-flow-port"); } portGroup.appendChild(portPort); portPort.__data__ = this.__data__; portPort.__portType__ = PORT_TYPE_OUTPUT; portPort.__portIndex__ = portIndex; portPort.addEventListener("mousedown", portMouseDownProxy); portPort.addEventListener("touchstart", portTouchStartProxy); portPort.addEventListener("mouseup", portMouseUpProxy); portPort.addEventListener("touchend", portTouchEndProxy); portPort.addEventListener("mouseover", portMouseOverProxy); portPort.addEventListener("mouseout", portMouseOutProxy); this.appendChild(portGroup); this.__outputs__.push(portGroup); } else { portGroup = this.__outputs__[portIndex]; } var x = d.w - 5; var y = (d.h/2)-((numOutputs-1)/2)*13; portGroup.setAttribute("transform","translate("+x+","+((y+13*portIndex)-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); icon = thisNode.select(".red-ui-flow-node-icon"); faIcon = thisNode.select(".fa-lg"); } icon.attr("y",function(){return (d.h-d3.select(this).attr("height"))/2;}); this.__iconShade__.setAttribute("height", d.h ); this.__iconShadeBorder__.setAttribute("d", "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0 : 30) + " 1 l 0 " + (d.h - 2) ); faIcon.attr("y",(d.h+13)/2); } this.__changeBadge__.setAttribute("transform", "translate("+(d.w-10)+", -2)"); this.__changeBadge__.classList.toggle("hide", !(d.changed||d.moved)); this.__errorBadge__.setAttribute("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)"); this.__errorBadge__.classList.toggle("hide", 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)+")";}) }); if (d._def.button) { var buttonEnabled = isButtonEnabled(d); this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled); 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); } this.__buttonGroup__.setAttribute("transform", "translate("+x+",2)"); if (d._def.button.toggle) { this.__buttonGroupButton__.setAttribute("fill-opacity",d[d._def.button.toggle]?1:0.2) this.__buttonGroupBackground__.setAttribute("fill-opacity",d[d._def.button.toggle]?1:0.2) } if (typeof d._def.button.visible === "function") { // is defined and a function... if (d._def.button.visible.call(d) === false) { this.__buttonGroup__.style.display = "none"; } else { this.__buttonGroup__.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) { redrawStatus(d,this); } 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 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); var pathContents = document.createDocumentFragment(); d.added = true; var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path"); pathBack.__data__ = d; pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":"")); this.__pathBack__ = pathBack; pathContents.appendChild(pathBack); d3.select(pathBack) .on("mousedown",linkMouseDown) .on("touchstart",linkTouchStart) var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path"); pathOutline.__data__ = d; pathOutline.setAttribute("class","red-ui-flow-link-outline red-ui-flow-link-path"); this.__pathOutline__ = pathOutline; pathContents.appendChild(pathOutline); var pathLine = document.createElementNS("http://www.w3.org/2000/svg","path"); pathLine.__data__ = d; pathLine.setAttribute("class","red-ui-flow-link-line red-ui-flow-link-path"+ (d.link?" red-ui-flow-link-link":(activeSubflow?" red-ui-flow-subflow-link":""))); this.__pathLine__ = pathLine; pathContents.appendChild(pathLine); l[0][0].appendChild(pathContents); }); link.exit().remove(); link.each(function(d) { var link = d3.select(this); if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { 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)) { path = "" } this.__pathBack__.setAttribute("d",path); this.__pathOutline__.setAttribute("d",path); this.__pathLine__.setAttribute("d",path); this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d)); this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow); } this.classList.toggle("red-ui-flow-link-selected", !!(d===selected_link||d.selected)); var connectedToUnknown = !!(d.target.type == "unknown" || d.source.type == "unknown"); this.classList.toggle("red-ui-flow-link-unknown",!!(d.target.type == "unknown" || d.source.type == "unknown")) delete d.added; }) 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().each(function(d,i) { document.getElementById("group_select_"+d.id).remove() }).remove(); var groupEnter = group.enter().insert("svg:g").attr("class", "red-ui-flow-group") var addedGroups = false; groupEnter.each(function(d,i) { addedGroups = true; var g = d3.select(this); g.attr("id",d.id); var groupBorderRadius = 4; var selectGroup = groupSelectLayer.append('g').attr("class", "red-ui-flow-group").attr("id","group_select_"+d.id); selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) .classed("red-ui-flow-group-outline-select-background",true) .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) .attr("x",-4) .attr("y",-4); selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) .attr("x",-4) .attr("y",-4) selectGroup.on("mousedown", function() {groupMouseDown.call(g[0][0],d)}); selectGroup.on("mouseup", function() {groupMouseUp.call(g[0][0],d)}); selectGroup.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();}); selectGroup.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();}); g.append('rect').classed("red-ui-flow-group-outline",true).attr('rx',0.5).attr('ry',0.5); g.append('rect').classed("red-ui-flow-group-body",true) .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius).style({ "fill":d.fill||"none", "stroke": d.stroke||"none", }) g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp) g.on("touchstart", function() {groupMouseDown.call(g[0][0],d); d3.event.preventDefault();}); g.on("touchend", function() {groupMouseUp.call(g[0][0],d); d3.event.preventDefault();}); g.append('svg:text').attr("class","red-ui-flow-group-label"); d.dirty = true; }); if (addedGroups) { group.sort(function(a,b) { if (a._root === b._root) { return a._depth - b._depth; } else { return a._root.localeCompare(b._root); } }) } group[0].reverse(); group.each(function(d,i) { if (d.resize) { d.minWidth = 0; delete d.resize; } if (d.dirty || dirtyGroups[d.id]) { var g = d3.select(this); if (d.nodes.length > 0) { var minX = Number.POSITIVE_INFINITY; var minY = Number.POSITIVE_INFINITY; var maxX = 0; var maxY = 0; var margin = 26; d.nodes.forEach(function(n) { if (n.type !== "group") { minX = Math.min(minX,n.x-n.w/2-margin-((n._def.button && n._def.align!=="right")?20:0)); minY = Math.min(minY,n.y-n.h/2-margin); maxX = Math.max(maxX,n.x+n.w/2+margin+((n._def.button && n._def.align=="right")?20:0)); maxY = Math.max(maxY,n.y+n.h/2+margin); } else { minX = Math.min(minX,n.x-margin) minY = Math.min(minY,n.y-margin) maxX = Math.max(maxX,n.x+n.w+margin) maxY = Math.max(maxY,n.y+n.h+margin) } }); d.x = minX; d.y = minY; d.w = maxX - minX; d.h = maxY - minY; } else { d.w = 40; d.h = 40; } if (!d.minWidth) { if (d.style.label && d.name) { var labelParts = getLabelParts(d.name||"","red-ui-flow-group-label"); d.minWidth = labelParts.width + 8; d.labels = labelParts.lines; } else { d.minWidth = 40; d.labels = []; } } d.w = Math.max(d.minWidth,d.w); if (d.style.label && d.labels.length > 0) { var labelPos = d.style["label-position"] || "nw"; var h = (d.labels.length-1) * 16; if (labelPos[0] === "s") { h += 8; } d.h += h; if (labelPos[0] === "n") { if (d.nodes.length > 0) { d.y -= h; } } } g.attr("transform","translate("+d.x+","+d.y+")") g.selectAll(".red-ui-flow-group-outline") .attr("width",d.w) .attr("height",d.h) var selectGroup = document.getElementById("group_select_"+d.id); selectGroup.setAttribute("transform","translate("+d.x+","+d.y+")"); if (d.hovered) { selectGroup.classList.add("red-ui-flow-group-hovered") } else { selectGroup.classList.remove("red-ui-flow-group-hovered") } var selectGroupRect = selectGroup.children[0]; selectGroupRect.setAttribute("width",d.w+8) selectGroupRect.setAttribute("height",d.h+8) selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; selectGroupRect = selectGroup.children[1]; selectGroupRect.setAttribute("width",d.w+8) selectGroupRect.setAttribute("height",d.h+8) selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; if (d.highlighted) { selectGroup.classList.add("red-ui-flow-node-highlighted"); } else { selectGroup.classList.remove("red-ui-flow-node-highlighted"); } g.selectAll(".red-ui-flow-group-body") .attr("width",d.w) .attr("height",d.h) .style("stroke", d.style.stroke || "") .style("stroke-opacity", d.style.hasOwnProperty('stroke-opacity') ? d.style['stroke-opacity'] : "") .style("fill", d.style.fill || "") .style("fill-opacity", d.style.hasOwnProperty('fill-opacity') ? d.style['fill-opacity'] : "") var label = g.selectAll(".red-ui-flow-group-label"); label.classed("hide",!!!d.style.label) if (d.style.label) { var labelPos = d.style["label-position"] || "nw"; var labelX = 0; var labelY = 0; if (labelPos[0] === 'n') { labelY = 0+15; // Allow for font-height } else { labelY = d.h - 5 -(d.labels.length -1) * 16; } if (labelPos[1] === 'w') { labelX = 5; labelAnchor = "start" } else if (labelPos[1] === 'e') { labelX = d.w-5; labelAnchor = "end" } else { labelX = d.w/2; labelAnchor = "middle" } if (d.style.hasOwnProperty('color')) { label.style("fill",d.style.color) } else { label.style("fill",null) } label.attr("transform","translate("+labelX+","+labelY+")") .attr("text-anchor",labelAnchor); if (d.labels) { var ypos = 0; g.selectAll(".red-ui-flow-group-label-text").remove(); d.labels.forEach(function (name) { label.append("tspan") .classed("red-ui-flow-group-label-text", true) .text(name) .attr("x", 0) .attr("y", ypos); ypos += 16; }); } else { g.selectAll(".red-ui-flow-group-label-text").remove(); } } 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_groups = result[2]; var new_workspaces = result[3]; var new_subflows = result[4]; var new_default_workspace = result[5]; 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};}); new_ms = new_ms.concat(new_groups.filter(function(g) { return g.z === RED.workspaces.active()}).map(function(g) { return {n:g}})) 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) { if (mouse_position == null) { mouse_position = [0,0]; } var dx = mouse_position[0]; var dy = mouse_position[1]; if (new_ms.length > 0) { var root_node = new_ms[0].n; dx = root_node.x; dy = root_node.y; } var minX = 0; var minY = 0; var i; var node,group; for (i=0;i 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.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, groups:new_groups, 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++; } }) var newGroupCount = new_groups.length; 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 (newGroupCount > 0) { counts.push(RED._("clipboard.group",{count:newGroupCount})); } 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(); } function getSelection() { var selection = {}; var allNodes = new Set(); if (moving_set.length > 0) { moving_set.forEach(function(n) { if (n.n.type !== 'group') { allNodes.add(n.n); } }); } var selectedGroups = activeGroups.filter(function(g) { return g.selected && !g.active }); 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; } return { init: init, state:function(state) { if (state == null) { return mouse_mode } else { mouse_mode = state; } }, updateActive: updateActiveNodes, redraw: function(updateActive, syncRedraw) { if (updateActive) { updateActiveNodes(); updateSelection(); } if (syncRedraw) { _redraw(); } else { 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.nodes) { updateActiveNodes(); moving_set = []; // TODO: this selection group span groups // - if all in one group -> activate the group // - if in multiple groups (or group/no-group) // -> select the first 'set' of things in the same group/no-group selection.nodes.forEach(function(n) { if (n.type !== "group") { n.selected = true; n.dirty = true; moving_set.push({n:n}); } else { selectGroup(n,true); } }) } } } updateSelection(); redraw(true); }, selection: getSelection, 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; }, getGroupAtPoint: getGroupAt, getActiveGroup: function() { return activeGroup }, reveal: function(id,triggerHighlight) { if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) { RED.workspaces.show(id); } else { var node = RED.nodes.node(id) || RED.nodes.group(id); if (node) { if (node.z && (node.type === "group" || node._def.category !== 'config')) { node.dirty = true; RED.workspaces.show(node.z); var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor]; var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor]; var cx = node.x; var cy = node.y; if (node.type === "group") { cx += node.w/2; cy += node.h/2; } if (cx < scrollPos[0] || cy < scrollPos[1] || cx > screenSize[0]+scrollPos[0] || cy > screenSize[1]+scrollPos[1]) { var deltaX = '-='+(((scrollPos[0] - cx) + screenSize[0]/2)*scaleFactor); var deltaY = '-='+(((scrollPos[1] - cy) + screenSize[1]/2)*scaleFactor); chart.animate({ scrollLeft: deltaX, scrollTop: deltaY },200); } if (triggerHighlight !== false) { node.highlighted = true; 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) { 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) }, clickNodeButton: function(n) { if (n._def.button) { nodeButtonClicked(n); } }, clipboard: function() { return clipboard }, redrawStatus: redrawStatus }; })();