mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 15:22:55 +01:00
- Install blockly package (~500KB) - Create BlocklyWorkspace React component with serialization - Define custom Noodl blocks (Input/Output, Variables, Objects, Arrays) - Implement JavaScript code generators for all custom blocks - Add theme-aware styling for Blockly workspace - Export initialization functions for easy integration Part of TASK-012: Blockly Visual Logic Integration
20 KiB
20 KiB
Blockly Blocks Specification
This document defines the custom Blockly blocks for Noodl integration.
Block Categories & Colors
| Category | Color (HSL Hue) | Description |
|---|---|---|
| Inputs/Outputs | 230 (Blue) | Node I/O |
| Variables | 330 (Pink) | Noodl.Variables |
| Objects | 20 (Orange) | Noodl.Objects |
| Arrays | 260 (Purple) | Noodl.Arrays |
| Events | 180 (Cyan) | Signals & triggers |
| Logic | 210 (Standard) | If/else, comparisons |
| Math | 230 (Standard) | Math operations |
| Text | 160 (Standard) | String operations |
Inputs/Outputs Blocks
noodl_define_input
Declares an input port on the node.
// Block Definition
{
type: 'noodl_define_input',
message0: '📥 Define input %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myInput' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an input port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_input'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
noodl_get_input
Gets a value from a node input.
// Block Definition
{
type: 'noodl_get_input',
message0: '📥 get input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'value' }
],
output: null, // Can connect to any type
colour: 230,
tooltip: 'Gets the value from an input port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_input'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Inputs["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_define_output
Declares an output port on the node.
// Block Definition
{
type: 'noodl_define_output',
message0: '📤 Define output %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an output port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_output'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
noodl_set_output
Sets a value on a node output.
// Block Definition
{
type: 'noodl_set_output',
message0: '📤 set output %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 230,
tooltip: 'Sets the value of an output port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_output'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Outputs["' + name + '"] = ' + value + ';\n';
};
noodl_define_signal_input
Declares a signal input port.
// Block Definition
{
type: 'noodl_define_signal_input',
message0: '⚡ Define signal input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'trigger' }
],
colour: 180,
tooltip: 'Defines a signal input that can trigger logic',
helpUrl: ''
}
noodl_define_signal_output
Declares a signal output port.
// Block Definition
{
type: 'noodl_define_signal_output',
message0: '⚡ Define signal output %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
colour: 180,
tooltip: 'Defines a signal output that can trigger other nodes',
helpUrl: ''
}
noodl_send_signal
Sends a signal output.
// Block Definition
{
type: 'noodl_send_signal',
message0: '⚡ send signal %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
previousStatement: null,
nextStatement: null,
colour: 180,
tooltip: 'Sends a signal to connected nodes',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_send_signal'] = function(block) {
var name = block.getFieldValue('NAME');
return 'this.sendSignalOnOutput("' + name + '");\n';
};
Variables Blocks
noodl_get_variable
Gets a global variable value.
// Block Definition
{
type: 'noodl_get_variable',
message0: '📖 get variable %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
output: null,
colour: 330,
tooltip: 'Gets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Variables["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_set_variable
Sets a global variable value.
// Block Definition
{
type: 'noodl_set_variable',
message0: '✏️ set variable %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 330,
tooltip: 'Sets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Noodl.Variables["' + name + '"] = ' + value + ';\n';
};
Objects Blocks
noodl_get_object
Gets an object by ID.
// Block Definition
{
type: 'noodl_get_object',
message0: '📦 get object %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Gets a Noodl Object by its ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || '""';
var code = 'Noodl.Objects[' + id + ']';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_get_object_property
Gets a property from an object.
// Block Definition
{
type: 'noodl_get_object_property',
message0: '📖 get %1 from object %2',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' }
],
output: null,
colour: 20,
tooltip: 'Gets a property value from an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var code = object + '["' + property + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_set_object_property
Sets a property on an object.
// Block Definition
{
type: 'noodl_set_object_property',
message0: '✏️ set %1 on object %2 to %3',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 20,
tooltip: 'Sets a property value on an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return object + '["' + property + '"] = ' + value + ';\n';
};
noodl_create_object
Creates a new object.
// Block Definition
{
type: 'noodl_create_object',
message0: '➕ create object with ID %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Creates a new Noodl Object with the given ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_create_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || 'Noodl.Object.guid()';
var code = 'Noodl.Object.create(' + id + ')';
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
};
Arrays Blocks
noodl_get_array
Gets an array by name.
// Block Definition
{
type: 'noodl_get_array',
message0: '📋 get array %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myArray' }
],
output: 'Array',
colour: 260,
tooltip: 'Gets a Noodl Array by name',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_array'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Arrays["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_array_add
Adds an item to an array.
// Block Definition
{
type: 'noodl_array_add',
message0: '➕ add %1 to array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Adds an item to the end of an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_add'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.push(' + item + ');\n';
};
noodl_array_remove
Removes an item from an array.
// Block Definition
{
type: 'noodl_array_remove',
message0: '➖ remove %1 from array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Removes an item from an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_remove'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.splice(' + array + '.indexOf(' + item + '), 1);\n';
};
noodl_array_length
Gets the length of an array.
// Block Definition
{
type: 'noodl_array_length',
message0: '🔢 length of array %1',
args0: [
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
output: 'Number',
colour: 260,
tooltip: 'Gets the number of items in an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_length'] = function(block) {
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var code = array + '.length';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
noodl_array_foreach
Loops over array items.
// Block Definition
{
type: 'noodl_array_foreach',
message0: '🔄 for each %1 in %2',
args0: [
{ type: 'field_variable', name: 'VAR', variable: 'item' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Executes code for each item in the array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_foreach'] = function(block) {
var variable = Blockly.JavaScript.nameDB_.getName(
block.getFieldValue('VAR'), Blockly.VARIABLE_CATEGORY_NAME);
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
return 'for (var ' + variable + ' of ' + array + ') {\n' +
statements + '}\n';
};
Event Blocks
noodl_on_signal
Event handler for when a signal input is triggered.
// Block Definition
{
type: 'noodl_on_signal',
message0: '⚡ when %1 is triggered',
args0: [
{ type: 'field_input', name: 'SIGNAL', text: 'trigger' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 180,
tooltip: 'Runs code when the signal input is triggered',
helpUrl: ''
}
// Generator - This is a special case, generates a handler function
Blockly.JavaScript['noodl_on_signal'] = function(block) {
var signal = block.getFieldValue('SIGNAL');
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
// This generates a named handler that the runtime will call
return '// Handler for signal: ' + signal + '\n' +
'function _onSignal_' + signal + '() {\n' +
statements +
'}\n';
};
noodl_on_variable_change
Event handler for when a variable changes.
// Block Definition
{
type: 'noodl_on_variable_change',
message0: '👁️ when variable %1 changes',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 330,
tooltip: 'Runs code when the variable value changes',
helpUrl: ''
}
I/O Detection Algorithm
interface DetectedIO {
inputs: Array<{ name: string; type: string }>;
outputs: Array<{ name: string; type: string }>;
signalInputs: string[];
signalOutputs: string[];
}
function detectIO(workspace: Blockly.Workspace): DetectedIO {
const result: DetectedIO = {
inputs: [],
outputs: [],
signalInputs: [],
signalOutputs: []
};
const blocks = workspace.getAllBlocks(false);
for (const block of blocks) {
switch (block.type) {
case 'noodl_define_input':
result.inputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_get_input':
// Auto-detect from usage if not explicitly defined
const inputName = block.getFieldValue('NAME');
if (!result.inputs.find(i => i.name === inputName)) {
result.inputs.push({ name: inputName, type: '*' });
}
break;
case 'noodl_define_output':
result.outputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_set_output':
// Auto-detect from usage
const outputName = block.getFieldValue('NAME');
if (!result.outputs.find(o => o.name === outputName)) {
result.outputs.push({ name: outputName, type: '*' });
}
break;
case 'noodl_define_signal_input':
case 'noodl_on_signal':
const sigIn = block.getFieldValue('SIGNAL') || block.getFieldValue('NAME');
if (!result.signalInputs.includes(sigIn)) {
result.signalInputs.push(sigIn);
}
break;
case 'noodl_define_signal_output':
case 'noodl_send_signal':
const sigOut = block.getFieldValue('NAME');
if (!result.signalOutputs.includes(sigOut)) {
result.signalOutputs.push(sigOut);
}
break;
}
}
return result;
}
Toolbox Configuration
const LOGIC_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs/Outputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' },
{ kind: 'block', type: 'noodl_define_output' },
{ kind: 'block', type: 'noodl_set_output' },
{ kind: 'block', type: 'noodl_define_signal_input' },
{ kind: 'block', type: 'noodl_define_signal_output' },
{ kind: 'block', type: 'noodl_send_signal' }
]
},
{
kind: 'category',
name: 'Events',
colour: 180,
contents: [
{ kind: 'block', type: 'noodl_on_signal' },
{ kind: 'block', type: 'noodl_on_variable_change' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' },
{ kind: 'block', type: 'noodl_set_variable' }
]
},
{
kind: 'category',
name: 'Objects',
colour: 20,
contents: [
{ kind: 'block', type: 'noodl_get_object' },
{ kind: 'block', type: 'noodl_get_object_property' },
{ kind: 'block', type: 'noodl_set_object_property' },
{ kind: 'block', type: 'noodl_create_object' }
]
},
{
kind: 'category',
name: 'Arrays',
colour: 260,
contents: [
{ kind: 'block', type: 'noodl_get_array' },
{ kind: 'block', type: 'noodl_array_add' },
{ kind: 'block', type: 'noodl_array_remove' },
{ kind: 'block', type: 'noodl_array_length' },
{ kind: 'block', type: 'noodl_array_foreach' }
]
},
{ kind: 'sep' },
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Loops',
colour: 120,
contents: [
{ kind: 'block', type: 'controls_repeat_ext' },
{ kind: 'block', type: 'controls_whileUntil' },
{ kind: 'block', type: 'controls_for' },
{ kind: 'block', type: 'controls_flow_statements' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' },
{ kind: 'block', type: 'math_modulo' },
{ kind: 'block', type: 'math_random_int' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' },
{ kind: 'block', type: 'text_isEmpty' },
{ kind: 'block', type: 'text_indexOf' },
{ kind: 'block', type: 'text_charAt' }
]
}
]
};
// Simplified toolbox for Expression Builder
const EXPRESSION_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' }
]
},
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' }
]
}
]
};