mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
333
packages/noodl-runtime/src/nodedefinition.js
Normal file
333
packages/noodl-runtime/src/nodedefinition.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const Node = require('./node'),
|
||||
EdgeTriggeredInput = require('./edgetriggeredinput');
|
||||
|
||||
function registerInput(object, metadata, name, input) {
|
||||
if (object.hasOwnProperty(name)) {
|
||||
throw new Error('Input property ' + name + ' already registered');
|
||||
}
|
||||
|
||||
if (!input.set && !input.valueChangedToTrue) {
|
||||
input.set = () => {};
|
||||
}
|
||||
|
||||
if (input.set) {
|
||||
object[name] = {
|
||||
set: input.set
|
||||
};
|
||||
|
||||
//types to keep in the input on the node instances
|
||||
//color and textStyles are used for style updates
|
||||
//array is for supporting eval:ing strings
|
||||
const typesToSaveInInput = ['color', 'textStyle', 'array'];
|
||||
|
||||
typesToSaveInInput.forEach((type) => {
|
||||
if (input.type && (input.type === type || input.type.name === type)) {
|
||||
object[name].type = type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (input.setUnitType) {
|
||||
object[name].setUnitType = input.setUnitType;
|
||||
}
|
||||
|
||||
metadata.inputs[name] = {
|
||||
displayName: input.displayName,
|
||||
editorName: input.editorName,
|
||||
group: input.group,
|
||||
type: input.type,
|
||||
default: input.default,
|
||||
index: input.index,
|
||||
exportToEditor: input.hasOwnProperty('exportToEditor') ? input.exportToEditor : true,
|
||||
inputPriority: input.inputPriority || 0,
|
||||
tooltip: input.tooltip,
|
||||
tab: input.tab,
|
||||
popout: input.popout,
|
||||
allowVisualStates: input.allowVisualStates,
|
||||
nodeDoubleClickAction: input.nodeDoubleClickAction
|
||||
};
|
||||
|
||||
if (input.valueChangedToTrue) {
|
||||
metadata.inputs[name].type = {
|
||||
name: 'signal',
|
||||
allowConnectionsOnly: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function registerInputs(object, metadata, inputs) {
|
||||
Object.keys(inputs).forEach(function (inputName) {
|
||||
registerInput(object, metadata, inputName, inputs[inputName]);
|
||||
});
|
||||
}
|
||||
|
||||
function registerNumberedInputs(node, numberedInputs) {
|
||||
for (const inputName of Object.keys(numberedInputs)) {
|
||||
registerNumberedInput(node, inputName, numberedInputs[inputName]);
|
||||
}
|
||||
}
|
||||
|
||||
function registerNumberedInput(node, name, input) {
|
||||
const oldRegisterInputIfNeeded = node.registerInputIfNeeded;
|
||||
|
||||
node.registerInputIfNeeded = function (inputName) {
|
||||
if (oldRegisterInputIfNeeded) {
|
||||
oldRegisterInputIfNeeded.call(node, inputName);
|
||||
}
|
||||
|
||||
if (node.hasInput(inputName) || !inputName.startsWith(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(inputName.slice(name.length + 1)); // inputName is "nameOfInput xxx" where xxx is the index
|
||||
|
||||
node.registerInput(inputName, {
|
||||
type: input.type,
|
||||
set: input.createSetter.call(node, index)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function registerOutputsMetadata(metadata, outputs) {
|
||||
Object.keys(outputs).forEach(function (name) {
|
||||
var output = outputs[name];
|
||||
|
||||
metadata.outputs[name] = {
|
||||
displayName: output.displayName,
|
||||
editorName: output.editorName,
|
||||
group: output.group,
|
||||
type: output.type,
|
||||
index: output.index,
|
||||
exportToEditor: output.hasOwnProperty('exportToEditor') ? output.exportToEditor : true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function initializeDefaultValues(defaultValues, inputsMetadata) {
|
||||
Object.keys(inputsMetadata).forEach((name) => {
|
||||
const defaultValue = inputsMetadata[name].default;
|
||||
if (defaultValue === undefined) return;
|
||||
|
||||
if (inputsMetadata[name].type.defaultUnit) {
|
||||
defaultValues[name] = {
|
||||
unit: inputsMetadata[name].type.defaultUnit,
|
||||
value: defaultValue
|
||||
};
|
||||
} else {
|
||||
defaultValues[name] = defaultValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function defineNode(opts) {
|
||||
if (!opts.category) {
|
||||
throw new Error('Node must have a category');
|
||||
}
|
||||
|
||||
if (!opts.name) {
|
||||
throw new Error('Node must have a name');
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
category: opts.category,
|
||||
dynamicports: opts.dynamicports,
|
||||
exportDynamicPorts: opts.exportDynamicPorts,
|
||||
useVariants: opts.useVariants,
|
||||
allowChildren: opts.allowChildren,
|
||||
allowChildrenWithCategory: opts.allowChildrenWithCategory,
|
||||
singleton: opts.singleton,
|
||||
connectionPanel: opts.connectionPanel,
|
||||
allowAsChild: opts.allowAsChild,
|
||||
visualStates: opts.visualStates,
|
||||
panels: opts.panels,
|
||||
color: opts.color,
|
||||
usePortAsLabel: opts.usePortAsLabel,
|
||||
portLabelTruncationMode: opts.portLabelTruncationMode,
|
||||
name: opts.name,
|
||||
displayNodeName: opts.displayNodeName || opts.displayName,
|
||||
deprecated: opts.deprecated,
|
||||
haveComponentPorts: opts.haveComponentPorts,
|
||||
version: opts.version,
|
||||
module: opts.module,
|
||||
docs: opts.docs,
|
||||
allowAsExportRoot: opts.allowAsExportRoot,
|
||||
nodeDoubleClickAction: opts.nodeDoubleClickAction,
|
||||
searchTags: opts.searchTags
|
||||
};
|
||||
|
||||
opts._internal = opts._internal || {};
|
||||
|
||||
//prototypeExtensions - old API
|
||||
//methods - new API
|
||||
opts.prototypeExtensions = opts.methods || opts.prototypeExtensions || {};
|
||||
opts.inputs = opts.inputs || {};
|
||||
opts.outputs = opts.outputs || {};
|
||||
opts.initialize = opts.initialize || function () {};
|
||||
|
||||
let inputs = {};
|
||||
|
||||
registerInputs(inputs, metadata, opts.inputs);
|
||||
registerOutputsMetadata(metadata, opts.outputs);
|
||||
function NodeConstructor(context, id) {
|
||||
Node.call(this, context, id);
|
||||
}
|
||||
|
||||
Object.keys(opts.prototypeExtensions).forEach(function (propName) {
|
||||
if (!opts.prototypeExtensions[propName].value) {
|
||||
opts.prototypeExtensions[propName] = {
|
||||
value: opts.prototypeExtensions[propName]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
NodeConstructor.prototype = Object.create(Node.prototype, opts.prototypeExtensions);
|
||||
Object.defineProperty(NodeConstructor.prototype, 'name', {
|
||||
value: opts.name
|
||||
});
|
||||
|
||||
if (opts.getInspectInfo) NodeConstructor.prototype.getInspectInfo = opts.getInspectInfo;
|
||||
if (opts.nodeScopeDidInitialize) NodeConstructor.prototype.nodeScopeDidInitialize = opts.nodeScopeDidInitialize;
|
||||
|
||||
const nodeDefinition = function (context, id, nodeScope) {
|
||||
const node = new NodeConstructor(context, id);
|
||||
|
||||
//create all inputs. Use the inputs object for setters that don't have state and can be shared
|
||||
node._inputs = Object.create(inputs);
|
||||
|
||||
//all inputs that use the valueChangedToTrue have state and need to be instanced
|
||||
Object.keys(opts.inputs).forEach(function (name) {
|
||||
var input = opts.inputs[name];
|
||||
if (input.valueChangedToTrue) {
|
||||
node._inputs[name] = {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: input.valueChangedToTrue
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(opts.outputs).forEach(function (name) {
|
||||
var output = opts.outputs[name];
|
||||
if (output.type === 'signal') {
|
||||
node.registerOutput(name, {
|
||||
getter: function () {
|
||||
//signals are always emitted as a sequence of false, true, so this getter is never used
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
node.registerOutput(name, output);
|
||||
}
|
||||
});
|
||||
|
||||
opts.numberedInputs && registerNumberedInputs(node, opts.numberedInputs);
|
||||
|
||||
node.nodeScope = nodeScope;
|
||||
initializeDefaultValues(node._inputValues, metadata.inputs);
|
||||
|
||||
opts.initialize.call(node);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
nodeDefinition.metadata = metadata;
|
||||
|
||||
if (opts.numberedInputs) registerSetupFunctionForNumberedInputs(nodeDefinition, opts.name, opts.numberedInputs);
|
||||
|
||||
return nodeDefinition;
|
||||
}
|
||||
|
||||
function registerSetupFunctionForNumberedInputs(nodeDefinition, nodeType, numberedInputs) {
|
||||
const inputNames = Object.keys(numberedInputs);
|
||||
|
||||
if (!inputNames.length) return;
|
||||
|
||||
nodeDefinition.setupNumberedInputDynamicPorts = function (context, graphModel) {
|
||||
const editorConnection = context.editorConnection;
|
||||
|
||||
if (!editorConnection || !editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function collectPorts(node, inputName, input) {
|
||||
const connections = node.component.getConnectionsTo(node.id).map((c) => c.targetPort);
|
||||
|
||||
const allPortNames = Object.keys(node.parameters).concat(connections);
|
||||
const portNames = allPortNames.filter((p) => p.startsWith(inputName + ' '));
|
||||
|
||||
//Figure out how many we need to create
|
||||
//It needs to be the highest index + 1
|
||||
//Only parameters with values are present, e.g. input 3 can be missing even if input 4 is defined
|
||||
const maxIndex = portNames.length
|
||||
? 1 + Math.max(...portNames.map((p) => Number(p.slice(inputName.length + 1))))
|
||||
: 0;
|
||||
const numPorts = maxIndex + 1;
|
||||
|
||||
const ports = [];
|
||||
|
||||
for (let i = 0; i < numPorts; i++) {
|
||||
const port = {
|
||||
name: inputName + ' ' + i,
|
||||
displayName: (input.displayPrefix || inputName) + ' ' + i,
|
||||
type: input.type,
|
||||
plug: 'input',
|
||||
group: input.group
|
||||
};
|
||||
|
||||
if (input.hasOwnProperty('index')) {
|
||||
port.index = input.index + i;
|
||||
}
|
||||
|
||||
ports.push(port);
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
function updatePorts(node) {
|
||||
const ports = inputNames.map((inputName) => collectPorts(node, inputName, numberedInputs[inputName])).flat();
|
||||
editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.' + nodeType, (node) => {
|
||||
updatePorts(node);
|
||||
node.on('parameterUpdated', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
node.on('inputConnectionAdded', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
|
||||
node.on('inputConnectionRemoved', () => {
|
||||
updatePorts(node);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function extend(obj1, obj2) {
|
||||
for (var p in obj2) {
|
||||
if (p === 'initialize' && obj1.initialize) {
|
||||
var oldInit = obj1.initialize;
|
||||
obj1.initialize = function () {
|
||||
oldInit.call(this);
|
||||
obj2.initialize.call(this);
|
||||
};
|
||||
} else if (obj2[p] && obj2[p].constructor === Object) {
|
||||
obj1[p] = extend(obj1[p] || {}, obj2[p]);
|
||||
} else if (obj2[p] && obj2[p].constructor === Array && obj1[p] && obj1[p].constructor == Array) {
|
||||
obj1[p] = obj1[p].concat(obj2[p]);
|
||||
} else {
|
||||
obj1[p] = obj2[p];
|
||||
}
|
||||
}
|
||||
return obj1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
defineNode: defineNode,
|
||||
extend: extend
|
||||
};
|
||||
Reference in New Issue
Block a user