mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
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>
334 lines
9.7 KiB
JavaScript
334 lines
9.7 KiB
JavaScript
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
|
|
};
|