From 68331fc40c4390f9b9f5bec29baecec8ea5ab083 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 8 Jun 2022 21:56:17 +0100 Subject: [PATCH 01/12] implement flows runtime stop/start API and UI --- Gruntfile.js | 1 + .../@node-red/editor-api/lib/admin/flows.js | 23 +++ .../@node-red/editor-api/lib/admin/index.js | 6 + .../editor-client/src/images/start.svg | 4 + .../editor-client/src/images/stop.svg | 4 + .../@node-red/editor-client/src/js/red.js | 5 + .../@node-red/editor-client/src/js/runtime.js | 53 +++++++ .../editor-client/src/js/ui/common/menu.js | 12 ++ .../editor-client/src/js/ui/deploy.js | 133 ++++++++++++------ .../@node-red/editor-client/src/js/ui/view.js | 3 + .../editor-client/src/sass/flow.scss | 7 + .../@node-red/runtime/lib/api/flows.js | 84 ++++++++++- .../@node-red/runtime/lib/api/settings.js | 12 ++ .../@node-red/runtime/lib/flows/index.js | 50 +++++-- packages/node_modules/node-red/settings.js | 15 +- .../editor-api/lib/admin/flows_spec.js | 97 +++++++++++++ .../@node-red/runtime/lib/api/flows_spec.js | 122 ++++++++++++++++ .../@node-red/runtime/lib/flows/index_spec.js | 89 +++++++++++- 18 files changed, 657 insertions(+), 63 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/images/start.svg create mode 100644 packages/node_modules/@node-red/editor-client/src/images/stop.svg create mode 100644 packages/node_modules/@node-red/editor-client/src/js/runtime.js diff --git a/Gruntfile.js b/Gruntfile.js index 979b38051..af0aa0e95 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -142,6 +142,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/settings.js", "packages/node_modules/@node-red/editor-client/src/js/user.js", "packages/node_modules/@node-red/editor-client/src/js/comms.js", + "packages/node_modules/@node-red/editor-client/src/js/runtime.js", "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js index 11b30e446..611d9c2ca 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js @@ -68,5 +68,28 @@ module.exports = { }).catch(function(err) { apiUtils.rejectHandler(req,res,err); }) + }, + getState: function(req,res) { + const opts = { + user: req.user, + req: apiUtils.getRequestLogObject(req) + } + runtimeAPI.flows.getState(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) + }, + postState: function(req,res) { + const opts = { + user: req.user, + requestedState: req.get("Node-RED-Flow-Run-State-Change")||"", + req: apiUtils.getRequestLogObject(req) + } + runtimeAPI.flows.setState(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 87fd0dec0..078779f5a 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -54,6 +54,12 @@ module.exports = { adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler); adminApp.post("/flows",needsPermission("flows.write"),flows.post,apiUtil.errorHandler); + // Flows/state + adminApp.get("/flows/state", needsPermission("flows.read"), flows.getState, apiUtil.errorHandler); + if (!settings.runtimeState || settings.runtimeState.enabled !== false) { + adminApp.post("/flows/state", needsPermission("flows.write"), flows.postState, apiUtil.errorHandler); + } + // Flow adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,apiUtil.errorHandler); adminApp.post("/flow",needsPermission("flows.write"),flow.post,apiUtil.errorHandler); diff --git a/packages/node_modules/@node-red/editor-client/src/images/start.svg b/packages/node_modules/@node-red/editor-client/src/images/start.svg new file mode 100644 index 000000000..9623be86c --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/images/start.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/images/stop.svg b/packages/node_modules/@node-red/editor-client/src/images/stop.svg new file mode 100644 index 000000000..13b1a945a --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/images/stop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 57d2d1e33..939c32f2e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -297,6 +297,10 @@ var RED = (function() { // handled below return; } + if (notificationId === "flows-run-state") { + // handled in editor-client/src/js/runtime.js + return; + } if (notificationId === "project-update") { loader.start(RED._("event.loadingProject"), 0); RED.nodes.clear(); @@ -747,6 +751,7 @@ var RED = (function() { RED.keyboard.init(buildMainMenu); RED.nodes.init(); + RED.runtime.init() RED.comms.connect(); $("#red-ui-main-container").show(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/runtime.js b/packages/node_modules/@node-red/editor-client/src/js/runtime.js new file mode 100644 index 000000000..939517877 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/runtime.js @@ -0,0 +1,53 @@ +RED.runtime = (function() { + let state = "" + let settings = {ui: true, enabled: true}; + const STOPPED = "stopped" + const STARTED = "started" + return { + init: function() { + // refresh the current runtime status from server + settings = RED.settings.runtimeState; + RED.runtime.requestState() + + // {id:"flows-run-state", started: false, state: "stopped", retain:true} + RED.comms.subscribe("notification/flows-run-state",function(topic,msg) { + RED.events.emit("flows-run-state",msg); + RED.runtime.updateState(msg.state); + }); + }, + get state() { + return state + }, + get started() { + return state === STARTED + }, + get states() { + return { STOPPED, STARTED } + }, + updateState: function(newState) { + state = newState; + // disable pointer events on node buttons (e.g. inject/debug nodes) + $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state === STOPPED) + // show/hide Start/Stop based on current state + if(!RED.settings.runtimeState || RED.settings.runtimeState.ui !== false) { + RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) + RED.menu.setVisible("deploymenu-item-runtime-start", state === STOPPED) + } + }, + requestState: function(callback) { + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: 'flows/state', + success: function(data) { + RED.runtime.updateState(data.state) + if(callback) { + callback(data.state) + } + } + }); + } + } +})() \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js index 417189b33..f42da5830 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js @@ -139,6 +139,9 @@ RED.menu = (function() { if (opt.disabled) { item.addClass("disabled"); } + if (opt.visible === false) { + item.addClass("hide"); + } } @@ -249,6 +252,14 @@ RED.menu = (function() { } } + function setVisible(id,state) { + if (!state) { + $("#"+id).parent().addClass("hide"); + } else { + $("#"+id).parent().removeClass("hide"); + } + } + function addItem(id,opt) { var item = createMenuItem(opt); if (opt !== null && opt.group) { @@ -305,6 +316,7 @@ RED.menu = (function() { isSelected: isSelected, toggleSelected: toggleSelected, setDisabled: setDisabled, + setVisible: setVisible, addItem: addItem, removeItem: removeItem, setAction: setAction, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 5b73ed271..69460f8cf 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -63,16 +63,18 @@ RED.deploy = (function() { ''+ ''+ '').prependTo(".red-ui-header-toolbar"); - RED.menu.init({id:"red-ui-header-button-deploy-options", - options: [ - {id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}}, - {id:"deploymenu-item-flow",toggle:"deploy-type",icon:"red/images/deploy-flows.svg",label:RED._("deploy.modifiedFlows"),sublabel:RED._("deploy.modifiedFlowsDesc"), onselect:function(s) {if(s){changeDeploymentType("flows")}}}, - {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}}, - null, - {id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"}, - - ] - }); + const mainMenuItems = [ + {id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}}, + {id:"deploymenu-item-flow",toggle:"deploy-type",icon:"red/images/deploy-flows.svg",label:RED._("deploy.modifiedFlows"),sublabel:RED._("deploy.modifiedFlowsDesc"), onselect:function(s) {if(s){changeDeploymentType("flows")}}}, + {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}}, + null + ] + if(!RED.settings.runtimeState || RED.settings.runtimeState.ui !== false) { + mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:"Start"/*RED._("deploy.startFlows")*/,sublabel:"Start Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:start-flows", visible:false}) + mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:"Stop"/*RED._("deploy.startFlows")*/,sublabel:"Stop Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:stop-flows", visible:false}) + } + mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"}) + RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems }); } else if (type == "simple") { var label = options.label || RED._("deploy.deploy"); var icon = 'red/images/deploy-full-o.svg'; @@ -100,6 +102,8 @@ RED.deploy = (function() { RED.actions.add("core:deploy-flows",save); if (type === "default") { + RED.actions.add("core:stop-flows",function() { stopStartFlows("stop") }); + RED.actions.add("core:start-flows",function() { stopStartFlows("start") }); RED.actions.add("core:restart-flows",restart); RED.actions.add("core:set-deploy-type-to-full",function() { RED.menu.setSelected("deploymenu-item-full",true);}); RED.actions.add("core:set-deploy-type-to-modified-flows",function() { RED.menu.setSelected("deploymenu-item-flow",true); }); @@ -270,18 +274,74 @@ RED.deploy = (function() { function sanitize(html) { return html.replace(/&/g,"&").replace(//g,">") } - function restart() { - var startTime = Date.now(); - $(".red-ui-deploy-button-content").css('opacity',0); - $(".red-ui-deploy-button-spinner").show(); - var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled"); - $("#red-ui-header-button-deploy").addClass("disabled"); - deployInflight = true; + + function shadeShow() { $("#red-ui-header-shade").show(); $("#red-ui-editor-shade").show(); $("#red-ui-palette-shade").show(); $("#red-ui-sidebar-shade").show(); - + } + function shadeHide() { + $("#red-ui-header-shade").hide(); + $("#red-ui-editor-shade").hide(); + $("#red-ui-palette-shade").hide(); + $("#red-ui-sidebar-shade").hide(); + } + function deployButtonSetBusy(){ + $(".red-ui-deploy-button-content").css('opacity',0); + $(".red-ui-deploy-button-spinner").show(); + $("#red-ui-header-button-deploy").addClass("disabled"); + } + function deployButtonClearBusy(){ + $(".red-ui-deploy-button-content").css('opacity',1); + $(".red-ui-deploy-button-spinner").hide(); + } + function stopStartFlows(state) { + const startTime = Date.now(); + const deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled"); + deployInflight = true; + deployButtonSetBusy(); + shadeShow(); + RED.runtime.updateState(state); + $.ajax({ + url:"flows/state", + type: "POST", + data: {state: state}, + headers: { + "Node-RED-Flow-Run-State-Change": state + } + }).done(function(data,textStatus,xhr) { + if (deployWasEnabled) { + $("#red-ui-header-button-deploy").removeClass("disabled"); + } + RED.runtime.updateState((data && data.state) || "unknown" ) + RED.notify('

Done

',"success"); + }).fail(function(xhr,textStatus,err) { + if (deployWasEnabled) { + $("#red-ui-header-button-deploy").removeClass("disabled"); + } + if (xhr.status === 401) { + RED.notify("Not authorized" ,"error"); + } else if (xhr.responseText) { + RED.notify("Operation failed: " + xhr.responseText,"error"); + } else { + RED.notify("Operation failed: no response","error"); + } + RED.runtime.requestState() + }).always(function() { + const delta = Math.max(0,300-(Date.now()-startTime)); + setTimeout(function() { + deployButtonClearBusy(); + shadeHide() + deployInflight = false; + },delta); + }); + } + function restart() { + var startTime = Date.now(); + var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled"); + deployInflight = true; + deployButtonSetBusy(); $.ajax({ url:"flows", type: "POST", @@ -307,15 +367,10 @@ RED.deploy = (function() { RED.notify(RED._("deploy.deployFailed",{message:RED._("deploy.errors.noResponse")}),"error"); } }).always(function() { - deployInflight = false; var delta = Math.max(0,300-(Date.now()-startTime)); setTimeout(function() { - $(".red-ui-deploy-button-content").css('opacity',1); - $(".red-ui-deploy-button-spinner").hide(); - $("#red-ui-header-shade").hide(); - $("#red-ui-editor-shade").hide(); - $("#red-ui-palette-shade").hide(); - $("#red-ui-sidebar-shade").hide(); + deployButtonClearBusy(); + deployInflight = false; },delta); }); } @@ -450,21 +505,17 @@ RED.deploy = (function() { const nns = RED.nodes.createCompleteNodeSet(); const startTime = Date.now(); - $(".red-ui-deploy-button-content").css('opacity', 0); - $(".red-ui-deploy-button-spinner").show(); - $("#red-ui-header-button-deploy").addClass("disabled"); - + deployButtonSetBusy(); const data = { flows: nns }; - - if (!force) { + data.runtimeState = RED.runtime.state; + if (data.runtimeState === RED.runtime.states.STOPPED || force) { + data._rev = RED.nodes.version(); + } else { data.rev = RED.nodes.version(); } - + deployInflight = true; - $("#red-ui-header-shade").show(); - $("#red-ui-editor-shade").show(); - $("#red-ui-palette-shade").show(); - $("#red-ui-sidebar-shade").show(); + shadeShow(); $.ajax({ url: "flows", type: "POST", @@ -550,15 +601,11 @@ RED.deploy = (function() { RED.notify(RED._("deploy.deployFailed", { message: RED._("deploy.errors.noResponse") }), "error"); } }).always(function () { - deployInflight = false; const delta = Math.max(0, 300 - (Date.now() - startTime)); setTimeout(function () { - $(".red-ui-deploy-button-content").css('opacity', 1); - $(".red-ui-deploy-button-spinner").hide(); - $("#red-ui-header-shade").hide(); - $("#red-ui-editor-shade").hide(); - $("#red-ui-palette-shade").hide(); - $("#red-ui-sidebar-shade").hide(); + deployInflight = false; + deployButtonClearBusy() + shadeHide() }, delta); }); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index adab87e38..59aa3fe6e 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4792,6 +4792,9 @@ RED.view = (function() { if (d._def.button) { var buttonEnabled = isButtonEnabled(d); this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled); + if(RED.runtime && Object.hasOwn(RED.runtime,'started')) { + this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started); + } var x = d._def.align == "right"?d.w-6:-25; if (d._def.button.toggle && !d[d._def.button.toggle]) { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index 2e6de1932..105d32cd6 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -176,6 +176,13 @@ cursor: default; } } + &.red-ui-flow-node-button-stopped { + opacity: 0.4; + .red-ui-flow-node-button-button { + cursor: default; + pointer-events: none; + } + } } .red-ui-flow-node-button-button { cursor: pointer; diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index d28a0479a..83ef68021 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -73,6 +73,10 @@ var api = module.exports = { if (deploymentType === 'reload') { apiPromise = runtime.flows.loadFlows(true); } else { + //ensure the runtime running/stopped state matches the deploying editor. If not, then copy the _rev number to flows.rev + if(flows.hasOwnProperty('_rev') && !flows.hasOwnProperty('rev') && (flows.runtimeState !== "stopped" || runtime.flows.started)) { + flows.rev = flows._rev + } if (flows.hasOwnProperty('rev')) { var currentVersion = runtime.flows.getFlows().rev; if (currentVersion !== flows.rev) { @@ -255,5 +259,83 @@ var api = module.exports = { } } return sendCredentials; - } + }, + /** + * Gets running state of runtime flows + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {{state:string, started:boolean}} - the current run state of the flows + * @memberof @node-red/runtime_flows + */ + getState: async function(opts) { + runtime.log.audit({event: "flows.getState"}, opts.req); + const result = { + state: runtime.flows.started ? "started" : "stopped", + started: !!runtime.flows.started, + rev: runtime.flows.getFlows().rev + } + return result; + }, + /** + * Sets running state of runtime flows + * @param {Object} opts + * @param {Object} opts.req - the request to log (optional) + * @param {User} opts.user - the user calling the api + * @param {string} opts.requestedState - the requested state. Valid values are "start" and "stop". + * @return {Promise} - the active flow configuration + * @memberof @node-red/runtime_flows + */ + setState: async function(opts) { + opts = opts || {}; + const makeError = (error, errcode, statusCode) => { + const message = typeof error == "object" ? error.message : error + const err = typeof error == "object" ? error : new Error(message||"Unexpected Error") + err.status = err.status || statusCode || 400; + err.code = err.code || errcode || "unexpected_error" + runtime.log.audit({ + event: "flows.setState", + state: opts.requestedState || "", + error: errcode || "unexpected_error", + message: err.code + }, opts.req); + return err + } + + const getState = () => { + return { + state: runtime.flows.started ? "started" : "stopped", + started: !!runtime.flows.started, + rev: runtime.flows.getFlows().rev, + } + } + + if(runtime.settings.runtimeState ? runtime.settings.runtimeState.enabled === false : false) { + throw (makeError("Method Not Allowed", "not_allowed", 405)) + } + switch (opts.requestedState) { + case "start": + try { + try { + runtime.settings.set('flowsRunStateRequested', opts.requestedState); + } catch(err) { } + await runtime.flows.startFlows("full") + return getState() + } catch (err) { + throw (makeError(err, err.code, 500)) + } + case "stop": + try { + try { + runtime.settings.set('flowsRunStateRequested', opts.requestedState); + } catch(err) { } + await runtime.flows.stopFlows("full") + return getState() + } catch (err) { + throw (makeError(err, err.code, 500)) + } + default: + throw (makeError("Cannot set runtime state. Invalid state", "invalid_run_state", 400)) + } + }, } diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index f56b8ab61..e35a7861d 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -148,6 +148,18 @@ var api = module.exports = { enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true, ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true } + if(safeSettings.diagnostics.enabled === false) { + safeSettings.diagnostics.ui = false; // cannot have UI without endpoint + } + + safeSettings.runtimeState = { + //unless runtimeState.ui and runtimeState.enabled are explicitly false, they will default to true. + enabled: (runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === false) ? false : true, + ui: (runtime.settings.runtimeState && runtime.settings.runtimeState.ui === false) ? false : true + } + if(safeSettings.runtimeState.enabled === false) { + safeSettings.runtimeState.ui = false; // cannot have UI without endpoint + } runtime.settings.exportNodeSettings(safeSettings); runtime.plugins.exportPluginSettings(safeSettings); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index ae9131ec0..b707b4aaf 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -261,6 +261,7 @@ function getFlows() { async function start(type,diff,muteLog) { type = type||"full"; + let reallyStarted = started started = true; var i; // If there are missing types, report them, emit the necessary runtime event and return @@ -365,24 +366,42 @@ async function start(type,diff,muteLog) { } } // Having created or updated all flows, now start them. - for (id in activeFlows) { - if (activeFlows.hasOwnProperty(id)) { - try { - activeFlows[id].start(diff); + let startFlows = true + try { + startFlows = settings.get('flowsRunStateRequested'); + } catch(err) { + } + startFlows = (startFlows !== "stop"); - // Create a map of node id to flow id and also a subflowInstance lookup map - var activeNodes = activeFlows[id].getActiveNodes(); - Object.keys(activeNodes).forEach(function(nid) { - activeNodesToFlow[nid] = id; - }); - } catch(err) { - console.log(err.stack); + if (startFlows) { + for (id in activeFlows) { + if (activeFlows.hasOwnProperty(id)) { + try { + activeFlows[id].start(diff); + // Create a map of node id to flow id and also a subflowInstance lookup map + var activeNodes = activeFlows[id].getActiveNodes(); + Object.keys(activeNodes).forEach(function(nid) { + activeNodesToFlow[nid] = id; + }); + } catch(err) { + console.log(err.stack); + } } } + reallyStarted = true; + events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); + // Deprecated event + events.emit("nodes-started"); + } else { + started = false; } - events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); - // Deprecated event - events.emit("nodes-started"); + + const state = { + started: reallyStarted, + state: reallyStarted ? "started" : "stopped", + } + events.emit("runtime-event",{id:"flows-run-state", payload: state, retain:true}); + if (credentialsPendingReset === true) { credentialsPendingReset = false; @@ -390,7 +409,7 @@ async function start(type,diff,muteLog) { events.emit("runtime-event",{id:"runtime-state",retain:true}); } - if (!muteLog) { + if (!muteLog && reallyStarted) { if (type !== "full") { log.info(log._("nodes.flows.started-modified-"+type)); } else { @@ -471,6 +490,7 @@ function stop(type,diff,muteLog) { } } events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff}); + events.emit("runtime-event",{id:"flows-run-state", payload: {started: false, state: "stopped"}, retain:true}); // Deprecated event events.emit("nodes-stopped"); }); diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 2e2b7035e..72f81266d 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -242,6 +242,7 @@ module.exports = { /******************************************************************************* * Runtime Settings * - lang + * - runtimeState * - diagnostics * - logging * - contextStorage @@ -267,7 +268,19 @@ module.exports = { /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ ui: true, }, - + /** Configure runtimeState options + * - enabled: When `enabled` is `true` (or unset), runtime Start/Stop will + * be available at http://localhost:1880/flows/state + * - ui: When `ui` is `true` (or unset), the action `core:start-flows` and + * `core:stop-flows` be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button + */ + runtimeState: { + /** enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: true, + /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: true, + }, /** Configure the logging output */ logging: { /** Only console logging is currently supported */ diff --git a/test/unit/@node-red/editor-api/lib/admin/flows_spec.js b/test/unit/@node-red/editor-api/lib/admin/flows_spec.js index ac295a194..ba09c9fa1 100644 --- a/test/unit/@node-red/editor-api/lib/admin/flows_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/flows_spec.js @@ -32,7 +32,9 @@ describe("api/admin/flows", function() { app = express(); app.use(bodyParser.json()); app.get("/flows",flows.get); + app.get("/flows/state",flows.getState); app.post("/flows",flows.post); + app.post("/flows/state",flows.postState); }); it('returns flow - v1', function(done) { @@ -208,4 +210,99 @@ describe("api/admin/flows", function() { done(); }); }); + it('returns flows run state', function (done) { + var setFlows = sinon.spy(function () { return Promise.resolve(); }); + flows.init({ + flows: { + setFlows, + getState: async function () { + return { started: true, state: "started" }; + } + } + }); + request(app) + .get('/flows/state') + .set('Accept', 'application/json') + .set('Node-RED-Deployment-Type', 'reload') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + try { + res.body.should.have.a.property('started', true); + res.body.should.have.a.property('state', "started"); + done(); + } catch (e) { + return done(e); + } + }); + }); + it('sets flows run state - stopped', function (done) { + var setFlows = sinon.spy(function () { return Promise.resolve(); }); + flows.init({ + flows: { + setFlows: setFlows, + getState: async function () { + return { started: true, state: "started" }; + }, + setState: async function () { + return { started: false, state: "stopped" }; + }, + } + }); + request(app) + .post('/flows/state') + .set('Accept', 'application/json') + .set('Node-RED-Flow-Run-State-Change', 'stop') + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + try { + res.body.should.have.a.property('started', false); + res.body.should.have.a.property('state', "stopped"); + done(); + } catch (e) { + return done(e); + } + }); + }); + it('sets flows run state - bad value', function (done) { + var setFlows = sinon.spy(function () { return Promise.resolve(); }); + const makeError = (error, errcode, statusCode) => { + const message = typeof error == "object" ? error.message : error + const err = typeof error == "object" ? error : new Error(message||"Unexpected Error") + err.status = err.status || statusCode || 400; + err.code = err.code || errcode || "unexpected_error" + return err + } + flows.init({ + flows: { + setFlows: setFlows, + getState: async function () { + return { started: true, state: "started" }; + }, + setState: async function () { + var err = (makeError("Cannot set runtime state. Invalid state", "invalid_run_state", 400)) + var p = Promise.reject(err); + p.catch(()=>{}); + return p; + }, + } + }); + request(app) + .post('/flows/state') + .set('Accept', 'application/json') + .set('Node-RED-Flow-Run-State-Change', 'bad-state') + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property("code","invalid_run_state"); + done(); + }); + }); }); diff --git a/test/unit/@node-red/runtime/lib/api/flows_spec.js b/test/unit/@node-red/runtime/lib/api/flows_spec.js index 9062ef52f..0f560e3f8 100644 --- a/test/unit/@node-red/runtime/lib/api/flows_spec.js +++ b/test/unit/@node-red/runtime/lib/api/flows_spec.js @@ -427,4 +427,126 @@ describe("runtime-api/flows", function() { }); }); + describe("flow run state", function() { + var startFlows, stopFlows, runtime; + beforeEach(function() { + let flowsStarted = true; + let flowsState = "started"; + startFlows = sinon.spy(function(type) { + if (type !== "full") { + var err = new Error(); + // TODO: quirk of internal api - uses .code for .status + err.code = 400; + var p = Promise.reject(err); + p.catch(()=>{}); + return p; + } + flowsStarted = true; + flowsState = "started"; + return Promise.resolve(); + }); + stopFlows = sinon.spy(function(type) { + if (type !== "full") { + var err = new Error(); + // TODO: quirk of internal api - uses .code for .status + err.code = 400; + var p = Promise.reject(err); + p.catch(()=>{}); + return p; + } + flowsStarted = false; + flowsState = "stopped"; + return Promise.resolve(); + }); + runtime = { + log: mockLog(), + settings: { + runtimeState: { + enabled: true, + ui: true, + }, + }, + flows: { + get started() { + return flowsStarted; + }, + startFlows, + stopFlows, + getFlows: function() { return {rev:"currentRev",flows:[]} }, + } + } + }) + + it("gets flows run state", async function() { + flows.init(runtime); + const state = await flows.getState({}) + state.should.have.property("started", true) + state.should.have.property("state", "started") + }); + it("permits getting flows run state when setting disabled", async function() { + runtime.settings.runtimeState.enabled = false; + flows.init(runtime); + const state = await flows.getState({}) + state.should.have.property("started", true) + state.should.have.property("state", "started") + }); + it("start flows", async function() { + flows.init(runtime); + const state = await flows.setState({requestedState:"start"}) + state.should.have.property("started", true) + state.should.have.property("state", "started") + stopFlows.called.should.not.be.true(); + startFlows.called.should.be.true(); + }); + it("stop flows", async function() { + flows.init(runtime); + const state = await flows.setState({requestedState:"stop"}) + state.should.have.property("started", false) + state.should.have.property("state", "stopped") + stopFlows.called.should.be.true(); + startFlows.called.should.not.be.true(); + }); + it("rejects starting flows when setting disabled", async function() { + let err; + runtime.settings.runtimeState.enabled = false; + flows.init(runtime); + try { + await flows.setState({requestedState:"start"}) + } catch (error) { + err = error + } + stopFlows.called.should.not.be.true(); + startFlows.called.should.not.be.true(); + should(err).have.property("code", "not_allowed") + should(err).have.property("status", 405) + }); + it("rejects stopping flows when setting disabled", async function() { + let err; + runtime.settings.runtimeState.enabled = false; + flows.init(runtime); + try { + await flows.setState({requestedState:"stop"}) + } catch (error) { + err = error + } + stopFlows.called.should.not.be.true(); + startFlows.called.should.not.be.true(); + should(err).have.property("code", "not_allowed") + should(err).have.property("status", 405) + }); + it("rejects setting invalid flows run state", async function() { + let err; + flows.init(runtime); + try { + await flows.setState({requestedState:"bad-state"}) + } catch (error) { + err = error + } + stopFlows.called.should.not.be.true(); + startFlows.called.should.not.be.true(); + should(err).have.property("code", "invalid_run_state") + should(err).have.property("status", 400) + }); + }); + }); diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index 737846100..bacb94b0f 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -131,7 +131,7 @@ describe('flows/index', function() { // eventsOn.calledOnce.should.be.true(); // }); // }); - +/* describe('#setFlows', function() { it('sets the full flow', function(done) { var originalConfig = [ @@ -300,6 +300,7 @@ describe('flows/index', function() { }); }); }); + */ describe('#startFlows', function() { it('starts the loaded config', function(done) { @@ -321,6 +322,87 @@ describe('flows/index', function() { return flows.startFlows(); }); }); + it('emits runtime-event "flows-run-state" "started"', async function () { + var originalConfig = [ + { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, + { id: "t1", type: "tab" } + ]; + storage.getFlows = function () { + return Promise.resolve({ flows: originalConfig }); + } + let receivedEvent = null; + const handleEvent = (data) => { + console.log(data) + if(data && data.id === 'flows-run-state') { + receivedEvent = data; + } + } + events.on('runtime-event', handleEvent); + flows.init({ log: mockLog, settings: {}, storage: storage }); + await flows.load() + await flows.startFlows() + events.removeListener("runtime-event", handleEvent); + + //{id:"flows-run-state", payload: {started: true, state: "started"} + should(receivedEvent).not.be.null() + receivedEvent.should.have.property("id", "flows-run-state") + receivedEvent.should.have.property("payload", { started: true, state: "started" }) + receivedEvent.should.have.property("retain", true) + }); + it('emits runtime-event "flows-run-state" "stopped"', async function () { + const originalConfig = [ + { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, + { id: "t1", type: "tab" } + ]; + storage.getFlows = function () { + return Promise.resolve({ flows: originalConfig }); + } + let receivedEvent = null; + const handleEvent = (data) => { + if(data && data.id === 'flows-run-state') { + receivedEvent = data; + } + } + events.on('runtime-event', handleEvent); + flows.init({ log: mockLog, settings: {}, storage: storage }); + await flows.load() + await flows.startFlows() + await flows.stopFlows() + events.removeListener("runtime-event", handleEvent); + + //{id:"flows-run-state", payload: {started: true, state: "started"} + should(receivedEvent).not.be.null() + receivedEvent.should.have.property("id", "flows-run-state") + receivedEvent.should.have.property("payload", { started: false, state: "stopped" }) + receivedEvent.should.have.property("retain", true) + }); + // it('raises error when invalid flows run state requested', async function () { + // const originalConfig = [ + // { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, + // { id: "t1", type: "tab" } + // ]; + // storage.getFlows = function () { + // return Promise.resolve({ flows: originalConfig }); + // } + // let receivedEvent = null; + // const handleEvent = (data) => { + // if(data && data.id === 'flows-run-state') { + // receivedEvent = data; + // } + // } + // events.on('runtime-event', handleEvent); + // flows.init({ log: mockLog, settings: {}, storage: storage }); + // await flows.load() + // await flows.startFlows() + // await flows.stopFlows() + // events.removeListener("runtime-event", handleEvent); + + // //{id:"flows-run-state", payload: {started: true, state: "started"} + // should(receivedEvent).not.be.null() + // receivedEvent.should.have.property("id", "flows-run-state") + // receivedEvent.should.have.property("payload", { started: false, state: "stopped" }) + // receivedEvent.should.have.property("retain", true) + // }); it('does not start if nodes missing', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, @@ -415,7 +497,7 @@ describe('flows/index', function() { describe.skip('#get',function() { }); - +/* describe('#eachNode', function() { it('iterates the flow nodes', function(done) { var originalConfig = [ @@ -582,7 +664,7 @@ describe('flows/index', function() { ]; flows.init({log:mockLog, settings:{},storage:storage}); flows.setFlows(originalConfig).then(function() { - /*jshint immed: false */ + try { flows.checkTypeInUse("used-module"); done("type_in_use error not thrown"); @@ -666,4 +748,5 @@ describe('flows/index', function() { describe('#enableFlow', function() { it.skip("enableFlow"); }) + */ }); From d4e6136b09395799596eff1e588164009b2ea66c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 9 Jun 2022 15:27:50 +0100 Subject: [PATCH 02/12] re-enable tests i had temporarily disabled --- .../@node-red/runtime/lib/flows/index_spec.js | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index bacb94b0f..5e8f8a46e 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -131,7 +131,7 @@ describe('flows/index', function() { // eventsOn.calledOnce.should.be.true(); // }); // }); -/* + describe('#setFlows', function() { it('sets the full flow', function(done) { var originalConfig = [ @@ -300,7 +300,6 @@ describe('flows/index', function() { }); }); }); - */ describe('#startFlows', function() { it('starts the loaded config', function(done) { @@ -332,7 +331,6 @@ describe('flows/index', function() { } let receivedEvent = null; const handleEvent = (data) => { - console.log(data) if(data && data.id === 'flows-run-state') { receivedEvent = data; } @@ -376,33 +374,6 @@ describe('flows/index', function() { receivedEvent.should.have.property("payload", { started: false, state: "stopped" }) receivedEvent.should.have.property("retain", true) }); - // it('raises error when invalid flows run state requested', async function () { - // const originalConfig = [ - // { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, - // { id: "t1", type: "tab" } - // ]; - // storage.getFlows = function () { - // return Promise.resolve({ flows: originalConfig }); - // } - // let receivedEvent = null; - // const handleEvent = (data) => { - // if(data && data.id === 'flows-run-state') { - // receivedEvent = data; - // } - // } - // events.on('runtime-event', handleEvent); - // flows.init({ log: mockLog, settings: {}, storage: storage }); - // await flows.load() - // await flows.startFlows() - // await flows.stopFlows() - // events.removeListener("runtime-event", handleEvent); - - // //{id:"flows-run-state", payload: {started: true, state: "started"} - // should(receivedEvent).not.be.null() - // receivedEvent.should.have.property("id", "flows-run-state") - // receivedEvent.should.have.property("payload", { started: false, state: "stopped" }) - // receivedEvent.should.have.property("retain", true) - // }); it('does not start if nodes missing', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, @@ -497,7 +468,7 @@ describe('flows/index', function() { describe.skip('#get',function() { }); -/* + describe('#eachNode', function() { it('iterates the flow nodes', function(done) { var originalConfig = [ @@ -664,7 +635,7 @@ describe('flows/index', function() { ]; flows.init({log:mockLog, settings:{},storage:storage}); flows.setFlows(originalConfig).then(function() { - + /*jshint immed: false */ try { flows.checkTypeInUse("used-module"); done("type_in_use error not thrown"); @@ -748,5 +719,4 @@ describe('flows/index', function() { describe('#enableFlow', function() { it.skip("enableFlow"); }) - */ }); From 1b4f2b9c537b6c5f44fed5dbc83c1bd8cef6c3f1 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 9 Jun 2022 22:29:28 +0100 Subject: [PATCH 03/12] fix formatting (resolve merge conflict) --- packages/node_modules/node-red/settings.js | 142 ++++++++++----------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 72f81266d..e27a9a584 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -250,18 +250,18 @@ module.exports = { * - externalModules ******************************************************************************/ - /** Uncomment the following to run node-red in your preferred language. - * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko - * Some languages are more complete than others. - */ - // lang: "de", + /** Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", /** Configure diagnostics options * - enabled: When `enabled` is `true` (or unset), diagnostics data will * be available at http://localhost:1880/diagnostics * - ui: When `ui` is `true` (or unset), the action `show-system-info` will * be available to logged in users of node-red editor - */ + */ diagnostics: { /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ enabled: true, @@ -281,74 +281,74 @@ module.exports = { /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ ui: true, }, - /** Configure the logging output */ - logging: { - /** Only console logging is currently supported */ - console: { - /** Level of logging to be recorded. Options are: - * fatal - only those errors which make the application unusable should be recorded - * error - record errors which are deemed fatal for a particular request + fatal errors - * warn - record problems which are non fatal + errors + fatal errors - * info - record information about the general running of the application + warn + error + fatal errors - * debug - record information which is more verbose than info + info + warn + error + fatal errors - * trace - record very detailed logging + debug + info + warn + error + fatal errors - * off - turn off all logging (doesn't affect metrics or audit) - */ - level: "info", - /** Whether or not to include metric events in the log output */ - metrics: false, - /** Whether or not to include audit events in the log output */ - audit: false - } - }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit) + */ + level: "info", + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, - /** Context Storage - * The following property can be used to enable context storage. The configuration - * provided here will enable file-based context that flushes to disk every 30 seconds. - * Refer to the documentation for further options: https://nodered.org/docs/api/context/ - */ - //contextStorage: { - // default: { - // module:"localfilesystem" - // }, - //}, + /** Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/ + */ + //contextStorage: { + // default: { + // module:"localfilesystem" + // }, + //}, - /** `global.keys()` returns a list of all properties set in global context. - * This allows them to be displayed in the Context Sidebar within the editor. - * In some circumstances it is not desirable to expose them to the editor. The - * following property can be used to hide any property set in `functionGlobalContext` - * from being list by `global.keys()`. - * By default, the property is set to false to avoid accidental exposure of - * their values. Setting this to true will cause the keys to be listed. - */ - exportGlobalContextKeys: false, + /** `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, - /** Configure how the runtime will handle external npm modules. - * This covers: - * - whether the editor will allow new node modules to be installed - * - whether nodes, such as the Function node are allowed to have their - * own dynamically configured dependencies. - * The allow/denyList options can be used to limit what modules the runtime - * will install/load. It can use '*' as a wildcard that matches anything. - */ - externalModules: { - // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ - // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ - // palette: { /** Configuration for the Palette Manager */ - // allowInstall: true, /** Enable the Palette Manager in the editor */ - // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ - // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ - // allowList: ['*'], - // denyList: [], - // allowUpdateList: ['*'], - // denyUpdateList: [] - // }, - // modules: { /** Configuration for node-specified modules */ - // allowInstall: true, - // allowList: [], - // denyList: [] - // } - }, + /** Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, /******************************************************************************* From 2f1f587c50a24355e3329bc15030d5eebfdb7a06 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 27 Jun 2022 18:03:14 +0100 Subject: [PATCH 04/12] Use HTTP body instead of header for setting flows run state --- .../node_modules/@node-red/editor-api/lib/admin/flows.js | 2 +- .../node_modules/@node-red/editor-client/src/js/ui/deploy.js | 5 +---- test/unit/@node-red/editor-api/lib/admin/flows_spec.js | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js index 611d9c2ca..2ad233f8f 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js @@ -83,7 +83,7 @@ module.exports = { postState: function(req,res) { const opts = { user: req.user, - requestedState: req.get("Node-RED-Flow-Run-State-Change")||"", + requestedState: req.body.state||"", req: apiUtils.getRequestLogObject(req) } runtimeAPI.flows.setState(opts).then(function(result) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 69460f8cf..ab36df9ff 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -306,10 +306,7 @@ RED.deploy = (function() { $.ajax({ url:"flows/state", type: "POST", - data: {state: state}, - headers: { - "Node-RED-Flow-Run-State-Change": state - } + data: {state: state} }).done(function(data,textStatus,xhr) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled"); diff --git a/test/unit/@node-red/editor-api/lib/admin/flows_spec.js b/test/unit/@node-red/editor-api/lib/admin/flows_spec.js index ba09c9fa1..9ec6a3bc9 100644 --- a/test/unit/@node-red/editor-api/lib/admin/flows_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/flows_spec.js @@ -254,7 +254,7 @@ describe("api/admin/flows", function() { request(app) .post('/flows/state') .set('Accept', 'application/json') - .set('Node-RED-Flow-Run-State-Change', 'stop') + .send({state:'stop'}) .expect(200) .end(function (err, res) { if (err) { @@ -295,7 +295,7 @@ describe("api/admin/flows", function() { request(app) .post('/flows/state') .set('Accept', 'application/json') - .set('Node-RED-Flow-Run-State-Change', 'bad-state') + .send({state:'bad-state'}) .expect(400) .end(function(err,res) { if (err) { From 51baed493220261d33d84a31f464c22b1f19676a Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 27 Jun 2022 18:06:53 +0100 Subject: [PATCH 05/12] default stop/start feature to `enabled:false` --- .../@node-red/editor-api/lib/admin/index.js | 2 +- .../@node-red/editor-client/src/js/runtime.js | 6 +++--- .../@node-red/editor-client/src/js/ui/deploy.js | 2 +- .../node_modules/@node-red/runtime/lib/api/flows.js | 2 +- .../@node-red/runtime/lib/api/settings.js | 8 ++++---- packages/node_modules/node-red/settings.js | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 078779f5a..8406fa8e9 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -56,7 +56,7 @@ module.exports = { // Flows/state adminApp.get("/flows/state", needsPermission("flows.read"), flows.getState, apiUtil.errorHandler); - if (!settings.runtimeState || settings.runtimeState.enabled !== false) { + if (settings.runtimeState && settings.runtimeState.enabled === true) { adminApp.post("/flows/state", needsPermission("flows.write"), flows.postState, apiUtil.errorHandler); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/runtime.js b/packages/node_modules/@node-red/editor-client/src/js/runtime.js index 939517877..49960e382 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/runtime.js +++ b/packages/node_modules/@node-red/editor-client/src/js/runtime.js @@ -1,12 +1,12 @@ RED.runtime = (function() { let state = "" - let settings = {ui: true, enabled: true}; + let settings = {ui: false, enabled: false}; const STOPPED = "stopped" const STARTED = "started" return { init: function() { // refresh the current runtime status from server - settings = RED.settings.runtimeState; + settings = Object.assign({}, settings, RED.settings.runtimeState); RED.runtime.requestState() // {id:"flows-run-state", started: false, state: "stopped", retain:true} @@ -29,7 +29,7 @@ RED.runtime = (function() { // disable pointer events on node buttons (e.g. inject/debug nodes) $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state === STOPPED) // show/hide Start/Stop based on current state - if(!RED.settings.runtimeState || RED.settings.runtimeState.ui !== false) { + if(settings.enabled === true && settings.ui === true) { RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) RED.menu.setVisible("deploymenu-item-runtime-start", state === STOPPED) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index ab36df9ff..3766763c7 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -69,7 +69,7 @@ RED.deploy = (function() { {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}}, null ] - if(!RED.settings.runtimeState || RED.settings.runtimeState.ui !== false) { + if(RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:"Start"/*RED._("deploy.startFlows")*/,sublabel:"Start Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:start-flows", visible:false}) mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:"Stop"/*RED._("deploy.startFlows")*/,sublabel:"Stop Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:stop-flows", visible:false}) } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 83ef68021..b3c471a5a 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -310,7 +310,7 @@ var api = module.exports = { } } - if(runtime.settings.runtimeState ? runtime.settings.runtimeState.enabled === false : false) { + if(!runtime.settings.runtimeState || runtime.settings.runtimeState.enabled !== true) { throw (makeError("Method Not Allowed", "not_allowed", 405)) } switch (opts.requestedState) { diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 7e17f57b5..6c13596ce 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -153,11 +153,11 @@ var api = module.exports = { } safeSettings.runtimeState = { - //unless runtimeState.ui and runtimeState.enabled are explicitly false, they will default to true. - enabled: (runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === false) ? false : true, - ui: (runtime.settings.runtimeState && runtime.settings.runtimeState.ui === false) ? false : true + //unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false. + enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true, + ui: !!runtime.settings.runtimeState && runtime.settings.runtimeState.ui === true } - if(safeSettings.runtimeState.enabled === false) { + if(safeSettings.runtimeState.enabled !== true) { safeSettings.runtimeState.ui = false; // cannot have UI without endpoint } diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 26e5ca8de..fae6c8079 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -269,17 +269,17 @@ module.exports = { ui: true, }, /** Configure runtimeState options - * - enabled: When `enabled` is `true` (or unset), runtime Start/Stop will - * be available at http://localhost:1880/flows/state - * - ui: When `ui` is `true` (or unset), the action `core:start-flows` and - * `core:stop-flows` be available to logged in users of node-red editor + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor * Also, the deploy menu (when set to default) will show a stop or start button */ runtimeState: { /** enable or disable flows/state endpoint. Must be set to `false` to disable */ - enabled: true, + enabled: false, /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ - ui: true, + ui: false, }, /** Configure the logging output */ logging: { From 1b8a4577d5107e5704f51ad611729b1e9ee5bcc9 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 27 Jun 2022 18:07:22 +0100 Subject: [PATCH 06/12] improve UI, i18n and layout of stop/start feature --- .../editor-client/locales/en-US/editor.json | 11 + .../editor-client/src/js/ui/deploy.js | 46 +- .../editor-client/src/js/ui/view_copy.js | 6287 +++++++++++++++++ .../editor-client/src/sass/header.scss | 2 +- .../@node-red/runtime/lib/api/flows.js | 2 +- 5 files changed, 6328 insertions(+), 20 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index e3028272b..b25885e21 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -288,6 +288,17 @@ "copyMessageValue": "Value copied", "copyMessageValue_truncated": "Truncated value copied" }, + "stopstart":{ + "status": { + "state_changed": "Flows runtime has been changed to '__state__' state" + }, + "errors": { + "notAllowed": "Method not allowed", + "notAuthorized": "Not authorized", + "notFound": "Not found", + "noResponse": "No response from server" + } + }, "deploy": { "deploy": "Deploy", "full": "Full", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 3766763c7..46408cb6c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -297,41 +297,51 @@ RED.deploy = (function() { $(".red-ui-deploy-button-spinner").hide(); } function stopStartFlows(state) { - const startTime = Date.now(); - const deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled"); - deployInflight = true; - deployButtonSetBusy(); - shadeShow(); - RED.runtime.updateState(state); + const startTime = Date.now() + const deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled") + deployInflight = true + deployButtonSetBusy() + shadeShow() + RED.runtime.updateState(state) $.ajax({ url:"flows/state", type: "POST", data: {state: state} }).done(function(data,textStatus,xhr) { if (deployWasEnabled) { - $("#red-ui-header-button-deploy").removeClass("disabled"); + $("#red-ui-header-button-deploy").removeClass("disabled") } - RED.runtime.updateState((data && data.state) || "unknown" ) - RED.notify('

Done

',"success"); + RED.runtime.updateState((data && data.state) || "unknown") + RED.notify(RED._("stopstart.status.state_changed", data), "success") }).fail(function(xhr,textStatus,err) { if (deployWasEnabled) { - $("#red-ui-header-button-deploy").removeClass("disabled"); + $("#red-ui-header-button-deploy").removeClass("disabled") } if (xhr.status === 401) { - RED.notify("Not authorized" ,"error"); + RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAuthorized") }), "error") + } else if (xhr.status === 404) { + RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notFound") }), "error") + } else if (xhr.status === 405) { + RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAllowed") }), "error") } else if (xhr.responseText) { - RED.notify("Operation failed: " + xhr.responseText,"error"); + const errorDetail = { message: err ? (err + "") : "" } + try { + errorDetail.message = JSON.parse(xhr.responseText).message + } finally { + errorDetail.message = errorDetail.message || xhr.responseText + } + RED.notify(RED._("notification.error", errorDetail), "error") } else { - RED.notify("Operation failed: no response","error"); + RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.noResponse") }), "error") } RED.runtime.requestState() }).always(function() { - const delta = Math.max(0,300-(Date.now()-startTime)); - setTimeout(function() { - deployButtonClearBusy(); + const delta = Math.max(0, 300 - (Date.now() - startTime)) + setTimeout(function () { + deployButtonClearBusy() shadeHide() - deployInflight = false; - },delta); + deployInflight = false + }, delta); }); } function restart() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js new file mode 100644 index 000000000..569fee1d5 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js @@ -0,0 +1,6287 @@ +/** + * 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" + * |- "junctionLayer" + * |- "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 entryCoordinates = {x:-1, y:-1}; + var gridSize = 20; + var snapGrid = false; + + var activeSpliceLink; + var spliceActive = false; + var spliceTimer; + var groupHoverTimer; + + var activeSubflow = null; + var activeNodes = []; + var activeLinks = []; + var activeJunctions = []; + var activeFlowLinks = []; + var activeLinkNodes = {}; + var activeGroup = null; + var activeHoverGroup = null; + var activeGroups = []; + var dirtyGroups = {}; + + 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 lasso = null; + var slicePath = null; + var slicePathLast = 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 lastClickPosition = []; + var selectNodesOptions; + + let flashingNodeId; + + 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", + "gray": "#d3d3d3" + } + + var PORT_TYPE_INPUT = 1; + var PORT_TYPE_OUTPUT = 0; + + var chart; + var outer; + var eventLayer; + var gridLayer; + var linkLayer; + var junctionLayer; + var dragGroupLayer; + var groupSelectLayer; + var nodeLayer; + var groupLayer; + var drag_lines; + + var movingSet = (function() { + var setIds = new Set(); + var set = []; + var api = { + add: function(node) { + if (Array.isArray(node)) { + for (var i=0;i1) { + 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"); + junctionLayer = 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 || event.workspace === 0); + RED.menu.setDisabled("menu-item-workspace-delete",event.workspace === 0 || 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) { + resetMouseVars(); + 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(); } + } + }); + + //add search to status-toolbar + RED.statusBar.add({ + id: "view-search-tools", + align: "left", + hidden: false, + element: $(''+ + '' + + '' + + '' + + '? of ?' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '') + }) + $("#red-ui-view-searchtools-search").on("click", searchFlows); + RED.popover.tooltip($("#red-ui-view-searchtools-search"),RED._('actions.search-flows'),'core:search'); + $("#red-ui-view-searchtools-prev").on("click", searchPrev); + RED.popover.tooltip($("#red-ui-view-searchtools-prev"),RED._('actions.search-prev'),'core:search-previous'); + $("#red-ui-view-searchtools-next").on("click", searchNext); + RED.popover.tooltip($("#red-ui-view-searchtools-next"),RED._('actions.search-next'),'core:search-next'); + RED.popover.tooltip($("#red-ui-view-searchtools-close"),RED._('common.label.close')); + + // 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 = createNode(selected_tool); + if (!result) { + return; + } + var historyEvent = result.historyEvent; + var nn = result.node; + + RED.nodes.add(nn); + + var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); + if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { + nn.l = showLabel; + } + + var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); + var helperWidth = ui.helper.width(); + var helperHeight = ui.helper.height(); + var mousePos = d3.touches(this)[0]||d3.mouse(this); + + try { + var isLink = (nn.type === "link in" || nn.type === "link out") + var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; + + var label = RED.utils.getNodeLabel(nn, nn.type); + var labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (hideLabel) { + nn.w = node_height; + nn.h = Math.max(node_height,(nn.outputs || 0) * 15); + } else { + nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); + nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); + } + } catch(err) { + } + + mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); + mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); + mousePos[1] /= scaleFactor; + mousePos[0] /= scaleFactor; + + nn.x = mousePos[0]; + nn.y = mousePos[1]; + + if (snapGrid) { + var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); + nn.x -= gridOffset.x; + nn.y -= gridOffset.y; + } + + 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]; + } + + + 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; + movingSet.add(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,{generateIds: true, generateDefaultNames: true});}); + + RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() }) + + RED.events.on("view:selection-changed", function(selection) { + var hasSelection = (selection.nodes && selection.nodes.length > 0); + var hasMultipleSelection = hasSelection && selection.nodes.length > 1; + RED.menu.setDisabled("menu-item-edit-cut",!hasSelection); + RED.menu.setDisabled("menu-item-edit-copy",!hasSelection); + RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection); + + RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection); + }) + + RED.actions.add("core:delete-selection",deleteSelection); + RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) }); + RED.actions.add("core:edit-selected-node",editSelection); + RED.actions.add("core:go-to-selection",function() { + if (movingSet.length() > 0) { + var node = movingSet.get(0).n; + if (/^subflow:/.test(node.type)) { + RED.workspaces.show(node.type.substring(8)) + } else if (node.type === 'group') { + enterActiveGroup(node); + redraw(); + } + } + }); + 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:select-none", selectNone); + 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.annotations.init(); + RED.view.navigator.init(); + RED.view.tools.init(); + + + RED.view.annotations.register("red-ui-flow-node-changed",{ + type: "badge", + class: "red-ui-flow-node-changed", + element: function() { + var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle"); + changeBadge.setAttribute("cx",5); + changeBadge.setAttribute("cy",5); + changeBadge.setAttribute("r",5); + return changeBadge; + }, + show: function(n) { return n.changed||n.moved } + }) + + RED.view.annotations.register("red-ui-flow-node-error",{ + type: "badge", + class: "red-ui-flow-node-error", + element: function(d) { + var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path"); + errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z"); + return errorBadge + }, + tooltip: function(d) { + if (d.validationErrors && d.validationErrors.length > 0) { + return RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - ") + } + }, + show: function(n) { return !n.valid } + }) + + if (RED.settings.get("editor.view.view-store-zoom")) { + var userZoomLevel = parseFloat(RED.settings.getLocal('zoom-level')) + if (!isNaN(userZoomLevel)) { + scaleFactor = userZoomLevel + } + } + + var onScrollTimer = null; + function storeScrollPosition() { + workspaceScrollPositions[RED.workspaces.active()] = { + left:chart.scrollLeft(), + top:chart.scrollTop() + }; + RED.settings.setLocal('scroll-positions', JSON.stringify(workspaceScrollPositions) ) + } + chart.on("scroll", function() { + if (RED.settings.get("editor.view.view-store-position")) { + if (onScrollTimer) { + clearTimeout(onScrollTimer) + } + onScrollTimer = setTimeout(storeScrollPosition, 200); + } + }) + + if (RED.settings.get("editor.view.view-store-position")) { + var scrollPositions = RED.settings.getLocal('scroll-positions') + if (scrollPositions) { + try { + workspaceScrollPositions = JSON.parse(scrollPositions) + } catch(err) { + } + } + } + } + + + + 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 canvasMouseDown() { + if (RED.view.DEBUG) { + console.warn("canvasMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); + } + 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) { + selectedLinks.clear(); + updateSelection(); + } + if (mouse_mode === 0 && lasso) { + lasso.remove(); + lasso = null; + } + if (d3.event.touches || d3.event.button === 0) { + if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && (d3.event.metaKey || d3.event.ctrlKey) && !(d3.event.altKey || d3.event.shiftKey)) { + // Trigger quick add dialog + d3.event.stopPropagation(); + clearSelection(); + const 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({ position: point, group: clickedGroup }); + } else if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) { + // CTRL not being held + if (!d3.event.altKey) { + // ALT not held (shift is allowed) Trigger lasso + if (!touchStartTime) { + const 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(); + } + } else if (d3.event.altKey) { + //Alt [+shift] held - Begin slicing + clearSelection(); + mouse_mode = (d3.event.shiftKey) ? RED.state.SLICING_JUNCTION : RED.state.SLICING; + const point = d3.mouse(this); + slicePath = eventLayer.append("path").attr("class", "nr-ui-view-slice").attr("d", `M${point[0]} ${point[1]}`) + slicePathLast = point; + RED.view.redraw(); + } + } + } + } + + function showQuickAddDialog(options) { + options = options || {}; + var point = options.position || lastClickPosition; + var spliceLink = options.splice; + var targetGroup = options.group; + var touchTrigger = options.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 nn; + var historyEvent; + if (type === 'junction') { + nn = { + _def: {defaults:{}}, + type: 'junction', + z: RED.workspaces.active(), + id: RED.nodes.id(), + x: 0, + y: 0, + w: 0, h: 0, + outputs: 1, + inputs: 1, + dirty: true + } + historyEvent = { + t:'add', + junctions:[nn] + } + } else { + var result = createNode(type); + if (!result) { + return; + } + nn = result.node; + historyEvent = result.historyEvent; + } + if (keepAdding) { + mouse_mode = RED.state.QUICK_JOINING; + } + + 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 && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { + nn.l = showLabel; + } + if (quickAddLink) { + var drag_line = quickAddLink; + var src = null,dst,src_port; + if (drag_line.portType === PORT_TYPE_OUTPUT && (nn.inputs > 0 || drag_line.virtualLink) ) { + src = drag_line.node; + src_port = drag_line.port; + dst = nn; + } else if (drag_line.portType === PORT_TYPE_INPUT && (nn.outputs > 0 || drag_line.virtualLink)) { + src = nn; + dst = drag_line.node; + src_port = 0; + } + + if (src !== null) { + // Joining link nodes via virual wires. Need to update + // the src and dst links property + if (drag_line.virtualLink) { + historyEvent = { + t:'multi', + events: [historyEvent] + } + var oldSrcLinks = $.extend(true,{},{v:src.links}).v + var oldDstLinks = $.extend(true,{},{v:dst.links}).v + src.links.push(dst.id); + dst.links.push(src.id); + src.dirty = true; + dst.dirty = true; + + historyEvent.events.push({ + t:'edit', + node: src, + dirty: RED.nodes.dirty(), + changed: src.changed, + changes: { + links:oldSrcLinks + } + }); + historyEvent.events.push({ + t:'edit', + node: dst, + dirty: RED.nodes.dirty(), + changed: dst.changed, + changes: { + links:oldDstLinks + } + }); + src.changed = true; + dst.changed = true; + } else { + var link = {source: src, sourcePort:src_port, target: dst}; + RED.nodes.addLink(link); + historyEvent.links = [link]; + } + if (!keepAdding) { + quickAddLink.el.remove(); + quickAddLink = null; + if (mouse_mode === RED.state.QUICK_JOINING) { + if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { + showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); + } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { + showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); + } else { + resetMouseVars(); + } + } + } else { + quickAddLink.node = nn; + quickAddLink.port = 0; + } + } else { + hideDragLines(); + resetMouseVars(); + } + } else { + if (!keepAdding) { + if (mouse_mode === RED.state.QUICK_JOINING) { + if (nn.outputs > 0) { + showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); + } else if (nn.inputs > 0) { + showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); + } else { + resetMouseVars(); + } + } + } else { + if (nn.outputs > 0) { + quickAddLink = { + node: nn, + port: 0, + portType: PORT_TYPE_OUTPUT + } + } else if (nn.inputs > 0) { + quickAddLink = { + node: nn, + port: 0, + portType: PORT_TYPE_INPUT + } + } else { + resetMouseVars(); + } + } + } + if (nn.type === 'junction') { + RED.nodes.addJunction(nn); + } else { + 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); + } + movingSet.add(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 + } + if (entryCoordinates.x != -1) { + mouse_position = [entryCoordinates.x, entryCoordinates.y] + } else { + mouse_position = d3.touches(this)[0]||d3.mouse(this); + } +if(RED.view.DEBUG) { console.log(`mousemove ${JSON.stringify(mouse_position)}`)} + 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; + } else if (mouse_mode === RED.state.SLICING || mouse_mode === RED.state.SLICING_JUNCTION) { + if (slicePath) { + var delta = Math.max(1,Math.abs(slicePathLast[0]-mouse_position[0]))*Math.max(1,Math.abs(slicePathLast[1]-mouse_position[1])) + if (delta > 20) { + var currentPath = slicePath.attr("d") + currentPath += " L"+mouse_position[0]+" "+mouse_position[1] + slicePath.attr("d",currentPath); + slicePathLast = mouse_position + } + } + 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 && mouse_mode != RED.state.DETACHED_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) { + 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 (selectedLinks.length() > 0) { + selectedLinks.forEach(function(link) { + if (((mousedown_port_type === PORT_TYPE_OUTPUT && + link.source === mousedown_node && + link.sourcePort === mousedown_port_index + ) || + (mousedown_port_type === PORT_TYPE_INPUT && + link.target === mousedown_node + ))) { + existingLinks.push(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 (movingSet.length() === 1) { + node = movingSet.get(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 || mouse_mode == RED.state.DETACHED_DRAGGING) { + mousePos = mouse_position; + var minX = 0; + var minY = 0; + var maxX = space_width; + var maxY = space_height; + if(RED.view.DEBUG) { console.log(`mousemove - MOVING_ACTIVE ${JSON.stringify(mousePos)}`)} + 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 = movingSet.get(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")); + var ag = activeGroup; + if (!d3.event.shiftKey) { + clearSelection(); + if (ag) { + if (x < ag.x+ag.w && x2 > ag.x && y < ag.y+ag.h && y2 > ag.y) { + // There was an active group and the lasso intersects with it, + // so reenter the group + enterActiveGroup(ag); + activeGroup.selected = true; + } + } + } + activeGroups.forEach(function(g) { + if (!g.selected) { + if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) { + if (!activeGroup || RED.group.contains(activeGroup,g)) { + while (g.g && (!activeGroup || g.g !== activeGroup.id)) { + 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 (!activeGroup || RED.group.contains(activeGroup,n)) { + if (n.g && (!activeGroup || n.g !== activeGroup.id)) { + var group = RED.nodes.group(n.g); + while (group.g && (!activeGroup || group.g !== activeGroup.id)) { + group = RED.nodes.group(group.g); + } + if (!group.selected) { + selectGroup(group,true); + } + } else { + n.selected = true; + n.dirty = true; + movingSet.add(n); + } + } + } + } + }); + activeJunctions.forEach(function(n) { + if (!n.selected) { + if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { + n.selected = true; + n.dirty = true; + movingSet.add(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; + movingSet.add(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; + movingSet.add(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; + movingSet.add(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(); + } else if (mouse_mode == RED.state.SLICING) { + deleteSelection(); + slicePath.remove(); + slicePath = null; + RED.view.redraw(true); + } else if (mouse_mode == RED.state.SLICING_JUNCTION) { + var removedLinks = new Set() + var addedLinks = [] + var addedJunctions = [] + + var groupedLinks = {} + selectedLinks.forEach(function(l) { + var sourceId = l.source.id+":"+l.sourcePort + groupedLinks[sourceId] = groupedLinks[sourceId] || [] + groupedLinks[sourceId].push(l) + + groupedLinks[l.target.id] = groupedLinks[l.target.id] || [] + groupedLinks[l.target.id].push(l) + }); + var linkGroups = Object.keys(groupedLinks) + linkGroups.sort(function(A,B) { + return groupedLinks[B].length - groupedLinks[A].length + }) + linkGroups.forEach(function(gid) { + var links = groupedLinks[gid] + var junction = { + _def: {defaults:{}}, + type: 'junction', + z: RED.workspaces.active(), + id: RED.nodes.id(), + x: 0, + y: 0, + w: 0, h: 0, + outputs: 1, + inputs: 1, + dirty: true + } + links = links.filter(function(l) { return !removedLinks.has(l) }) + if (links.length === 0) { + return + } + links.forEach(function(l) { + junction.x += l._sliceLocation.x + junction.y += l._sliceLocation.y + }) + junction.x = Math.round(junction.x/links.length) + junction.y = Math.round(junction.y/links.length) + if (snapGrid) { + junction.x = (gridSize*Math.round(junction.x/gridSize)); + junction.y = (gridSize*Math.round(junction.y/gridSize)); + } + + var nodeGroups = new Set() + + RED.nodes.addJunction(junction) + addedJunctions.push(junction) + let newLink + if (gid === links[0].source.id+":"+links[0].sourcePort) { + newLink = { + source: links[0].source, + sourcePort: links[0].sourcePort, + target: junction + } + } else { + newLink = { + source: junction, + sourcePort: 0, + target: links[0].target + } + } + addedLinks.push(newLink) + RED.nodes.addLink(newLink) + links.forEach(function(l) { + removedLinks.add(l) + RED.nodes.removeLink(l) + let newLink + if (gid === l.target.id) { + newLink = { + source: l.source, + sourcePort: l.sourcePort, + target: junction + } + } else { + newLink = { + source: junction, + sourcePort: 0, + target: l.target + } + } + addedLinks.push(newLink) + RED.nodes.addLink(newLink) + nodeGroups.add(l.source.g || "__NONE__") + nodeGroups.add(l.target.g || "__NONE__") + }) + if (nodeGroups.size === 1) { + var group = nodeGroups.values().next().value + if (group !== "__NONE__") { + RED.group.addToGroup(RED.nodes.group(group), junction) + } + } + }) + slicePath.remove(); + slicePath = null; + + if (addedJunctions.length > 0) { + RED.history.push({ + t: 'add', + links: addedLinks, + junctions: addedJunctions, + removedLinks: Array.from(removedLinks) + }) + RED.nodes.dirty(true) + } + RED.view.redraw(true); + } + if (mouse_mode == RED.state.MOVING_ACTIVE) { + if (movingSet.length() > 0) { + var addedToGroup = null; + if (activeHoverGroup) { + for (var j=0;j 0 && mouse_mode == RED.state.MOVING_ACTIVE) { + 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: movingSet.get(0).n + }; + var link2 = { + source:movingSet.get(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; + // movingSet.add(mousedown_node); + // } + // } + if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) { + // if (mousedown_node) { + // delete mousedown_node.gSelected; + // } + if (mouse_mode === RED.state.DETACHED_DRAGGING) { + var ns = []; + for (var j=0;j 0.3) { + zoomView(scaleFactor-0.1); + } + } + function zoomZero() { zoomView(1); } + function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); } + function searchPrev() { RED.actions.invoke("core:search-previous"); } + function searchNext() { RED.actions.invoke("core:search-next"); } + + + 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(); + if (RED.settings.get("editor.view.view-store-zoom")) { + RED.settings.setLocal('zoom-level', factor.toFixed(1)) + } + } + + function selectNone() { + if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) { + return; + } + if (mouse_mode === RED.state.DETACHED_DRAGGING) { + for (var j=0;j 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 && selectedLinks.length() > 0) { + selectedLinks.forEach(function(link) { + if (link.link) { + activeLinks.push(link); + activeLinkNodes[link.source.id] = link.source; + link.source.dirty = true; + activeLinkNodes[link.target.id] = link.target; + 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; + } else if (key === 'links') { + return value.map(function(link) { + return link.source.id+":"+link.sourcePort+":"+link.target.id; + }); + } + return value; + }); + if (selectionJSON !== lastSelection) { + lastSelection = selectionJSON; + RED.events.emit("view:selection-changed",selection); + } + } + + function editSelection() { + if (movingSet.length() > 0) { + var node = movingSet.get(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(reconnectWires) { + 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: [], + junctions: [], + workspaces: [], + subflows: [] + } + var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0); + + for (var i=0;i 0 || selectedLinks.length() > 0) { + var result; + var node; + var removedNodes = []; + var removedLinks = []; + var removedGroups = []; + var removedJunctions = []; + var removedSubflowOutputs = []; + var removedSubflowInputs = []; + var removedSubflowStatus; + var subflowInstances = []; + var historyEvents = []; + var addToRemovedLinks = function(links) { + if(!links) { return; } + var _links = Array.isArray(links) ? links : [links]; + _links.forEach(function(l) { + removedLinks.push(l); + selectedLinks.remove(l); + }) + } + if (reconnectWires) { + var reconnectResult = RED.nodes.detachNodes(movingSet.nodes()) + var addedLinks = reconnectResult.newLinks; + if (addedLinks.length > 0) { + historyEvents.push({ t:'add', links: addedLinks }) + } + addToRemovedLinks(reconnectResult.removedLinks) + } + + var startDirty = RED.nodes.dirty(); + var startChanged = false; + var selectedGroups = []; + if (movingSet.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) { + addToRemovedLinks(result.links); + } + } + // Assume 0/1 inputs + if (removedSubflowInputs.length == 1) { + result = RED.subflow.removeInput(); + if (result) { + addToRemovedLinks(result.links); + } + } + if (removedSubflowStatus) { + result = RED.subflow.removeStatus(); + if (result) { + addToRemovedLinks(result.links); + } + } + + var instances = RED.subflow.refresh(true); + if (instances) { + subflowInstances = instances.instances; + } + movingSet.clear(); + if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus || removedGroups.length > 0 || removedJunctions.length > 0) { + RED.nodes.dirty(true); + } + } + + if (selectedLinks.length() > 0) { + selectedLinks.forEach(function(link) { + if (link.link) { + var sourceId = link.source.id; + var targetId = link.target.id; + var sourceIdIndex = link.target.links.indexOf(sourceId); + var targetIdIndex = link.source.links.indexOf(targetId); + historyEvents.push({ + t: "edit", + node: link.source, + changed: link.source.changed, + changes: { + links: $.extend(true,{},{v:link.source.links}).v + } + }) + historyEvents.push({ + t: "edit", + node: link.target, + changed: link.target.changed, + changes: { + links: $.extend(true,{},{v:link.target.links}).v + } + }) + link.source.changed = true; + link.target.changed = true; + link.target.links.splice(sourceIdIndex,1); + link.source.links.splice(targetIdIndex,1); + link.source.dirty = true; + link.target.dirty = true; + + } else { + RED.nodes.removeLink(link); + removedLinks.push(link); + } + }) + } + RED.nodes.dirty(true); + var historyEvent = { + t:"delete", + nodes:removedNodes, + links:removedLinks, + groups: removedGroups, + junctions: removedJunctions, + subflowOutputs:removedSubflowOutputs, + subflowInputs:removedSubflowInputs, + subflow: { + id: activeSubflow?activeSubflow.id:undefined, + instances: subflowInstances + }, + dirty:startDirty + }; + if (removedSubflowStatus) { + historyEvent.subflow.status = removedSubflowStatus; + } + if (historyEvents.length > 0) { + historyEvents.unshift(historyEvent); + RED.history.push({ + t:"multi", + events: historyEvents + }) + } else { + RED.history.push(historyEvent); + } + + selectedLinks.clear(); + 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 junctionCount = 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 detachSelectedNodes() { + var selection = RED.view.selection(); + if (selection.nodes) { + const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes); + if (removedLinks.length || newLinks.length) { + RED.history.push({ + t: "multi", + events: [ + { t:'delete', links: removedLinks }, + { t:'add', links: newLinks } + ], + dirty: RED.nodes.dirty() + }) + RED.nodes.dirty(true) + } + prepareDrag([selection.nodes[0].x,selection.nodes[0].y]); + mouse_mode = RED.state.DETACHED_DRAGGING; + RED.view.redraw(true); + } + } + + 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(); + } + mousedown_link = select_link; + if (select_link) { + selectedLinks.clear(); + selectedLinks.add(select_link); + updateSelection(); + } else { + selectedLinks.clear(); + } + } + redraw(); + return; + } + + resetMouseVars(); + hideDragLines(); + if (select_link) { + selectedLinks.clear(); + selectedLinks.add(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 junctionMouseOver(junction, d, portType) { + var active = (portType === undefined) || + (mouse_mode !== RED.state.JOINING && mouse_mode !== RED.state.QUICK_JOINING) || + (drag_lines.length > 0 && drag_lines[0].portType !== portType && !drag_lines[0].virtualLink) + junction.classed("red-ui-flow-junction-hovered", active); + } + function junctionMouseOut(junction, d) { + junction.classed("red-ui-flow-junction-hovered",false); + } + + function prepareDrag(mouse) { + mouse_mode = RED.state.MOVING; + // Called when movingSet should be prepared to be dragged + for (i=0;i 0 && clickElapsed < dblClickInterval) { + mouse_mode = RED.state.DEFAULT; + if (d.type != "subflow") { + if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) { + RED.workspaces.show(d.type.substring(8)); + } else { + 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; + movingSet.add(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); + } + } + + + document.addEventListener('pointerlockchange', changeCallback, false); + document.addEventListener('mozpointerlockchange', changeCallback, false); + document.addEventListener('webkitpointerlockchange', changeCallback, false); + document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; + function getPosition(canvas, event) { + var x, y; + + if (event.x != undefined && event.y != undefined) { + x = event.x; + y = event.y; + } else // Firefox method to get the position + { + x = event.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + + x -= canvas.offsetLeft == null ? canvas.clientLeft : canvas.offsetLeft; + y -= canvas.offsetTop == null ? canvas.clientTop : canvas.offsetTop; + + return {x:x, y:y}; + } + //temporary proxy for when mouse is locked + function canvasMouseMove_locked(e) { + + var canvas = $(eventLayer[0])[0]; + + + // if we enter this for the first time, get the initial position + if (entryCoordinates.x == -1) { + entryCoordinates = getPosition(canvas, e); + } + + + //get a reference to the canvas + var movementX = e.movementX || + e.mozMovementX || + e.webkitMovementX || + 0; + + var movementY = e.movementY || + e.mozMovementY || + e.webkitMovementY || + 0; + + + // calculate the new coordinates where we should draw the ship + entryCoordinates.x = entryCoordinates.x + movementX; + entryCoordinates.y = entryCoordinates.y + movementY; + + if (entryCoordinates.x > chart.width() -65) { + entryCoordinates.x = chart.width()-65; + } else if (entryCoordinates.x < 0) { + entryCoordinates.x = 0; + } + + if (entryCoordinates.y > chart.height() - 85) { + entryCoordinates.y = chart.height() - 85; + } else if (entryCoordinates.y < 0) { + entryCoordinates.y = 0; + } + + + // determine the direction + var direction = 0; + if (movementX > 0) { + direction = 1; + } else if (movementX < 0) { + direction = -1; + } + // console.log(entryCoordinates) + + + d3.event = e + canvasMouseMove.call(this,e); + //canvasMouseMove.call(document.querySelector("g.red-ui-workspace-chart-event-layer")); + } + function changeCallback(e) { + var canvas = $(eventLayer[0])[0] + // var workspaceChart = $("g.red-ui-workspace-chart-event-layer")[0]; + // var workspaceChart = $("#red-ui-workspace-chart")[0]; + if (document.pointerLockElement === canvas || + document.mozPointerLockElement === canvas || + document.webkitPointerLockElement === canvas) { + + // we've got a pointerlock for our element, add a mouselistener + canvas.addEventListener("mousemove", canvasMouseMove_locked, false); + // outer.on("mousemove", canvasMouseMove_locked) + // document.addEventListener("mousemove", canvasMouseMove_locked, false); + } else { + + // pointer lock is no longer active, remove the callback + canvas.removeEventListener("mousemove", canvasMouseMove_locked, false); + // document.removeEventListener("mousemove", canvasMouseMove_locked, false); + // if(outer.off) {outer.off("mousemove", canvasMouseMove_locked)} + // if(outer.removeEventListener) {outer.removeEventListener("mousemove", canvasMouseMove_locked)} + // and reset the entry coordinates + entryCoordinates = {x:-1, y:-1}; + } + } + + function nodeMouseDown(d) { + if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); } + try { + // workspaceChart = $("g.red-ui-workspace-chart-event-layer")[0] + var canvas = $(eventLayer[0])[0] + canvas.requestPointerLock = canvas.requestPointerLock || + canvas.mozRequestPointerLock || + canvas.webkitRequestPointerLock; + canvas.requestPointerLock() + console.log("got pointer lock") + } catch (error) { + console.error(error) + } + + 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 || mouse_mode == RED.state.DETACHED_DRAGGING) { + var historyEvent = RED.history.peek(); + 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: movingSet.get(0).n + }; + var link2 = { + source:movingSet.get(0).n, + sourcePort:0, + target: spliceLink.target + }; + RED.nodes.addLink(link1); + RED.nodes.addLink(link2); + + historyEvent.links = [link1,link2]; + historyEvent.removedLinks = [spliceLink]; + updateActiveNodes(); + } + + if (activeHoverGroup) { + for (var j=0;j 30 ? 25 : (mousedown_node.w > 0 ? 8 : 3); + if (edgeDelta < targetEdgeDelta) { + if (clickPosition < 0) { + cnodes = [mousedown_node].concat(RED.nodes.getAllUpstreamNodes(mousedown_node)); + } else { + cnodes = [mousedown_node].concat(RED.nodes.getAllDownstreamNodes(mousedown_node)); + } + } else { + cnodes = RED.nodes.getAllFlowNodes(mousedown_node); + } + for (var n=0;n 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 junctionMouseOverProxy(e) { junctionMouseOver(d3.select(this), this.__data__, this.__portType__) } + function junctionMouseOutProxy(e) { junctionMouseOut(d3.select(this), this.__data__) } + + function linkMouseDown(d) { + if (RED.view.DEBUG) { + console.warn("linkMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); + } + if (mouse_mode === RED.state.SELECTING_NODE) { + d3.event.stopPropagation(); + return; + } + if (d3.event.button === 2) { + return + } + mousedown_link = d; + + if (!(d3.event.metaKey || d3.event.ctrlKey)) { + clearSelection(); + } + if (d3.event.metaKey || d3.event.ctrlKey) { + if (!selectedLinks.has(mousedown_link)) { + selectedLinks.add(mousedown_link); + } else { + if (selectedLinks.length() !== 1) { + selectedLinks.remove(mousedown_link); + } + } + } else { + selectedLinks.add(mousedown_link); + } + updateSelection(); + redraw(); + focusView(); + d3.event.stopPropagation(); + if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && (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({position:point, splice:mousedown_link, group:clickedGroup}); + } + } + function linkTouchStart(d) { + if (mouse_mode === RED.state.SELECTING_NODE) { + d3.event.stopPropagation(); + return; + } + mousedown_link = d; + clearSelection(); + selectedLinks.clear(); + selectedLinks.add(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.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) { + var ag = activeGroup; + clearSelection(); + if (ag && g.g === ag.id) { + enterActiveGroup(ag); + activeGroup.selected = true; + } + } + if (activeGroup) { + if (!RED.group.contains(activeGroup,g)) { + // Clicked on a group that is outside the activeGroup + exitActiveGroup(); + } else { + } + } + selectGroup(g,true);//!wasSelected); + } else if (activeGroup && g.g !== activeGroup.id){ + 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) { + movingSet.add(g); + } + if (includeNodes) { + var currentSet = new Set(movingSet.nodes()); + var allNodes = RED.group.getNodes(g,true); + allNodes.forEach(function(n) { + if (!currentSet.has(n)) { + movingSet.add(n) + // n.selected = true; + } + n.dirty = true; + }) + } + } + function enterActiveGroup(group) { + if (activeGroup) { + exitActiveGroup(); + } + group.active = true; + group.dirty = true; + activeGroup = group; + movingSet.remove(group); + } + 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 = movingSet.length()-1; i >= 0; i -= 1) { + var msn = movingSet.get(i); + if (nodeSet.has(msn.n) || msn.n === g) { + msn.n.selected = false; + msn.n.dirty = true; + movingSet.remove(msn.n,i) + } + } + } + function getGroupAt(x,y) { + // x,y expected to be in node-co-ordinate space + 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:(movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}}); + options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}}); + options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}}); + options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); + options.push({name:"edit",disabled:(movingSet.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({ + position:[pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()], + touchTrigger: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 redrawStatus(d,nodeEl) { + if (d.z !== RED.workspaces.active()) { + return; + } + if (!nodeEl) { + nodeEl = document.getElementById(d.id); + } + if (nodeEl) { + // Do not show node status if: + // - global flag set + // - node has no status + // - node is disabled + if (!showStatus || !d.status || d.d === true) { + 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") + outGroup.each(function(d,i) { + var node = d3.select(this); + var nodeContents = document.createDocumentFragment(); + + d.h = 40; + d.resize = true; + d.dirty = true; + + var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); + mainRect.__data__ = d; + mainRect.setAttribute("class", "red-ui-flow-subflow-port"); + mainRect.setAttribute("rx", 8); + mainRect.setAttribute("ry", 8); + mainRect.setAttribute("width", 40); + mainRect.setAttribute("height", 40); + node[0][0].__mainRect__ = mainRect; + d3.select(mainRect) + .on("mouseup",nodeMouseUp) + .on("mousedown",nodeMouseDown) + .on("touchstart",nodeTouchStart) + .on("touchend",nodeTouchEnd) + nodeContents.appendChild(mainRect); + + var output_groupEl = document.createElementNS("http://www.w3.org/2000/svg","g"); + output_groupEl.setAttribute("x",0); + output_groupEl.setAttribute("y",0); + node[0][0].__outputLabelGroup__ = output_groupEl; + + var output_output = document.createElementNS("http://www.w3.org/2000/svg","text"); + output_output.setAttribute("class","red-ui-flow-port-label"); + output_output.style["font-size"] = "10px"; + output_output.textContent = "output"; + output_groupEl.appendChild(output_output); + node[0][0].__outputOutput__ = output_output; + + var output_number = document.createElementNS("http://www.w3.org/2000/svg","text"); + output_number.setAttribute("class","red-ui-flow-port-label red-ui-flow-port-index"); + output_number.setAttribute("x",0); + output_number.setAttribute("y",0); + output_number.textContent = d.i+1; + output_groupEl.appendChild(output_number); + node[0][0].__outputNumber__ = output_number; + + var output_border = document.createElementNS("http://www.w3.org/2000/svg","path"); + output_border.setAttribute("d","M 40 1 l 0 38") + output_border.setAttribute("class", "red-ui-flow-node-icon-shade-border") + output_groupEl.appendChild(output_border); + node[0][0].__outputBorder__ = output_border; + + nodeContents.appendChild(output_groupEl); + + var text = document.createElementNS("http://www.w3.org/2000/svg","g"); + text.setAttribute("class","red-ui-flow-port-label"); + text.setAttribute("transform","translate(38,0)"); + text.setAttribute('style', 'fill : #888'); // hard coded here! + node[0][0].__textGroup__ = text; + nodeContents.append(text); + + var portEl = document.createElementNS("http://www.w3.org/2000/svg","g"); + portEl.setAttribute('transform','translate(-5,15)') + + var port = document.createElementNS("http://www.w3.org/2000/svg","rect"); + port.setAttribute("class","red-ui-flow-port"); + port.setAttribute("rx",3); + port.setAttribute("ry",3); + port.setAttribute("width",10); + port.setAttribute("height",10); + portEl.appendChild(port); + port.__data__ = d; + + d3.select(port) + .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);}); + + node[0][0].__port__ = portEl + nodeContents.appendChild(portEl); + node[0][0].appendChild(nodeContents); + }); + + 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 port_height = 40; + + var self = this; + var thisNode = d3.select(this); + + dirtyNodes[d.id] = d; + + var label = getPortLabel(activeSubflow, PORT_TYPE_OUTPUT, d.i) || ""; + var hideLabel = (label.length < 1) + + var labelParts; + if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label) { + labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) { + d.resize = true; + } + this.__label__ = label; + this.__labelLineCount__ = labelParts.lines.length; + + if (hideLabel) { + d.h = Math.max(port_height,(d.outputs || 0) * 15); + } else { + d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, port_height); + } + this.__hideLabel__ = hideLabel; + } + + if (d.resize) { + var ow = d.w; + if (hideLabel) { + d.w = port_height; + } else { + d.w = Math.max(port_height,20*(Math.ceil((labelParts.width+50+7)/20)) ); + } + if (ow !== undefined) { + d.x += (d.w-ow)/2; + } + d.resize = false; + } + + 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; i0?7:0))/20)) ); + } + if (ow !== undefined) { + d.x += (d.w-ow)/2; + } + d.resize = false; + } + if (d._colorChanged) { + var newColor = RED.utils.getNodeColor(d.type,d._def); + this.__mainRect__.setAttribute("fill",newColor); + if (this.__buttonGroupButton__) { + this.__buttonGroupButton__.settAttribute("fill",newColor); + } + delete d._colorChanged; + } + //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(); + RED.hooks.trigger("viewRemovePort",{ + node:d, + el:this, + port:port, + portType: "output", + portIndex: this.__outputs__.length + }) + 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); + portGroup.__port__ = 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); + RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex}) + } 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); + if(RED.runtime && Object.hasOwn(RED.runtime,'started')) { + this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started); + } + + 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; + } + } + } + } + + RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) + }); + + if (nodesReordered) { + node.sort(function(a,b) { + return a._index - b._index; + }) + } + + var junction = junctionLayer.selectAll(".red-ui-flow-junction").data( + activeJunctions, + d => d.id + ) + var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction") + junctionEnter.each(function(d,i) { + var junction = d3.select(this); + var contents = document.createDocumentFragment(); + // d.added = true; + var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect"); + junctionBack.setAttribute("class","red-ui-flow-junction-background"); + junctionBack.setAttribute("x",-5); + junctionBack.setAttribute("y",-5); + junctionBack.setAttribute("width",10); + junctionBack.setAttribute("height",10); + junctionBack.setAttribute("rx",3); + junctionBack.setAttribute("ry",3); + junctionBack.__data__ = d; + this.__junctionBack__ = junctionBack; + contents.appendChild(junctionBack); + + var junctionInput = document.createElementNS("http://www.w3.org/2000/svg","rect"); + junctionInput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-input"); + junctionInput.setAttribute("x",-5); + junctionInput.setAttribute("y",-5); + junctionInput.setAttribute("width",10); + junctionInput.setAttribute("height",10); + junctionInput.setAttribute("rx",3); + junctionInput.setAttribute("ry",3); + junctionInput.__data__ = d; + junctionInput.__portType__ = PORT_TYPE_INPUT; + junctionInput.__portIndex__ = 0; + this.__junctionInput__ = junctionOutput; + contents.appendChild(junctionInput); + junctionInput.addEventListener("mouseup", portMouseUpProxy); + junctionInput.addEventListener("mousedown", portMouseDownProxy); + + + this.__junctionInput__ = junctionInput; + contents.appendChild(junctionInput); + var junctionOutput = document.createElementNS("http://www.w3.org/2000/svg","rect"); + junctionOutput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-output"); + junctionOutput.setAttribute("x",-5); + junctionOutput.setAttribute("y",-5); + junctionOutput.setAttribute("width",10); + junctionOutput.setAttribute("height",10); + junctionOutput.setAttribute("rx",3); + junctionOutput.setAttribute("ry",3); + junctionOutput.__data__ = d; + junctionOutput.__portType__ = PORT_TYPE_OUTPUT; + junctionOutput.__portIndex__ = 0; + this.__junctionOutput__ = junctionOutput; + contents.appendChild(junctionOutput); + junctionOutput.addEventListener("mouseup", portMouseUpProxy); + junctionOutput.addEventListener("mousedown", portMouseDownProxy); + + junctionOutput.addEventListener("mouseover", junctionMouseOverProxy); + junctionOutput.addEventListener("mouseout", junctionMouseOutProxy); + junctionInput.addEventListener("mouseover", junctionMouseOverProxy); + junctionInput.addEventListener("mouseout", junctionMouseOutProxy); + junctionBack.addEventListener("mouseover", junctionMouseOverProxy); + junctionBack.addEventListener("mouseout", junctionMouseOutProxy); + + // These handlers expect to be registered as d3 events + d3.select(junctionBack).on("mousedown", nodeMouseDown).on("mouseup", nodeMouseUp); + + junction[0][0].appendChild(contents); + }) + junction.exit().remove(); + junction.each(function(d) { + var junction = d3.select(this); + this.setAttribute("transform", "translate(" + (d.x) + "," + (d.y) + ")"); + if (d.dirty) { + junction.classed("red-ui-flow-junction-dragging", mouse_mode === RED.state.MOVING_ACTIVE && movingSet.has(d)) + junction.classed("selected", !!d.selected) + dirtyNodes[d.id] = d; + + 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) + .on("mousemove", function(d) { + if (mouse_mode === RED.state.SLICING) { + + selectedLinks.add(d) + l.classed("red-ui-flow-link-splice",true) + redraw() + } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) { + if (!l.classed("red-ui-flow-link-splice")) { + // Find intersection point + var lineLength = pathLine.getTotalLength(); + var pos; + var delta = Infinity; + for (var i = 0; i < lineLength; i++) { + var linePos = pathLine.getPointAtLength(i); + var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) + var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) + var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY + if (posDelta < delta) { + pos = linePos + delta = posDelta + } + } + d._sliceLocation = pos + selectedLinks.add(d) + l.classed("red-ui-flow-link-splice",true) + redraw() + } + } + }) + + 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 || 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||0); + d.y1 = d.source.y+y; + d.x2 = d.target.x-(d.target.w/2||0); + 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); + + 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; + movingSet.add(n); + if (targets.length === 1) { + RED.view.reveal(n.id); + } + }); + 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._index - b._index; + } + }) + } + group[0].reverse(); + var groupOpCount=0; + group.each(function(d,i) { + groupOpCount++ + if (d.resize) { + d.minWidth = 0; + delete d.resize; + } + if (d.dirty || dirtyGroups[d.id]) { + var g = d3.select(this); + var recalculateLabelOffsets = false; + if (d.nodes.length > 0) { + // If the group was just moved, all of its contents was + // also moved - so no need to recalculate its bounding box + if (!d.groupMoved) { + var minX = Number.POSITIVE_INFINITY; + var minY = Number.POSITIVE_INFINITY; + var maxX = 0; + var maxY = 0; + var margin = 26; + d.nodes.forEach(function(n) { + groupOpCount++ + 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; + recalculateLabelOffsets = true; + // if set explicitly to false, this group has just been + // imported so needed this initial resize calculation. + // Now that's done, delete the flag so the normal + // logic kicks in. + if (d.groupMoved === false) { + delete d.groupMoved; + } + } else { + delete d.groupMoved; + } + } else { + d.w = 40; + d.h = 40; + recalculateLabelOffsets = true; + } + if (recalculateLabelOffsets) { + 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.active || 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.active || 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" + * @param {String/Array} newNodesObj nodes to import + * @param {Object} options options object + * + * Options: + * - addFlow - whether to import nodes to a new tab + * - touchImport - whether this is a touch import. If not, imported nodes are + * attachedto mouse for placing - "IMPORT_DRAGGING" state + * - generateIds - whether to automatically generate new ids for all imported nodes + * - generateDefaultNames - whether to automatically update any nodes with clashing + * default names + */ + function importNodes(newNodesObj,options) { + options = options || { + addFlow: false, + touchImport: false, + generateIds: false, + generateDefaultNames: false + } + var addNewFlow = options.addFlow + var touchImport = options.touchImport; + + if (mouse_mode === RED.state.SELECTING_NODE) { + return; + } + + var nodesToImport; + if (typeof newNodesObj === "string") { + if (newNodesObj === "") { + return; + } + try { + nodesToImport = JSON.parse(newNodesObj); + } catch(err) { + var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); + e.code = "NODE_RED"; + throw e; + } + } else { + nodesToImport = newNodesObj; + } + + if (!$.isArray(nodesToImport)) { + nodesToImport = [nodesToImport]; + } + if (options.generateDefaultNames) { + RED.actions.invoke("core:generate-node-names", nodesToImport, { + renameBlank: false, + renameClash: true, + generateHistory: false + }) + } + + try { + var activeSubflowChanged; + if (activeSubflow) { + activeSubflowChanged = activeSubflow.changed; + } + var result = RED.nodes.import(nodesToImport,{generateIds:options.generateIds, addFlow: addNewFlow, importMap: options.importMap}); + if (result) { + var new_nodes = result.nodes; + var new_links = result.links; + var new_groups = result.groups; + var new_junctions = result.junctions; + var new_workspaces = result.workspaces; + var new_subflows = result.subflows; + var removedNodes = result.removedNodes; + var new_default_workspace = result.missingWorkspace; + 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() }); + new_ms = new_ms.concat(new_groups.filter(function(g) { return g.z === RED.workspaces.active()})) + new_ms = new_ms.concat(new_junctions.filter(function(j) { return j.z === RED.workspaces.active()})) + var new_node_ids = new_nodes.map(function(n){ n.changed = true; return n.id; }); + + clearSelection(); + movingSet.clear(); + movingSet.add(new_ms); + + + // TODO: pick a more sensible root node + if (movingSet.length() > 0) { + if (mouse_position == null) { + mouse_position = [0,0]; + } + + var dx = mouse_position[0]; + var dy = mouse_position[1]; + if (movingSet.length() > 0) { + var root_node = movingSet.get(0).n; + dx = root_node.x; + dy = root_node.y; + } + + var minX = 0; + var minY = 0; + var i; + var node,group; + var l =movingSet.length(); + 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)) + + + } + } + + } + + var historyEvent = { + t:"add", + nodes:new_node_ids, + links:new_links, + groups:new_groups, + junctions: new_junctions, + workspaces:new_workspaces, + subflows:new_subflows, + dirty:RED.nodes.dirty() + }; + if (movingSet.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 + } + } + } + if (removedNodes) { + var replaceEvent = { + t: "replace", + config: removedNodes + } + historyEvent = { + t:"multi", + events: [ + replaceEvent, + historyEvent + ] + } + } + + 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; + var newJunctionCount = new_junctions.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:newConfigNodeCount})); + } + if (new_subflows.length > 0) { + counts.push(RED._("clipboard.subflow",{count:new_subflows.length})); + } + if (removedNodes && removedNodes.length > 0) { + counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length})); + } + if (counts.length > 0) { + var countList = "
  • "+counts.join("
  • ")+"
"; + RED.notify("

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

"+countList,{id:"clipboard"}); + } + + } + } catch(error) { + if (error.code === "import_conflict") { + // Pass this up for the called to resolve + throw error; + } else 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 (movingSet.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 (movingSet.length() > 0) { + movingSet.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 (selectedLinks.length() > 0) { + selection.links = selectedLinks.toArray(); + selection.link = selection.links[0]; + } + return selection; + } + + /** + * Create a node from a type string. + * **NOTE:** Can throw on error - use `try` `catch` block when calling + * @param {string} type The node type to create + * @param {number} [x] (optional) The horizontal position on the workspace + * @param {number} [y] (optional)The vertical on the workspace + * @param {string} [z] (optional) The flow tab this node will belong to. Defaults to active workspace. + * @returns An object containing the `node` and a `historyEvent` + * @private + */ + function createNode(type, x, y, z) { + var m = /^subflow:(.+)$/.exec(type); + var activeSubflow = z ? RED.nodes.subflow(z) : null; + if (activeSubflow && m) { + var subflowId = m[1]; + if (subflowId === activeSubflow.id) { + throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) + } + if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { + throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) + } + } + + var nn = { id: RED.nodes.id(), z: 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 = RED.view.node_width; + nn.h = Math.max(RED.view.node_height, (nn.outputs || 0) * 15); + nn.resize = true; + if (x != null && typeof x == "number" && x >= 0) { + nn.x = x; + } + if (y != null && typeof y == "number" && y >= 0) { + nn.y = y; + } + 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 calculateNodeDimensions(node) { + var result = [node_width,node_height]; + try { + var isLink = (node.type === "link in" || node.type === "link out") + var hideLabel = node.hasOwnProperty('l')?!node.l : isLink; + var label = RED.utils.getNodeLabel(node, node.type); + var labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (hideLabel) { + result[1] = Math.max(node_height,(node.outputs || 0) * 15); + } else { + result[1] = Math.max(6+24*labelParts.lines.length,(node.outputs || 0) * 15, 30); + } + if (hideLabel) { + result[0] = node_height; + } else { + result[0] = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(node._def.inputs>0?7:0))/20)) ); + } + }catch(err) { + console.log("Error",node); + } + return result; + } + + + function flashNode(n) { + let node = n; + if(typeof node === "string") { node = RED.nodes.node(n); } + if(!node) { return; } + + const flashingNode = flashingNodeId && RED.nodes.node(flashingNodeId); + if(flashingNode) { + //cancel current flashing node before flashing new node + clearInterval(flashingNode.__flashTimer); + delete flashingNode.__flashTimer; + flashingNode.dirty = true; + flashingNode.highlighted = false; + } + node.__flashTimer = setInterval(function(flashEndTime, n) { + n.dirty = true; + if (flashEndTime >= Date.now()) { + n.highlighted = !n.highlighted; + } else { + clearInterval(n.__flashTimer); + delete n.__flashTimer; + flashingNodeId = null; + n.highlighted = false; + } + RED.view.redraw(); + }, 100, Date.now() + 2200, node) + flashingNodeId = node.id; + node.highlighted = true; + RED.view.redraw(); + } + 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; + movingSet.clear(); + movingSet.add(selectedNode); + } + } else if (selection) { + if (selection.nodes) { + updateActiveNodes(); + movingSet.clear(); + // 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; + movingSet.add(n); + } else { + selectGroup(n,true); + } + }) + } + } + } + updateSelection(); + redraw(true); + }, + selection: getSelection, + clearSelection: clearSelection, + createNode: createNode, + /** default node width */ + get node_width() { + return node_width; + }, + /** default node height */ + get node_height() { + return node_height; + }, + /** snap to grid option state */ + get snapGrid() { + return snapGrid; + }, + /** gets the current scale factor */ + scale: function() { + return scaleFactor; + }, + getLinksAtPoint: function(x,y) { + // x,y must be in SVG co-ordinate space + // if they come from a node.x/y, they will need to be scaled using + // scaleFactor first. + 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, null, null, true); + } 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[0].clientWidth/scaleFactor,chart[0].clientHeight/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) { + flashNode(node); + } + } 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; + }, + getSubflowPorts: function() { + var result = []; + if (activeSubflow) { + var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;}); + subflowOutputs.each(function(d,i) { result.push(d) }) + var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;}); + subflowInputs.each(function(d,i) { result.push(d) }) + var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;}); + subflowStatus.each(function(d,i) { result.push(d) }) + } + return result; + }, + 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; + movingSet.add(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 = movingSet.nodes() + 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, + showQuickAddDialog:showQuickAddDialog, + calculateNodeDimensions: calculateNodeDimensions, + getElementPosition:getElementPosition, + showTooltip:showTooltip + }; +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/header.scss b/packages/node_modules/@node-red/editor-client/src/sass/header.scss index 697a90729..bfe5c283d 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/header.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/header.scss @@ -219,7 +219,7 @@ span.red-ui-menu-sublabel { color: $header-menu-sublabel-color; font-size: 13px; - display: inline-block; + display: block; text-indent: 0px; } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index b3c471a5a..2e71cbdca 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -335,7 +335,7 @@ var api = module.exports = { throw (makeError(err, err.code, 500)) } default: - throw (makeError("Cannot set runtime state. Invalid state", "invalid_run_state", 400)) + throw (makeError(`Cannot change flows runtime state to '${opts.requestedState}'}`, "invalid_run_state", 400)) } }, } From 68c1e49f6289616f770514db27fa2576a23aa777 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Mon, 27 Jun 2022 18:12:45 +0100 Subject: [PATCH 07/12] Delete view_copy.js Remove file that slipped through the net --- .../editor-client/src/js/ui/view_copy.js | 6287 ----------------- 1 file changed, 6287 deletions(-) delete mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js deleted file mode 100644 index 569fee1d5..000000000 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view_copy.js +++ /dev/null @@ -1,6287 +0,0 @@ -/** - * 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" - * |- "junctionLayer" - * |- "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 entryCoordinates = {x:-1, y:-1}; - var gridSize = 20; - var snapGrid = false; - - var activeSpliceLink; - var spliceActive = false; - var spliceTimer; - var groupHoverTimer; - - var activeSubflow = null; - var activeNodes = []; - var activeLinks = []; - var activeJunctions = []; - var activeFlowLinks = []; - var activeLinkNodes = {}; - var activeGroup = null; - var activeHoverGroup = null; - var activeGroups = []; - var dirtyGroups = {}; - - 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 lasso = null; - var slicePath = null; - var slicePathLast = 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 lastClickPosition = []; - var selectNodesOptions; - - let flashingNodeId; - - 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", - "gray": "#d3d3d3" - } - - var PORT_TYPE_INPUT = 1; - var PORT_TYPE_OUTPUT = 0; - - var chart; - var outer; - var eventLayer; - var gridLayer; - var linkLayer; - var junctionLayer; - var dragGroupLayer; - var groupSelectLayer; - var nodeLayer; - var groupLayer; - var drag_lines; - - var movingSet = (function() { - var setIds = new Set(); - var set = []; - var api = { - add: function(node) { - if (Array.isArray(node)) { - for (var i=0;i1) { - 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"); - junctionLayer = 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 || event.workspace === 0); - RED.menu.setDisabled("menu-item-workspace-delete",event.workspace === 0 || 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) { - resetMouseVars(); - 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(); } - } - }); - - //add search to status-toolbar - RED.statusBar.add({ - id: "view-search-tools", - align: "left", - hidden: false, - element: $(''+ - '' + - '' + - '' + - '? of ?' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '') - }) - $("#red-ui-view-searchtools-search").on("click", searchFlows); - RED.popover.tooltip($("#red-ui-view-searchtools-search"),RED._('actions.search-flows'),'core:search'); - $("#red-ui-view-searchtools-prev").on("click", searchPrev); - RED.popover.tooltip($("#red-ui-view-searchtools-prev"),RED._('actions.search-prev'),'core:search-previous'); - $("#red-ui-view-searchtools-next").on("click", searchNext); - RED.popover.tooltip($("#red-ui-view-searchtools-next"),RED._('actions.search-next'),'core:search-next'); - RED.popover.tooltip($("#red-ui-view-searchtools-close"),RED._('common.label.close')); - - // 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 = createNode(selected_tool); - if (!result) { - return; - } - var historyEvent = result.historyEvent; - var nn = result.node; - - RED.nodes.add(nn); - - var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); - if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { - nn.l = showLabel; - } - - var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); - var helperWidth = ui.helper.width(); - var helperHeight = ui.helper.height(); - var mousePos = d3.touches(this)[0]||d3.mouse(this); - - try { - var isLink = (nn.type === "link in" || nn.type === "link out") - var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; - - var label = RED.utils.getNodeLabel(nn, nn.type); - var labelParts = getLabelParts(label, "red-ui-flow-node-label"); - if (hideLabel) { - nn.w = node_height; - nn.h = Math.max(node_height,(nn.outputs || 0) * 15); - } else { - nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); - nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); - } - } catch(err) { - } - - mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); - mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); - mousePos[1] /= scaleFactor; - mousePos[0] /= scaleFactor; - - nn.x = mousePos[0]; - nn.y = mousePos[1]; - - if (snapGrid) { - var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); - nn.x -= gridOffset.x; - nn.y -= gridOffset.y; - } - - 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]; - } - - - 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; - movingSet.add(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,{generateIds: true, generateDefaultNames: true});}); - - RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() }) - - RED.events.on("view:selection-changed", function(selection) { - var hasSelection = (selection.nodes && selection.nodes.length > 0); - var hasMultipleSelection = hasSelection && selection.nodes.length > 1; - RED.menu.setDisabled("menu-item-edit-cut",!hasSelection); - RED.menu.setDisabled("menu-item-edit-copy",!hasSelection); - RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection); - RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection); - - RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection); - RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection); - }) - - RED.actions.add("core:delete-selection",deleteSelection); - RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) }); - RED.actions.add("core:edit-selected-node",editSelection); - RED.actions.add("core:go-to-selection",function() { - if (movingSet.length() > 0) { - var node = movingSet.get(0).n; - if (/^subflow:/.test(node.type)) { - RED.workspaces.show(node.type.substring(8)) - } else if (node.type === 'group') { - enterActiveGroup(node); - redraw(); - } - } - }); - 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:select-none", selectNone); - 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.annotations.init(); - RED.view.navigator.init(); - RED.view.tools.init(); - - - RED.view.annotations.register("red-ui-flow-node-changed",{ - type: "badge", - class: "red-ui-flow-node-changed", - element: function() { - var changeBadge = document.createElementNS("http://www.w3.org/2000/svg","circle"); - changeBadge.setAttribute("cx",5); - changeBadge.setAttribute("cy",5); - changeBadge.setAttribute("r",5); - return changeBadge; - }, - show: function(n) { return n.changed||n.moved } - }) - - RED.view.annotations.register("red-ui-flow-node-error",{ - type: "badge", - class: "red-ui-flow-node-error", - element: function(d) { - var errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path"); - errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z"); - return errorBadge - }, - tooltip: function(d) { - if (d.validationErrors && d.validationErrors.length > 0) { - return RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - ") - } - }, - show: function(n) { return !n.valid } - }) - - if (RED.settings.get("editor.view.view-store-zoom")) { - var userZoomLevel = parseFloat(RED.settings.getLocal('zoom-level')) - if (!isNaN(userZoomLevel)) { - scaleFactor = userZoomLevel - } - } - - var onScrollTimer = null; - function storeScrollPosition() { - workspaceScrollPositions[RED.workspaces.active()] = { - left:chart.scrollLeft(), - top:chart.scrollTop() - }; - RED.settings.setLocal('scroll-positions', JSON.stringify(workspaceScrollPositions) ) - } - chart.on("scroll", function() { - if (RED.settings.get("editor.view.view-store-position")) { - if (onScrollTimer) { - clearTimeout(onScrollTimer) - } - onScrollTimer = setTimeout(storeScrollPosition, 200); - } - }) - - if (RED.settings.get("editor.view.view-store-position")) { - var scrollPositions = RED.settings.getLocal('scroll-positions') - if (scrollPositions) { - try { - workspaceScrollPositions = JSON.parse(scrollPositions) - } catch(err) { - } - } - } - } - - - - 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 canvasMouseDown() { - if (RED.view.DEBUG) { - console.warn("canvasMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); - } - 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) { - selectedLinks.clear(); - updateSelection(); - } - if (mouse_mode === 0 && lasso) { - lasso.remove(); - lasso = null; - } - if (d3.event.touches || d3.event.button === 0) { - if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && (d3.event.metaKey || d3.event.ctrlKey) && !(d3.event.altKey || d3.event.shiftKey)) { - // Trigger quick add dialog - d3.event.stopPropagation(); - clearSelection(); - const 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({ position: point, group: clickedGroup }); - } else if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) { - // CTRL not being held - if (!d3.event.altKey) { - // ALT not held (shift is allowed) Trigger lasso - if (!touchStartTime) { - const 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(); - } - } else if (d3.event.altKey) { - //Alt [+shift] held - Begin slicing - clearSelection(); - mouse_mode = (d3.event.shiftKey) ? RED.state.SLICING_JUNCTION : RED.state.SLICING; - const point = d3.mouse(this); - slicePath = eventLayer.append("path").attr("class", "nr-ui-view-slice").attr("d", `M${point[0]} ${point[1]}`) - slicePathLast = point; - RED.view.redraw(); - } - } - } - } - - function showQuickAddDialog(options) { - options = options || {}; - var point = options.position || lastClickPosition; - var spliceLink = options.splice; - var targetGroup = options.group; - var touchTrigger = options.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 nn; - var historyEvent; - if (type === 'junction') { - nn = { - _def: {defaults:{}}, - type: 'junction', - z: RED.workspaces.active(), - id: RED.nodes.id(), - x: 0, - y: 0, - w: 0, h: 0, - outputs: 1, - inputs: 1, - dirty: true - } - historyEvent = { - t:'add', - junctions:[nn] - } - } else { - var result = createNode(type); - if (!result) { - return; - } - nn = result.node; - historyEvent = result.historyEvent; - } - if (keepAdding) { - mouse_mode = RED.state.QUICK_JOINING; - } - - 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 && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { - nn.l = showLabel; - } - if (quickAddLink) { - var drag_line = quickAddLink; - var src = null,dst,src_port; - if (drag_line.portType === PORT_TYPE_OUTPUT && (nn.inputs > 0 || drag_line.virtualLink) ) { - src = drag_line.node; - src_port = drag_line.port; - dst = nn; - } else if (drag_line.portType === PORT_TYPE_INPUT && (nn.outputs > 0 || drag_line.virtualLink)) { - src = nn; - dst = drag_line.node; - src_port = 0; - } - - if (src !== null) { - // Joining link nodes via virual wires. Need to update - // the src and dst links property - if (drag_line.virtualLink) { - historyEvent = { - t:'multi', - events: [historyEvent] - } - var oldSrcLinks = $.extend(true,{},{v:src.links}).v - var oldDstLinks = $.extend(true,{},{v:dst.links}).v - src.links.push(dst.id); - dst.links.push(src.id); - src.dirty = true; - dst.dirty = true; - - historyEvent.events.push({ - t:'edit', - node: src, - dirty: RED.nodes.dirty(), - changed: src.changed, - changes: { - links:oldSrcLinks - } - }); - historyEvent.events.push({ - t:'edit', - node: dst, - dirty: RED.nodes.dirty(), - changed: dst.changed, - changes: { - links:oldDstLinks - } - }); - src.changed = true; - dst.changed = true; - } else { - var link = {source: src, sourcePort:src_port, target: dst}; - RED.nodes.addLink(link); - historyEvent.links = [link]; - } - if (!keepAdding) { - quickAddLink.el.remove(); - quickAddLink = null; - if (mouse_mode === RED.state.QUICK_JOINING) { - if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { - showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); - } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { - showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); - } else { - resetMouseVars(); - } - } - } else { - quickAddLink.node = nn; - quickAddLink.port = 0; - } - } else { - hideDragLines(); - resetMouseVars(); - } - } else { - if (!keepAdding) { - if (mouse_mode === RED.state.QUICK_JOINING) { - if (nn.outputs > 0) { - showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); - } else if (nn.inputs > 0) { - showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); - } else { - resetMouseVars(); - } - } - } else { - if (nn.outputs > 0) { - quickAddLink = { - node: nn, - port: 0, - portType: PORT_TYPE_OUTPUT - } - } else if (nn.inputs > 0) { - quickAddLink = { - node: nn, - port: 0, - portType: PORT_TYPE_INPUT - } - } else { - resetMouseVars(); - } - } - } - if (nn.type === 'junction') { - RED.nodes.addJunction(nn); - } else { - 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); - } - movingSet.add(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 - } - if (entryCoordinates.x != -1) { - mouse_position = [entryCoordinates.x, entryCoordinates.y] - } else { - mouse_position = d3.touches(this)[0]||d3.mouse(this); - } -if(RED.view.DEBUG) { console.log(`mousemove ${JSON.stringify(mouse_position)}`)} - 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; - } else if (mouse_mode === RED.state.SLICING || mouse_mode === RED.state.SLICING_JUNCTION) { - if (slicePath) { - var delta = Math.max(1,Math.abs(slicePathLast[0]-mouse_position[0]))*Math.max(1,Math.abs(slicePathLast[1]-mouse_position[1])) - if (delta > 20) { - var currentPath = slicePath.attr("d") - currentPath += " L"+mouse_position[0]+" "+mouse_position[1] - slicePath.attr("d",currentPath); - slicePathLast = mouse_position - } - } - 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 && mouse_mode != RED.state.DETACHED_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) { - 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 (selectedLinks.length() > 0) { - selectedLinks.forEach(function(link) { - if (((mousedown_port_type === PORT_TYPE_OUTPUT && - link.source === mousedown_node && - link.sourcePort === mousedown_port_index - ) || - (mousedown_port_type === PORT_TYPE_INPUT && - link.target === mousedown_node - ))) { - existingLinks.push(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 (movingSet.length() === 1) { - node = movingSet.get(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 || mouse_mode == RED.state.DETACHED_DRAGGING) { - mousePos = mouse_position; - var minX = 0; - var minY = 0; - var maxX = space_width; - var maxY = space_height; - if(RED.view.DEBUG) { console.log(`mousemove - MOVING_ACTIVE ${JSON.stringify(mousePos)}`)} - 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 = movingSet.get(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")); - var ag = activeGroup; - if (!d3.event.shiftKey) { - clearSelection(); - if (ag) { - if (x < ag.x+ag.w && x2 > ag.x && y < ag.y+ag.h && y2 > ag.y) { - // There was an active group and the lasso intersects with it, - // so reenter the group - enterActiveGroup(ag); - activeGroup.selected = true; - } - } - } - activeGroups.forEach(function(g) { - if (!g.selected) { - if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) { - if (!activeGroup || RED.group.contains(activeGroup,g)) { - while (g.g && (!activeGroup || g.g !== activeGroup.id)) { - 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 (!activeGroup || RED.group.contains(activeGroup,n)) { - if (n.g && (!activeGroup || n.g !== activeGroup.id)) { - var group = RED.nodes.group(n.g); - while (group.g && (!activeGroup || group.g !== activeGroup.id)) { - group = RED.nodes.group(group.g); - } - if (!group.selected) { - selectGroup(group,true); - } - } else { - n.selected = true; - n.dirty = true; - movingSet.add(n); - } - } - } - } - }); - activeJunctions.forEach(function(n) { - if (!n.selected) { - if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { - n.selected = true; - n.dirty = true; - movingSet.add(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; - movingSet.add(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; - movingSet.add(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; - movingSet.add(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(); - } else if (mouse_mode == RED.state.SLICING) { - deleteSelection(); - slicePath.remove(); - slicePath = null; - RED.view.redraw(true); - } else if (mouse_mode == RED.state.SLICING_JUNCTION) { - var removedLinks = new Set() - var addedLinks = [] - var addedJunctions = [] - - var groupedLinks = {} - selectedLinks.forEach(function(l) { - var sourceId = l.source.id+":"+l.sourcePort - groupedLinks[sourceId] = groupedLinks[sourceId] || [] - groupedLinks[sourceId].push(l) - - groupedLinks[l.target.id] = groupedLinks[l.target.id] || [] - groupedLinks[l.target.id].push(l) - }); - var linkGroups = Object.keys(groupedLinks) - linkGroups.sort(function(A,B) { - return groupedLinks[B].length - groupedLinks[A].length - }) - linkGroups.forEach(function(gid) { - var links = groupedLinks[gid] - var junction = { - _def: {defaults:{}}, - type: 'junction', - z: RED.workspaces.active(), - id: RED.nodes.id(), - x: 0, - y: 0, - w: 0, h: 0, - outputs: 1, - inputs: 1, - dirty: true - } - links = links.filter(function(l) { return !removedLinks.has(l) }) - if (links.length === 0) { - return - } - links.forEach(function(l) { - junction.x += l._sliceLocation.x - junction.y += l._sliceLocation.y - }) - junction.x = Math.round(junction.x/links.length) - junction.y = Math.round(junction.y/links.length) - if (snapGrid) { - junction.x = (gridSize*Math.round(junction.x/gridSize)); - junction.y = (gridSize*Math.round(junction.y/gridSize)); - } - - var nodeGroups = new Set() - - RED.nodes.addJunction(junction) - addedJunctions.push(junction) - let newLink - if (gid === links[0].source.id+":"+links[0].sourcePort) { - newLink = { - source: links[0].source, - sourcePort: links[0].sourcePort, - target: junction - } - } else { - newLink = { - source: junction, - sourcePort: 0, - target: links[0].target - } - } - addedLinks.push(newLink) - RED.nodes.addLink(newLink) - links.forEach(function(l) { - removedLinks.add(l) - RED.nodes.removeLink(l) - let newLink - if (gid === l.target.id) { - newLink = { - source: l.source, - sourcePort: l.sourcePort, - target: junction - } - } else { - newLink = { - source: junction, - sourcePort: 0, - target: l.target - } - } - addedLinks.push(newLink) - RED.nodes.addLink(newLink) - nodeGroups.add(l.source.g || "__NONE__") - nodeGroups.add(l.target.g || "__NONE__") - }) - if (nodeGroups.size === 1) { - var group = nodeGroups.values().next().value - if (group !== "__NONE__") { - RED.group.addToGroup(RED.nodes.group(group), junction) - } - } - }) - slicePath.remove(); - slicePath = null; - - if (addedJunctions.length > 0) { - RED.history.push({ - t: 'add', - links: addedLinks, - junctions: addedJunctions, - removedLinks: Array.from(removedLinks) - }) - RED.nodes.dirty(true) - } - RED.view.redraw(true); - } - if (mouse_mode == RED.state.MOVING_ACTIVE) { - if (movingSet.length() > 0) { - var addedToGroup = null; - if (activeHoverGroup) { - for (var j=0;j 0 && mouse_mode == RED.state.MOVING_ACTIVE) { - 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: movingSet.get(0).n - }; - var link2 = { - source:movingSet.get(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; - // movingSet.add(mousedown_node); - // } - // } - if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) { - // if (mousedown_node) { - // delete mousedown_node.gSelected; - // } - if (mouse_mode === RED.state.DETACHED_DRAGGING) { - var ns = []; - for (var j=0;j 0.3) { - zoomView(scaleFactor-0.1); - } - } - function zoomZero() { zoomView(1); } - function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); } - function searchPrev() { RED.actions.invoke("core:search-previous"); } - function searchNext() { RED.actions.invoke("core:search-next"); } - - - 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(); - if (RED.settings.get("editor.view.view-store-zoom")) { - RED.settings.setLocal('zoom-level', factor.toFixed(1)) - } - } - - function selectNone() { - if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) { - return; - } - if (mouse_mode === RED.state.DETACHED_DRAGGING) { - for (var j=0;j 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 && selectedLinks.length() > 0) { - selectedLinks.forEach(function(link) { - if (link.link) { - activeLinks.push(link); - activeLinkNodes[link.source.id] = link.source; - link.source.dirty = true; - activeLinkNodes[link.target.id] = link.target; - 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; - } else if (key === 'links') { - return value.map(function(link) { - return link.source.id+":"+link.sourcePort+":"+link.target.id; - }); - } - return value; - }); - if (selectionJSON !== lastSelection) { - lastSelection = selectionJSON; - RED.events.emit("view:selection-changed",selection); - } - } - - function editSelection() { - if (movingSet.length() > 0) { - var node = movingSet.get(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(reconnectWires) { - 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: [], - junctions: [], - workspaces: [], - subflows: [] - } - var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0); - - for (var i=0;i 0 || selectedLinks.length() > 0) { - var result; - var node; - var removedNodes = []; - var removedLinks = []; - var removedGroups = []; - var removedJunctions = []; - var removedSubflowOutputs = []; - var removedSubflowInputs = []; - var removedSubflowStatus; - var subflowInstances = []; - var historyEvents = []; - var addToRemovedLinks = function(links) { - if(!links) { return; } - var _links = Array.isArray(links) ? links : [links]; - _links.forEach(function(l) { - removedLinks.push(l); - selectedLinks.remove(l); - }) - } - if (reconnectWires) { - var reconnectResult = RED.nodes.detachNodes(movingSet.nodes()) - var addedLinks = reconnectResult.newLinks; - if (addedLinks.length > 0) { - historyEvents.push({ t:'add', links: addedLinks }) - } - addToRemovedLinks(reconnectResult.removedLinks) - } - - var startDirty = RED.nodes.dirty(); - var startChanged = false; - var selectedGroups = []; - if (movingSet.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) { - addToRemovedLinks(result.links); - } - } - // Assume 0/1 inputs - if (removedSubflowInputs.length == 1) { - result = RED.subflow.removeInput(); - if (result) { - addToRemovedLinks(result.links); - } - } - if (removedSubflowStatus) { - result = RED.subflow.removeStatus(); - if (result) { - addToRemovedLinks(result.links); - } - } - - var instances = RED.subflow.refresh(true); - if (instances) { - subflowInstances = instances.instances; - } - movingSet.clear(); - if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus || removedGroups.length > 0 || removedJunctions.length > 0) { - RED.nodes.dirty(true); - } - } - - if (selectedLinks.length() > 0) { - selectedLinks.forEach(function(link) { - if (link.link) { - var sourceId = link.source.id; - var targetId = link.target.id; - var sourceIdIndex = link.target.links.indexOf(sourceId); - var targetIdIndex = link.source.links.indexOf(targetId); - historyEvents.push({ - t: "edit", - node: link.source, - changed: link.source.changed, - changes: { - links: $.extend(true,{},{v:link.source.links}).v - } - }) - historyEvents.push({ - t: "edit", - node: link.target, - changed: link.target.changed, - changes: { - links: $.extend(true,{},{v:link.target.links}).v - } - }) - link.source.changed = true; - link.target.changed = true; - link.target.links.splice(sourceIdIndex,1); - link.source.links.splice(targetIdIndex,1); - link.source.dirty = true; - link.target.dirty = true; - - } else { - RED.nodes.removeLink(link); - removedLinks.push(link); - } - }) - } - RED.nodes.dirty(true); - var historyEvent = { - t:"delete", - nodes:removedNodes, - links:removedLinks, - groups: removedGroups, - junctions: removedJunctions, - subflowOutputs:removedSubflowOutputs, - subflowInputs:removedSubflowInputs, - subflow: { - id: activeSubflow?activeSubflow.id:undefined, - instances: subflowInstances - }, - dirty:startDirty - }; - if (removedSubflowStatus) { - historyEvent.subflow.status = removedSubflowStatus; - } - if (historyEvents.length > 0) { - historyEvents.unshift(historyEvent); - RED.history.push({ - t:"multi", - events: historyEvents - }) - } else { - RED.history.push(historyEvent); - } - - selectedLinks.clear(); - 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 junctionCount = 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 detachSelectedNodes() { - var selection = RED.view.selection(); - if (selection.nodes) { - const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes); - if (removedLinks.length || newLinks.length) { - RED.history.push({ - t: "multi", - events: [ - { t:'delete', links: removedLinks }, - { t:'add', links: newLinks } - ], - dirty: RED.nodes.dirty() - }) - RED.nodes.dirty(true) - } - prepareDrag([selection.nodes[0].x,selection.nodes[0].y]); - mouse_mode = RED.state.DETACHED_DRAGGING; - RED.view.redraw(true); - } - } - - 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(); - } - mousedown_link = select_link; - if (select_link) { - selectedLinks.clear(); - selectedLinks.add(select_link); - updateSelection(); - } else { - selectedLinks.clear(); - } - } - redraw(); - return; - } - - resetMouseVars(); - hideDragLines(); - if (select_link) { - selectedLinks.clear(); - selectedLinks.add(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 junctionMouseOver(junction, d, portType) { - var active = (portType === undefined) || - (mouse_mode !== RED.state.JOINING && mouse_mode !== RED.state.QUICK_JOINING) || - (drag_lines.length > 0 && drag_lines[0].portType !== portType && !drag_lines[0].virtualLink) - junction.classed("red-ui-flow-junction-hovered", active); - } - function junctionMouseOut(junction, d) { - junction.classed("red-ui-flow-junction-hovered",false); - } - - function prepareDrag(mouse) { - mouse_mode = RED.state.MOVING; - // Called when movingSet should be prepared to be dragged - for (i=0;i 0 && clickElapsed < dblClickInterval) { - mouse_mode = RED.state.DEFAULT; - if (d.type != "subflow") { - if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) { - RED.workspaces.show(d.type.substring(8)); - } else { - 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; - movingSet.add(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); - } - } - - - document.addEventListener('pointerlockchange', changeCallback, false); - document.addEventListener('mozpointerlockchange', changeCallback, false); - document.addEventListener('webkitpointerlockchange', changeCallback, false); - document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; - function getPosition(canvas, event) { - var x, y; - - if (event.x != undefined && event.y != undefined) { - x = event.x; - y = event.y; - } else // Firefox method to get the position - { - x = event.clientX + document.body.scrollLeft + - document.documentElement.scrollLeft; - y = event.clientY + document.body.scrollTop + - document.documentElement.scrollTop; - } - - x -= canvas.offsetLeft == null ? canvas.clientLeft : canvas.offsetLeft; - y -= canvas.offsetTop == null ? canvas.clientTop : canvas.offsetTop; - - return {x:x, y:y}; - } - //temporary proxy for when mouse is locked - function canvasMouseMove_locked(e) { - - var canvas = $(eventLayer[0])[0]; - - - // if we enter this for the first time, get the initial position - if (entryCoordinates.x == -1) { - entryCoordinates = getPosition(canvas, e); - } - - - //get a reference to the canvas - var movementX = e.movementX || - e.mozMovementX || - e.webkitMovementX || - 0; - - var movementY = e.movementY || - e.mozMovementY || - e.webkitMovementY || - 0; - - - // calculate the new coordinates where we should draw the ship - entryCoordinates.x = entryCoordinates.x + movementX; - entryCoordinates.y = entryCoordinates.y + movementY; - - if (entryCoordinates.x > chart.width() -65) { - entryCoordinates.x = chart.width()-65; - } else if (entryCoordinates.x < 0) { - entryCoordinates.x = 0; - } - - if (entryCoordinates.y > chart.height() - 85) { - entryCoordinates.y = chart.height() - 85; - } else if (entryCoordinates.y < 0) { - entryCoordinates.y = 0; - } - - - // determine the direction - var direction = 0; - if (movementX > 0) { - direction = 1; - } else if (movementX < 0) { - direction = -1; - } - // console.log(entryCoordinates) - - - d3.event = e - canvasMouseMove.call(this,e); - //canvasMouseMove.call(document.querySelector("g.red-ui-workspace-chart-event-layer")); - } - function changeCallback(e) { - var canvas = $(eventLayer[0])[0] - // var workspaceChart = $("g.red-ui-workspace-chart-event-layer")[0]; - // var workspaceChart = $("#red-ui-workspace-chart")[0]; - if (document.pointerLockElement === canvas || - document.mozPointerLockElement === canvas || - document.webkitPointerLockElement === canvas) { - - // we've got a pointerlock for our element, add a mouselistener - canvas.addEventListener("mousemove", canvasMouseMove_locked, false); - // outer.on("mousemove", canvasMouseMove_locked) - // document.addEventListener("mousemove", canvasMouseMove_locked, false); - } else { - - // pointer lock is no longer active, remove the callback - canvas.removeEventListener("mousemove", canvasMouseMove_locked, false); - // document.removeEventListener("mousemove", canvasMouseMove_locked, false); - // if(outer.off) {outer.off("mousemove", canvasMouseMove_locked)} - // if(outer.removeEventListener) {outer.removeEventListener("mousemove", canvasMouseMove_locked)} - // and reset the entry coordinates - entryCoordinates = {x:-1, y:-1}; - } - } - - function nodeMouseDown(d) { - if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); } - try { - // workspaceChart = $("g.red-ui-workspace-chart-event-layer")[0] - var canvas = $(eventLayer[0])[0] - canvas.requestPointerLock = canvas.requestPointerLock || - canvas.mozRequestPointerLock || - canvas.webkitRequestPointerLock; - canvas.requestPointerLock() - console.log("got pointer lock") - } catch (error) { - console.error(error) - } - - 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 || mouse_mode == RED.state.DETACHED_DRAGGING) { - var historyEvent = RED.history.peek(); - 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: movingSet.get(0).n - }; - var link2 = { - source:movingSet.get(0).n, - sourcePort:0, - target: spliceLink.target - }; - RED.nodes.addLink(link1); - RED.nodes.addLink(link2); - - historyEvent.links = [link1,link2]; - historyEvent.removedLinks = [spliceLink]; - updateActiveNodes(); - } - - if (activeHoverGroup) { - for (var j=0;j 30 ? 25 : (mousedown_node.w > 0 ? 8 : 3); - if (edgeDelta < targetEdgeDelta) { - if (clickPosition < 0) { - cnodes = [mousedown_node].concat(RED.nodes.getAllUpstreamNodes(mousedown_node)); - } else { - cnodes = [mousedown_node].concat(RED.nodes.getAllDownstreamNodes(mousedown_node)); - } - } else { - cnodes = RED.nodes.getAllFlowNodes(mousedown_node); - } - for (var n=0;n 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 junctionMouseOverProxy(e) { junctionMouseOver(d3.select(this), this.__data__, this.__portType__) } - function junctionMouseOutProxy(e) { junctionMouseOut(d3.select(this), this.__data__) } - - function linkMouseDown(d) { - if (RED.view.DEBUG) { - console.warn("linkMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); - } - if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); - return; - } - if (d3.event.button === 2) { - return - } - mousedown_link = d; - - if (!(d3.event.metaKey || d3.event.ctrlKey)) { - clearSelection(); - } - if (d3.event.metaKey || d3.event.ctrlKey) { - if (!selectedLinks.has(mousedown_link)) { - selectedLinks.add(mousedown_link); - } else { - if (selectedLinks.length() !== 1) { - selectedLinks.remove(mousedown_link); - } - } - } else { - selectedLinks.add(mousedown_link); - } - updateSelection(); - redraw(); - focusView(); - d3.event.stopPropagation(); - if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && (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({position:point, splice:mousedown_link, group:clickedGroup}); - } - } - function linkTouchStart(d) { - if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); - return; - } - mousedown_link = d; - clearSelection(); - selectedLinks.clear(); - selectedLinks.add(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.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) { - var ag = activeGroup; - clearSelection(); - if (ag && g.g === ag.id) { - enterActiveGroup(ag); - activeGroup.selected = true; - } - } - if (activeGroup) { - if (!RED.group.contains(activeGroup,g)) { - // Clicked on a group that is outside the activeGroup - exitActiveGroup(); - } else { - } - } - selectGroup(g,true);//!wasSelected); - } else if (activeGroup && g.g !== activeGroup.id){ - 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) { - movingSet.add(g); - } - if (includeNodes) { - var currentSet = new Set(movingSet.nodes()); - var allNodes = RED.group.getNodes(g,true); - allNodes.forEach(function(n) { - if (!currentSet.has(n)) { - movingSet.add(n) - // n.selected = true; - } - n.dirty = true; - }) - } - } - function enterActiveGroup(group) { - if (activeGroup) { - exitActiveGroup(); - } - group.active = true; - group.dirty = true; - activeGroup = group; - movingSet.remove(group); - } - 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 = movingSet.length()-1; i >= 0; i -= 1) { - var msn = movingSet.get(i); - if (nodeSet.has(msn.n) || msn.n === g) { - msn.n.selected = false; - msn.n.dirty = true; - movingSet.remove(msn.n,i) - } - } - } - function getGroupAt(x,y) { - // x,y expected to be in node-co-ordinate space - 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:(movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}}); - options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}}); - options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}}); - options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}}); - options.push({name:"edit",disabled:(movingSet.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({ - position:[pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()], - touchTrigger: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 redrawStatus(d,nodeEl) { - if (d.z !== RED.workspaces.active()) { - return; - } - if (!nodeEl) { - nodeEl = document.getElementById(d.id); - } - if (nodeEl) { - // Do not show node status if: - // - global flag set - // - node has no status - // - node is disabled - if (!showStatus || !d.status || d.d === true) { - 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") - outGroup.each(function(d,i) { - var node = d3.select(this); - var nodeContents = document.createDocumentFragment(); - - d.h = 40; - d.resize = true; - d.dirty = true; - - var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); - mainRect.__data__ = d; - mainRect.setAttribute("class", "red-ui-flow-subflow-port"); - mainRect.setAttribute("rx", 8); - mainRect.setAttribute("ry", 8); - mainRect.setAttribute("width", 40); - mainRect.setAttribute("height", 40); - node[0][0].__mainRect__ = mainRect; - d3.select(mainRect) - .on("mouseup",nodeMouseUp) - .on("mousedown",nodeMouseDown) - .on("touchstart",nodeTouchStart) - .on("touchend",nodeTouchEnd) - nodeContents.appendChild(mainRect); - - var output_groupEl = document.createElementNS("http://www.w3.org/2000/svg","g"); - output_groupEl.setAttribute("x",0); - output_groupEl.setAttribute("y",0); - node[0][0].__outputLabelGroup__ = output_groupEl; - - var output_output = document.createElementNS("http://www.w3.org/2000/svg","text"); - output_output.setAttribute("class","red-ui-flow-port-label"); - output_output.style["font-size"] = "10px"; - output_output.textContent = "output"; - output_groupEl.appendChild(output_output); - node[0][0].__outputOutput__ = output_output; - - var output_number = document.createElementNS("http://www.w3.org/2000/svg","text"); - output_number.setAttribute("class","red-ui-flow-port-label red-ui-flow-port-index"); - output_number.setAttribute("x",0); - output_number.setAttribute("y",0); - output_number.textContent = d.i+1; - output_groupEl.appendChild(output_number); - node[0][0].__outputNumber__ = output_number; - - var output_border = document.createElementNS("http://www.w3.org/2000/svg","path"); - output_border.setAttribute("d","M 40 1 l 0 38") - output_border.setAttribute("class", "red-ui-flow-node-icon-shade-border") - output_groupEl.appendChild(output_border); - node[0][0].__outputBorder__ = output_border; - - nodeContents.appendChild(output_groupEl); - - var text = document.createElementNS("http://www.w3.org/2000/svg","g"); - text.setAttribute("class","red-ui-flow-port-label"); - text.setAttribute("transform","translate(38,0)"); - text.setAttribute('style', 'fill : #888'); // hard coded here! - node[0][0].__textGroup__ = text; - nodeContents.append(text); - - var portEl = document.createElementNS("http://www.w3.org/2000/svg","g"); - portEl.setAttribute('transform','translate(-5,15)') - - var port = document.createElementNS("http://www.w3.org/2000/svg","rect"); - port.setAttribute("class","red-ui-flow-port"); - port.setAttribute("rx",3); - port.setAttribute("ry",3); - port.setAttribute("width",10); - port.setAttribute("height",10); - portEl.appendChild(port); - port.__data__ = d; - - d3.select(port) - .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);}); - - node[0][0].__port__ = portEl - nodeContents.appendChild(portEl); - node[0][0].appendChild(nodeContents); - }); - - 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 port_height = 40; - - var self = this; - var thisNode = d3.select(this); - - dirtyNodes[d.id] = d; - - var label = getPortLabel(activeSubflow, PORT_TYPE_OUTPUT, d.i) || ""; - var hideLabel = (label.length < 1) - - var labelParts; - if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label) { - labelParts = getLabelParts(label, "red-ui-flow-node-label"); - if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) { - d.resize = true; - } - this.__label__ = label; - this.__labelLineCount__ = labelParts.lines.length; - - if (hideLabel) { - d.h = Math.max(port_height,(d.outputs || 0) * 15); - } else { - d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, port_height); - } - this.__hideLabel__ = hideLabel; - } - - if (d.resize) { - var ow = d.w; - if (hideLabel) { - d.w = port_height; - } else { - d.w = Math.max(port_height,20*(Math.ceil((labelParts.width+50+7)/20)) ); - } - if (ow !== undefined) { - d.x += (d.w-ow)/2; - } - d.resize = false; - } - - 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; i0?7:0))/20)) ); - } - if (ow !== undefined) { - d.x += (d.w-ow)/2; - } - d.resize = false; - } - if (d._colorChanged) { - var newColor = RED.utils.getNodeColor(d.type,d._def); - this.__mainRect__.setAttribute("fill",newColor); - if (this.__buttonGroupButton__) { - this.__buttonGroupButton__.settAttribute("fill",newColor); - } - delete d._colorChanged; - } - //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(); - RED.hooks.trigger("viewRemovePort",{ - node:d, - el:this, - port:port, - portType: "output", - portIndex: this.__outputs__.length - }) - 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); - portGroup.__port__ = 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); - RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex}) - } 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); - if(RED.runtime && Object.hasOwn(RED.runtime,'started')) { - this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started); - } - - 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; - } - } - } - } - - RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) - }); - - if (nodesReordered) { - node.sort(function(a,b) { - return a._index - b._index; - }) - } - - var junction = junctionLayer.selectAll(".red-ui-flow-junction").data( - activeJunctions, - d => d.id - ) - var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction") - junctionEnter.each(function(d,i) { - var junction = d3.select(this); - var contents = document.createDocumentFragment(); - // d.added = true; - var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect"); - junctionBack.setAttribute("class","red-ui-flow-junction-background"); - junctionBack.setAttribute("x",-5); - junctionBack.setAttribute("y",-5); - junctionBack.setAttribute("width",10); - junctionBack.setAttribute("height",10); - junctionBack.setAttribute("rx",3); - junctionBack.setAttribute("ry",3); - junctionBack.__data__ = d; - this.__junctionBack__ = junctionBack; - contents.appendChild(junctionBack); - - var junctionInput = document.createElementNS("http://www.w3.org/2000/svg","rect"); - junctionInput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-input"); - junctionInput.setAttribute("x",-5); - junctionInput.setAttribute("y",-5); - junctionInput.setAttribute("width",10); - junctionInput.setAttribute("height",10); - junctionInput.setAttribute("rx",3); - junctionInput.setAttribute("ry",3); - junctionInput.__data__ = d; - junctionInput.__portType__ = PORT_TYPE_INPUT; - junctionInput.__portIndex__ = 0; - this.__junctionInput__ = junctionOutput; - contents.appendChild(junctionInput); - junctionInput.addEventListener("mouseup", portMouseUpProxy); - junctionInput.addEventListener("mousedown", portMouseDownProxy); - - - this.__junctionInput__ = junctionInput; - contents.appendChild(junctionInput); - var junctionOutput = document.createElementNS("http://www.w3.org/2000/svg","rect"); - junctionOutput.setAttribute("class","red-ui-flow-junction-port red-ui-flow-junction-port-output"); - junctionOutput.setAttribute("x",-5); - junctionOutput.setAttribute("y",-5); - junctionOutput.setAttribute("width",10); - junctionOutput.setAttribute("height",10); - junctionOutput.setAttribute("rx",3); - junctionOutput.setAttribute("ry",3); - junctionOutput.__data__ = d; - junctionOutput.__portType__ = PORT_TYPE_OUTPUT; - junctionOutput.__portIndex__ = 0; - this.__junctionOutput__ = junctionOutput; - contents.appendChild(junctionOutput); - junctionOutput.addEventListener("mouseup", portMouseUpProxy); - junctionOutput.addEventListener("mousedown", portMouseDownProxy); - - junctionOutput.addEventListener("mouseover", junctionMouseOverProxy); - junctionOutput.addEventListener("mouseout", junctionMouseOutProxy); - junctionInput.addEventListener("mouseover", junctionMouseOverProxy); - junctionInput.addEventListener("mouseout", junctionMouseOutProxy); - junctionBack.addEventListener("mouseover", junctionMouseOverProxy); - junctionBack.addEventListener("mouseout", junctionMouseOutProxy); - - // These handlers expect to be registered as d3 events - d3.select(junctionBack).on("mousedown", nodeMouseDown).on("mouseup", nodeMouseUp); - - junction[0][0].appendChild(contents); - }) - junction.exit().remove(); - junction.each(function(d) { - var junction = d3.select(this); - this.setAttribute("transform", "translate(" + (d.x) + "," + (d.y) + ")"); - if (d.dirty) { - junction.classed("red-ui-flow-junction-dragging", mouse_mode === RED.state.MOVING_ACTIVE && movingSet.has(d)) - junction.classed("selected", !!d.selected) - dirtyNodes[d.id] = d; - - 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) - .on("mousemove", function(d) { - if (mouse_mode === RED.state.SLICING) { - - selectedLinks.add(d) - l.classed("red-ui-flow-link-splice",true) - redraw() - } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) { - if (!l.classed("red-ui-flow-link-splice")) { - // Find intersection point - var lineLength = pathLine.getTotalLength(); - var pos; - var delta = Infinity; - for (var i = 0; i < lineLength; i++) { - var linePos = pathLine.getPointAtLength(i); - var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) - var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) - var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY - if (posDelta < delta) { - pos = linePos - delta = posDelta - } - } - d._sliceLocation = pos - selectedLinks.add(d) - l.classed("red-ui-flow-link-splice",true) - redraw() - } - } - }) - - 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 || 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||0); - d.y1 = d.source.y+y; - d.x2 = d.target.x-(d.target.w/2||0); - 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); - - 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; - movingSet.add(n); - if (targets.length === 1) { - RED.view.reveal(n.id); - } - }); - 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._index - b._index; - } - }) - } - group[0].reverse(); - var groupOpCount=0; - group.each(function(d,i) { - groupOpCount++ - if (d.resize) { - d.minWidth = 0; - delete d.resize; - } - if (d.dirty || dirtyGroups[d.id]) { - var g = d3.select(this); - var recalculateLabelOffsets = false; - if (d.nodes.length > 0) { - // If the group was just moved, all of its contents was - // also moved - so no need to recalculate its bounding box - if (!d.groupMoved) { - var minX = Number.POSITIVE_INFINITY; - var minY = Number.POSITIVE_INFINITY; - var maxX = 0; - var maxY = 0; - var margin = 26; - d.nodes.forEach(function(n) { - groupOpCount++ - 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; - recalculateLabelOffsets = true; - // if set explicitly to false, this group has just been - // imported so needed this initial resize calculation. - // Now that's done, delete the flag so the normal - // logic kicks in. - if (d.groupMoved === false) { - delete d.groupMoved; - } - } else { - delete d.groupMoved; - } - } else { - d.w = 40; - d.h = 40; - recalculateLabelOffsets = true; - } - if (recalculateLabelOffsets) { - 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.active || 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.active || 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" - * @param {String/Array} newNodesObj nodes to import - * @param {Object} options options object - * - * Options: - * - addFlow - whether to import nodes to a new tab - * - touchImport - whether this is a touch import. If not, imported nodes are - * attachedto mouse for placing - "IMPORT_DRAGGING" state - * - generateIds - whether to automatically generate new ids for all imported nodes - * - generateDefaultNames - whether to automatically update any nodes with clashing - * default names - */ - function importNodes(newNodesObj,options) { - options = options || { - addFlow: false, - touchImport: false, - generateIds: false, - generateDefaultNames: false - } - var addNewFlow = options.addFlow - var touchImport = options.touchImport; - - if (mouse_mode === RED.state.SELECTING_NODE) { - return; - } - - var nodesToImport; - if (typeof newNodesObj === "string") { - if (newNodesObj === "") { - return; - } - try { - nodesToImport = JSON.parse(newNodesObj); - } catch(err) { - var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); - e.code = "NODE_RED"; - throw e; - } - } else { - nodesToImport = newNodesObj; - } - - if (!$.isArray(nodesToImport)) { - nodesToImport = [nodesToImport]; - } - if (options.generateDefaultNames) { - RED.actions.invoke("core:generate-node-names", nodesToImport, { - renameBlank: false, - renameClash: true, - generateHistory: false - }) - } - - try { - var activeSubflowChanged; - if (activeSubflow) { - activeSubflowChanged = activeSubflow.changed; - } - var result = RED.nodes.import(nodesToImport,{generateIds:options.generateIds, addFlow: addNewFlow, importMap: options.importMap}); - if (result) { - var new_nodes = result.nodes; - var new_links = result.links; - var new_groups = result.groups; - var new_junctions = result.junctions; - var new_workspaces = result.workspaces; - var new_subflows = result.subflows; - var removedNodes = result.removedNodes; - var new_default_workspace = result.missingWorkspace; - 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() }); - new_ms = new_ms.concat(new_groups.filter(function(g) { return g.z === RED.workspaces.active()})) - new_ms = new_ms.concat(new_junctions.filter(function(j) { return j.z === RED.workspaces.active()})) - var new_node_ids = new_nodes.map(function(n){ n.changed = true; return n.id; }); - - clearSelection(); - movingSet.clear(); - movingSet.add(new_ms); - - - // TODO: pick a more sensible root node - if (movingSet.length() > 0) { - if (mouse_position == null) { - mouse_position = [0,0]; - } - - var dx = mouse_position[0]; - var dy = mouse_position[1]; - if (movingSet.length() > 0) { - var root_node = movingSet.get(0).n; - dx = root_node.x; - dy = root_node.y; - } - - var minX = 0; - var minY = 0; - var i; - var node,group; - var l =movingSet.length(); - 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)) - - - } - } - - } - - var historyEvent = { - t:"add", - nodes:new_node_ids, - links:new_links, - groups:new_groups, - junctions: new_junctions, - workspaces:new_workspaces, - subflows:new_subflows, - dirty:RED.nodes.dirty() - }; - if (movingSet.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 - } - } - } - if (removedNodes) { - var replaceEvent = { - t: "replace", - config: removedNodes - } - historyEvent = { - t:"multi", - events: [ - replaceEvent, - historyEvent - ] - } - } - - 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; - var newJunctionCount = new_junctions.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:newConfigNodeCount})); - } - if (new_subflows.length > 0) { - counts.push(RED._("clipboard.subflow",{count:new_subflows.length})); - } - if (removedNodes && removedNodes.length > 0) { - counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length})); - } - if (counts.length > 0) { - var countList = "
  • "+counts.join("
  • ")+"
"; - RED.notify("

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

"+countList,{id:"clipboard"}); - } - - } - } catch(error) { - if (error.code === "import_conflict") { - // Pass this up for the called to resolve - throw error; - } else 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 (movingSet.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 (movingSet.length() > 0) { - movingSet.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 (selectedLinks.length() > 0) { - selection.links = selectedLinks.toArray(); - selection.link = selection.links[0]; - } - return selection; - } - - /** - * Create a node from a type string. - * **NOTE:** Can throw on error - use `try` `catch` block when calling - * @param {string} type The node type to create - * @param {number} [x] (optional) The horizontal position on the workspace - * @param {number} [y] (optional)The vertical on the workspace - * @param {string} [z] (optional) The flow tab this node will belong to. Defaults to active workspace. - * @returns An object containing the `node` and a `historyEvent` - * @private - */ - function createNode(type, x, y, z) { - var m = /^subflow:(.+)$/.exec(type); - var activeSubflow = z ? RED.nodes.subflow(z) : null; - if (activeSubflow && m) { - var subflowId = m[1]; - if (subflowId === activeSubflow.id) { - throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) - } - if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { - throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) - } - } - - var nn = { id: RED.nodes.id(), z: 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 = RED.view.node_width; - nn.h = Math.max(RED.view.node_height, (nn.outputs || 0) * 15); - nn.resize = true; - if (x != null && typeof x == "number" && x >= 0) { - nn.x = x; - } - if (y != null && typeof y == "number" && y >= 0) { - nn.y = y; - } - 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 calculateNodeDimensions(node) { - var result = [node_width,node_height]; - try { - var isLink = (node.type === "link in" || node.type === "link out") - var hideLabel = node.hasOwnProperty('l')?!node.l : isLink; - var label = RED.utils.getNodeLabel(node, node.type); - var labelParts = getLabelParts(label, "red-ui-flow-node-label"); - if (hideLabel) { - result[1] = Math.max(node_height,(node.outputs || 0) * 15); - } else { - result[1] = Math.max(6+24*labelParts.lines.length,(node.outputs || 0) * 15, 30); - } - if (hideLabel) { - result[0] = node_height; - } else { - result[0] = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(node._def.inputs>0?7:0))/20)) ); - } - }catch(err) { - console.log("Error",node); - } - return result; - } - - - function flashNode(n) { - let node = n; - if(typeof node === "string") { node = RED.nodes.node(n); } - if(!node) { return; } - - const flashingNode = flashingNodeId && RED.nodes.node(flashingNodeId); - if(flashingNode) { - //cancel current flashing node before flashing new node - clearInterval(flashingNode.__flashTimer); - delete flashingNode.__flashTimer; - flashingNode.dirty = true; - flashingNode.highlighted = false; - } - node.__flashTimer = setInterval(function(flashEndTime, n) { - n.dirty = true; - if (flashEndTime >= Date.now()) { - n.highlighted = !n.highlighted; - } else { - clearInterval(n.__flashTimer); - delete n.__flashTimer; - flashingNodeId = null; - n.highlighted = false; - } - RED.view.redraw(); - }, 100, Date.now() + 2200, node) - flashingNodeId = node.id; - node.highlighted = true; - RED.view.redraw(); - } - 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; - movingSet.clear(); - movingSet.add(selectedNode); - } - } else if (selection) { - if (selection.nodes) { - updateActiveNodes(); - movingSet.clear(); - // 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; - movingSet.add(n); - } else { - selectGroup(n,true); - } - }) - } - } - } - updateSelection(); - redraw(true); - }, - selection: getSelection, - clearSelection: clearSelection, - createNode: createNode, - /** default node width */ - get node_width() { - return node_width; - }, - /** default node height */ - get node_height() { - return node_height; - }, - /** snap to grid option state */ - get snapGrid() { - return snapGrid; - }, - /** gets the current scale factor */ - scale: function() { - return scaleFactor; - }, - getLinksAtPoint: function(x,y) { - // x,y must be in SVG co-ordinate space - // if they come from a node.x/y, they will need to be scaled using - // scaleFactor first. - 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, null, null, true); - } 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[0].clientWidth/scaleFactor,chart[0].clientHeight/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) { - flashNode(node); - } - } 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; - }, - getSubflowPorts: function() { - var result = []; - if (activeSubflow) { - var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;}); - subflowOutputs.each(function(d,i) { result.push(d) }) - var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;}); - subflowInputs.each(function(d,i) { result.push(d) }) - var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;}); - subflowStatus.each(function(d,i) { result.push(d) }) - } - return result; - }, - 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; - movingSet.add(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 = movingSet.nodes() - 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, - showQuickAddDialog:showQuickAddDialog, - calculateNodeDimensions: calculateNodeDimensions, - getElementPosition:getElementPosition, - showTooltip:showTooltip - }; -})(); From f33848e16b6fa0ebaf0432e1dbd7fa3d1cded965 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Jun 2022 10:27:44 +0100 Subject: [PATCH 08/12] Rework start/stop api to use runtime-event notification message --- .../@node-red/editor-api/lib/admin/flows.js | 2 +- .../editor-client/locales/en-US/editor.json | 10 +- .../@node-red/editor-client/src/js/nodes.js | 2 +- .../@node-red/editor-client/src/js/red.js | 7 +- .../@node-red/editor-client/src/js/runtime.js | 67 +++++-------- .../editor-client/src/js/ui/deploy.js | 23 ++--- .../@node-red/editor-client/src/js/ui/view.js | 2 +- .../@node-red/runtime/lib/api/flows.js | 31 +++--- .../@node-red/runtime/lib/flows/index.js | 96 ++++++++++--------- .../@node-red/runtime/lib/index.js | 6 +- .../runtime/locales/en-US/runtime.json | 1 + 11 files changed, 107 insertions(+), 140 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js index 2ad233f8f..4d8679aac 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js @@ -83,7 +83,7 @@ module.exports = { postState: function(req,res) { const opts = { user: req.user, - requestedState: req.body.state||"", + state: req.body.state || "", req: apiUtils.getRequestLogObject(req) } runtimeAPI.flows.setState(opts).then(function(result) { diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index b25885e21..ab2fba982 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -169,6 +169,10 @@ } }, "notification": { + "state": { + "flowsStopped": "Flows stopped", + "flowsStarted": "Flows started" + }, "warning": "Warning: __message__", "warnings": { "undeployedChanges": "node has undeployed changes", @@ -291,12 +295,6 @@ "stopstart":{ "status": { "state_changed": "Flows runtime has been changed to '__state__' state" - }, - "errors": { - "notAllowed": "Method not allowed", - "notAuthorized": "Not authorized", - "notFound": "Not found", - "noResponse": "No response from server" } }, "deploy": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 052fad558..b390dab53 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -14,7 +14,7 @@ * limitations under the License. **/ -/** +/** * An Interface to nodes and utility functions for creating/adding/deleting nodes and links * @namespace RED.nodes */ diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 939c32f2e..55446418b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -336,7 +336,6 @@ var RED = (function() { id: notificationId } if (notificationId === "runtime-state") { - RED.events.emit("runtime-state",msg); if (msg.error === "safe-mode") { options.buttons = [ { @@ -477,9 +476,9 @@ var RED = (function() { } else if (persistentNotifications.hasOwnProperty(notificationId)) { persistentNotifications[notificationId].close(); delete persistentNotifications[notificationId]; - if (notificationId === 'runtime-state') { - RED.events.emit("runtime-state",msg); - } + } + if (notificationId === 'runtime-state') { + RED.events.emit("runtime-state",msg); } }); RED.comms.subscribe("status/#",function(topic,msg) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/runtime.js b/packages/node_modules/@node-red/editor-client/src/js/runtime.js index 49960e382..eac985467 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/runtime.js +++ b/packages/node_modules/@node-red/editor-client/src/js/runtime.js @@ -1,53 +1,36 @@ RED.runtime = (function() { let state = "" - let settings = {ui: false, enabled: false}; - const STOPPED = "stopped" - const STARTED = "started" + let settings = { ui: false, enabled: false }; + const STOPPED = "stop" + const STARTED = "start" + const SAFE = "safe" + return { init: function() { // refresh the current runtime status from server settings = Object.assign({}, settings, RED.settings.runtimeState); - RED.runtime.requestState() - - // {id:"flows-run-state", started: false, state: "stopped", retain:true} - RED.comms.subscribe("notification/flows-run-state",function(topic,msg) { - RED.events.emit("flows-run-state",msg); - RED.runtime.updateState(msg.state); - }); - }, - get state() { - return state - }, - get started() { - return state === STARTED - }, - get states() { - return { STOPPED, STARTED } - }, - updateState: function(newState) { - state = newState; - // disable pointer events on node buttons (e.g. inject/debug nodes) - $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state === STOPPED) - // show/hide Start/Stop based on current state - if(settings.enabled === true && settings.ui === true) { - RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) - RED.menu.setVisible("deploymenu-item-runtime-start", state === STOPPED) - } - }, - requestState: function(callback) { - $.ajax({ - headers: { - "Accept":"application/json" - }, - cache: false, - url: 'flows/state', - success: function(data) { - RED.runtime.updateState(data.state) - if(callback) { - callback(data.state) + RED.events.on("runtime-state", function(msg) { + if (msg.state) { + const currentState = state + state = msg.state + $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state !== STARTED) + if(settings.enabled === true && settings.ui === true) { + RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) + RED.menu.setVisible("deploymenu-item-runtime-start", state !== STARTED) + } + // Do not notify the user about this event if: + // - This is the very first event we've received after loading the editor (currentState = '') + // - The state matches what we already thought was the case (state === currentState) + // - The event was triggered by a deploy (msg.deploy === true) + // - The event is a safe mode event - that gets notified separately + if (currentState !== '' && state !== currentState && !msg.deploy && state !== SAFE) { + RED.notify(RED._("notification.state.flows"+(state === STOPPED?'Stopped':'Started'), msg), "success") } } }); + }, + get started() { + return state === STARTED } } -})() \ No newline at end of file +})() diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 46408cb6c..809202c99 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -69,7 +69,7 @@ RED.deploy = (function() { {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}}, null ] - if(RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { + if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:"Start"/*RED._("deploy.startFlows")*/,sublabel:"Start Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:start-flows", visible:false}) mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:"Stop"/*RED._("deploy.startFlows")*/,sublabel:"Stop Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:stop-flows", visible:false}) } @@ -302,7 +302,6 @@ RED.deploy = (function() { deployInflight = true deployButtonSetBusy() shadeShow() - RED.runtime.updateState(state) $.ajax({ url:"flows/state", type: "POST", @@ -311,30 +310,23 @@ RED.deploy = (function() { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled") } - RED.runtime.updateState((data && data.state) || "unknown") - RED.notify(RED._("stopstart.status.state_changed", data), "success") }).fail(function(xhr,textStatus,err) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled") } if (xhr.status === 401) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAuthorized") }), "error") - } else if (xhr.status === 404) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notFound") }), "error") - } else if (xhr.status === 405) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAllowed") }), "error") + RED.notify(RED._("notification.error", { message: RED._("user.notAuthorized") }), "error") } else if (xhr.responseText) { const errorDetail = { message: err ? (err + "") : "" } try { errorDetail.message = JSON.parse(xhr.responseText).message - } finally { + } finally { errorDetail.message = errorDetail.message || xhr.responseText } RED.notify(RED._("notification.error", errorDetail), "error") } else { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.noResponse") }), "error") + RED.notify(RED._("notification.error", { message: RED._("deploy.errors.noResponse") }), "error") } - RED.runtime.requestState() }).always(function() { const delta = Math.max(0, 300 - (Date.now() - startTime)) setTimeout(function () { @@ -514,13 +506,10 @@ RED.deploy = (function() { deployButtonSetBusy(); const data = { flows: nns }; - data.runtimeState = RED.runtime.state; - if (data.runtimeState === RED.runtime.states.STOPPED || force) { - data._rev = RED.nodes.version(); - } else { + if (!force) { data.rev = RED.nodes.version(); } - + deployInflight = true; shadeShow(); $.ajax({ diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index eca5c01ef..02b8df5d8 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4877,7 +4877,7 @@ RED.view = (function() { if (d._def.button) { var buttonEnabled = isButtonEnabled(d); this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled); - if(RED.runtime && Object.hasOwn(RED.runtime,'started')) { + if (RED.runtime && Object.hasOwn(RED.runtime,'started')) { this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started); } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 2e71cbdca..b91635201 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -73,10 +73,6 @@ var api = module.exports = { if (deploymentType === 'reload') { apiPromise = runtime.flows.loadFlows(true); } else { - //ensure the runtime running/stopped state matches the deploying editor. If not, then copy the _rev number to flows.rev - if(flows.hasOwnProperty('_rev') && !flows.hasOwnProperty('rev') && (flows.runtimeState !== "stopped" || runtime.flows.started)) { - flows.rev = flows._rev - } if (flows.hasOwnProperty('rev')) { var currentVersion = runtime.flows.getFlows().rev; if (currentVersion !== flows.rev) { @@ -271,9 +267,7 @@ var api = module.exports = { getState: async function(opts) { runtime.log.audit({event: "flows.getState"}, opts.req); const result = { - state: runtime.flows.started ? "started" : "stopped", - started: !!runtime.flows.started, - rev: runtime.flows.getFlows().rev + state: runtime.flows.state() } return result; }, @@ -282,7 +276,7 @@ var api = module.exports = { * @param {Object} opts * @param {Object} opts.req - the request to log (optional) * @param {User} opts.user - the user calling the api - * @param {string} opts.requestedState - the requested state. Valid values are "start" and "stop". + * @param {string} opts.state - the requested state. Valid values are "start" and "stop". * @return {Promise} - the active flow configuration * @memberof @node-red/runtime_flows */ @@ -295,7 +289,7 @@ var api = module.exports = { err.code = err.code || errcode || "unexpected_error" runtime.log.audit({ event: "flows.setState", - state: opts.requestedState || "", + state: opts.state || "", error: errcode || "unexpected_error", message: err.code }, opts.req); @@ -304,21 +298,22 @@ var api = module.exports = { const getState = () => { return { - state: runtime.flows.started ? "started" : "stopped", - started: !!runtime.flows.started, - rev: runtime.flows.getFlows().rev, + state: runtime.flows.state() } } if(!runtime.settings.runtimeState || runtime.settings.runtimeState.enabled !== true) { throw (makeError("Method Not Allowed", "not_allowed", 405)) } - switch (opts.requestedState) { + switch (opts.state) { case "start": try { try { - runtime.settings.set('flowsRunStateRequested', opts.requestedState); - } catch(err) { } + runtime.settings.set('runtimeFlowState', opts.state); + } catch(err) {} + if (runtime.settings.safeMode) { + delete runtime.settings.safeMode + } await runtime.flows.startFlows("full") return getState() } catch (err) { @@ -327,15 +322,15 @@ var api = module.exports = { case "stop": try { try { - runtime.settings.set('flowsRunStateRequested', opts.requestedState); - } catch(err) { } + runtime.settings.set('runtimeFlowState', opts.state); + } catch(err) {} await runtime.flows.stopFlows("full") return getState() } catch (err) { throw (makeError(err, err.code, 500)) } default: - throw (makeError(`Cannot change flows runtime state to '${opts.requestedState}'}`, "invalid_run_state", 400)) + throw (makeError(`Cannot change flows runtime state to '${opts.state}'}`, "invalid_run_state", 400)) } }, } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index b707b4aaf..b2de8ea73 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -36,6 +36,8 @@ var activeFlowConfig = null; var activeFlows = {}; var started = false; +var state = 'stop' + var credentialsPendingReset = false; var activeNodesToFlow = {}; @@ -50,6 +52,7 @@ function init(runtime) { storage = runtime.storage; log = runtime.log; started = false; + state = 'stop'; if (!typeEventRegistered) { events.on('type-registered',function(type) { if (activeFlowConfig && activeFlowConfig.missingTypes.length > 0) { @@ -214,19 +217,26 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) { // Flows are running (or should be) // Stop the active flows (according to deploy type and the diff) - return stop(type,diff,muteLog).then(() => { + return stop(type,diff,muteLog,true).then(() => { // Once stopped, allow context to remove anything no longer needed return context.clean(activeFlowConfig) }).then(() => { + if (!isLoad) { + log.info(log._("nodes.flows.updated-flows")); + } // Start the active flows - start(type,diff,muteLog).then(() => { + start(type,diff,muteLog,true).then(() => { events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); }); // Return the new revision asynchronously to the actual start return flowRevision; }).catch(function(err) { }) } else { + if (!isLoad) { + log.info(log._("nodes.flows.updated-flows")); + } events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); + return flowRevision; } }); } @@ -259,10 +269,10 @@ function getFlows() { return activeConfig; } -async function start(type,diff,muteLog) { - type = type||"full"; - let reallyStarted = started +async function start(type,diff,muteLog,isDeploy) { + type = type || "full"; started = true; + state = 'start' var i; // If there are missing types, report them, emit the necessary runtime event and return if (activeFlowConfig.missingTypes.length > 0) { @@ -284,7 +294,7 @@ async function start(type,diff,muteLog) { log.info(log._("nodes.flows.missing-type-install-2")); log.info(" "+settings.userDir); } - events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); return; } @@ -298,7 +308,7 @@ async function start(type,diff,muteLog) { missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()}) log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`); } - events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true}); + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true}); return; } @@ -307,10 +317,20 @@ async function start(type,diff,muteLog) { log.info("*****************************************************************") log.info(log._("nodes.flows.safe-mode")); log.info("*****************************************************************") - events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true}); + state = 'safe' + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'safe', error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true}); return; } + const runtimeState = settings.get('runtimeFlowState') || 'start' + if (runtimeState === 'stop') { + log.info(log._("nodes.flows.stopped-flows")); + events.emit("runtime-event",{id:"runtime-state",payload:{ state: 'stop', deploy:isDeploy },retain:true}); + state = 'stop' + started = false + return + } + if (!muteLog) { if (type !== "full") { log.info(log._("nodes.flows.starting-modified-"+type)); @@ -365,51 +385,31 @@ async function start(type,diff,muteLog) { } } } - // Having created or updated all flows, now start them. - let startFlows = true - try { - startFlows = settings.get('flowsRunStateRequested'); - } catch(err) { - } - startFlows = (startFlows !== "stop"); - - if (startFlows) { - for (id in activeFlows) { - if (activeFlows.hasOwnProperty(id)) { - try { - activeFlows[id].start(diff); - // Create a map of node id to flow id and also a subflowInstance lookup map - var activeNodes = activeFlows[id].getActiveNodes(); - Object.keys(activeNodes).forEach(function(nid) { - activeNodesToFlow[nid] = id; - }); - } catch(err) { - console.log(err.stack); - } + for (id in activeFlows) { + if (activeFlows.hasOwnProperty(id)) { + try { + activeFlows[id].start(diff); + // Create a map of node id to flow id and also a subflowInstance lookup map + var activeNodes = activeFlows[id].getActiveNodes(); + Object.keys(activeNodes).forEach(function(nid) { + activeNodesToFlow[nid] = id; + }); + } catch(err) { + console.log(err.stack); } } - reallyStarted = true; - events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); - // Deprecated event - events.emit("nodes-started"); - } else { - started = false; } - - const state = { - started: reallyStarted, - state: reallyStarted ? "started" : "stopped", - } - events.emit("runtime-event",{id:"flows-run-state", payload: state, retain:true}); - + events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); + // Deprecated event + events.emit("nodes-started"); if (credentialsPendingReset === true) { credentialsPendingReset = false; } else { - events.emit("runtime-event",{id:"runtime-state",retain:true}); + events.emit("runtime-event",{id:"runtime-state", payload:{ state: 'start', deploy:isDeploy}, retain:true}); } - if (!muteLog && reallyStarted) { + if (!muteLog) { if (type !== "full") { log.info(log._("nodes.flows.started-modified-"+type)); } else { @@ -419,7 +419,7 @@ async function start(type,diff,muteLog) { return; } -function stop(type,diff,muteLog) { +function stop(type,diff,muteLog,isDeploy) { if (!started) { return Promise.resolve(); } @@ -439,6 +439,7 @@ function stop(type,diff,muteLog) { } } started = false; + state = 'stop' var promises = []; var stopList; var removedList = diff.removed; @@ -490,7 +491,8 @@ function stop(type,diff,muteLog) { } } events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff}); - events.emit("runtime-event",{id:"flows-run-state", payload: {started: false, state: "stopped"}, retain:true}); + + events.emit("runtime-event",{ id:"runtime-state", payload:{ state: 'stop', deploy:isDeploy }, retain:true }); // Deprecated event events.emit("nodes-stopped"); }); @@ -810,7 +812,7 @@ module.exports = { stopFlows: stop, get started() { return started }, - + state: () => { return state }, // handleError: handleError, // handleStatus: handleStatus, diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 8e1d2b487..88b3b8293 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -215,7 +215,7 @@ function start() { } } return redNodes.loadContextsPlugin().then(function () { - redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {}); + redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {}); started = true; }); }); @@ -399,12 +399,12 @@ module.exports = { * @memberof @node-red/runtime */ version: externalAPI.version, - + /** * @memberof @node-red/diagnostics */ diagnostics:externalAPI.diagnostics, - + storage: storage, events: events, hooks: hooks, diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index 3b46d0c9f..ecd010abb 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -122,6 +122,7 @@ "stopped-flows": "Stopped flows", "stopped": "Stopped", "stopping-error": "Error stopping node: __message__", + "updated-flows": "Updated flows", "added-flow": "Adding flow: __label__", "updated-flow": "Updated flow: __label__", "removed-flow": "Removed flow: __label__", From 7580f7491a02aba9c9f60076fdb68a811af93dd5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Jun 2022 10:45:06 +0100 Subject: [PATCH 09/12] Update flow state tests to match changes in api --- .../@node-red/runtime/lib/api/flows_spec.js | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/unit/@node-red/runtime/lib/api/flows_spec.js b/test/unit/@node-red/runtime/lib/api/flows_spec.js index 0f560e3f8..deed6a9b6 100644 --- a/test/unit/@node-red/runtime/lib/api/flows_spec.js +++ b/test/unit/@node-red/runtime/lib/api/flows_spec.js @@ -431,7 +431,7 @@ describe("runtime-api/flows", function() { var startFlows, stopFlows, runtime; beforeEach(function() { let flowsStarted = true; - let flowsState = "started"; + let flowsState = "start"; startFlows = sinon.spy(function(type) { if (type !== "full") { var err = new Error(); @@ -442,7 +442,7 @@ describe("runtime-api/flows", function() { return p; } flowsStarted = true; - flowsState = "started"; + flowsState = "start"; return Promise.resolve(); }); stopFlows = sinon.spy(function(type) { @@ -455,7 +455,7 @@ describe("runtime-api/flows", function() { return p; } flowsStarted = false; - flowsState = "stopped"; + flowsState = "stop"; return Promise.resolve(); }); runtime = { @@ -473,6 +473,7 @@ describe("runtime-api/flows", function() { startFlows, stopFlows, getFlows: function() { return {rev:"currentRev",flows:[]} }, + state: function() { return flowsState} } } }) @@ -480,29 +481,25 @@ describe("runtime-api/flows", function() { it("gets flows run state", async function() { flows.init(runtime); const state = await flows.getState({}) - state.should.have.property("started", true) - state.should.have.property("state", "started") + state.should.have.property("state", "start") }); it("permits getting flows run state when setting disabled", async function() { runtime.settings.runtimeState.enabled = false; flows.init(runtime); const state = await flows.getState({}) - state.should.have.property("started", true) - state.should.have.property("state", "started") + state.should.have.property("state", "start") }); it("start flows", async function() { flows.init(runtime); - const state = await flows.setState({requestedState:"start"}) - state.should.have.property("started", true) - state.should.have.property("state", "started") + const state = await flows.setState({state:"start"}) + state.should.have.property("state", "start") stopFlows.called.should.not.be.true(); startFlows.called.should.be.true(); }); it("stop flows", async function() { flows.init(runtime); - const state = await flows.setState({requestedState:"stop"}) - state.should.have.property("started", false) - state.should.have.property("state", "stopped") + const state = await flows.setState({state:"stop"}) + state.should.have.property("state", "stop") stopFlows.called.should.be.true(); startFlows.called.should.not.be.true(); }); @@ -511,7 +508,7 @@ describe("runtime-api/flows", function() { runtime.settings.runtimeState.enabled = false; flows.init(runtime); try { - await flows.setState({requestedState:"start"}) + await flows.setState({state:"start"}) } catch (error) { err = error } @@ -525,7 +522,7 @@ describe("runtime-api/flows", function() { runtime.settings.runtimeState.enabled = false; flows.init(runtime); try { - await flows.setState({requestedState:"stop"}) + await flows.setState({state:"stop"}) } catch (error) { err = error } @@ -538,7 +535,7 @@ describe("runtime-api/flows", function() { let err; flows.init(runtime); try { - await flows.setState({requestedState:"bad-state"}) + await flows.setState({state:"bad-state"}) } catch (error) { err = error } From b59a3b15f361283d97ddd14bba27fac3841aece1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Jun 2022 11:41:19 +0100 Subject: [PATCH 10/12] Fix flow unit tests --- .../@node-red/runtime/lib/flows/index.js | 5 +- .../@node-red/runtime/lib/flows/index_spec.js | 66 ++----------------- 2 files changed, 11 insertions(+), 60 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index b2de8ea73..1b5476a3f 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -322,7 +322,10 @@ async function start(type,diff,muteLog,isDeploy) { return; } - const runtimeState = settings.get('runtimeFlowState') || 'start' + let runtimeState + try { + runtimeState = settings.get('runtimeFlowState') || 'start' + } catch (err) {} if (runtimeState === 'stop') { log.info(log._("nodes.flows.stopped-flows")); events.emit("runtime-event",{id:"runtime-state",payload:{ state: 'stop', deploy:isDeploy },retain:true}); diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index 5e8f8a46e..1a0f2a73c 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -321,59 +321,6 @@ describe('flows/index', function() { return flows.startFlows(); }); }); - it('emits runtime-event "flows-run-state" "started"', async function () { - var originalConfig = [ - { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, - { id: "t1", type: "tab" } - ]; - storage.getFlows = function () { - return Promise.resolve({ flows: originalConfig }); - } - let receivedEvent = null; - const handleEvent = (data) => { - if(data && data.id === 'flows-run-state') { - receivedEvent = data; - } - } - events.on('runtime-event', handleEvent); - flows.init({ log: mockLog, settings: {}, storage: storage }); - await flows.load() - await flows.startFlows() - events.removeListener("runtime-event", handleEvent); - - //{id:"flows-run-state", payload: {started: true, state: "started"} - should(receivedEvent).not.be.null() - receivedEvent.should.have.property("id", "flows-run-state") - receivedEvent.should.have.property("payload", { started: true, state: "started" }) - receivedEvent.should.have.property("retain", true) - }); - it('emits runtime-event "flows-run-state" "stopped"', async function () { - const originalConfig = [ - { id: "t1-1", x: 10, y: 10, z: "t1", type: "test", wires: [] }, - { id: "t1", type: "tab" } - ]; - storage.getFlows = function () { - return Promise.resolve({ flows: originalConfig }); - } - let receivedEvent = null; - const handleEvent = (data) => { - if(data && data.id === 'flows-run-state') { - receivedEvent = data; - } - } - events.on('runtime-event', handleEvent); - flows.init({ log: mockLog, settings: {}, storage: storage }); - await flows.load() - await flows.startFlows() - await flows.stopFlows() - events.removeListener("runtime-event", handleEvent); - - //{id:"flows-run-state", payload: {started: true, state: "started"} - should(receivedEvent).not.be.null() - receivedEvent.should.have.property("id", "flows-run-state") - receivedEvent.should.have.property("payload", { started: false, state: "stopped" }) - receivedEvent.should.have.property("retain", true) - }); it('does not start if nodes missing', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, @@ -449,12 +396,13 @@ describe('flows/index', function() { try { flowCreate.called.should.be.false(); receivedEvent.should.have.property('id','runtime-state'); - receivedEvent.should.have.property('payload', - { error: 'missing-modules', - type: 'warning', - text: 'notification.warnings.missing-modules', - modules: [] } - ); + receivedEvent.should.have.property('payload', { + state: 'stop', + error: 'missing-modules', + type: 'warning', + text: 'notification.warnings.missing-modules', + modules: [] + }); done(); }catch(err) { From b7bdcc4e57d8f8eca619a63d527578768dcb1b03 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:37:53 +0100 Subject: [PATCH 11/12] Update packages/node_modules/@node-red/editor-client/locales/en-US/editor.json Remove unused i18n entries (these are safe to remove - were added in the original PR) --- .../@node-red/editor-client/locales/en-US/editor.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index ab2fba982..d09e6e977 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -292,11 +292,6 @@ "copyMessageValue": "Value copied", "copyMessageValue_truncated": "Truncated value copied" }, - "stopstart":{ - "status": { - "state_changed": "Flows runtime has been changed to '__state__' state" - } - }, "deploy": { "deploy": "Deploy", "full": "Full", From 1839c1972e7d335e59e8ea8e13eebecb46599be5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Jun 2022 20:21:27 +0100 Subject: [PATCH 12/12] Update packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js Co-authored-by: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> --- .../@node-red/editor-client/src/js/ui/deploy.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 809202c99..f0d4754f4 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -102,8 +102,10 @@ RED.deploy = (function() { RED.actions.add("core:deploy-flows",save); if (type === "default") { - RED.actions.add("core:stop-flows",function() { stopStartFlows("stop") }); - RED.actions.add("core:start-flows",function() { stopStartFlows("start") }); + if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { + RED.actions.add("core:stop-flows",function() { stopStartFlows("stop") }); + RED.actions.add("core:start-flows",function() { stopStartFlows("start") }); + } RED.actions.add("core:restart-flows",restart); RED.actions.add("core:set-deploy-type-to-full",function() { RED.menu.setSelected("deploymenu-item-full",true);}); RED.actions.add("core:set-deploy-type-to-modified-flows",function() { RED.menu.setSelected("deploymenu-item-flow",true); });