UNPKG

node-red-contrib-f2flowchart

Version:

A derived Node-Red Function node with the ability to generate flowcharts from the JS codes

633 lines (575 loc) 26.4 kB
<script> //Load js2flowchart.js for drawing flowchart var script = document.createElement('script'); script.type="text/javascript"; script.src = "/functions-to-flowcharts/js/lib/js2flowchart.js"; document.head.appendChild(script); //Load FileSaver for exporting SVG var script = document.createElement('script'); script.type="text/javascript"; script.src = "/functions-to-flowcharts/js/lib/FileSaver.min.js"; document.head.appendChild(script); </script> <script type="text/html" data-template-name="f2flowchart"> <style> .func-tabs-row { margin-bottom: 0; } #node-input-libs-container-row .red-ui-editableList-container { padding: 0px; } #node-input-libs-container-row .red-ui-editableList-container li { padding:5px; } #node-input-libs-container-row .red-ui-editableList-item-remove { right: 5px; } .node-libs-entry { display: flex; } .node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container { flex-grow: 1; } .node-libs-entry > code,.node-libs-entry > span { line-height: 30px; } .node-libs-entry > input[type=text] { border-radius: 0; border-left: none; border-top: none; border-right: none; padding-top: 2px; padding-bottom: 2px; margin-top: 4px; margin-bottom: 2px; height: 26px; } .node-libs-entry > span > i { display: none; } .node-libs-entry > span.input-error > i { display: inline; } </style> <input type="hidden" id="node-input-func"> <input type="hidden" id="node-input-noerr"> <input type="hidden" id="node-input-finalize"> <input type="hidden" id="node-input-initialize"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> <div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div> </div> <div class="form-row func-tabs-row"> <ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul> </div> <div id="func-tabs-content" style="min-height: calc(100% - 95px);"> <div id="func-tab-config" style="display:none"> <div class="form-row"> <label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label> <input id="node-input-outputs" style="width: 60px;" value="1"> </div> <div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;"> <label><i class="fa fa-cubes"></i> <span data-i18n="function.label.modules"></span></label> </div> <div class="form-row node-input-libs-row hide" id="node-input-libs-container-row"> <ol id="node-input-libs-container"></ol> </div> </div> <div id="func-tab-init" style="display:none"> <div class="form-row node-text-editor-row" style="position:relative"> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <!-- <div style="position: absolute; right:0; bottom: calc(100% - 40px);"><button id="node-init-expand-jsfc" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> --> </div> </div> <div id="func-tab-body" style="display:none"> <div class="form-row node-text-editor-row" style="position:relative"> <div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <!-- <div style="position: absolute; right:0; bottom: calc(100% - 40px);"><button id="node-function-expand-jsfc" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> --> </div> </div> <div id="func-tab-jsflow" style="display:none"> <div class="form-row node-text-editor-row" style="position:relative;border:1px solid #ccc;border-radius:5px;border-style:dotted"> <div style="height: 100%;" id="fcsvg"></div> <button id="downloadSVGFile" style="position: absolute; right:5px; top: 15px; z-index: 99;">DOWNLOAD SVG</button> </div> </div> <div id="func-tab-finalize" style="display:none"> <div class="form-row node-text-editor-row" style="position:relative"> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <!-- <div style="position: absolute; right:0; bottom: calc(100% - 40px);"><button id="node-finalize-expand-jsfc" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> --> </div> </div> </div> </script> <script type="text/javascript"> (function() { var invalidModuleVNames = [ 'console', 'util', 'Buffer', 'Date', 'RED', 'node', '__node__', 'context', 'flow', 'global', 'env', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'promisify' ] var knownFunctionNodes = {}; RED.events.on("nodes:add", function(n) { if (n.type === "function") { knownFunctionNodes[n.id] = n; } }) RED.events.on("nodes:remove", function(n) { if (n.type === "function") { delete knownFunctionNodes[n.id]; } }) var missingModules = []; var missingModuleReasons = {}; RED.events.on("runtime-state", function(event) { if (event.error === "missing-modules") { missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module }); for (var id in knownFunctionNodes) { if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) { RED.editor.validateNode(knownFunctionNodes[id]) } } } else if (!event.text) { missingModuleReasons = {}; missingModules = []; for (var id in knownFunctionNodes) { if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) { RED.editor.validateNode(knownFunctionNodes[id]) } } } RED.view.redraw(); }); var installAllowList = ['*']; var installDenyList = []; var modulesEnabled = true; if (RED.settings.get('externalModules.modules.allowInstall', true) === false) { modulesEnabled = false; } var settingsAllowList = RED.settings.get("externalModules.modules.allowList") var settingsDenyList = RED.settings.get("externalModules.modules.denyList") if (settingsAllowList || settingsDenyList) { installAllowList = settingsAllowList; installDenyList = settingsDenyList } installAllowList = RED.utils.parseModuleList(installAllowList); installDenyList = RED.utils.parseModuleList(installDenyList); // object that maps from library name to its descriptor var allLibs = []; function moduleName(module) { var match = /^([^@]+)@(.+)/.exec(module); if (match) { return [match[1], match[2]]; } return [module, undefined]; } function getAllUsedModules() { var moduleSet = new Set(); for (var id in knownFunctionNodes) { if (knownFunctionNodes.hasOwnProperty(id)) { if (knownFunctionNodes[id].libs) { for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) { if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) { moduleSet.add(knownFunctionNodes[id].libs[i].module); } } } } } var modules = Array.from(moduleSet); modules.sort(); return modules; } function prepareLibraryConfig(node) { $(".node-input-libs-row").show(); var usedModules = getAllUsedModules(); var typedModules = usedModules.map(function(l) { return {icon:"fa fa-cube", value:l,label:l,hasValue:false} }) typedModules.push({ value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg" }) var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({ addItem: function(container,i,opt) { var parent = container.parent(); var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container); var fieldWidth = "260px"; $('<code>const </code>').appendTo(row0); var fvar = $("<input/>", { class: "node-input-libs-var red-ui-font-code", placeholder: RED._("node-red:function.require.var"), type: "text" }).css({ width: "120px", "margin-left": "5px" }).appendTo(row0).val(opt.var); var vnameWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0); RED.popover.tooltip(vnameWarning.find("i"),function() { var val = fvar.val(); if (invalidModuleVNames.indexOf(val) !== -1) { return RED._("node-red:function.error.moduleNameReserved",{name:val}) } else { return RED._("node-red:function.error.moduleNameError",{name:val}) } }) $('<code> = require(</code>').appendTo(row0); var fmodule = $("<input/>", { class: "node-input-libs-val", placeholder: RED._("node-red:function.require.module"), type: "text" }).css({ width: "180px", }).appendTo(row0).typedInput({ types: typedModules, default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_" }); if (usedModules.indexOf(opt.module) === -1) { fmodule.typedInput('value', opt.module); } $('<code>)</code>').appendTo(row0); var moduleWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0); RED.popover.tooltip(moduleWarning.find("i"),function() { var val = fmodule.typedInput("type"); if (val === "_custom_") { val = fmodule.val(); } var errors = []; if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) { return RED._("node-red:function.error.moduleNotAllowed",{module:val}); } else { return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]}); } }) fvar.on("change keyup paste", function (e) { var v = $(this).val().trim(); if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) { fvar.addClass("input-error"); vnameWarning.addClass("input-error"); } else { fvar.removeClass("input-error"); vnameWarning.removeClass("input-error"); } }); fmodule.on("change keyup paste", function (e) { var val = $(this).typedInput("type"); if (val === "_custom_") { val = $(this).val(); } var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() }); fvar.val(varName); fvar.trigger("change"); if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) { fmodule.removeClass("input-error"); moduleWarning.removeClass("input-error"); } else { fmodule.addClass("input-error"); moduleWarning.addClass("input-error"); } }); if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) { fmodule.removeClass("input-error"); moduleWarning.removeClass("input-error"); } else { fmodule.addClass("input-error"); moduleWarning.addClass("input-error"); } if (opt.var) { fvar.trigger("change"); } }, removable: true }); var libs = node.libs || []; for (var i=0,l=libs.length;i<l; i++) { libList.editableList('addItem',libs[i]) } } RED.nodes.registerType('f2flowchart',{ color:"#fdd0a2", category: 'function', defaults: { name: {value:""}, func: {value:"\nreturn msg;"}, outputs: {value:1}, noerr: {value:0,required:true,validate:function(v) { return !v; }}, initialize: {value:""}, finalize: {value:""}, libs: {value: [], validate: function(v) { if (!v) { return true; } for (var i=0,l=v.length;i<l;i++) { var m = v[i]; if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) { return false } if (m.var === "" || / /.test(m.var)) { return false; } if (missingModules.indexOf(m.module) > -1) { return false; } if (invalidModuleVNames.indexOf(m.var) !== -1){ return false; } } return true; }} }, inputs:1, outputs:1, icon: "function.svg", label: function() { return this.name||"Func-to-Flowchart"; }, labelStyle: function() { return this.name?"node_label_italic":""; }, oneditprepare: function() { var that = this; var tabs = RED.tabs.create({ id: "func-tabs", onchange: function(tab) { $("#func-tabs-content").children().hide(); $("#" + tab.id).show(); // add comment if (tab.id === "func-tab-jsflow") { var code = "(msg) => {\n" + that.editor.getValue() + "\n}" ; var svg = window.js2flowchart.convertCodeToSvg(code); $("#fcsvg").html(svg); } } }); tabs.addTab({ id: "func-tab-config", iconClass: "fa fa-cog", label: "Setup" }); tabs.addTab({ id: "func-tab-init", label: "On Start" }); tabs.addTab({ id: "func-tab-body", label: "On Message" }); tabs.addTab({ //The tab to show generated flowchart id: "func-tab-jsflow", label: "OnMsg Chart" }); tabs.addTab({ id: "func-tab-finalize", label: "On Stop" }); tabs.activateTab("func-tab-body"); $( "#node-input-outputs" ).spinner({ min:0, change: function(event, ui) { var value = this.value; if (!value.match(/^\d+$/)) { value = 1; } else if (value < this.min) { value = this.min; } if (value !== this.value) { $(this).spinner("value", value); } } }); var buildEditor = function(id, value, defaultValue) { var editor = RED.editor.createEditor({ id: id, mode: 'ace/mode/nrjavascript', value: value || defaultValue || "", globals: { msg:true, context:true, RED: true, util: true, flow: true, global: true, console: true, Buffer: true, setTimeout: true, clearTimeout: true, setInterval: true, clearInterval: true } }); if (defaultValue && value === "") { editor.moveCursorTo(defaultValue.split("\n").length - 1, 0); } return editor; } this.initEditor = buildEditor('node-input-init-editor',$("#node-input-initialize").val(),RED._("node-red:function.text.initialize")) this.editor = buildEditor('node-input-func-editor',$("#node-input-func").val()) this.finalizeEditor = buildEditor('node-input-finalize-editor',$("#node-input-finalize").val(),RED._("node-red:function.text.finalize")) //Download SVG if the button gets clicked $("#downloadSVGFile").on('click', ()=> { const fileName = `flowchart_${(new Date().toString()).replace(/ /g,'_')}.svg`, file = new File([$("#fcsvg").html()], fileName, {type: 'image/svg+xml;charset=utf-8'}); window.saveAs(file, fileName); }); RED.library.create({ url:"functions", // where to get the data from type:"function", // the type of object the library is for editor:this.editor, // the field name the main text body goes to mode:"ace/mode/nrjavascript", fields:[ 'name', 'outputs', { name: 'initialize', get: function() { return that.initEditor.getValue(); }, set: function(v) { that.initEditor.setValue(v||RED._("node-red:function.text.initialize"), -1); } }, { name: 'finalize', get: function() { return that.finalizeEditor.getValue(); }, set: function(v) { that.finalizeEditor.setValue(v||RED._("node-red:function.text.finalize"), -1); } }, { name: 'info', get: function() { return that.infoEditor.getValue(); }, set: function(v) { that.infoEditor.setValue(v||"", -1); } } ], ext:"js" }); this.editor.focus(); var expandButtonClickHandler = function(editor) { return function(e) { e.preventDefault(); var value = editor.getValue(); RED.editor.editJavaScript({ value: value, width: "Infinity", cursor: editor.getCursorPosition(), mode: "ace/mode/nrjavascript", complete: function(v,cursor) { editor.setValue(v, -1); editor.gotoLine(cursor.row+1,cursor.column,false); setTimeout(function() { editor.focus(); },300); } }) } } $("#node-init-expand-js").on("click", expandButtonClickHandler(this.initEditor)); $("#node-function-expand-js").on("click", expandButtonClickHandler(this.editor)); $("#node-finalize-expand-js").on("click", expandButtonClickHandler(this.finalizeEditor)); RED.popover.tooltip($("#node-init-expand-js"), RED._("node-red:common.label.expand")); RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand")); RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand")); if (RED.settings.functionExternalModules !== false) { prepareLibraryConfig(that); } }, oneditsave: function() { var node = this; var noerr = 0; $("#node-input-noerr").val(0); var disposeEditor = function(editorName,targetName,defaultValue) { var editor = node[editorName]; var annot = editor.getSession().getAnnotations(); for (var k=0; k < annot.length; k++) { if (annot[k].type === "error") { noerr += annot.length; break; } } var val = editor.getValue(); if (defaultValue) { if (val.trim() == defaultValue.trim()) { val = ""; } } editor.destroy(); delete node[editorName]; $("#"+targetName).val(val); } disposeEditor("editor","node-input-func"); disposeEditor("initEditor","node-input-initialize", RED._("node-red:function.text.initialize")); disposeEditor("finalizeEditor","node-input-finalize", RED._("node-red:function.text.finalize")); $("#node-input-noerr").val(noerr); this.noerr = noerr; if (RED.settings.functionExternalModules === true) { var libs = $("#node-input-libs-container").editableList("items"); node.libs = []; libs.each(function(i) { var item = $(this); var v = item.find(".node-input-libs-var").val(); var n = item.find(".node-input-libs-val").typedInput("type"); if (n === "_custom_") { n = item.find(".node-input-libs-val").val(); } if ((!v || (v === "")) || (!n || (n === ""))) { return; } node.libs.push({ var: v, module: n }); }); } else { node.libs = []; } }, oneditcancel: function() { var node = this; node.editor.destroy(); delete node.editor; node.initEditor.destroy(); delete node.initEditor; node.finalizeEditor.destroy(); delete node.finalizeEditor; }, oneditresize: function(size) { var rows = $("#dialog-form>div:not(.node-text-editor-row)"); var height = $("#dialog-form").height(); for (var i=0; i<rows.length; i++) { height -= $(rows[i]).outerHeight(true); } var editorRow = $("#dialog-form>div.node-text-editor-row"); height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); $("#dialog-form .node-text-editor").css("height",height+"px"); var height = size.height; $("#node-input-init-editor").css("height", (height -45-48)+"px"); $("#node-input-func-editor").css("height", (height -45-48)+"px"); $("#node-input-finalize-editor").css("height", (height -45-48)+"px"); this.initEditor.resize(); this.editor.resize(); this.finalizeEditor.resize(); $("#node-input-libs-container").css("height", (height - 185)+"px"); } }); })(); </script>