diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
new file mode 100644
index 0000000..4582139
--- /dev/null
+++ b/.github/workflows/static.yml
@@ -0,0 +1,43 @@
+# Simple workflow for deploying static content to GitHub Pages
+name: Deploy static content to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["feature/functions"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup Pages
+ uses: actions/configure-pages@v5
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ # Upload entire repository
+ path: 'src'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5546b9d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,12 @@
+{
+ "name": "codewire",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "codewire",
+ "version": "1.0.0"
+ }
+ }
+}
diff --git a/src/assets/starter.json b/src/assets/starter.json
new file mode 100644
index 0000000..8849bf6
--- /dev/null
+++ b/src/assets/starter.json
@@ -0,0 +1 @@
+{"variables":[{"name":"quantity","dataType":"Number","value":"100"},{"name":"unit_price","dataType":"Number","value":"1000"}],"nodesData":[{"position":{"x":329.66989447061565,"y":220.91332290742616},"nodeDescription":{"nodeTitle":"Begin","color":"Begin","rows":2,"colums":10,"execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"10","outOrder":0}}}},{"position":{"x":297.7233644556677,"y":381.88250599193964},"nodeDescription":{"nodeTitle":"Get quantity","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"409","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":481.099434396144,"y":410.6046845633682},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"419","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"424","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"429","outOrder":0}}}},{"position":{"x":288.7184820151915,"y":508.8933750395587},"nodeDescription":{"nodeTitle":"Get unit_price","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"444","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":1084.7283004870192,"y":258.0682468154959},"nodeDescription":{"nodeTitle":"Call display_dollar_price","execIn":true,"pinExecInId":"475","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"476","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"display_dollar_price","docString":"Function to fetch USD exchange rate and do the INR to USD conversion","inputs":{"input0":{"inputTitle":"inr_amount","dataType":"Number","defValue":"0","pinInId":"477","isInputBoxRequired":true}}}},{"position":{"x":705.7092310126123,"y":164.6421543315391},"nodeDescription":{"nodeTitle":"Call get_discounted_price","execIn":true,"pinExecInId":"540","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"541","outOrder":0}},"color":"Call","rows":3,"colums":14,"isCallFunction":true,"calledFunctionName":"get_discounted_price","docString":"Function to get the discounted prices.","inputs":{"input0":{"inputTitle":"total_amount","dataType":"Number","defValue":"0","pinInId":"542","isInputBoxRequired":true},"input1":{"inputTitle":"discount_perc","dataType":"Number","defValue":"0","pinInId":"547","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"discounted_amount","dataType":"Number","pinOutId":"552","outOrder":1}}}},{"position":{"x":1784.8503547906926,"y":255.8692215133575},"nodeDescription":{"nodeTitle":"Print","color":"Print","rows":3,"colums":12,"execIn":true,"pinExecInId":"1122","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1123","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":"'hello'","pinInId":"1124","isInputBoxRequired":true}}}},{"position":{"x":1447.643880113113,"y":264.1809681973247},"nodeDescription":{"nodeTitle":"Call recursive_factorial","execIn":true,"pinExecInId":"1100","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1101","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"recursive_factorial","docString":"","inputs":{"input0":{"inputTitle":"num","dataType":"Number","defValue":"10","pinInId":"1102","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"factorial","dataType":"Number","pinOutId":"1107","outOrder":1}}}}],"wireData":[{"srcId":"444","destId":"424"},{"srcId":"409","destId":"419"},{"srcId":"10","destId":"540"},{"srcId":"541","destId":"475"},{"srcId":"429","destId":"542"},{"srcId":"552","destId":"477"},{"srcId":"476","destId":"1100"},{"srcId":"1101","destId":"1122"},{"srcId":"1107","destId":"1124"}],"groupsData":[{"position":{"x":1398.019131518612,"y":137.39846411260328},"width":625.3779656250006,"height":381.60818718750033,"name":"Calling a recursive function for test"}],"functions":[{"name":"get_discounted_price","inputParams":[{"name":"total_amount","dataType":"Number","defValue":"0"},{"name":"discount_perc","dataType":"Number","defValue":"0"}],"outputParams":[{"name":"discounted_amount","dataType":"Number"}],"variables":[],"docString":"Function to get the discounted prices.","nodesData":[{"position":{"x":369.2488819263885,"y":218.72366699514882},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"38","outOrder":0}},"color":"FunctionBegin","rows":3,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"total_amount","dataType":"Number","pinOutId":"39","outOrder":1},"output1":{"outputTitle":"discount_perc","dataType":"Number","pinOutId":"41","outOrder":2}}}},{"position":{"x":858.8968414596872,"y":322.2400151803591},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"51","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"56","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"61","outOrder":0}}}},{"position":{"x":787.2415126615012,"y":495.4826455658467},"nodeDescription":{"nodeTitle":"Divide","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"76","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":"100","pinInId":"81","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"86","outOrder":0}}}},{"position":{"x":563.2052314823627,"y":446.50305372911185},"nodeDescription":{"nodeTitle":"Subtract","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":"100","pinInId":"97","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"102","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"107","outOrder":0}}}},{"position":{"x":1078.0120126333236,"y":196.95495951215565},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"129","color":"Return","rows":2,"colums":12,"isDeletable":false,"inputs":{"input0":{"inputTitle":"discounted_amount","dataType":"Number","defValue":null,"pinInId":"130","isInputBoxRequired":false}}}}],"wireData":[{"srcId":"38","destId":"129"},{"srcId":"61","destId":"130"},{"srcId":"86","destId":"56"},{"srcId":"107","destId":"76"},{"srcId":"41","destId":"102"},{"srcId":"39","destId":"51"}],"groupsData":[]},{"name":"display_dollar_price","inputParams":[{"name":"inr_amount","dataType":"Number","defValue":"0"}],"outputParams":[],"variables":[],"docString":"Function to fetch USD exchange rate and do the INR to USD conversion","nodesData":[{"position":{"x":200.40309411510998,"y":116.32079483144673},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"161","outOrder":0}},"color":"FunctionBegin","rows":2,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"inr_amount","dataType":"Number","pinOutId":"162","outOrder":1}}}},{"position":{"x":630.439618606527,"y":595.1399853189833},"nodeDescription":{"nodeTitle":"HttpRequest","color":"Func","rows":2,"colums":12,"execIn":true,"pinExecInId":"177","execOut":{"execOut0":{"execOutTitle":"OnSuccess","pinExecOutId":"178","outOrder":0},"execOut1":{"execOutTitle":"OnFail","pinExecOutId":"180","outOrder":2},"execOut2":{"execOutTitle":"Continue","pinExecOutId":"182","outOrder":3}},"inputs":{"input0":{"inputTitle":"URL","dataType":"String","defValue":"'https://api.frankfurter.app/latest?from=USD&to=INR'","pinInId":"184","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"JSON","dataType":"Data","pinOutId":"189","outOrder":1}}}},{"position":{"x":922.9646427992587,"y":749.5765375036962},"nodeDescription":{"nodeTitle":"GetByName(JSON)","color":"Obj","rows":2,"colums":12,"inputs":{"input0":{"inputTitle":"JSON","dataType":"Data","defValue":null,"pinInId":"230","isInputBoxRequired":false},"input1":{"inputTitle":"Name","dataType":"String","defValue":"'rates'","pinInId":"232","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Data","dataType":"Data","pinOutId":"237","outOrder":0}}}},{"position":{"x":1173.4523588393488,"y":734.2697894033207},"nodeDescription":{"nodeTitle":"GetByName(JSON)","color":"Obj","rows":2,"colums":12,"inputs":{"input0":{"inputTitle":"JSON","dataType":"Data","defValue":null,"pinInId":"252","isInputBoxRequired":false},"input1":{"inputTitle":"Name","dataType":"String","defValue":"'INR'","pinInId":"254","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Data","dataType":"Data","pinOutId":"259","outOrder":0}}}},{"position":{"x":1427.9573136257832,"y":746.651492565948},"nodeDescription":{"nodeTitle":"ParseFloat","color":"Str","rows":2,"colums":11,"inputs":{"input0":{"inputTitle":"String","dataType":"String","defValue":"'0.0'","pinInId":"275","isInputBoxRequired":false}},"outputs":{"output0":{"outputTitle":"Number","dataType":"Number","pinOutId":"277","outOrder":0}}}},{"position":{"x":1525.9905834056824,"y":145.19684752858797},"nodeDescription":{"nodeTitle":"Divide","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"297","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"302","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"307","outOrder":0}}}},{"position":{"x":1750.1563808724957,"y":203.03720934837258},"nodeDescription":{"nodeTitle":"Round","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":0,"pinInId":"469","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"474","outOrder":0}}}},{"position":{"x":1980.7732783708489,"y":195.18502976456716},"nodeDescription":{"nodeTitle":"ToString","color":"Str","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":0,"pinInId":"510","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"String","pinOutId":"515","outOrder":0}}}},{"position":{"x":2250.8344686723267,"y":211.72541952720098},"nodeDescription":{"nodeTitle":"Concat","color":"Str","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"A","dataType":"String","defValue":"''","pinInId":"490","isInputBoxRequired":true},"input1":{"inputTitle":"B","dataType":"String","defValue":"' USD'","pinInId":"495","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"String","pinOutId":"500","outOrder":0}}}},{"position":{"x":2849.4639972846294,"y":368.03953046698064},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"155","color":"Return","rows":2,"colums":12,"isDeletable":false}},{"position":{"x":2486.452960358817,"y":392.9088963218603},"nodeDescription":{"nodeTitle":"Print","color":"Print","rows":3,"colums":12,"execIn":true,"pinExecInId":"205","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"206","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Data","defValue":"'hello'","pinInId":"207","isInputBoxRequired":true}}}}],"wireData":[{"srcId":"161","destId":"177"},{"srcId":"189","destId":"230"},{"srcId":"237","destId":"252"},{"srcId":"259","destId":"275"},{"srcId":"162","destId":"297"},{"srcId":"277","destId":"302"},{"srcId":"307","destId":"469"},{"srcId":"474","destId":"510"},{"srcId":"515","destId":"490"},{"srcId":"178","destId":"205"},{"srcId":"206","destId":"155"},{"srcId":"500","destId":"207"}],"groupsData":[{"position":{"x":460.312324873067,"y":525.8005840089143},"width":1239.5888109776342,"height":428.3992868424674,"name":"Get Data From the API"}]},{"name":"recursive_factorial","inputParams":[{"name":"num","dataType":"Number","defValue":"10"}],"outputParams":[{"name":"factorial","dataType":"Number"}],"variables":[{"name":"ret","dataType":"Number","value":"1"}],"docString":"","nodesData":[{"position":{"x":1023.0066749449601,"y":223.81688341778772},"nodeDescription":{"nodeTitle":"Subtract","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"702","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":"1","pinInId":"707","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"712","outOrder":0}}}},{"position":{"x":1298.3243883332486,"y":134.7520340063008},"nodeDescription":{"nodeTitle":"Call recursive_factorial","execIn":true,"pinExecInId":"904","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"905","outOrder":0}},"color":"Call","rows":2,"colums":14,"isCallFunction":true,"calledFunctionName":"recursive_factorial","docString":"","inputs":{"input0":{"inputTitle":"num","dataType":"Number","defValue":"10","pinInId":"906","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"factorial","dataType":"Number","pinOutId":"911","outOrder":1}}}},{"position":{"x":1699.9591584998007,"y":182.40218599576008},"nodeDescription":{"nodeTitle":"Set ret","execIn":true,"pinExecInId":"1019","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1020","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":0,"pinInId":"1021"}},"outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1026","outOrder":1}},"color":"Func","rows":2,"colums":12}},{"position":{"x":1928.7081075436731,"y":-55.477309898947595},"nodeDescription":{"nodeTitle":"Return","execIn":true,"pinExecInId":"523","color":"Return","rows":2,"colums":12,"isDeletable":false,"inputs":{"input0":{"inputTitle":"factorial","dataType":"Number","defValue":null,"pinInId":"524","isInputBoxRequired":false}}}},{"position":{"x":1574.750390698565,"y":-150.03088000044787},"nodeDescription":{"nodeTitle":"Set ret","execIn":true,"pinExecInId":"1052","execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"1053","outOrder":0}},"inputs":{"input0":{"inputTitle":"Value","dataType":"Number","defValue":"1","pinInId":"1054"}},"outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1059","outOrder":1}},"color":"Func","rows":2,"colums":12}},{"position":{"x":1584.2318667750133,"y":72.17595609453402},"nodeDescription":{"nodeTitle":"Get ret","outputs":{"output0":{"outputTitle":"Value(Ref)","dataType":"Number","pinOutId":"1080","outOrder":0}},"color":"Get","rows":2,"colums":10}},{"position":{"x":52.12485457043243,"y":78.97408515991168},"nodeDescription":{"nodeTitle":"FunctionBegin","execIn":false,"pinExecInId":null,"execOut":{"execOut0":{"execOutTitle":null,"pinExecOutId":"515","outOrder":0}},"color":"FunctionBegin","rows":2,"colums":12,"isDeletable":false,"outputs":{"output0":{"outputTitle":"num","dataType":"Number","pinOutId":"516","outOrder":1}}}},{"position":{"x":752.6042405443744,"y":-146.98291402171543},"nodeDescription":{"nodeTitle":"If/Else","color":"Logic","rows":3,"colums":12,"execIn":true,"pinExecInId":"792","execOut":{"execOut0":{"execOutTitle":"True","pinExecOutId":"793","outOrder":0},"execOut1":{"execOutTitle":"False","pinExecOutId":"795","outOrder":1},"execOut2":{"execOutTitle":"Done","pinExecOutId":"797","outOrder":2}},"inputs":{"input0":{"inputTitle":"Bool","dataType":"Boolean","defValue":true,"pinInId":"799","isInputBoxRequired":true}}}},{"position":{"x":572.6646227778107,"y":15.169287303525039},"nodeDescription":{"nodeTitle":"Equals","color":"Logic","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Data","defValue":0,"pinInId":"817","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Data","defValue":"1","pinInId":"822","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Boolean","pinOutId":"827","outOrder":0}}}},{"position":{"x":1485.3160975104106,"y":460.73643125691024},"nodeDescription":{"nodeTitle":"Multiply","color":"Math","rows":2,"colums":10,"inputs":{"input0":{"inputTitle":"ValueA","dataType":"Number","defValue":0,"pinInId":"606","isInputBoxRequired":true},"input1":{"inputTitle":"ValueB","dataType":"Number","defValue":0,"pinInId":"611","isInputBoxRequired":true}},"outputs":{"output0":{"outputTitle":"Result","dataType":"Number","pinOutId":"616","outOrder":0}}}}],"wireData":[{"srcId":"1080","destId":"524"},{"srcId":"1053","destId":"523"},{"srcId":"905","destId":"1019"},{"srcId":"1020","destId":"523"},{"srcId":"712","destId":"906"},{"srcId":"516","destId":"702"},{"srcId":"515","destId":"792"},{"srcId":"793","destId":"1052"},{"srcId":"795","destId":"904"},{"srcId":"516","destId":"817"},{"srcId":"827","destId":"799"},{"srcId":"911","destId":"606"},{"srcId":"516","destId":"611"},{"srcId":"616","destId":"1021"}],"groupsData":[]}]}
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
index c7fe01e..671d3c6 100644
--- a/src/index.html
+++ b/src/index.html
@@ -27,6 +27,33 @@
+
+
+
+
@@ -64,8 +91,20 @@
+
+
+
diff --git a/src/js/app.js b/src/js/app.js
index 1d6ce79..44a5a24 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -12,37 +12,49 @@ import { Export, Import, Save, prompLastSave } from './persistence/saveAndLoad.j
import { showAlert, prompRefreshOrStarter } from './ui/dialogs.js'
import { refresh } from './compiler/codePreview.js'
import { enableNodeGroups } from './editor/nodeGroup.js'
-// var width = window.innerWidth;
-// var height = window.innerHeight;
+import { tabManager } from './editor/tabManager.js'
+import { initFunctionPanel } from './ui/functionPanel.js'
+
let stage = AppStage.getStage(document.getElementById("container").clientWidth, document.getElementById("container").clientHeight, 'container');
var layer = new Konva.Layer({
id: 'main_layer'
});
+let wireLayer = new Konva.Layer({
+ id: 'wireLayer',
+});
let dragLayer = new Konva.Layer({
id: 'dragLayer',
});
+stage.add(wireLayer);
stage.add(layer);
stage.add(dragLayer);
+layer.moveToBottom();
stage.container().style.backgroundPosition = `${stage.position().x} ${stage.position().y}`;
-// stage.on("wheel", () => {
-// if (inputIsFocused) {
-// document.getElementById("number-ip").blur();
-// document.getElementById("string-ip").blur();
-// document.getElementById("bool-ip").blur();
-// }
-// });
+tabManager.init(stage, layer, wireLayer, dragLayer);
+
// SelectionBox.setSelectionBox(layer, stage);
-enableNodeGroups(stage, layer);
+enableNodeGroups(stage);
Delete.enableDelete(stage, layer);
-// DragAndDrop.DragAndDrop(stage, layer);
-Wiring.enableWiring(stage, layer);
-ContextMenu.contextMenu(stage, layer);
+Wiring.enableWiring(stage);
+ContextMenu.contextMenu(stage);
let panel = new leftPanel();
variableList.init(layer, stage);
-// layer.toggleHitCanvas();
-// document.getElementById("number-ip").value = 12;
+tabManager.getTab('main').variables = variableList.variables;
+initFunctionPanel();
layer.draw();
+
+document.getElementById('add-tab-btn').addEventListener('click', () => {
+ const name = prompt('Enter function name:');
+ if (name && name.trim()) {
+ tabManager.createTab(name.trim());
+ }
+});
+
+tabManager.on('tabSwitched', ({ to }) => {
+ variableList.layer = to.layer;
+ variableList.switchToTab(to.variables);
+});
document.getElementById("Run").addEventListener("click", (e) => {
try {
let script = new VSToJS(stage, layer, "Run").script;
diff --git a/src/js/compiler/compiler.js b/src/js/compiler/compiler.js
index 27e9ae1..09b0956 100644
--- a/src/js/compiler/compiler.js
+++ b/src/js/compiler/compiler.js
@@ -2,6 +2,7 @@ import { variableList } from '../ui/variableList.js'
import { showAlert } from '../ui/dialogs.js'
import { BuilInFunctions } from './builtInFunctions.js'
import { runExecCodegen, runExprCodegen, hasType } from '../registry/index.js'
+import { tabManager } from '../editor/tabManager.js'
export var VSToJS = class {
constructor(stage, layer, isRunOrCode) {
@@ -10,7 +11,13 @@ export var VSToJS = class {
this.nodeCount = 0;
this.isRunOrCode = isRunOrCode;
this.compileIssues = [];
- for (let variable of variableList.variables) {
+ this._callCounter = 0;
+
+ this.compileFunctionTabs();
+
+ const mainTab = tabManager.getTab('main');
+ const globalVars = mainTab ? mainTab.variables : variableList.variables;
+ for (let variable of globalVars) {
this.script += `let ${variable.name} = ${variable.value};\n`;
}
let begin = this.getBegin(stage);
@@ -88,6 +95,33 @@ export var VSToJS = class {
}
}
}
+ compileFunctionTabs() {
+ const funcTabs = tabManager.getAllFunctionTabs();
+ for (const tab of funcTabs) {
+ const funcBegin = tab.layer.findOne('#FunctionBegin');
+ if (!funcBegin) continue;
+
+ const params = tab.inputParams.map(p => p.name).join(', ');
+ const savedScript = this.script;
+ this.script = '';
+
+ if (tab.variables && tab.variables.length > 0) {
+ for (const v of tab.variables) {
+ this.script += `let ${v.name} = ${v.value};\n`;
+ }
+ }
+
+ const execOutPins = this.getExecOut(funcBegin);
+ for (let each of execOutPins) {
+ this.coreAlgorithm(each);
+ }
+
+ const bodyScript = this.script;
+ this.script = savedScript;
+ this.script += `function ${tab.name}(${params}) {\n${bodyScript}}\n`;
+ }
+ }
+
getBegin(stage) {
let X = stage.find("#Begin");
if (X.length == 0) {
@@ -127,7 +161,8 @@ export var VSToJS = class {
X.push({ node: aNode.wire.attrs.src.getParent(), isWire: true, wire: aNode.wire, srcOutputPinNumber: this.getSrcOutputPinNumber(aNode.wire.attrs.src.getParent(), aNode.wire) });
}
else {
- X.push({ node: aNode.textBox.textBox.text(), isWire: false, wire: null, srcOutputPinNumber: null });
+ const val = aNode.textBox ? aNode.textBox.textBox.text() : 'undefined';
+ X.push({ node: val, isWire: false, wire: null, srcOutputPinNumber: null });
}
}
return X;
@@ -144,6 +179,54 @@ export var VSToJS = class {
return;
}
+ const nodeType = node.customClass.type.typeOfNode;
+
+ if (nodeType === 'FunctionBegin') {
+ let execOutPins = this.getExecOut(node);
+ for (let each of execOutPins) {
+ this.coreAlgorithm(each);
+ }
+ return;
+ }
+
+ if (nodeType === 'Return') {
+ let inputPins = this.getInputPins(node);
+ if (inputPins.length > 0) {
+ const nd = node.customClass.nodeDescription;
+ const outputNames = [];
+ if (nd.inputs) {
+ for (const key of Object.keys(nd.inputs)) {
+ outputNames.push(nd.inputs[key].inputTitle);
+ }
+ }
+ if (inputPins.length === 1) {
+ this.script += `return ${this.handleInputs(inputPins[0])};\n`;
+ } else {
+ const pairs = outputNames.map((name, i) =>
+ `${name}: ${this.handleInputs(inputPins[i])}`
+ ).join(', ');
+ this.script += `return { ${pairs} };\n`;
+ }
+ } else {
+ this.script += `return;\n`;
+ }
+ return;
+ }
+
+ if (nodeType.startsWith('Call ')) {
+ let inputPins = this.getInputPins(node);
+ let execOutPins = this.getExecOut(node);
+ const funcName = node.customClass.nodeDescription.calledFunctionName || nodeType.slice(5);
+ const args = inputPins.map(ip => this.handleInputs(ip)).join(', ');
+ const varName = `_call_${funcName}_${this._callCounter++}`;
+ this.script += `let ${varName} = ${funcName}(${args});\n`;
+ node._callResultVar = varName;
+ for (let each of execOutPins) {
+ this.coreAlgorithm(each);
+ }
+ return;
+ }
+
let execOutPins = this.getExecOut(node);
let inputPins = this.getInputPins(node);
if (node.customClass.type.isGetSet) {
@@ -176,6 +259,35 @@ export var VSToJS = class {
return "undefined";
}
+ const nodeType = inputNode.node.customClass.type.typeOfNode;
+
+ if (nodeType === 'FunctionBegin') {
+ const nd = inputNode.node.customClass.nodeDescription;
+ if (nd.outputs) {
+ const outputKey = `output${inputNode.srcOutputPinNumber}`;
+ const paramName = nd.outputs[outputKey]?.outputTitle;
+ if (paramName) return paramName;
+ }
+ return 'undefined';
+ }
+
+ if (nodeType.startsWith('Call ')) {
+ const resultVar = inputNode.node._callResultVar;
+ if (resultVar) {
+ const nd = inputNode.node.customClass.nodeDescription;
+ if (nd.outputs) {
+ const outputKeys = Object.keys(nd.outputs);
+ if (outputKeys.length === 1) {
+ return resultVar;
+ }
+ const outputName = nd.outputs[`output${inputNode.srcOutputPinNumber}`]?.outputTitle;
+ if (outputName) return `${resultVar}.${outputName}`;
+ }
+ return resultVar;
+ }
+ return 'undefined';
+ }
+
let inputPins = this.getInputPins(inputNode.node);
if (inputNode.node.customClass.type.isGetSet) {
return `${inputNode.node.customClass.type.typeOfNode.slice(4)}`;
diff --git a/src/js/core/colorMap.js b/src/js/core/colorMap.js
index 7aa9d54..51b7201 100644
--- a/src/js/core/colorMap.js
+++ b/src/js/core/colorMap.js
@@ -26,6 +26,9 @@ export const colorMap = {
'Get': '#2e7d32', // Array / access – green
'Str': '#ad1457', // String – pink/magenta
'Obj': '#00695c', // Object / Map – teal
+ 'FunctionBegin': '#ff6d00', // Function entry – warm orange
+ 'Return': '#d50000', // Function exit – red
+ 'Call': '#00bfa5', // Function call – teal
'GroupBody': 'rgba(255, 255, 255, 0.31)',
'GroupBorder': 'rgba(255,255,255,0.3)',
'GroupTitleBar': 'rgba(41,68,150,0.8)',
diff --git a/src/js/editor/contextMenu.js b/src/js/editor/contextMenu.js
index cbfd5d3..b0a95bb 100644
--- a/src/js/editor/contextMenu.js
+++ b/src/js/editor/contextMenu.js
@@ -4,6 +4,7 @@ import { variableList } from '../ui/variableList.js'
import { deleteProgramNode, deleteWire } from './deleteHandler.js'
import { getMenuOrderGroupedByCategory, getDefinition } from '../registry/index.js'
import { colorMap, lightenHex } from '../core/colorMap.js'
+import { tabManager } from './tabManager.js'
const CATEGORY_LABELS = {
Begin: 'Flow',
@@ -19,7 +20,7 @@ const CATEGORY_LABELS = {
const DEFAULT_EXPANDED_CATEGORIES = new Set(['Begin', 'Logic']);
export var ContextMenu = {
- contextMenu: function (stage, layer) {
+ contextMenu: function (stage) {
let contextMenu = document.getElementById("ctx-menu-container");
let contextMenuList = document.getElementById("context-menu");
let deleteCtxMenu = document.getElementById("delete-ctx-container");
@@ -126,6 +127,7 @@ export var ContextMenu = {
const category = section.dataset.category;
const expanded = DEFAULT_EXPANDED_CATEGORIES.has(category);
section.classList.toggle('ctx-menu-section--collapsed', !expanded);
+ section.classList.remove('hidden');
const arr = section.querySelector('.ctx-menu-section-arrow');
if (arr) arr.textContent = expanded ? '\u25BC' : '\u25B6';
const items = section.querySelectorAll('.context-menu-items');
@@ -133,6 +135,23 @@ export var ContextMenu = {
});
contextMenuList.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden'));
}
+ ContextMenu.resetFilter = function () {
+ const bar = document.getElementById('ctx-search-bar');
+ const list = document.getElementById('context-menu');
+ if (bar) bar.value = '';
+ if (list) {
+ list.querySelectorAll('.ctx-menu-section').forEach((section) => {
+ const category = section.dataset.category;
+ const expanded = DEFAULT_EXPANDED_CATEGORIES.has(category);
+ section.classList.toggle('ctx-menu-section--collapsed', !expanded);
+ section.classList.remove('hidden');
+ const arr = section.querySelector('.ctx-menu-section-arrow');
+ if (arr) arr.textContent = expanded ? '\u25BC' : '\u25B6';
+ section.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden'));
+ });
+ list.querySelectorAll('.context-menu-items').forEach((el) => el.classList.remove('hidden'));
+ }
+ };
function toggleDeleteCtxMenu(location, show) {
if (show) {
deleteCtxMenu.classList.toggle("hidden", false);
@@ -157,7 +176,7 @@ export var ContextMenu = {
ContextMenu.addEventToCtxMenuItems = function (e) {
e.addEventListener('click', function (ev) {
if (ev.target.classList.contains('ctx-menu-section-header')) return;
- makeNode(e, stage, layer, toggleContextMenu);
+ makeNode(e, stage, tabManager.getActiveLayer(), toggleContextMenu);
});
};
searchBar.addEventListener("input", (e) => {
@@ -229,7 +248,7 @@ export var ContextMenu = {
stage.draw();
}
else if (parentGroup && parentGroup.name() == 'aProgramNodeGroup') {
- deleteProgramNode(e, layer, stage);
+ deleteProgramNode(e, tabManager.getActiveLayer(), stage);
stage.draw();
}
else if (e.target.name() == "isConnection") {
@@ -259,11 +278,12 @@ export var ContextMenu = {
let nodeType = e.target.innerHTML + " " + draggedVariableInfo.name;
let xx = e.target.parentElement.getBoundingClientRect().x - stage.getContainer().getBoundingClientRect().x;
let yy = e.target.parentElement.getBoundingClientRect().y - stage.getContainer().getBoundingClientRect().y;
+ let activeLayer = tabManager.getActiveLayer();
if (e.target.innerHTML == "Get") {
- Nodes.CreateNode(nodeType, { x: xx, y: yy }, layer, stage, "Get", draggedVariableInfo.dataType, null);
+ Nodes.CreateNode(nodeType, { x: xx, y: yy }, activeLayer, stage, "Get", draggedVariableInfo.dataType, null);
}
else {
- Nodes.CreateNode(nodeType, { x: xx, y: yy }, layer, stage, "Set", draggedVariableInfo.dataType, null);
+ Nodes.CreateNode(nodeType, { x: xx, y: yy }, activeLayer, stage, "Set", draggedVariableInfo.dataType, null);
}
});
@@ -278,7 +298,19 @@ export var ContextMenu = {
});
stage.getContainer().addEventListener('drop', (e) => {
e.preventDefault();
- if (e.dataTransfer.getData("variableName")) {
+ if (e.dataTransfer.getData("functionTabId")) {
+ const funcTabId = e.dataTransfer.getData("functionTabId");
+ const funcTab = tabManager.getTab(funcTabId);
+ if (funcTab) {
+ const containerRect = stage.getContainer().getBoundingClientRect();
+ const x = e.clientX - containerRect.x;
+ const y = e.clientY - containerRect.y;
+ const activeLayer = tabManager.getActiveLayer();
+ Nodes.CreateCallNode(funcTab.name, funcTab.inputParams, funcTab.outputParams,
+ { x, y }, activeLayer, stage, funcTab.docString || '');
+ activeLayer.draw();
+ }
+ } else if (e.dataTransfer.getData("variableName")) {
toggleGetSetCtxMenu([e.clientX, e.clientY], true);
draggedVariableInfo = {
name: e.dataTransfer.getData("variableName"),
@@ -287,6 +319,13 @@ export var ContextMenu = {
}
e.stopPropagation();
});
+
+ tabManager.on('tabSwitched', () => {
+ toggleContextMenu([0, 0], false);
+ toggleDeleteCtxMenu([], false);
+ toggleGetSetCtxMenu([], false);
+ ContextMenu.resetFilter();
+ });
}
}
diff --git a/src/js/editor/deleteHandler.js b/src/js/editor/deleteHandler.js
index d9d950a..9481894 100644
--- a/src/js/editor/deleteHandler.js
+++ b/src/js/editor/deleteHandler.js
@@ -5,6 +5,7 @@ export var Delete = {
}
export function deleteProgramNode(e, layer, stage) {
let node = e.target.getParent();
+ if (node.customClass && node.customClass.isDeletable === false) return;
deleteNodeByGroup(node, stage);
}
diff --git a/src/js/editor/nodeGroup.js b/src/js/editor/nodeGroup.js
index bfd9ba4..b5dce85 100644
--- a/src/js/editor/nodeGroup.js
+++ b/src/js/editor/nodeGroup.js
@@ -1,4 +1,5 @@
-import { colorMap } from '../core/colorMap.js'
+import { colorMap } from '../core/colorMap.js';
+import { tabManager } from './tabManager.js';
let placeLocation = function (location, stage) {
return {
@@ -183,7 +184,7 @@ function setupGroupDrag(grp, layer, stage) {
grp.on('dragstart', (e) => {
e.cancelBubble = true;
- wireLayer = stage.findOne('#wireLayer');
+ wireLayer = tabManager.getActiveWireLayer();
containedNodes = findContainedNodes(grp, layer);
nodeOffsets = containedNodes.map((node) => ({
node,
@@ -233,6 +234,7 @@ export function createNodeGroup(position, width, height, name, layer, stage) {
stroke: colorMap['GroupBorder'],
strokeWidth: 1.5,
cornerRadius: 6,
+ listening: false,
});
const titleBar = new Konva.Rect({
@@ -333,7 +335,7 @@ export function getGroupsData(layer) {
return groups;
}
-export function enableNodeGroups(stage, layer) {
+export function enableNodeGroups(stage) {
let isDrawing = false;
let startX = 0;
let startY = 0;
@@ -342,6 +344,7 @@ export function enableNodeGroups(stage, layer) {
stage.on('mousedown', (e) => {
if (!e.evt.shiftKey || e.target !== stage || e.evt.button !== 0) return;
+ const layer = tabManager.getActiveLayer();
isDrawing = true;
const pos = placeLocation(stage.getPointerPosition(), stage);
startX = pos.x;
@@ -365,6 +368,7 @@ export function enableNodeGroups(stage, layer) {
stage.on('mousemove', () => {
if (!isDrawing || !previewRect) return;
+ const layer = tabManager.getActiveLayer();
const pos = placeLocation(stage.getPointerPosition(), stage);
previewRect.setAttrs({
x: Math.min(startX, pos.x),
@@ -379,6 +383,7 @@ export function enableNodeGroups(stage, layer) {
if (!isDrawing || !previewRect) return;
isDrawing = false;
+ const layer = tabManager.getActiveLayer();
const w = previewRect.width();
const h = previewRect.height();
const x = previewRect.x();
diff --git a/src/js/editor/orphanOverlay.js b/src/js/editor/orphanOverlay.js
new file mode 100644
index 0000000..1e99a61
--- /dev/null
+++ b/src/js/editor/orphanOverlay.js
@@ -0,0 +1,85 @@
+import { deleteNodeByGroup } from './deleteHandler.js';
+
+/**
+ * Apply the same red overlay and "Deleted Var" / "Deleted Func" indicator used
+ * when a variable or function is deleted but nodes still reference it.
+ * @param {Konva.Group} grp - The program node group (must have customClass.bodyRect)
+ * @param {Konva.Layer} layer - Layer for draw()
+ * @param {Konva.Stage} stage - Stage for deleteNodeByGroup
+ * @param {string} [label='Deleted Var'] - Label text (e.g. 'Deleted Func' for call nodes)
+ */
+export function applyOrphanOverlay(grp, layer, stage, label = 'Deleted Var') {
+ const cc = grp.customClass;
+ if (!cc) return;
+ cc.isOrphaned = true;
+
+ const bodyRect = cc.bodyRect;
+ const overlay = new Konva.Rect({
+ x: bodyRect.x(),
+ y: bodyRect.y(),
+ width: bodyRect.width(),
+ height: bodyRect.height(),
+ fill: 'rgba(255, 0, 0, 0.25)',
+ cornerRadius: 5,
+ listening: false,
+ });
+ grp.add(overlay);
+
+ const boxWidth = 120;
+ const boxHeight = 36;
+ const boxX = bodyRect.width() / 2 - boxWidth / 2;
+ const boxY = -boxHeight - 5;
+
+ const indicatorGrp = new Konva.Group({ x: boxX, y: boxY });
+
+ const bg = new Konva.Rect({
+ width: boxWidth,
+ height: boxHeight,
+ fill: 'rgba(30, 0, 0, 0.85)',
+ cornerRadius: 3,
+ stroke: '#f44',
+ strokeWidth: 1,
+ });
+ indicatorGrp.add(bg);
+
+ const labelText = new Konva.Text({
+ text: label,
+ fontSize: 9,
+ fontFamily: 'Verdana',
+ fill: '#f88',
+ width: boxWidth,
+ align: 'center',
+ y: 3,
+ });
+ indicatorGrp.add(labelText);
+
+ const removeBtn = new Konva.Text({
+ text: 'Remove',
+ fontSize: 10,
+ fontFamily: 'Verdana',
+ fill: '#fff',
+ width: boxWidth,
+ align: 'center',
+ y: 18,
+ });
+ removeBtn.on('mouseenter', () => {
+ removeBtn.fill('#f44');
+ document.body.style.cursor = 'pointer';
+ if (layer) layer.draw();
+ });
+ removeBtn.on('mouseleave', () => {
+ removeBtn.fill('#fff');
+ document.body.style.cursor = 'default';
+ if (layer) layer.draw();
+ });
+ removeBtn.on('click', (e) => {
+ e.cancelBubble = true;
+ document.body.style.cursor = 'default';
+ deleteNodeByGroup(grp, stage);
+ });
+ indicatorGrp.add(removeBtn);
+
+ grp.add(indicatorGrp);
+ cc._orphanOverlay = overlay;
+ cc._orphanIndicator = indicatorGrp;
+}
diff --git a/src/js/editor/tabManager.js b/src/js/editor/tabManager.js
new file mode 100644
index 0000000..157e11e
--- /dev/null
+++ b/src/js/editor/tabManager.js
@@ -0,0 +1,273 @@
+import { showAlert, showConfirm } from '../ui/dialogs.js';
+
+const JS_RESERVED = new Set([
+ 'break','case','catch','continue','debugger','default','delete','do',
+ 'else','finally','for','function','if','in','instanceof','new','return',
+ 'switch','this','throw','try','typeof','var','void','while','with',
+ 'class','const','enum','export','extends','import','super','implements',
+ 'interface','let','package','private','protected','public','static','yield',
+ 'await','async','null','undefined','true','false','NaN','Infinity',
+]);
+
+export function isValidFunctionName(name) {
+ if (!name || name.length === 0) return false;
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) return false;
+ if (JS_RESERVED.has(name)) return false;
+ return true;
+}
+
+class TabManager {
+ constructor() {
+ this._stage = null;
+ this._tabs = new Map();
+ this._activeTabId = null;
+ this._nextId = 1;
+ this._listeners = {};
+ this._sharedDragLayer = null;
+ }
+
+ init(stage, mainLayer, mainWireLayer, dragLayer) {
+ this._stage = stage;
+ this._sharedDragLayer = dragLayer;
+ const mainTab = {
+ id: 'main',
+ name: 'Main',
+ type: 'main',
+ layer: mainLayer,
+ wireLayer: mainWireLayer,
+ inputParams: [],
+ outputParams: [],
+ beginNodeId: null,
+ returnNodeId: null,
+ variables: [],
+ };
+ this._tabs.set('main', mainTab);
+ this._activeTabId = 'main';
+ this._renderTabBar();
+ }
+
+ getActiveTab() { return this._tabs.get(this._activeTabId); }
+ getActiveTabId() { return this._activeTabId; }
+ getActiveLayer() { return this._tabs.get(this._activeTabId).layer; }
+ getActiveWireLayer() { return this._tabs.get(this._activeTabId).wireLayer; }
+ getDragLayer() { return this._sharedDragLayer; }
+ getStage() { return this._stage; }
+
+ getTab(id) { return this._tabs.get(id); }
+ getAllTabs() { return [...this._tabs.values()]; }
+ getAllFunctionTabs() { return [...this._tabs.values()].filter(t => t.type === 'function'); }
+
+ createTab(name) {
+ if (!isValidFunctionName(name)) {
+ showAlert('Invalid function name. Use letters, numbers, and underscores. Must start with a letter or underscore.');
+ return null;
+ }
+ if (this._isDuplicateName(name)) {
+ showAlert(`Function "${name}" already exists.`);
+ return null;
+ }
+
+ const id = `func_${this._nextId++}`;
+ const layer = new Konva.Layer({ id: `layer_${id}` });
+ const wireLayer = new Konva.Layer({ id: `wireLayer_${id}` });
+
+ this._stage.add(wireLayer);
+ this._stage.add(layer);
+ layer.moveToBottom();
+
+ layer.hide();
+ wireLayer.hide();
+
+ const tab = {
+ id,
+ name,
+ type: 'function',
+ layer,
+ wireLayer,
+ inputParams: [],
+ outputParams: [],
+ beginNodeId: null,
+ returnNodeId: null,
+ variables: [],
+ docString: '',
+ };
+
+ this._tabs.set(id, tab);
+ this._renderTabBar();
+ this.switchTab(id);
+ this._emit('tabCreated', tab);
+ return tab;
+ }
+
+ switchTab(id) {
+ if (!this._tabs.has(id) || id === this._activeTabId) return;
+
+ const prevTab = this._tabs.get(this._activeTabId);
+ prevTab.layer.hide();
+ prevTab.wireLayer.hide();
+
+ this._activeTabId = id;
+ const newTab = this._tabs.get(id);
+ newTab.layer.show();
+ newTab.wireLayer.show();
+
+ this._stage.draw();
+ this._updateTabBarSelection();
+ this._emit('tabSwitched', { from: prevTab, to: newTab });
+ }
+
+ renameTab(id, newName) {
+ if (id === 'main') return false;
+ if (!isValidFunctionName(newName)) {
+ showAlert('Invalid function name.');
+ return false;
+ }
+ if (this._isDuplicateName(newName, id)) {
+ showAlert(`Function "${newName}" already exists.`);
+ return false;
+ }
+
+ const tab = this._tabs.get(id);
+ if (!tab) return false;
+ const oldName = tab.name;
+ tab.name = newName;
+ this._renderTabBar();
+ this._emit('tabRenamed', { tab, oldName, newName });
+ return true;
+ }
+
+ closeTab(id) {
+ if (id === 'main') return false;
+ const tab = this._tabs.get(id);
+ if (!tab) return false;
+
+ if (this._activeTabId === id) {
+ this.switchTab('main');
+ }
+
+ tab.layer.destroy();
+ tab.wireLayer.destroy();
+ this._tabs.delete(id);
+
+ this._renderTabBar();
+ this._emit('tabClosed', tab);
+ this._stage.draw();
+ return true;
+ }
+
+ getFunctionDefinition(tabId) {
+ const tab = this._tabs.get(tabId);
+ if (!tab || tab.type !== 'function') return null;
+ return {
+ name: tab.name,
+ inputs: [...tab.inputParams],
+ outputs: [...tab.outputParams],
+ };
+ }
+
+ on(event, fn) {
+ if (!this._listeners[event]) this._listeners[event] = [];
+ this._listeners[event].push(fn);
+ }
+
+ off(event, fn) {
+ if (!this._listeners[event]) return;
+ this._listeners[event] = this._listeners[event].filter(f => f !== fn);
+ }
+
+ _emit(event, data) {
+ if (this._listeners[event]) {
+ this._listeners[event].forEach(fn => fn(data));
+ }
+ }
+
+ _isDuplicateName(name, excludeId) {
+ for (const [id, tab] of this._tabs) {
+ if (id !== excludeId && tab.name === name) return true;
+ }
+ return false;
+ }
+
+ _renderTabBar() {
+ const tabList = document.getElementById('tab-list');
+ if (!tabList) return;
+ tabList.innerHTML = '';
+
+ for (const [id, tab] of this._tabs) {
+ const tabEl = document.createElement('div');
+ tabEl.className = 'tab-item';
+ if (id === this._activeTabId) tabEl.classList.add('tab-active');
+ tabEl.dataset.tabId = id;
+
+ const nameEl = document.createElement('span');
+ nameEl.className = 'tab-name';
+ nameEl.textContent = tab.name;
+ tabEl.appendChild(nameEl);
+
+ if (tab.type === 'function') {
+ nameEl.addEventListener('dblclick', (e) => {
+ e.stopPropagation();
+ this._startRename(tabEl, tab);
+ });
+
+ const closeBtn = document.createElement('span');
+ closeBtn.className = 'tab-close';
+ closeBtn.textContent = '\u00D7';
+ closeBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ showConfirm(
+ `Close function "${tab.name}"? The function definition and its contents will be removed.`,
+ () => { this.closeTab(id); }
+ );
+ });
+ tabEl.appendChild(closeBtn);
+ }
+
+ tabEl.addEventListener('click', () => {
+ this.switchTab(id);
+ });
+
+ tabList.appendChild(tabEl);
+ }
+ }
+
+ _updateTabBarSelection() {
+ const tabList = document.getElementById('tab-list');
+ if (!tabList) return;
+ tabList.querySelectorAll('.tab-item').forEach(el => {
+ el.classList.toggle('tab-active', el.dataset.tabId === this._activeTabId);
+ });
+ }
+
+ _startRename(tabEl, tab) {
+ const nameEl = tabEl.querySelector('.tab-name');
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.className = 'tab-rename-input';
+ input.value = tab.name;
+
+ nameEl.replaceWith(input);
+ input.focus();
+ input.select();
+
+ let finished = false;
+ const doFinish = () => {
+ if (finished) return;
+ finished = true;
+ const newName = input.value.trim();
+ if (newName && newName !== tab.name) {
+ this.renameTab(tab.id, newName);
+ } else {
+ this._renderTabBar();
+ }
+ };
+
+ input.addEventListener('blur', doFinish);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
+ if (e.key === 'Escape') { input.value = tab.name; input.blur(); }
+ });
+ }
+}
+
+export const tabManager = new TabManager();
diff --git a/src/js/nodes/nodeFactory.js b/src/js/nodes/nodeFactory.js
index 3869127..fac9337 100644
--- a/src/js/nodes/nodeFactory.js
+++ b/src/js/nodes/nodeFactory.js
@@ -2,7 +2,11 @@ import { InputBox } from './nodeInputBox.js'
import { colorMap } from '../core/colorMap.js'
import { setLocationOfNode } from './nodePosition.js';
import { buildNodeDescription, hasType } from '../registry/index.js';
-import { deleteNodeByGroup } from '../editor/deleteHandler.js';
+import { deleteNodeByGroup, deleteWire } from '../editor/deleteHandler.js';
+import { tabManager } from '../editor/tabManager.js';
+import { addConnectionWire } from './wiring.js';
+import { applyMismatchToWire, isTypeCompatible } from '../utils/wireMismatch.js';
+
let placeLocation = function (location) {
//"this" is stage
return {
@@ -204,13 +208,13 @@ export var Nodes = {
return text;
},
optimizeDrag: function (grp, stage, layer) {
- let dragLayer = stage.findOne('#dragLayer');
- let wireLayer = stage.findOne('#wireLayer');
function moveWireToLayer(aWire, targetLayer) {
aWire.moveTo(targetLayer);
if (aWire._closeIndicator) aWire._closeIndicator.moveTo(targetLayer);
}
grp.on('dragstart', () => {
+ let dragLayer = tabManager.getDragLayer();
+ let wireLayer = tabManager.getActiveWireLayer();
grp.moveTo(dragLayer);
for (let each of grp.customClass.execInPins) {
for (let aWire of each.wire) {
@@ -235,6 +239,8 @@ export var Nodes = {
layer.draw();
})
grp.on('dragend', () => {
+ let dragLayer = tabManager.getDragLayer();
+ let wireLayer = tabManager.getActiveWireLayer();
grp.moveTo(layer);
for (let each of grp.customClass.execInPins) {
for (let aWire of each.wire) {
@@ -283,7 +289,12 @@ export var Nodes = {
});
if (nodeDescription.nodeTitle == 'Begin') {
this.grp.id('Begin');
+ } else if (nodeDescription.nodeTitle == 'FunctionBegin') {
+ this.grp.id('FunctionBegin');
+ } else if (nodeDescription.nodeTitle == 'Return') {
+ this.grp.id('Return');
}
+ this.isDeletable = nodeDescription.isDeletable !== false;
this.grp.customClass = this;
// this.grp.on('dblclick', (e) => {
// console.table(e.currentTarget.customClass);
@@ -306,7 +317,7 @@ export var Nodes = {
this.grp.add(borderRect);
let closeBtn = null;
- if (nodeDescription.nodeTitle !== 'Begin') {
+ if (nodeDescription.nodeTitle !== 'Begin' && nodeDescription.isDeletable !== false) {
const btnSize = 16;
const btnX = width - btnSize - 4;
const btnY = Math.round((23 - btnSize) / 2);
@@ -529,7 +540,359 @@ export var Nodes = {
}
if (!nodeDescription) return;
new this.ProgramNode(nodeDescription, location, layer, stage);
- }
+ },
+
+ buildFunctionBeginDescription: function (inputParams) {
+ const nd = {
+ nodeTitle: 'FunctionBegin',
+ execIn: false,
+ pinExecInId: null,
+ execOut: { execOut0: { execOutTitle: null, pinExecOutId: null, outOrder: 0 } },
+ color: 'FunctionBegin',
+ rows: 2,
+ colums: 12,
+ isDeletable: false,
+ };
+ if (inputParams && inputParams.length > 0) {
+ nd.outputs = {};
+ inputParams.forEach((p, i) => {
+ nd.outputs[`output${i}`] = {
+ outputTitle: p.name,
+ dataType: p.dataType,
+ pinOutId: null,
+ outOrder: i + 1,
+ };
+ });
+ nd.rows = Math.max(2, inputParams.length + 1);
+ }
+ return nd;
+ },
+
+ buildReturnDescription: function (outputParams) {
+ const nd = {
+ nodeTitle: 'Return',
+ execIn: true,
+ pinExecInId: null,
+ color: 'Return',
+ rows: 2,
+ colums: 12,
+ isDeletable: false,
+ };
+ if (outputParams && outputParams.length > 0) {
+ nd.inputs = {};
+ outputParams.forEach((p, i) => {
+ nd.inputs[`input${i}`] = {
+ inputTitle: p.name,
+ dataType: p.dataType,
+ defValue: null,
+ pinInId: null,
+ isInputBoxRequired: false,
+ };
+ });
+ nd.rows = Math.max(2, outputParams.length + 1);
+ }
+ return nd;
+ },
+
+ CreateFunctionBeginNode: function (inputParams, location, layer, stage) {
+ const nd = this.buildFunctionBeginDescription(inputParams);
+ const node = new this.ProgramNode(nd, location, layer, stage);
+ node.grp.id('FunctionBegin');
+ return node;
+ },
+
+ CreateReturnNode: function (outputParams, location, layer, stage) {
+ const nd = this.buildReturnDescription(outputParams);
+ const node = new this.ProgramNode(nd, location, layer, stage);
+ node.grp.id('Return');
+ return node;
+ },
+
+ rebuildFunctionBeginNode: function (node, inputParams, layer, stage, wireLayer) {
+ const programNode = node;
+ const grp = programNode.grp;
+ const oldNd = programNode.nodeDescription;
+ const oldOutputs = oldNd.outputs ? Object.keys(oldNd.outputs).sort().map(k => oldNd.outputs[k]) : [];
+
+ const savedOutputs = [];
+ for (let j = 0; j < programNode.outputPins.length; j++) {
+ const pin = programNode.outputPins[j];
+ const wires = [...(Array.isArray(pin.wire) ? pin.wire : (pin.wire ? [pin.wire] : []))].filter(Boolean);
+ savedOutputs[j] = wires.map(w => {
+ const destPin = w.attrs.dest;
+ const oldType = oldOutputs[j]?.dataType;
+ deleteWire(w);
+ return { destPin, oldType };
+ });
+ }
+
+ const pos = grp.position();
+ const grpId = grp.id();
+ grp.destroy();
+
+ const newNd = this.buildFunctionBeginDescription(inputParams);
+ const newNode = new this.ProgramNode(newNd, {
+ x: pos.x * stage.scaleX() + stage.x(),
+ y: pos.y * stage.scaleY() + stage.y()
+ }, layer, stage);
+ newNode.grp.id(grpId);
+ const newCC = newNode.grp.customClass;
+
+ const outPins = newNode.grp.find('.pin').filter(p => p.attrs.pinType === 'outp');
+ outPins.sort((a, b) => {
+ const ai = parseInt(String(a.attrs.helper || '').split('-')[1], 10) || 0;
+ const bi = parseInt(String(b.attrs.helper || '').split('-')[1], 10) || 0;
+ return ai - bi;
+ });
+
+ for (let j = 0; j < savedOutputs.length && j < (inputParams || []).length; j++) {
+ const newSrcPin = outPins[j];
+ if (!newSrcPin) continue;
+ for (const { destPin, oldType } of savedOutputs[j]) {
+ addConnectionWire(destPin, newSrcPin, stage, 1, wireLayer);
+ const newType = (inputParams || [])[j]?.dataType;
+ if (!isTypeCompatible(newType, destPin.attrs.pinDataType)) {
+ const inpIdx = destPin.attrs.helper ? parseInt(String(destPin.attrs.helper).split('-')[1], 10) : 0;
+ const wireToMark = destPin.getParent().customClass?.inputPins?.[inpIdx]?.wire;
+ if (wireToMark) applyMismatchToWire(wireToMark, wireLayer, stage);
+ }
+ }
+ }
+ return newNode;
+ },
+
+ buildCallNodeDescription: function (funcName, inputParams, outputParams, docString) {
+ const nd = {
+ nodeTitle: `Call ${funcName}`,
+ execIn: true,
+ pinExecInId: null,
+ execOut: { execOut0: { execOutTitle: null, pinExecOutId: null, outOrder: 0 } },
+ color: 'Call',
+ rows: 2,
+ colums: 14,
+ isCallFunction: true,
+ calledFunctionName: funcName,
+ docString: docString || '',
+ };
+ const maxPins = Math.max(
+ (inputParams ? inputParams.length : 0),
+ (outputParams ? outputParams.length : 0)
+ );
+ nd.rows = Math.max(2, maxPins + 1);
+ if (inputParams && inputParams.length > 0) {
+ nd.inputs = {};
+ const defByType = { Number: 0, Boolean: true, String: "'hello'", Array: '[]' };
+ inputParams.forEach((p, i) => {
+ const defVal = p.defValue != null ? p.defValue : defByType[p.dataType];
+ nd.inputs[`input${i}`] = {
+ inputTitle: p.name,
+ dataType: p.dataType,
+ defValue: defVal,
+ pinInId: null,
+ isInputBoxRequired: true,
+ };
+ });
+ }
+ if (outputParams && outputParams.length > 0) {
+ nd.outputs = {};
+ outputParams.forEach((p, i) => {
+ nd.outputs[`output${i}`] = {
+ outputTitle: p.name,
+ dataType: p.dataType,
+ pinOutId: null,
+ outOrder: i + 1,
+ };
+ });
+ }
+ return nd;
+ },
+
+ CreateCallNode: function (funcName, inputParams, outputParams, location, layer, stage, docString) {
+ const nd = this.buildCallNodeDescription(funcName, inputParams, outputParams, docString);
+ return new this.ProgramNode(nd, location, layer, stage);
+ },
+
+ updateCallNodesDocString: function (funcName, docString) {
+ const allTabs = tabManager.getAllTabs();
+ for (const tab of allTabs) {
+ const layer = tab.layer;
+ if (!layer) continue;
+ layer.find('.aProgramNodeGroup').forEach((grp) => {
+ const cc = grp.customClass;
+ if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== funcName) return;
+ cc.nodeDescription.docString = docString || '';
+ });
+ }
+ },
+
+ /**
+ * Update all Call nodes that call the given function with the current definition.
+ * Smart wire matching: preserve wires when params match by index; remove wires for
+ * deleted params; apply dashed mismatch when type changes.
+ */
+ updateCallNodesToDefinition: function (funcName, inputParams, outputParams, docString) {
+ const allTabs = tabManager.getAllTabs();
+ const stage = tabManager.getStage();
+ const inputParamsArr = inputParams || [];
+ const outputParamsArr = outputParams || [];
+
+ for (const tab of allTabs) {
+ const layer = tab.layer;
+ const wireLayer = tab.wireLayer;
+ if (!layer || !wireLayer) continue;
+
+ const toUpdate = [];
+ layer.find('.aProgramNodeGroup').forEach((grp) => {
+ const cc = grp.customClass;
+ if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== funcName) return;
+ if (cc.isOrphaned) return;
+ toUpdate.push({ grp, layer, wireLayer, programNode: cc });
+ });
+
+ for (const { grp, layer, wireLayer, programNode } of toUpdate) {
+ const nd = programNode.nodeDescription;
+ const oldInputs = nd.inputs ? Object.keys(nd.inputs).sort().map(k => nd.inputs[k]) : [];
+ const oldOutputs = nd.outputs ? Object.keys(nd.outputs).sort().map(k => nd.outputs[k]) : [];
+
+ const savedInputs = [];
+ for (let i = 0; i < programNode.inputPins.length; i++) {
+ const pin = programNode.inputPins[i];
+ if (pin.wire) {
+ savedInputs[i] = { srcPin: pin.wire.attrs.src, oldType: oldInputs[i]?.dataType };
+ deleteWire(pin.wire);
+ }
+ }
+
+ const savedOutputs = [];
+ for (let j = 0; j < programNode.outputPins.length; j++) {
+ const pin = programNode.outputPins[j];
+ const wires = [...(Array.isArray(pin.wire) ? pin.wire : (pin.wire ? [pin.wire] : []))].filter(Boolean);
+ savedOutputs[j] = wires.map(w => {
+ const destPin = w.attrs.dest;
+ const oldType = oldOutputs[j]?.dataType;
+ deleteWire(w);
+ return { destPin, oldType };
+ });
+ }
+
+ const savedExecIn = [];
+ if (programNode.execInPins && programNode.execInPins[0]) {
+ const wires = [...(programNode.execInPins[0].wire || [])].filter(Boolean);
+ for (const w of wires) {
+ savedExecIn.push({ srcPin: w.attrs.src });
+ deleteWire(w);
+ }
+ }
+
+ const savedExecOut = [];
+ if (programNode.execOutPins) {
+ for (const ep of programNode.execOutPins) {
+ if (ep.wire) {
+ savedExecOut.push({ destPin: ep.wire.attrs.dest });
+ deleteWire(ep.wire);
+ }
+ }
+ }
+
+ const pos = grp.position();
+ grp.destroy();
+
+ const newNode = Nodes.CreateCallNode(funcName, inputParamsArr, outputParamsArr, pos, layer, stage, docString || '');
+ const newCC = newNode.grp.customClass;
+
+ for (let i = 0; i < savedInputs.length && i < inputParamsArr.length; i++) {
+ const saved = savedInputs[i];
+ if (!saved) continue;
+ const newDestPin = newCC.inputPins[i]?.thisNode;
+ if (!newDestPin) continue;
+ addConnectionWire(newDestPin, saved.srcPin, stage, 1, wireLayer);
+ const newType = inputParamsArr[i]?.dataType;
+ if (!isTypeCompatible(saved.srcPin.attrs.pinDataType, newType)) {
+ const wire = newCC.inputPins[i].wire;
+ if (wire) applyMismatchToWire(wire, wireLayer, stage);
+ }
+ }
+
+ const outPins = newNode.grp.find('.pin').filter(p => p.attrs.pinType === 'outp');
+ outPins.sort((a, b) => {
+ const ai = parseInt(String(a.attrs.helper || '').split('-')[1], 10) || 0;
+ const bi = parseInt(String(b.attrs.helper || '').split('-')[1], 10) || 0;
+ return ai - bi;
+ });
+
+ for (let j = 0; j < savedOutputs.length && j < outputParamsArr.length; j++) {
+ const newSrcPin = outPins[j];
+ if (!newSrcPin) continue;
+ for (const { destPin, oldType } of savedOutputs[j]) {
+ addConnectionWire(destPin, newSrcPin, stage, 1, wireLayer);
+ const newType = outputParamsArr[j]?.dataType;
+ if (!isTypeCompatible(newType, destPin.attrs.pinDataType)) {
+ const inpIdx = destPin.attrs.helper ? parseInt(String(destPin.attrs.helper).split('-')[1], 10) : 0;
+ const wireToMark = destPin.getParent().customClass?.inputPins?.[inpIdx]?.wire;
+ if (wireToMark) applyMismatchToWire(wireToMark, wireLayer, stage);
+ }
+ }
+ }
+
+ for (const { srcPin } of savedExecIn) {
+ if (newCC.execInPins && newCC.execInPins[0]) {
+ const destPin = newCC.execInPins[0].thisNode;
+ addConnectionWire(destPin, srcPin, stage, 1, wireLayer);
+ }
+ }
+
+ for (let k = 0; k < savedExecOut.length && newCC.execOutPins && newCC.execOutPins[k]; k++) {
+ const { destPin } = savedExecOut[k];
+ const srcPin = newCC.execOutPins[k].thisNode;
+ addConnectionWire(destPin, srcPin, stage, 1, wireLayer);
+ }
+ }
+ }
+ if (stage) stage.draw();
+ },
+
+ rebuildReturnNode: function (node, outputParams, layer, stage, wireLayer) {
+ const programNode = node;
+ const grp = programNode.grp;
+ const oldNd = programNode.nodeDescription;
+ const oldInputs = oldNd.inputs ? Object.keys(oldNd.inputs).sort().map(k => oldNd.inputs[k]) : [];
+
+ const savedInputs = [];
+ for (let i = 0; i < programNode.inputPins.length; i++) {
+ const pin = programNode.inputPins[i];
+ if (pin.wire) {
+ savedInputs[i] = { srcPin: pin.wire.attrs.src, oldType: oldInputs[i]?.dataType };
+ deleteWire(pin.wire);
+ }
+ }
+
+ const pos = grp.position();
+ const grpId = grp.id();
+ grp.destroy();
+
+ const newNd = this.buildReturnDescription(outputParams);
+ const newNode = new this.ProgramNode(newNd, {
+ x: pos.x * stage.scaleX() + stage.x(),
+ y: pos.y * stage.scaleY() + stage.y()
+ }, layer, stage);
+ newNode.grp.id(grpId);
+ const newCC = newNode.grp.customClass;
+
+ const outputParamsArr = outputParams || [];
+ for (let i = 0; i < savedInputs.length && i < outputParamsArr.length; i++) {
+ const saved = savedInputs[i];
+ if (!saved) continue;
+ const newDestPin = newCC.inputPins[i]?.thisNode;
+ if (!newDestPin) continue;
+ addConnectionWire(newDestPin, saved.srcPin, stage, 1, wireLayer);
+ const newType = outputParamsArr[i]?.dataType;
+ if (!isTypeCompatible(saved.srcPin.attrs.pinDataType, newType)) {
+ const wire = newCC.inputPins[i].wire;
+ if (wire) applyMismatchToWire(wire, wireLayer, stage);
+ }
+ }
+ return newNode;
+ },
};
/*
diff --git a/src/js/nodes/wiring.js b/src/js/nodes/wiring.js
index 0b0cb72..d200002 100644
--- a/src/js/nodes/wiring.js
+++ b/src/js/nodes/wiring.js
@@ -1,5 +1,6 @@
import { deleteWire, deleteHalfWire } from '../editor/deleteHandler.js'
import { colorMap } from '../core/colorMap.js'
+import { tabManager } from '../editor/tabManager.js'
let placeLocation = function (location, stage) {
return {
x: (location.x - stage.x()) / stage.scaleX(),
@@ -7,7 +8,7 @@ let placeLocation = function (location, stage) {
};
}
export var Wiring = {
- enableWiring: function (stage, layer) {
+ enableWiring: function (stage) {
let currentPinType = null;
let currentPinDataType = null;
function isValidMatch(pinType, targetPinDataType) {
@@ -22,19 +23,18 @@ export var Wiring = {
let isWiring = false;
let src = null;
let dest = null;
- let wireLayer = new Konva.Layer({
- id: 'wireLayer',
- });
+ let activeWireLayer = null;
+ let activeNodeLayer = null;
let drawWire = null;
let potentialTarget = null;
let dir = 0;
let wireColor = null;
let originPreOccupied = null;
- stage.add(wireLayer);
- wireLayer.zIndex(0);
stage.on('mousedown', (e) => {
if (e.target.name() == 'pin' && e.evt.button == 0) {
+ activeWireLayer = tabManager.getActiveWireLayer();
+ activeNodeLayer = tabManager.getActiveLayer();
src = e.target;
currentPinType = e.target.attrs.pinType;
currentPinDataType = e.target.attrs.pinDataType;
@@ -61,8 +61,8 @@ export var Wiring = {
target: src,
});
setWirePoints(destLoc, srcLoc, dir, drawWire);
- wireLayer.add(drawWire);
- wireLayer.draw();
+ activeWireLayer.add(drawWire);
+ activeWireLayer.draw();
}
});
stage.on('mouseup', (e) => {
@@ -86,7 +86,7 @@ export var Wiring = {
let srcLoc = placeLocation(src.getAbsolutePosition(), stage);
let destLoc = placeLocation(stage.getPointerPosition(), stage);
setWirePoints(destLoc, srcLoc, dir, drawWire);
- wireLayer.draw();
+ activeWireLayer.draw();
}
})
@@ -97,7 +97,7 @@ export var Wiring = {
deleteHalfWire(drawWire, originPreOccupied);
if (e.target && src && e.target.name() == 'pin' && src != e.target && src.getParent() !== e.target.getParent() && isValidMatch(e.target.attrs.pinType, e.target.attrs.pinDataType)) {
dest = e.target;
- addConnectionWire(dest, src, stage, dir, wireLayer);
+ addConnectionWire(dest, src, stage, dir, activeWireLayer);
}
src.getParent().draggable(true);
src = null;
@@ -109,8 +109,8 @@ export var Wiring = {
currentPinType = null;
currentPinDataType = null;
wireColor = null;
- wireLayer.draw();
- layer.draw();
+ activeWireLayer.draw();
+ activeNodeLayer.draw();
}
}
else {
@@ -126,8 +126,8 @@ export var Wiring = {
currentPinType = null;
currentPinDataType = null;
wireColor = null;
- wireLayer.draw();
- layer.draw();
+ activeWireLayer.draw();
+ activeNodeLayer.draw();
}
}
}
diff --git a/src/js/persistence/saveAndLoad.js b/src/js/persistence/saveAndLoad.js
index 60218b2..1a06fd6 100644
--- a/src/js/persistence/saveAndLoad.js
+++ b/src/js/persistence/saveAndLoad.js
@@ -3,6 +3,7 @@ import { addConnectionWire } from '../nodes/wiring.js'
import { variableList } from '../ui/variableList.js'
import { showAlert, vscriptOnLoad } from '../ui/dialogs.js'
import { getGroupsData, createNodeGroup } from '../editor/nodeGroup.js'
+import { tabManager } from '../editor/tabManager.js'
function writeError(err, msg) {
document.getElementById("console-window").classList.toggle("hidden", false);
let codeDoc = document.getElementById("console").contentWindow.document;
@@ -33,52 +34,81 @@ let placeLocation = function (location) {
y: (location.y - this.y()) / this.scaleY()
};
}
+function serializeLayerData(layer, wireLayer) {
+ const nodesData = [];
+ const wireData = [];
+ layer.find('.aProgramNodeGroup').forEach((node) => {
+ if (node.name() === 'aProgramNodeGroup') {
+ nodesData.push({
+ position: node.position(),
+ nodeDescription: node.customClass.nodeDescription,
+ });
+ }
+ });
+ wireLayer.find('.isConnection').forEach((aWire) => {
+ if (aWire.name() === 'isConnection') {
+ wireData.push({
+ srcId: aWire.attrs.src.id(),
+ destId: aWire.attrs.dest.id(),
+ });
+ }
+ });
+ return { nodesData, wireData };
+}
+
+function buildFullExportData(layer, wireLayer) {
+ const { nodesData, wireData } = serializeLayerData(layer, wireLayer);
+ const mainTab = tabManager.getTab('main');
+ const globalVars = mainTab ? mainTab.variables : variableList.variables;
+ const funcTabs = tabManager.getAllFunctionTabs();
+ const functions = funcTabs.map(ft => {
+ const ftData = serializeLayerData(ft.layer, ft.wireLayer);
+ return {
+ name: ft.name,
+ inputParams: ft.inputParams.map(p => ({ name: p.name, dataType: p.dataType, defValue: p.defValue })),
+ outputParams: ft.outputParams.map(p => ({ name: p.name, dataType: p.dataType })),
+ variables: ft.variables.map(v => ({ name: v.name, dataType: v.dataType, value: v.value })),
+ docString: ft.docString || '',
+ nodesData: ftData.nodesData,
+ wireData: ftData.wireData,
+ groupsData: getGroupsData(ft.layer),
+ };
+ });
+ return {
+ variables: globalVars,
+ nodesData,
+ wireData,
+ groupsData: getGroupsData(layer),
+ functions,
+ };
+}
+
export class Export {
constructor(stage, layer, wireLayer) {
document.getElementById('export').addEventListener("click", (e) => {
- let exportScript = [];
- let nodesData = [];
- let wireData = [];
- layer.find('.aProgramNodeGroup').forEach((node, index) => {
- if (node.name() == 'aProgramNodeGroup') {
- let nodeData = {
- position: node.position(),
- nodeDescription: node.customClass.nodeDescription,
- };
- nodesData.push(nodeData);
- }
- });
- wireLayer.find('.isConnection').forEach((aWire, index) => {
- if (aWire.name() == 'isConnection') {
- let wireD = {
- srcId: aWire.attrs.src.id(),
- destId: aWire.attrs.dest.id(),
- }
- wireData.push(wireD);
- }
- })
- exportScript = {
- variables: variableList.variables,
- nodesData: nodesData,
- wireData: wireData,
- groupsData: getGroupsData(layer),
- }
+ const exportScript = buildFullExportData(layer, wireLayer);
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportScript));
let exportAnchorElem = document.getElementById('exportAnchorElem');
exportAnchorElem.setAttribute("href", dataStr);
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
exportAnchorElem.setAttribute("download", `wireScript_${ts}.json`);
exportAnchorElem.click();
-
- // console.log(JSON.stringify(exportScript));
- // console.log(JSON.parse(JSON.stringify(exportScript)));
});
}
}
export function refresh(layer, wireLayer) {
+ const funcTabs = tabManager.getAllFunctionTabs();
+ for (const ft of [...funcTabs]) {
+ tabManager.closeTab(ft.id);
+ }
+ if (tabManager.getActiveTabId() !== 'main') {
+ tabManager.switchTab('main');
+ }
layer.destroyChildren();
wireLayer.destroyChildren();
variableList.deleteAllVariables();
+ const mainTab = tabManager.getTab('main');
+ if (mainTab) mainTab.variables = variableList.variables;
layer.draw();
wireLayer.draw();
}
@@ -100,33 +130,7 @@ export class Import {
export class Save {
constructor(stage, layer, wireLayer) {
document.getElementById('save').addEventListener("click", (e) => {
- let exportScript = [];
- let nodesData = [];
- let wireData = [];
- layer.find('.aProgramNodeGroup').forEach((node, index) => {
- if (node.name() == 'aProgramNodeGroup') {
- let nodeData = {
- position: node.position(),
- nodeDescription: node.customClass.nodeDescription,
- };
- nodesData.push(nodeData);
- }
- });
- wireLayer.find('.isConnection').forEach((aWire, index) => {
- if (aWire.name() == 'isConnection') {
- let wireD = {
- srcId: aWire.attrs.src.id(),
- destId: aWire.attrs.dest.id(),
- }
- wireData.push(wireD);
- }
- })
- exportScript = {
- variables: variableList.variables,
- nodesData: nodesData,
- wireData: wireData,
- groupsData: getGroupsData(layer),
- }
+ const exportScript = buildFullExportData(layer, wireLayer);
localStorage.setItem('lastLoadWireScriptJSON', JSON.stringify(exportScript));
let savingWindow = document.getElementById("saving");
// let importMenu = document.getElementById("import-menu");
@@ -173,49 +177,85 @@ export function prompLastSave(stage, layer, wireLayer) {
}
}
-function printContent(json, stage, layer, wireLayer) {
- for (let aNode of json.nodesData) {
+function loadLayerContent(nodesData, wireData, targetLayer, wireLayer, stage) {
+ for (let aNode of nodesData) {
try {
- new Nodes.ProgramNode(aNode.nodeDescription, { x: aNode.position.x * stage.scaleX() + stage.x(), y: aNode.position.y * stage.scaleY() + stage.y() }, layer, stage);
-
- }
- catch (err) {
+ new Nodes.ProgramNode(aNode.nodeDescription,
+ { x: aNode.position.x * stage.scaleX() + stage.x(), y: aNode.position.y * stage.scaleY() + stage.y() },
+ targetLayer, stage);
+ } catch (err) {
writeError(err, "Error Occurred In Importing The JSON(Node Description Not Valid)");
}
}
- // let X = layer.findOne('Group');
- // console.log(layer.children);
- // console.log(X);
- for (let aWire of json.wireData) {
- // console.log(`${aWire.srcId}`, `${aWire.destId}`);
- let src = layer.findOne(`#${aWire.srcId}`);
- let dest = layer.findOne(`#${aWire.destId}`);
- // console.log(src, dest);
+ for (let aWire of wireData) {
+ let src = targetLayer.findOne(`#${aWire.srcId}`);
+ let dest = targetLayer.findOne(`#${aWire.destId}`);
try {
addConnectionWire(dest, src, stage, 1, wireLayer);
- }
- catch (err) {
+ } catch (err) {
writeError(err, "Error Occurred In Importing The JSON(Wire Data Not Valid)");
}
}
+}
+
+function printContent(json, stage, layer, wireLayer) {
+ loadLayerContent(json.nodesData, json.wireData, layer, wireLayer, stage);
+
+ const mainTab = tabManager.getTab('main');
for (let aVariable of json.variables) {
try {
variableList.addVariable(aVariable);
- }
- catch (err) {
+ } catch (err) {
writeError(err, "Error Occurred In Importing The JSON(Variable Data Not Valid)");
}
}
+ if (mainTab) mainTab.variables = variableList.variables;
if (json.groupsData) {
for (let g of json.groupsData) {
try {
createNodeGroup(g.position, g.width, g.height, g.name, layer, stage);
- }
- catch (err) {
+ } catch (err) {
writeError(err, "Error Occurred In Importing The JSON(Group Data Not Valid)");
}
}
}
+
+ if (json.functions && json.functions.length > 0) {
+ for (const funcData of json.functions) {
+ try {
+ tabManager._suppressAutoNodes = true;
+ const tab = tabManager.createTab(funcData.name);
+ tabManager._suppressAutoNodes = false;
+ if (!tab) continue;
+
+ tab.inputParams = funcData.inputParams || [];
+ tab.outputParams = funcData.outputParams || [];
+ tab.variables = funcData.variables || [];
+ tab.docString = funcData.docString || '';
+
+ loadLayerContent(funcData.nodesData, funcData.wireData, tab.layer, tab.wireLayer, stage);
+
+ const beginNode = tab.layer.findOne('#FunctionBegin');
+ if (beginNode) tab.beginNodeId = beginNode.id();
+ const returnNode = tab.layer.findOne('#Return');
+ if (returnNode) tab.returnNodeId = returnNode.id();
+
+ if (funcData.groupsData) {
+ for (let g of funcData.groupsData) {
+ createNodeGroup(g.position, g.width, g.height, g.name, tab.layer, stage);
+ }
+ }
+
+ tab.layer.draw();
+ tab.wireLayer.draw();
+ } catch (err) {
+ tabManager._suppressAutoNodes = false;
+ writeError(err, "Error Occurred In Importing The JSON(Function Data Not Valid)");
+ }
+ }
+ tabManager.switchTab('main');
+ }
+
layer.draw();
wireLayer.draw();
}
diff --git a/src/js/ui/dialogs.js b/src/js/ui/dialogs.js
index 10bebc3..d7a0b7c 100644
--- a/src/js/ui/dialogs.js
+++ b/src/js/ui/dialogs.js
@@ -97,11 +97,20 @@ export function prompRefreshOrStarter(type, stage) {
}) ;
}
}
-export function vscriptOnLoad(stage) {
- // stage.setScale({x: 0.5, y: 0.5});
- // const starterFile = { "variables": [{ "name": "coolStuff", "dataType": "String", "value": "'https://www.youtube.com/watch?v=dQw4w9WgXcQ'" }], "nodesData": [{ "position": { "x": 285.76949478124993, "y": 160.00829895214838 }, "nodeDescription": { "nodeTitle": "Begin", "execIn": false, "pinExecInId": null, "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "10", "outOrder": 0 } }, "color": "Begin", "rows": 2, "colums": 10 } }, { "position": { "x": 501.3872296698097, "y": 161.2761797590843 }, "nodeDescription": { "nodeTitle": "Confirm", "execIn": true, "pinExecInId": "62", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "63", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Message", "dataType": "String", "defValue": "'u wanna see some cool stuff ?'", "pinInId": "64" } }, "outputs": { "output0": { "outputTitle": "Ok?", "dataType": "Boolean", "pinOutId": "69", "outOrder": 1 } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1216.4116108593755, "y": 150.01528070898422 }, "nodeDescription": { "nodeTitle": "OpenWindow", "execIn": true, "pinExecInId": "16", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "17", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "18" } }, "outputs": { "output0": { "outputTitle": "Success?", "dataType": "Boolean", "pinOutId": "23", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1203.519704023151, "y": 382.23448287513907 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "101", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "102", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'u missed some nice stuff'", "pinInId": "103" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1202.448197106272, "y": 520.6776970135538 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "115", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "116", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'After closing this dialog, right click on the editor to get bunch of new nodes and create your own scripts :)'", "pinInId": "117" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 773.9416135561129, "y": 211.52728595257065 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "78", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "79", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "81", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "83", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "85" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 1162.8208273281257, "y": 280.7125038125001 }, "nodeDescription": { "nodeTitle": "Get coolStuff", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "32", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }], "wireData": [{ "srcId": "10", "destId": "62" }, { "srcId": "63", "destId": "78" }, { "srcId": "79", "destId": "16" }, { "srcId": "81", "destId": "101" }, { "srcId": "83", "destId": "115" }, { "srcId": "69", "destId": "85" }, { "srcId": "32", "destId": "18" }] };
- const starterFile = { "variables": [{ "name": "coolStuff", "dataType": "String", "value": "'https://www.youtube.com/watch?v=hOHKltAiKXQ'" }, { "name": "fib0", "dataType": "Number", "value": "0" }, { "name": "fib1", "dataType": "Number", "value": "1" }, { "name": "tmp", "dataType": "Number", "value": "0" }, { "name": "fibArray", "dataType": "Array", "value": "[]" }, { "name": "catFactsApi", "dataType": "String", "value": "'https://catfact.ninja/fact'" }], "nodesData": [{ "position": { "x": 640.1957355310791, "y": 1149.6123184434612 }, "nodeDescription": { "nodeTitle": "Confirm", "execIn": true, "pinExecInId": "62", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "63", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Message", "dataType": "String", "defValue": "'u wanna see some cool stuff ?'", "pinInId": "64" } }, "outputs": { "output0": { "outputTitle": "Ok?", "dataType": "Boolean", "pinOutId": "69", "outOrder": 1 } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 943.0808918774987, "y": 1154.2878748323233 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "78", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "79", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "81", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "83", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "85" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 624.8490952267291, "y": 76.13062296509307 }, "nodeDescription": { "nodeTitle": "ForLoop", "pinExecInId": "168", "execIn": true, "execOut": { "execOut0": { "execOutTitle": "Loop Body", "pinExecOutId": "169", "outOrder": 0 }, "execOut1": { "execOutTitle": "Completed", "pinExecOutId": "171", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "From", "dataType": "Number", "defValue": 0, "pinInId": "173" }, "input1": { "inputTitle": "To(Excl)", "dataType": "Number", "defValue": 10, "pinInId": "178" }, "input2": { "inputTitle": "Increment", "dataType": "Number", "defValue": 1, "pinInId": "183" } }, "outputs": { "output0": { "outputTitle": "Index", "dataType": "Number", "pinOutId": "188", "outOrder": 1 } }, "color": "Logic", "rows": 2, "colums": 12 } }, { "position": { "x": 1062.358745795078, "y": 9.64809972519835 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "197", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "198", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'hello'", "pinInId": "199" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 882.7650007538189, "y": 159.85163279515638 }, "nodeDescription": { "nodeTitle": "Get fib0", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "161", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1236.2691694516263, "y": 163.34281389620685 }, "nodeDescription": { "nodeTitle": "Add", "inputs": { "input0": { "inputTitle": "ValueA", "dataType": "Number", "defValue": 0, "pinInId": "240" }, "input1": { "inputTitle": "ValueB", "dataType": "Number", "defValue": 0, "pinInId": "245" } }, "outputs": { "output0": { "outputTitle": "Result", "dataType": "Number", "pinOutId": "250", "outOrder": 0 } }, "color": "Math", "rows": 2, "colums": 10 } }, { "position": { "x": 935.1522585885414, "y": 253.74975644182052 }, "nodeDescription": { "nodeTitle": "Get fib1", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "231", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1296.8417070314447, "y": 16.433630516210982 }, "nodeDescription": { "nodeTitle": "Set tmp", "execIn": true, "pinExecInId": "213", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "214", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "215" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "220", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1833.5365239733978, "y": 18.884241374940608 }, "nodeDescription": { "nodeTitle": "Set fib1", "execIn": true, "pinExecInId": "288", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "289", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "290" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "295", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1547.3197219286844, "y": -149.4043843715472 }, "nodeDescription": { "nodeTitle": "Set fib0", "execIn": true, "pinExecInId": "263", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "264", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "265" } }, "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "270", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 360.3458171820312, "y": 74.17517317011703 }, "nodeDescription": { "nodeTitle": "Begin", "execIn": false, "pinExecInId": null, "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "10", "outOrder": 0 } }, "color": "Begin", "rows": 2, "colums": 10 } }, { "position": { "x": 1551.216765448137, "y": 5.548682440772666 }, "nodeDescription": { "nodeTitle": "Get fib1", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "Number", "pinOutId": "279", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1512.771776695504, "y": 1416.9201863915812 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "115", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "116", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'After closing this dialog, right click on the editor to get bunch of new nodes and create your own scripts :)'", "pinInId": "117" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1561.0501231968399, "y": 1251.0411440014332 }, "nodeDescription": { "nodeTitle": "Alert", "execIn": true, "pinExecInId": "101", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "102", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'u missed some nice stuff'", "pinInId": "103" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1821.096442061649, "y": 1241.9167263753077 }, "nodeDescription": { "nodeTitle": "Get coolStuff", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "32", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1902.6031409132486, "y": 1076.0798713887261 }, "nodeDescription": { "nodeTitle": "OpenWindow", "execIn": true, "pinExecInId": "16", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "17", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "18" } }, "outputs": { "output0": { "outputTitle": "Success?", "dataType": "Boolean", "pinOutId": "23", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 1338.2444748633134, "y": 543.136740532748 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "334", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "335", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'hello'", "pinInId": "336" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 669.8079691212556, "y": 560.617817270978 }, "nodeDescription": { "nodeTitle": "HttpRequest", "execIn": true, "pinExecInId": "306", "execOut": { "execOut0": { "execOutTitle": "OnSuccess", "pinExecOutId": "307", "outOrder": 0 }, "execOut1": { "execOutTitle": "OnFail", "pinExecOutId": "309", "outOrder": 2 }, "execOut2": { "execOutTitle": "Continue", "pinExecOutId": "311", "outOrder": 3 } }, "inputs": { "input0": { "inputTitle": "URL", "dataType": "String", "defValue": "'link'", "pinInId": "313" } }, "outputs": { "output0": { "outputTitle": "JSON", "dataType": "Data", "pinOutId": "318", "outOrder": 1 } }, "color": "Func", "rows": 2, "colums": 12 } }, { "position": { "x": 445.32242406433573, "y": 657.8024685377316 }, "nodeDescription": { "nodeTitle": "Get catFactsApi", "outputs": { "output0": { "outputTitle": "Value(Ref)", "dataType": "String", "pinOutId": "325", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 10 } }, { "position": { "x": 1355.211745865666, "y": 786.6439536440772 }, "nodeDescription": { "nodeTitle": "Print", "execIn": true, "pinExecInId": "352", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "353", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Value", "dataType": "Data", "defValue": "'API REQUEST UNSUCCESSFUL'", "pinInId": "354" } }, "color": "Print", "rows": 3, "colums": 12 } }, { "position": { "x": 1096.6222103758923, "y": 617.8530961704762 }, "nodeDescription": { "nodeTitle": "GetByName(JSON)", "inputs": { "input0": { "inputTitle": "JSON", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "368" }, "input1": { "inputTitle": "Name", "dataType": "String", "defValue": "'fact'", "pinInId": "370" } }, "outputs": { "output0": { "outputTitle": "Data", "dataType": "Data", "pinOutId": "375", "outOrder": 0 } }, "color": "Get", "rows": 2, "colums": 12 } }, { "position": { "x": 3719.492065534509, "y": 45.67011863451796 }, "nodeDescription": { "nodeTitle": "If/Else", "execIn": true, "pinExecInId": "407", "execOut": { "execOut0": { "execOutTitle": "True", "pinExecOutId": "408", "outOrder": 0 }, "execOut1": { "execOutTitle": "False", "pinExecOutId": "410", "outOrder": 1 }, "execOut2": { "execOutTitle": "Done", "pinExecOutId": "412", "outOrder": 2 } }, "inputs": { "input0": { "inputTitle": "Bool", "dataType": "Boolean", "defValue": true, "pinInId": "414" } }, "color": "Logic", "rows": 3, "colums": 12 } }, { "position": { "x": 3497.0899677450816, "y": 121.65556858361023 }, "nodeDescription": { "nodeTitle": "ForEachLoop", "pinExecInId": "442", "execIn": true, "execOut": { "execOut0": { "execOutTitle": "Loop Body", "pinExecOutId": "443", "outOrder": 0 }, "execOut1": { "execOutTitle": "Completed", "pinExecOutId": "445", "outOrder": 4 } }, "inputs": { "input0": { "inputTitle": "Array", "dataType": "Array", "isInputBoxRequired": false, "pinInId": "447" } }, "outputs": { "output0": { "outputTitle": "Value", "dataType": "Data", "pinOutId": "449", "outOrder": 1 }, "output1": { "outputTitle": "Index", "dataType": "Number", "pinOutId": "451", "outOrder": 2 }, "output2": { "outputTitle": "Array", "dataType": "Array", "pinOutId": "453", "outOrder": 3 } }, "color": "Logic", "rows": 2, "colums": 12 } }, { "position": { "x": 3755.240342745082, "y": 248.99431858361027 }, "nodeDescription": { "nodeTitle": "BinarySearch(Num)", "inputs": { "input0": { "inputTitle": "Value", "dataType": "Number", "defValue": 0, "pinInId": "424" }, "input1": { "inputTitle": "Array", "dataType": "Array", "isInputBoxRequired": false, "pinInId": "429" } }, "outputs": { "output0": { "outputTitle": "Exist", "dataType": "Boolean", "pinOutId": "431", "outOrder": 0 }, "output1": { "outputTitle": "Lower Bound", "dataType": "Number", "pinOutId": "433", "outOrder": 1 }, "output2": { "outputTitle": "Upper Bound", "dataType": "Number", "pinOutId": "435", "outOrder": 2 } }, "color": "Func", "rows": 2, "colums": 13 } }, { "position": { "x": 4023.809342745082, "y": 103.13356858361016 }, "nodeDescription": { "nodeTitle": "Continue", "execIn": true, "pinExecInId": "460", "color": "Logic", "rows": 2, "colums": 10 } }, { "position": { "x": 4030.755092745082, "y": 188.79781858361025 }, "nodeDescription": { "nodeTitle": "Break", "execIn": true, "pinExecInId": "466", "color": "Logic", "rows": 2, "colums": 10 } }, { "position": { "x": 4043.538201260707, "y": 288.14623690444165 }, "nodeDescription": { "nodeTitle": "Random", "outputs": { "output0": { "outputTitle": "Random[0,1)", "dataType": "Number", "pinOutId": "472", "outOrder": 0 } }, "color": "Math", "rows": 2, "colums": 10 } }, { "position": { "x": 4047.9482012607077, "y": 365.32127055007163 }, "nodeDescription": { "nodeTitle": "Swap", "execIn": true, "pinExecInId": "479", "execOut": { "execOut0": { "execOutTitle": null, "pinExecOutId": "480", "outOrder": 0 } }, "inputs": { "input0": { "inputTitle": "Ref1", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "481" }, "input1": { "inputTitle": "Ref2", "dataType": "Data", "isInputBoxRequired": false, "pinInId": "483" } }, "outputs": { "output0": { "outputTitle": "Ref1", "dataType": "Data", "pinOutId": "485", "outOrder": 1 }, "output1": { "outputTitle": "Ref2", "dataType": "Data", "pinOutId": "487", "outOrder": 2 } }, "color": "Func", "rows": 2, "colums": 12 } }], "wireData": [{ "srcId": "10", "destId": "168" }, { "srcId": "63", "destId": "78" }, { "srcId": "69", "destId": "85" }, { "srcId": "169", "destId": "197" }, { "srcId": "161", "destId": "199" }, { "srcId": "161", "destId": "240" }, { "srcId": "231", "destId": "245" }, { "srcId": "198", "destId": "213" }, { "srcId": "250", "destId": "215" }, { "srcId": "220", "destId": "290" }, { "srcId": "214", "destId": "263" }, { "srcId": "264", "destId": "288" }, { "srcId": "279", "destId": "265" }, { "srcId": "83", "destId": "115" }, { "srcId": "81", "destId": "101" }, { "srcId": "79", "destId": "16" }, { "srcId": "32", "destId": "18" }, { "srcId": "307", "destId": "334" }, { "srcId": "325", "destId": "313" }, { "srcId": "309", "destId": "352" }, { "srcId": "318", "destId": "368" }, { "srcId": "375", "destId": "336" }] }
- let starterJSON = JSON.stringify(starterFile);
- new Import(stage, stage.findOne("#main_layer"), stage.findOne("#wireLayer"), starterJSON);
+const STARTER_FILE_PATH = 'assets/starter.json';
+
+export async function vscriptOnLoad(stage) {
+ const layer = stage.findOne('#main_layer');
+ const wireLayer = stage.findOne('#wireLayer');
+ try {
+ const res = await fetch(STARTER_FILE_PATH);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const starterJSON = await res.text();
+ new Import(stage, layer, wireLayer, starterJSON);
+ } catch (err) {
+ console.error('Failed to load starter.json:', err);
+ showAlert('Could not load starter.json. Using empty project.');
+ refresh(layer, wireLayer);
+ }
}
diff --git a/src/js/ui/functionPanel.js b/src/js/ui/functionPanel.js
new file mode 100644
index 0000000..f8c2334
--- /dev/null
+++ b/src/js/ui/functionPanel.js
@@ -0,0 +1,522 @@
+import { colorMap } from '../core/colorMap.js';
+import { tabManager, isValidFunctionName } from '../editor/tabManager.js';
+import { applyOrphanOverlay } from '../editor/orphanOverlay.js';
+import { showAlert } from './dialogs.js';
+import { Nodes } from '../nodes/nodeFactory.js';
+import { showDocTooltip, hideDocTooltip } from '../utils/docTooltip.js';
+
+const DATA_TYPES = ['Number', 'Boolean', 'String', 'Array'];
+
+function createDefaultValueInput(dataType, currentValue) {
+ if (dataType === 'Boolean') {
+ const sel = document.createElement('select');
+ sel.className = 'param-def-value';
+ const optT = document.createElement('option');
+ optT.value = 'true';
+ optT.textContent = 'True';
+ const optF = document.createElement('option');
+ optF.value = 'false';
+ optF.textContent = 'False';
+ sel.appendChild(optT);
+ sel.appendChild(optF);
+ if (currentValue === 'false') optF.selected = true;
+ return sel;
+ }
+ const inp = document.createElement('input');
+ inp.className = 'param-def-value';
+ inp.type = dataType === 'Number' ? 'number' : 'text';
+ if (dataType === 'Number') inp.value = currentValue != null ? currentValue : 0;
+ else if (dataType === 'String') {
+ let display = currentValue != null ? currentValue : '';
+ if (typeof display === 'string' && display.startsWith("'") && display.endsWith("'")) display = display.slice(1, -1);
+ inp.value = display;
+ } else if (dataType === 'Array') inp.value = currentValue || '[]';
+ inp.placeholder = dataType === 'Array' ? '[1, 2, 3]' : 'Default value';
+ return inp;
+}
+
+function extractDefValue(dataType, el) {
+ const raw = el.value;
+ if (dataType === 'Boolean') return raw === 'true';
+ if (dataType === 'Number') return raw.toString();
+ if (dataType === 'String') return `'${raw}'`;
+ if (dataType === 'Array') return raw;
+ return raw;
+}
+
+function buildParamForm(sectionId, onAdd, options = {}) {
+ const { includeDefaultValue: includeDef = false } = options;
+ const content = document.getElementById(sectionId);
+ if (!content) return;
+ content.innerHTML = '';
+
+ const sectionInner = document.createElement('div');
+ sectionInner.className = 'left-panel-section-inner';
+
+ const form = document.createElement('div');
+ form.className = 'var-inline-edit';
+
+ const typeSelect = document.createElement('select');
+ for (const t of DATA_TYPES) {
+ const opt = document.createElement('option');
+ opt.value = t;
+ opt.textContent = t;
+ typeSelect.appendChild(opt);
+ }
+
+ const nameInput = document.createElement('input');
+ nameInput.type = 'text';
+ nameInput.placeholder = 'Name';
+
+ const defValueContainer = document.createElement('div');
+ defValueContainer.className = 'param-def-value-container';
+ let defValueEl = null;
+ if (includeDef) {
+ defValueEl = createDefaultValueInput(typeSelect.value, null);
+ defValueContainer.appendChild(defValueEl);
+ typeSelect.addEventListener('change', () => {
+ defValueContainer.innerHTML = '';
+ defValueEl = createDefaultValueInput(typeSelect.value);
+ defValueContainer.appendChild(defValueEl);
+ });
+ }
+
+ const actionsDiv = document.createElement('div');
+ actionsDiv.classList.add('var-edit-actions');
+ const addBtn = document.createElement('button');
+ addBtn.classList.add('var-edit-save');
+ addBtn.textContent = 'Add';
+ addBtn.addEventListener('click', () => {
+ const name = nameInput.value.trim();
+ const dataType = typeSelect.value;
+ if (!name) { showAlert("Parameter name can't be empty!"); return; }
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
+ showAlert("Parameter name must be a valid identifier"); return;
+ }
+ const param = { name, dataType };
+ if (includeDef && defValueEl) {
+ param.defValue = extractDefValue(dataType, defValueEl);
+ }
+ onAdd(param);
+ nameInput.value = '';
+ if (includeDef && defValueEl) {
+ defValueContainer.innerHTML = '';
+ defValueEl = createDefaultValueInput(typeSelect.value, null);
+ defValueContainer.appendChild(defValueEl);
+ }
+ });
+ actionsDiv.appendChild(addBtn);
+
+ form.appendChild(typeSelect);
+ form.appendChild(nameInput);
+ if (includeDef) form.appendChild(defValueContainer);
+ form.appendChild(actionsDiv);
+ sectionInner.appendChild(form);
+
+ const listEl = document.createElement('ul');
+ listEl.className = 'param-list';
+ sectionInner.appendChild(listEl);
+ content.appendChild(sectionInner);
+
+ return listEl;
+}
+
+function renderParamList(listEl, params, onRemove, onEdit) {
+ listEl.innerHTML = '';
+ params.forEach((param, idx) => {
+ const li = document.createElement('li');
+ li.className = 'left-panel-variable';
+ li.style.borderWidth = '2px';
+ li.style.borderStyle = 'solid';
+ li.style.borderColor = colorMap[param.dataType] || '#fff';
+ li.style.boxShadow = `inset 0px 0px 5px ${colorMap[param.dataType] || '#fff'}`;
+ li.style.backgroundColor = 'transparent';
+
+ const nameSpan = document.createElement('span');
+ nameSpan.className = 'var-name-text';
+ nameSpan.textContent = param.name;
+ nameSpan.style.overflow = 'hidden';
+ nameSpan.style.textOverflow = 'ellipsis';
+ nameSpan.style.whiteSpace = 'nowrap';
+ nameSpan.style.flex = '1';
+ li.appendChild(nameSpan);
+
+ const typeSpan = document.createElement('span');
+ typeSpan.style.fontSize = '0.85rem';
+ typeSpan.style.opacity = '0.6';
+ typeSpan.style.marginLeft = '0.3rem';
+ typeSpan.textContent = param.dataType;
+ li.appendChild(typeSpan);
+
+ if (param.defValue != null && param.defValue !== '') {
+ const defSpan = document.createElement('span');
+ defSpan.style.fontSize = '0.75rem';
+ defSpan.style.opacity = '0.5';
+ defSpan.style.marginLeft = '0.3rem';
+ const display = typeof param.defValue === 'string' && param.defValue.startsWith("'") && param.defValue.endsWith("'")
+ ? param.defValue.slice(1, -1) : String(param.defValue);
+ defSpan.textContent = `= ${display}`;
+ defSpan.title = 'Default value';
+ li.appendChild(defSpan);
+ }
+
+ const actionsDiv = document.createElement('div');
+ actionsDiv.classList.add('var-actions');
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.classList.add('var-delete-btn');
+ deleteBtn.innerHTML = '';
+ deleteBtn.title = 'Remove parameter';
+ deleteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ onRemove(idx);
+ });
+ actionsDiv.appendChild(deleteBtn);
+ li.appendChild(actionsDiv);
+
+ li.addEventListener('mouseover', () => {
+ li.style.boxShadow = `inset 0px 0px 30px ${colorMap[param.dataType] || '#fff'}`;
+ });
+ li.addEventListener('mouseleave', () => {
+ li.style.boxShadow = `inset 0px 0px 5px ${colorMap[param.dataType] || '#fff'}`;
+ });
+
+ listEl.appendChild(li);
+ });
+}
+
+let inputListEl = null;
+let outputListEl = null;
+
+function refreshInputList() {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function' || !inputListEl) return;
+ renderParamList(inputListEl, tab.inputParams, (idx) => {
+ tab.inputParams.splice(idx, 1);
+ refreshInputList();
+ tabManager._emit('functionParamsChanged', { tab, paramType: 'input' });
+ });
+}
+
+function refreshOutputList() {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function' || !outputListEl) return;
+ renderParamList(outputListEl, tab.outputParams, (idx) => {
+ tab.outputParams.splice(idx, 1);
+ refreshOutputList();
+ tabManager._emit('functionParamsChanged', { tab, paramType: 'output' });
+ });
+}
+
+export function initFunctionPanel() {
+ document.getElementById('input-params').addEventListener('click', () => {
+ document.getElementById('input-params-content').classList.toggle('hidden');
+ document.getElementById('input-params-arrow').classList.toggle('left-panel-tab-arrow-up');
+ });
+
+ document.getElementById('output-params').addEventListener('click', () => {
+ document.getElementById('output-params-content').classList.toggle('hidden');
+ document.getElementById('output-params-arrow').classList.toggle('left-panel-tab-arrow-up');
+ });
+
+ document.getElementById('doc-string').addEventListener('click', () => {
+ document.getElementById('doc-string-content').classList.toggle('hidden');
+ document.getElementById('doc-string-arrow').classList.toggle('left-panel-tab-arrow-up');
+ });
+
+ const docStringInput = document.getElementById('doc-string-input');
+ if (docStringInput) {
+ docStringInput.addEventListener('input', () => {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function') return;
+ tab.docString = docStringInput.value.trim();
+ Nodes.updateCallNodesDocString(tab.name, tab.docString);
+ tabManager._emit('functionDocChanged', { tab });
+ });
+ }
+
+ const updateCallersBtn = document.getElementById('update-callers-btn');
+ if (updateCallersBtn) {
+ updateCallersBtn.addEventListener('click', () => {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function') return;
+ Nodes.updateCallNodesToDefinition(tab.name, tab.inputParams, tab.outputParams, tab.docString || '');
+ });
+ }
+
+ document.getElementById('list-of-functions').addEventListener('click', () => {
+ document.getElementById('list-of-functions-content').classList.toggle('hidden');
+ document.getElementById('list-of-functions-down-icon').classList.toggle('left-panel-tab-arrow-up');
+ });
+
+ inputListEl = buildParamForm('input-params-content', (param) => {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function') return;
+ if (tab.inputParams.some(p => p.name === param.name)) {
+ showAlert('Input parameter already exists'); return;
+ }
+ tab.inputParams.push(param);
+ refreshInputList();
+ tabManager._emit('functionParamsChanged', { tab, paramType: 'input' });
+ }, { includeDefaultValue: true });
+
+ outputListEl = buildParamForm('output-params-content', (param) => {
+ const tab = tabManager.getActiveTab();
+ if (!tab || tab.type !== 'function') return;
+ if (tab.outputParams.some(p => p.name === param.name)) {
+ showAlert('Output parameter already exists'); return;
+ }
+ tab.outputParams.push(param);
+ refreshOutputList();
+ tabManager._emit('functionParamsChanged', { tab, paramType: 'output' });
+ });
+
+ tabManager.on('tabSwitched', ({ to }) => {
+ updatePanelVisibility(to);
+ });
+
+ tabManager.on('tabCreated', (tab) => {
+ updatePanelVisibility(tab);
+ if (tab.type === 'function' && !tabManager._suppressAutoNodes) {
+ createFunctionNodes(tab);
+ }
+ refreshFunctionsList();
+ refreshContextMenuFunctions();
+ });
+
+ tabManager.on('tabClosed', (tab) => {
+ if (tab && tab.type === 'function') {
+ const stage = tabManager.getStage();
+ const remainingTabs = tabManager.getAllTabs();
+ for (const t of remainingTabs) {
+ const layer = t.layer;
+ if (!layer) continue;
+ layer.find('.aProgramNodeGroup').forEach((grp) => {
+ const cc = grp.customClass;
+ if (!cc || !cc.nodeDescription || !cc.nodeDescription.isCallFunction || cc.nodeDescription.calledFunctionName !== tab.name) return;
+ if (cc.isOrphaned) return;
+ applyOrphanOverlay(grp, layer, stage, 'Deleted Func');
+ });
+ }
+ if (stage) stage.draw();
+ }
+ refreshFunctionsList();
+ refreshContextMenuFunctions();
+ });
+
+ tabManager.on('tabRenamed', () => {
+ refreshFunctionsList();
+ refreshContextMenuFunctions();
+ });
+
+ tabManager.on('functionParamsChanged', ({ tab, paramType }) => {
+ syncFunctionNodes(tab, paramType);
+ Nodes.updateCallNodesToDefinition(tab.name, tab.inputParams, tab.outputParams, tab.docString || '');
+ refreshFunctionsList();
+ refreshContextMenuFunctions();
+ });
+
+ tabManager.on('functionDocChanged', () => {
+ refreshFunctionsList();
+ });
+}
+
+function createFunctionNodes(tab) {
+ const stage = tabManager.getStage();
+ const containerRect = stage.container().getBoundingClientRect();
+ const beginX = containerRect.width * 0.2;
+ const beginY = containerRect.height * 0.3;
+ const returnX = containerRect.width * 0.7;
+ const returnY = containerRect.height * 0.3;
+
+ const beginNode = Nodes.CreateFunctionBeginNode(
+ tab.inputParams, { x: beginX, y: beginY }, tab.layer, stage
+ );
+ tab.beginNodeId = beginNode.grp.id();
+
+ const returnNode = Nodes.CreateReturnNode(
+ tab.outputParams, { x: returnX, y: returnY }, tab.layer, stage
+ );
+ tab.returnNodeId = returnNode.grp.id();
+}
+
+function syncFunctionNodes(tab, paramType) {
+ const stage = tabManager.getStage();
+ const wireLayer = tab.wireLayer;
+
+ if (paramType === 'input') {
+ const beginGrp = tab.layer.findOne('#FunctionBegin') || tab.layer.findOne(`#${tab.beginNodeId}`);
+ if (beginGrp && beginGrp.customClass) {
+ const newNode = Nodes.rebuildFunctionBeginNode(beginGrp.customClass, tab.inputParams, tab.layer, stage, wireLayer);
+ tab.beginNodeId = newNode.grp.id();
+ }
+ } else if (paramType === 'output') {
+ const returnGrp = tab.layer.findOne('#Return') || tab.layer.findOne(`#${tab.returnNodeId}`);
+ if (returnGrp && returnGrp.customClass) {
+ const newNode = Nodes.rebuildReturnNode(returnGrp.customClass, tab.outputParams, tab.layer, stage, wireLayer);
+ tab.returnNodeId = newNode.grp.id();
+ }
+ }
+}
+
+function updatePanelVisibility(tab) {
+ const inputSection = document.getElementById('input-params-section');
+ const outputSection = document.getElementById('output-params-section');
+ const docSection = document.getElementById('doc-string-section');
+ const docStringInput = document.getElementById('doc-string-input');
+ const updateCallersSection = document.getElementById('update-callers-section');
+
+ if (tab.type === 'function') {
+ inputSection.classList.remove('hidden');
+ outputSection.classList.remove('hidden');
+ if (docSection) docSection.classList.remove('hidden');
+ if (updateCallersSection) updateCallersSection.classList.remove('hidden');
+ if (docStringInput) docStringInput.value = tab.docString || '';
+ refreshInputList();
+ refreshOutputList();
+ } else {
+ inputSection.classList.add('hidden');
+ outputSection.classList.add('hidden');
+ if (docSection) docSection.classList.add('hidden');
+ if (updateCallersSection) updateCallersSection.classList.add('hidden');
+ }
+}
+
+function refreshFunctionsList() {
+ const listEl = document.getElementById('function-list');
+ if (!listEl) return;
+ listEl.innerHTML = '';
+
+ const funcTabs = tabManager.getAllFunctionTabs();
+ for (const ft of funcTabs) {
+ const li = document.createElement('li');
+ li.className = 'left-panel-variable';
+ li.style.borderWidth = '2px';
+ li.style.borderStyle = 'solid';
+ li.style.borderColor = colorMap['Call'] || '#00bfa5';
+ li.style.boxShadow = `inset 0px 0px 5px ${colorMap['Call'] || '#00bfa5'}`;
+ li.style.backgroundColor = 'transparent';
+ li.setAttribute('draggable', 'true');
+
+ const nameSpan = document.createElement('span');
+ nameSpan.className = 'var-name-text';
+ nameSpan.textContent = ft.name;
+ nameSpan.style.overflow = 'hidden';
+ nameSpan.style.textOverflow = 'ellipsis';
+ nameSpan.style.whiteSpace = 'nowrap';
+ nameSpan.style.flex = '1';
+ li.appendChild(nameSpan);
+
+ let docIcon = null;
+ if (ft.docString) {
+ docIcon = document.createElement('span');
+ docIcon.className = 'function-list-doc-icon';
+ docIcon.textContent = 'i';
+ docIcon.title = 'Documentation';
+ li.appendChild(docIcon);
+ }
+
+ const infoSpan = document.createElement('span');
+ infoSpan.style.fontSize = '0.8rem';
+ infoSpan.style.opacity = '0.5';
+ infoSpan.style.marginLeft = '0.3rem';
+ const inCount = ft.inputParams.length;
+ const outCount = ft.outputParams.length;
+ infoSpan.textContent = `(${inCount}→${outCount})`;
+ li.appendChild(infoSpan);
+
+ li.addEventListener('mouseover', () => {
+ li.style.boxShadow = `inset 0px 0px 30px ${colorMap['Call'] || '#00bfa5'}`;
+ });
+ li.addEventListener('mouseleave', () => {
+ li.style.boxShadow = `inset 0px 0px 5px ${colorMap['Call'] || '#00bfa5'}`;
+ if (docIcon) hideDocTooltip(docIcon);
+ });
+
+ if (docIcon) {
+ li.addEventListener('mouseenter', (e) => {
+ showDocTooltip(docIcon, ft.docString);
+ });
+ }
+
+ li.addEventListener('dblclick', () => {
+ tabManager.switchTab(ft.id);
+ });
+
+ li.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('functionTabId', ft.id);
+ e.dataTransfer.setData('functionName', ft.name);
+ });
+
+ listEl.appendChild(li);
+ }
+}
+
+function refreshContextMenuFunctions() {
+ let section = document.getElementById('context-menu-functions-section');
+ const contextMenu = document.getElementById('context-menu');
+ if (!contextMenu) return;
+
+ if (section) {
+ section.remove();
+ }
+
+ const funcTabs = tabManager.getAllFunctionTabs();
+ if (funcTabs.length === 0) return;
+
+ section = document.createElement('div');
+ section.className = 'ctx-menu-section';
+ section.id = 'context-menu-functions-section';
+ section.dataset.category = 'Functions';
+
+ const callColor = colorMap['Call'] || '#00bfa5';
+ const header = document.createElement('div');
+ header.className = 'ctx-menu-section-header';
+ header.style.borderLeftColor = callColor;
+ header.style.color = callColor;
+
+ const arrow = document.createElement('span');
+ arrow.className = 'ctx-menu-section-arrow';
+ arrow.textContent = '\u25BC';
+ const label = document.createElement('span');
+ label.textContent = 'Functions';
+ header.appendChild(arrow);
+ header.appendChild(label);
+
+ header.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const collapsed = section.classList.toggle('ctx-menu-section--collapsed');
+ arrow.textContent = collapsed ? '\u25B6' : '\u25BC';
+ });
+
+ const body = document.createElement('div');
+ body.className = 'ctx-menu-section-body';
+
+ for (const ft of funcTabs) {
+ const item = document.createElement('div');
+ item.className = 'context-menu-items';
+ item.textContent = `Call ${ft.name}`;
+ item.dataset.functionTabId = ft.id;
+ item.style.borderLeftColor = callColor;
+ item.style.color = callColor;
+
+ item.addEventListener('click', () => {
+ const stage = tabManager.getStage();
+ const activeLayer = tabManager.getActiveLayer();
+ const rect = item.getBoundingClientRect();
+ const containerRect = stage.container().getBoundingClientRect();
+ const x = rect.x - containerRect.x;
+ const y = rect.y - containerRect.y;
+ Nodes.CreateCallNode(ft.name, ft.inputParams, ft.outputParams,
+ { x, y }, activeLayer, stage, ft.docString || '');
+ activeLayer.draw();
+ document.getElementById('ctx-menu-container').classList.add('hidden');
+ });
+
+ body.appendChild(item);
+ }
+
+ section.appendChild(header);
+ section.appendChild(body);
+ contextMenu.appendChild(section);
+}
diff --git a/src/js/ui/variableList.js b/src/js/ui/variableList.js
index 6659db3..dcf4cb6 100644
--- a/src/js/ui/variableList.js
+++ b/src/js/ui/variableList.js
@@ -1,7 +1,10 @@
import { colorMap, lightenHex } from '../core/colorMap.js'
import { ContextMenu } from '../editor/contextMenu.js'
import { deleteNodeByGroup, deleteWire } from '../editor/deleteHandler.js'
+import { applyOrphanOverlay } from '../editor/orphanOverlay.js'
+import { applyMismatchToWire, removeMismatchFromWire } from '../utils/wireMismatch.js'
import { showAlert, showConfirm } from './dialogs.js'
+import { tabManager } from '../editor/tabManager.js'
class VariableList {
@@ -161,78 +164,7 @@ class VariableList {
}
_applyOrphanOverlay(grp) {
- const cc = grp.customClass;
- cc.isOrphaned = true;
-
- const bodyRect = cc.bodyRect;
- const overlay = new Konva.Rect({
- x: bodyRect.x(),
- y: bodyRect.y(),
- width: bodyRect.width(),
- height: bodyRect.height(),
- fill: 'rgba(255, 0, 0, 0.25)',
- cornerRadius: 5,
- listening: false,
- });
- grp.add(overlay);
-
- const boxWidth = 120;
- const boxHeight = 36;
- const boxX = bodyRect.width() / 2 - boxWidth / 2;
- const boxY = -boxHeight - 5;
-
- const indicatorGrp = new Konva.Group({ x: boxX, y: boxY });
-
- const bg = new Konva.Rect({
- width: boxWidth,
- height: boxHeight,
- fill: 'rgba(30, 0, 0, 0.85)',
- cornerRadius: 3,
- stroke: '#f44',
- strokeWidth: 1,
- });
- indicatorGrp.add(bg);
-
- const label = new Konva.Text({
- text: 'Deleted Var',
- fontSize: 9,
- fontFamily: 'Verdana',
- fill: '#f88',
- width: boxWidth,
- align: 'center',
- y: 3,
- });
- indicatorGrp.add(label);
-
- const removeBtn = new Konva.Text({
- text: 'Remove',
- fontSize: 10,
- fontFamily: 'Verdana',
- fill: '#fff',
- width: boxWidth,
- align: 'center',
- y: 18,
- });
- removeBtn.on('mouseenter', () => {
- removeBtn.fill('#f44');
- document.body.style.cursor = 'pointer';
- this.layer.draw();
- });
- removeBtn.on('mouseleave', () => {
- removeBtn.fill('#fff');
- document.body.style.cursor = 'default';
- this.layer.draw();
- });
- removeBtn.on('click', (e) => {
- e.cancelBubble = true;
- document.body.style.cursor = 'default';
- deleteNodeByGroup(grp, this.stage);
- });
- indicatorGrp.add(removeBtn);
-
- grp.add(indicatorGrp);
- cc._orphanOverlay = overlay;
- cc._orphanIndicator = indicatorGrp;
+ applyOrphanOverlay(grp, this.layer, this.stage, 'Deleted Var');
}
// ---- EDIT FLOW ----
@@ -516,7 +448,7 @@ class VariableList {
if (this.layer) this.layer.draw();
if (this.stage) {
- const wireLayer = this.stage.findOne('#wireLayer');
+ const wireLayer = tabManager.getActiveWireLayer();
if (wireLayer) wireLayer.draw();
}
}
@@ -525,7 +457,7 @@ class VariableList {
_markMismatchedWires(grp, newDataType) {
const cc = grp.customClass;
- const wireLayer = this.stage ? this.stage.findOne('#wireLayer') : null;
+ const wireLayer = this.stage ? tabManager.getActiveWireLayer() : null;
if (!wireLayer) return;
for (const pin of cc.inputPins) {
@@ -533,9 +465,9 @@ class VariableList {
const srcPin = pin.wire.attrs.src;
const srcType = srcPin && srcPin.attrs.pinDataType;
if (srcType && srcType !== 'Data' && newDataType !== 'Data' && srcType !== newDataType) {
- this._applyMismatchToWire(pin.wire, wireLayer);
+ applyMismatchToWire(pin.wire, wireLayer, this.stage);
} else {
- this._removeMismatchFromWire(pin.wire);
+ removeMismatchFromWire(pin.wire);
}
}
}
@@ -547,127 +479,48 @@ class VariableList {
const destPin = w.attrs.dest;
const destType = destPin && destPin.attrs.pinDataType;
if (destType && destType !== 'Data' && newDataType !== 'Data' && destType !== newDataType) {
- this._applyMismatchToWire(w, wireLayer);
+ applyMismatchToWire(w, wireLayer, this.stage);
} else {
- this._removeMismatchFromWire(w);
+ removeMismatchFromWire(w);
}
}
}
}
}
- _applyMismatchToWire(wire, wireLayer) {
- if (wire.isMismatched) return;
- wire.isMismatched = true;
- wire.dash([10, 5]);
-
- const r = 8;
- const indicatorGrp = new Konva.Group({ x: 0, y: 0 });
-
- const bg = new Konva.Circle({
- radius: r,
- fill: 'rgba(50, 20, 0, 0.9)',
- stroke: '#ff8800',
- strokeWidth: 1.5,
- });
- indicatorGrp.add(bg);
-
- const cross1 = new Konva.Line({
- points: [-4, -4, 4, 4],
- stroke: '#fff',
- strokeWidth: 2,
- lineCap: 'round',
- });
- const cross2 = new Konva.Line({
- points: [-4, 4, 4, -4],
- stroke: '#fff',
- strokeWidth: 2,
- lineCap: 'round',
- });
- indicatorGrp.add(cross1);
- indicatorGrp.add(cross2);
-
- const tooltip = new Konva.Label({ x: r + 4, y: -10, visible: false });
- tooltip.add(new Konva.Tag({
- fill: 'rgba(30, 10, 0, 0.9)',
- cornerRadius: 3,
- stroke: '#ff8800',
- strokeWidth: 0.5,
- }));
- tooltip.add(new Konva.Text({
- text: 'Type Mismatch',
- fontSize: 10,
- fontFamily: 'Verdana',
- fill: '#ffaa44',
- padding: 4,
- }));
- indicatorGrp.add(tooltip);
-
- indicatorGrp.on('mouseenter', () => {
- bg.fill('#ff4400');
- cross1.stroke('#fff');
- cross2.stroke('#fff');
- tooltip.visible(true);
- document.body.style.cursor = 'pointer';
- wireLayer.draw();
- });
- indicatorGrp.on('mouseleave', () => {
- bg.fill('rgba(50, 20, 0, 0.9)');
- tooltip.visible(false);
- document.body.style.cursor = 'default';
- wireLayer.draw();
- });
- indicatorGrp.on('click', (e) => {
- e.cancelBubble = true;
- document.body.style.cursor = 'default';
- indicatorGrp.destroy();
- deleteWire(wire);
- if (this.stage) this.stage.draw();
+ deleteAllVariables() {
+ this.variables.length = 0;
+ document.getElementById("variable-list").innerHTML = '';
+ this.variablesElements.forEach((item) => {
+ item.el.remove();
});
-
- const updatePosition = () => {
- const pts = wire.points();
- if (pts.length >= 4) {
- indicatorGrp.x((pts[0] + pts[pts.length - 2]) / 2);
- indicatorGrp.y((pts[1] + pts[pts.length - 1]) / 2);
- }
- };
- updatePosition();
-
- const srcNode = wire.attrs.src?.getParent();
- const destNode = wire.attrs.dest?.getParent();
- if (srcNode) srcNode.on('dragmove.mismatch', updatePosition);
- if (destNode) destNode.on('dragmove.mismatch', updatePosition);
- wire._mismatchDragCleanup = () => {
- if (srcNode) srcNode.off('dragmove.mismatch');
- if (destNode) destNode.off('dragmove.mismatch');
- };
-
- wireLayer.add(indicatorGrp);
- wire._mismatchIndicator = indicatorGrp;
- }
-
- _removeMismatchFromWire(wire) {
- if (!wire.isMismatched) return;
- wire.isMismatched = false;
- wire.dash([]);
- if (wire._mismatchDragCleanup) {
- wire._mismatchDragCleanup();
- wire._mismatchDragCleanup = null;
- }
- if (wire._mismatchIndicator) {
- wire._mismatchIndicator.destroy();
- wire._mismatchIndicator = null;
- }
+ this.variablesElements = [];
}
- deleteAllVariables() {
- this.variables = [];
+ switchToTab(tabVariables) {
document.getElementById("variable-list").innerHTML = '';
this.variablesElements.forEach((item) => {
item.el.remove();
});
this.variablesElements = [];
+
+ this.variables = tabVariables;
+
+ for (const variable of this.variables) {
+ const el = this.makeLeftPanelVariableListItem(variable);
+ document.getElementById("variable-list").appendChild(el);
+
+ const set = this.makeContextMenuItem(variable, 'set');
+ const get = this.makeContextMenuItem(variable, 'get');
+ const variablesBody = document.getElementById("context-menu-variables-body");
+ const container = variablesBody || document.getElementById("context-menu");
+ container.appendChild(get);
+ container.appendChild(set);
+ ContextMenu.addEventToCtxMenuItems(set);
+ ContextMenu.addEventToCtxMenuItems(get);
+ this.variablesElements.push({ name: variable.name, type: 'get', el: get });
+ this.variablesElements.push({ name: variable.name, type: 'set', el: set });
+ }
}
}
diff --git a/src/js/utils/docTooltip.js b/src/js/utils/docTooltip.js
new file mode 100644
index 0000000..1338cef
--- /dev/null
+++ b/src/js/utils/docTooltip.js
@@ -0,0 +1,46 @@
+/**
+ * Doc tooltip for DOM elements (e.g. function list info icon).
+ * Uses the same CSS class as call-node-doc-tooltip.
+ */
+let _el = null;
+let _owner = null;
+
+function getOrCreateEl() {
+ if (_el) return _el;
+ const el = document.createElement('div');
+ el.className = 'call-node-doc-tooltip';
+ el.setAttribute('aria-hidden', 'true');
+ document.body.appendChild(el);
+ _el = el;
+ return el;
+}
+
+export function showDocTooltip(anchorEl, docString) {
+ if (!docString || !anchorEl) return;
+ const el = getOrCreateEl();
+ _owner = anchorEl;
+ el.textContent = docString;
+ el.style.display = 'block';
+ el.style.visibility = 'hidden';
+
+ const rect = anchorEl.getBoundingClientRect();
+ const tw = el.offsetWidth;
+ const th = el.offsetHeight;
+ const gap = 8;
+ let left = rect.left + (rect.width / 2) - (tw / 2);
+ let top = rect.top - th - gap;
+ left = Math.max(6, Math.min(left, document.documentElement.clientWidth - tw - 6));
+ top = Math.max(6, Math.min(top, document.documentElement.clientHeight - th - 6));
+ el.style.left = Math.round(left) + 'px';
+ el.style.top = Math.round(top) + 'px';
+ el.style.visibility = 'visible';
+}
+
+export function hideDocTooltip(anchorEl) {
+ if (_owner !== anchorEl) return;
+ _owner = null;
+ if (_el) {
+ _el.style.display = 'none';
+ _el.textContent = '';
+ }
+}
diff --git a/src/js/utils/wireMismatch.js b/src/js/utils/wireMismatch.js
new file mode 100644
index 0000000..d5f0ac3
--- /dev/null
+++ b/src/js/utils/wireMismatch.js
@@ -0,0 +1,127 @@
+import { deleteWire } from '../editor/deleteHandler.js';
+import { tabManager } from '../editor/tabManager.js';
+
+/**
+ * Check if two data types are compatible (no mismatch).
+ * Data type is universal; otherwise types must match.
+ */
+export function isTypeCompatible(srcType, destType) {
+ if (!srcType || !destType) return true;
+ if (srcType === 'Data' || destType === 'Data') return true;
+ return srcType === destType;
+}
+
+/**
+ * Apply dashed line and type-mismatch indicator to a wire.
+ * @param {Konva.Line} wire
+ * @param {Konva.Layer} wireLayer
+ * @param {Konva.Stage} [stage] - for draw() after delete
+ */
+export function applyMismatchToWire(wire, wireLayer, stage) {
+ if (wire.isMismatched) return;
+ wire.isMismatched = true;
+ wire.dash([10, 5]);
+
+ const r = 8;
+ const indicatorGrp = new Konva.Group({ x: 0, y: 0 });
+
+ const bg = new Konva.Circle({
+ radius: r,
+ fill: 'rgba(50, 20, 0, 0.9)',
+ stroke: '#ff8800',
+ strokeWidth: 1.5,
+ });
+ indicatorGrp.add(bg);
+
+ const cross1 = new Konva.Line({
+ points: [-4, -4, 4, 4],
+ stroke: '#fff',
+ strokeWidth: 2,
+ lineCap: 'round',
+ });
+ const cross2 = new Konva.Line({
+ points: [-4, 4, 4, -4],
+ stroke: '#fff',
+ strokeWidth: 2,
+ lineCap: 'round',
+ });
+ indicatorGrp.add(cross1);
+ indicatorGrp.add(cross2);
+
+ const tooltip = new Konva.Label({ x: r + 4, y: -10, visible: false });
+ tooltip.add(new Konva.Tag({
+ fill: 'rgba(30, 10, 0, 0.9)',
+ cornerRadius: 3,
+ stroke: '#ff8800',
+ strokeWidth: 0.5,
+ }));
+ tooltip.add(new Konva.Text({
+ text: 'Type Mismatch',
+ fontSize: 10,
+ fontFamily: 'Verdana',
+ fill: '#ffaa44',
+ padding: 4,
+ }));
+ indicatorGrp.add(tooltip);
+
+ indicatorGrp.on('mouseenter', () => {
+ bg.fill('#ff4400');
+ cross1.stroke('#fff');
+ cross2.stroke('#fff');
+ tooltip.visible(true);
+ document.body.style.cursor = 'pointer';
+ wireLayer.draw();
+ });
+ indicatorGrp.on('mouseleave', () => {
+ bg.fill('rgba(50, 20, 0, 0.9)');
+ tooltip.visible(false);
+ document.body.style.cursor = 'default';
+ wireLayer.draw();
+ });
+ indicatorGrp.on('click', (e) => {
+ e.cancelBubble = true;
+ document.body.style.cursor = 'default';
+ indicatorGrp.destroy();
+ deleteWire(wire);
+ const s = stage || (wireLayer && wireLayer.getStage()) || tabManager.getStage();
+ if (s) s.draw();
+ });
+
+ const updatePosition = () => {
+ const pts = wire.points();
+ if (pts.length >= 4) {
+ indicatorGrp.x((pts[0] + pts[pts.length - 2]) / 2);
+ indicatorGrp.y((pts[1] + pts[pts.length - 1]) / 2);
+ }
+ };
+ updatePosition();
+
+ const srcNode = wire.attrs.src?.getParent();
+ const destNode = wire.attrs.dest?.getParent();
+ if (srcNode) srcNode.on('dragmove.mismatch', updatePosition);
+ if (destNode) destNode.on('dragmove.mismatch', updatePosition);
+ wire._mismatchDragCleanup = () => {
+ if (srcNode) srcNode.off('dragmove.mismatch');
+ if (destNode) destNode.off('dragmove.mismatch');
+ };
+
+ wireLayer.add(indicatorGrp);
+ wire._mismatchIndicator = indicatorGrp;
+}
+
+/**
+ * Remove mismatch styling from a wire.
+ */
+export function removeMismatchFromWire(wire) {
+ if (!wire.isMismatched) return;
+ wire.isMismatched = false;
+ wire.dash([]);
+ if (wire._mismatchDragCleanup) {
+ wire._mismatchDragCleanup();
+ wire._mismatchDragCleanup = null;
+ }
+ if (wire._mismatchIndicator) {
+ wire._mismatchIndicator.destroy();
+ wire._mismatchIndicator = null;
+ }
+}
diff --git a/src/style.css b/src/style.css
index 77a2094..1d7dd21 100644
--- a/src/style.css
+++ b/src/style.css
@@ -71,10 +71,18 @@ background-size: 10rem 10rem;
overflow-y: auto;
transition: left 0.5s;
}
- #create-variables{
+ #create-variables,
+ .left-panel-section-inner{
width: 85%;
margin: 0.4rem auto;
padding: 0.4rem 0;
+ box-sizing: border-box;
+ }
+ #input-params-section,
+ #output-params-section,
+ #doc-string-section,
+ #update-callers-section{
+ width: 100%;
}
#ctx-menu-container{
position: absolute;
@@ -628,6 +636,64 @@ iframe{
.left-panel-variable .var-actions .var-delete-btn:hover{
color: #f44;
}
+.update-callers-btn{
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ box-sizing: border-box;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.9rem;
+ white-space: nowrap;
+ border: 1px solid #00bfa5;
+ border-radius: 6px;
+ background: #00897b;
+ color: #fff;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+}
+.update-callers-btn:hover{
+ background: #00a090;
+ border-color: #00e5cc;
+ color: #fff;
+}
+
+.call-node-doc-tooltip{
+ position: fixed;
+ z-index: 10000;
+ max-width: 260px;
+ padding: 8px 10px;
+ font-size: 12px;
+ font-family: Verdana, sans-serif;
+ line-height: 1.35;
+ color: #eee;
+ background: rgba(30, 30, 35, 0.97);
+ border: 1px solid #00bfa5;
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
+ pointer-events: none;
+ display: none;
+ word-wrap: break-word;
+}
+
+.function-list-doc-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ margin-left: 6px;
+ font-size: 11px;
+ font-weight: bold;
+ font-family: Verdana, sans-serif;
+ color: #00bfa5;
+ cursor: help;
+ flex-shrink: 0;
+}
+
+.function-list-doc-icon:hover {
+ color: #00e5cc;
+}
+
.variableList{
display: flex;
width: 100%;
@@ -844,4 +910,159 @@ iframe{
display: flex;
gap: 0.5rem;
justify-content: center;
+}
+/* ── Tab bar ── */
+#tab-bar{
+ position: absolute;
+ top: 0;
+ left: 20rem;
+ height: 3rem;
+ display: flex;
+ align-items: center;
+ z-index: 4;
+ background: rgba(30, 32, 36, 0.88);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+ border-bottom: 1px solid rgba(214, 90, 49, 0.3);
+ user-select: none;
+ transition: left 0.5s;
+}
+#left-panel.closed-left-panel ~ #tab-bar{
+ left: 3rem;
+}
+#tab-list{
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+.tab-item{
+ display: flex;
+ align-items: center;
+ padding: 0 1.2rem;
+ height: 100%;
+ color: rgba(255,255,255,0.55);
+ font-size: 1.1rem;
+ cursor: pointer;
+ white-space: nowrap;
+ border-right: 1px solid rgba(255,255,255,0.08);
+ transition: background 0.15s, color 0.15s;
+ position: relative;
+}
+.tab-item:hover{
+ background: rgba(255,255,255,0.07);
+ color: #fff;
+}
+.tab-item.tab-active{
+ background: rgba(214, 90, 49, 0.18);
+ color: #fff;
+ box-shadow: inset 0 -2px 0 #d65a31;
+}
+.tab-name{
+ pointer-events: auto;
+}
+.tab-close{
+ margin-left: 0.6rem;
+ font-size: 1.3rem;
+ line-height: 1;
+ opacity: 0;
+ transition: opacity 0.15s, color 0.15s;
+}
+.tab-item:hover .tab-close{
+ opacity: 0.5;
+}
+.tab-close:hover{
+ opacity: 1 !important;
+ color: #f44;
+}
+#add-tab-btn{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 2.2rem;
+ width: 2.2rem;
+ margin-left: 0.4rem;
+ background: none;
+ border: 1px solid rgba(255,255,255,0.15);
+ border-radius: 4px;
+ color: rgba(255,255,255,0.45);
+ font-size: 1.5rem;
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
+}
+#add-tab-btn:hover{
+ background: rgba(214, 90, 49, 0.25);
+ color: #fff;
+ border-color: #d65a31;
+}
+.tab-rename-input{
+ background: rgba(255,255,255,0.1);
+ border: 1px solid #d65a31;
+ color: #fff;
+ font-size: 1.1rem;
+ font-family: inherit;
+ padding: 0.1rem 0.4rem;
+ outline: none;
+ width: 10rem;
+ border-radius: 2px;
+}
+/* ── Input/Output Parameters: same alignment as Add Variable, Variable List, Functions ── */
+.left-panel-section-inner .var-inline-edit{
+ width: 100%;
+}
+.param-def-value-container{
+ width: 100%;
+}
+.param-def-value-container .param-def-value{
+ width: 100%;
+ box-sizing: border-box;
+}
+.left-panel-section-inner .var-edit-actions{
+ justify-content: stretch;
+}
+.left-panel-section-inner .var-edit-save{
+ width: 100%;
+ text-align: center;
+}
+.left-panel-section-inner .param-list{
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+.left-panel-section-inner .param-list .left-panel-variable{
+ width: 100%;
+ margin: 0.2rem 0;
+ box-sizing: border-box;
+}
+
+.doc-string-textarea{
+ width: 100%;
+ box-sizing: border-box;
+ font-size: 0.9rem;
+ padding: 0.3rem 0.4rem;
+ background: rgba(255,255,255,0.1);
+ border: 1px solid rgba(255,255,255,0.3);
+ color: white;
+ outline: none;
+ border-radius: 2px;
+ resize: vertical;
+ min-height: 4rem;
+ font-family: inherit;
+}
+.doc-string-textarea:focus{
+ border-color: #d65a31;
+}
+.doc-string-textarea::placeholder{
+ color: rgba(255,255,255,0.4);
+}
+
+.param-list{
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
}
\ No newline at end of file