Files
fluxscape/packages/noodl-viewer-react/src/react-component-node.js
Michael Cartner b9c60b07dc 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>
2024-01-26 11:52:55 +01:00

1157 lines
36 KiB
JavaScript

'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import DOMBoundingBoxObserver from './dom-boundingbox-oberver';
import Layout from './layout';
import mergeDeep from './mergedeep';
import NodeSharedPortDefinitions from './node-shared-port-definitions';
import transitionParameter from './node-transitions';
function addOutputPropHandler(node, propCallbacks, propPath) {
const props = propPath ? node.props[propPath] : node.props;
for (const propName in propCallbacks) {
if (props[propName]) {
const prevCb = props[propName];
props[propName] = () => {
prevCb();
propCallbacks[propName].call(node);
};
} else {
props[propName] = propCallbacks[propName].bind(node);
}
}
node.forceUpdate();
}
function addPrimitiveOutputPropHandler(node, name, output) {
let prop;
if (output.type === 'signal') {
prop = () => {
node.sendSignalOnOutput(name);
};
} else {
prop = (...args) => {
node.outputPropValues[name] = output.getValue ? output.getValue.call(node, ...args) : args[0];
node.flagOutputDirty(name);
output.onChange && output.onChange.call(node, node.outputPropValues[name]);
};
}
addOutputPropHandler(node, { [name]: prop }, output.propPath);
}
function defineRegularInputProp(input, name) {
if (!input.type) throw new Error(`input ${name} is missing a type`);
if (input.type.units) {
input.set = function (value) {
const props = input.propPath ? this.props[input.propPath] : this.props;
if (value && value.value !== undefined) {
props[name] = value.value + value.unit;
} else {
delete props[name];
}
if (input.onChange) {
input.onChange.call(this, value);
}
this.forceUpdate();
};
} else {
input.set = function (value) {
const props = input.propPath ? this.props[input.propPath] : this.props;
if (value !== undefined) {
props[name] = value;
} else {
delete props[name];
}
if (input.onChange) {
input.onChange.call(this, value);
}
this.forceUpdate();
};
}
}
function flattenArray(target, array) {
for (const e of array) {
if (Array.isArray(e)) {
flattenArray(target, e);
} else if (e !== undefined) {
target.push(e);
}
}
}
class NoodlReactComponent extends React.Component {
componentDidMount() {
this.props.noodlNode.sendSignalOnOutput('didMount');
}
componentWillUnmount() {
this.props.noodlNode.sendSignalOnOutput('willUnmount');
//Remove
const noodlNode = this.props.noodlNode;
if (noodlNode.currentVisualStates) {
const statesToRemove = ['hover', 'pressed', 'focused'];
const vs = noodlNode.currentVisualStates.filter((s) => !statesToRemove.includes(s));
noodlNode.setVisualStates(vs);
}
}
render() {
//So the props are a bit tricky here...
//this.props consist of:
// - the props passed by the ReactComponentNode.render() function, just {noodlNode}
// - any additional third party props coming from the parent react component.
//E.g. the drag node adds event handlers, style, and className to this.props.
const { noodlNode, style, ...otherProps } = this.props;
let finalStyle = noodlNode.style;
//check if there's additional styling from the react parent
//if so, we need to combine it with the style from the Noodl node
//let the extra style take priority over the noodl style, if they share some attributes
//this is wrapped in an if for performance reasons, "..." is quite slow
if (style) {
finalStyle = {
...noodlNode.style, //styling from the noodl node
...style //styling from the react component parent
};
}
const props = {
ref: (ref) => {
noodlNode.innerReactComponentRef = ref;
},
style: finalStyle,
//the noodl props coming from the node
//some components actually have a "style" used for something other than css,
//so make sure this comes after the "style" prop above, so it can overwrite it
//(e.g. Jesper's Icon Material UI)
...noodlNode.props,
//otherProps can be empty, but some react components add additional props to their children
...otherProps
};
if (noodlNode.noodlNodeAsProp) {
props.noodlNode = noodlNode;
//nodes that want the noodlNode also get the parent layout
//since it's used by all built in nodes for layout purposes
const parent = noodlNode.getVisualParentNode();
if (parent && parent.props.layout) {
props.parentLayout = parent.props.layout;
}
}
//optimization. This is used by forceUpdate() to only render this node once per frame.
noodlNode.renderedAtFrame = noodlNode.context.frameNumber;
if (noodlNode.useFrame) {
if (props.textStyle !== undefined) {
// Apply text style
props.style = finalStyle = Object.assign({}, props.textStyle, finalStyle);
}
Layout.size(finalStyle, props);
Layout.align(finalStyle, props);
/* if(finalStyle.opacity === 0) {
finalStyle.pointerEvents = 'none';
}*/
}
return React.createElement(noodlNode.reactComponent, props, noodlNode.renderChildren());
}
}
function setStylesOnDOMNode(rootElement, styles, styleTag) {
let element = rootElement;
if (styleTag) {
//check if the root element has the style tag, if not, find the child that does
if (element.getAttribute('noodl-style-tag') !== styleTag) {
element = rootElement.querySelector(`[noodl-style-tag=${styleTag}]`);
}
}
if (!element) return;
for (const p in styles) {
element.style[p] = styles[p];
}
}
let reactKeyCounter = 0;
function createNodeFromReactComponent(def) {
// visual frame props
const { frame } = def;
if (frame !== undefined) {
if (frame.dimensions) {
NodeSharedPortDefinitions.addDimensions(def, typeof frame.dimensions === 'object' ? frame.dimensions : undefined);
}
if (frame.position) NodeSharedPortDefinitions.addTransformInputs(def);
if (frame.margins) NodeSharedPortDefinitions.addMarginInputs(def);
if (frame.padding) NodeSharedPortDefinitions.addPaddingInputs(def);
if (frame.align) NodeSharedPortDefinitions.addAlignInputs(def);
// NodeSharedPortDefinitions.addSharedVisualInputs(ReactComponentNode);
// NodeSharedPortDefinitions.addPointerEventOutputs(ReactComponentNode);
}
const {
initialize,
inputs,
inputProps,
inputCss,
outputs,
outputProps,
dynamicports,
defaultCss = {},
methods
} = def;
//assign default values to style
const startStyle = Object.assign({}, defaultCss);
const startStyles = {};
for (const name in inputCss) {
const input = inputCss[name];
const hasDefault = input.hasOwnProperty('default') && input.applyDefault !== false;
if (input.styleTag && !startStyles.hasOwnProperty(input.styleTag)) {
startStyles[input.styleTag] = {};
}
if (hasDefault) {
const value = input.type.units ? input.default + input.type.defaultUnit : input.default;
if (input.styleTag) {
startStyles[input.styleTag][name] = value;
} else {
startStyle[name] = value;
}
}
}
function boundingBoxObserverCallback(attribute, rect) {
this.clientBoundingRect = rect;
if (attribute === 'x') {
this.flagOutputDirty('screenPositionX');
} else if (attribute === 'y') {
this.flagOutputDirty('screenPositionY');
} else if (attribute === 'width') {
this.flagOutputDirty('boundingWidth');
} else if (attribute === 'height') {
this.flagOutputDirty('boundingHeight');
}
}
const useVariants = def.useVariants !== undefined ? def.useVariants : true;
const ReactComponentNode = {
name: def.name,
docs: def.docs,
displayNodeName: def.displayNodeName || def.displayName,
shortDesc: '',
category: 'Visual',
allowChildren: def.allowChildren === undefined ? true : def.allowChildren, //default to true
visualStates: def.visualStates,
allowAsExportRoot: def.allowAsExportRoot,
singleton: def.singleton,
useVariants,
usePortAsLabel: def.usePortAsLabel,
portLabelTruncationMode: def.portLabelTruncationMode,
connectionPanel: def.connectionPanel,
nodeDoubleClickAction: def.nodeDoubleClickAction,
initialize() {
this.reactKey = 'key' + reactKeyCounter;
reactKeyCounter++;
this.children = [];
if (hasChildCountOutput) {
this.childrenCount = 0;
}
this.props = { styles: {} };
this.outputPropValues = {};
this.style = Object.assign({}, startStyle);
for (const styleTag in startStyles) {
this.props.styles[styleTag] = Object.assign({}, startStyles[styleTag]);
}
this.childIndex = 0;
this.clientBoundingRect = {};
this.noodlNodeAsProp = def.noodlNodeAsProp ? true : false;
const pollDelay = this.context && this.context.runningInCanvas ? 300 : 0;
this.boundingBoxObserver = new DOMBoundingBoxObserver(boundingBoxObserverCallback.bind(this), pollDelay);
this.wantsToBeMounted = true;
this.useFrame = !!frame;
//assign default values to props
for (const name in inputProps) {
const input = inputProps[name];
if (input.propPath && !this.props.hasOwnProperty(input.propPath)) {
this.props[input.propPath] = {};
}
const props = input.propPath ? this.props[input.propPath] : this.props;
if (input.hasOwnProperty('default')) {
if (input.type.defaultUnit && input.default !== undefined) {
props[name] = input.default + input.type.defaultUnit;
} else {
props[name] = input.default;
}
}
}
//set ut props that send data on noodl outputs
for (const outputName in outputProps) {
const output = outputProps[outputName];
if (output.propPath && !this.props.hasOwnProperty(output.propPath)) {
this.props[output.propPath] = {};
}
if (!output.props) {
addPrimitiveOutputPropHandler(this, outputName, output);
} else {
addOutputPropHandler(this, output.props, output.propPath);
}
}
this.reactComponentRef = null;
this.reactComponent = def.getReactComponent.call(this);
if (initialize) {
initialize.call(this);
}
},
getInspectInfo: def.getInspectInfo,
nodeScopeDidInitialize: def.nodeScopeDidInitialize,
dynamicports,
inputs: {
cssClassName: {
index: 100010,
displayName: 'CSS Class',
group: 'Advanced HTML',
type: 'string',
default: '',
set(value) {
this.props.className = value;
this.forceUpdate();
}
},
styleCss: {
index: 100011,
displayName: 'CSS Style',
group: 'Advanced HTML',
type: {
name: 'string',
codeeditor: 'text',
allowEditOnly: true
},
default: '/* background-color: red; */',
set(value) {
this.updateAdvancedStyle({ content: value });
}
}
},
outputs: {
childIndex: {
displayName: 'Child Index',
type: 'number',
get() {
return this.childIndex;
}
},
this: {
displayName: 'This',
type: 'reference',
get() {
return this;
}
},
screenPositionX: {
group: 'Bounding Box',
displayName: 'Screen Position X',
type: 'number',
get() {
return this.clientBoundingRect.x;
},
onFirstConnectionAdded() {
this.boundingBoxObserver.addObserver();
},
onLastConnectionRemoved() {
this.boundingBoxObserver.removeObserver();
}
},
screenPositionY: {
group: 'Bounding Box',
displayName: 'Screen Position Y',
type: 'number',
get() {
return this.clientBoundingRect.y;
},
onFirstConnectionAdded() {
this.boundingBoxObserver.addObserver();
},
onLastConnectionRemoved() {
this.boundingBoxObserver.removeObserver();
}
},
boundingWidth: {
group: 'Bounding Box',
displayName: 'Width',
type: 'number',
get() {
return this.clientBoundingRect.width;
},
onFirstConnectionAdded() {
this.boundingBoxObserver.addObserver();
},
onLastConnectionRemoved() {
this.boundingBoxObserver.removeObserver();
}
},
boundingHeight: {
group: 'Bounding Box',
displayName: 'Height',
type: 'number',
get() {
return this.clientBoundingRect.height;
},
onFirstConnectionAdded() {
this.boundingBoxObserver.addObserver();
},
onLastConnectionRemoved() {
this.boundingBoxObserver.removeObserver();
}
},
didMount: {
group: 'Mounted',
displayName: 'Did Mount',
type: 'signal'
},
willUnmount: {
group: 'Mounted',
displayName: 'Will Unmount',
type: 'signal'
}
},
methods: {
updateAdvancedStyle(params) {
//remove previous styles first
if (this.customCssStyles) {
this.removeStyle(Object.keys(this.customCssStyles));
this.customCssStyles = undefined;
}
let style;
let errorMessage = '';
let rawCss = (params.content || '').replace('\n', '');
// strip away comments
let css = '';
while (rawCss.length) {
let nextComment = rawCss.indexOf('/*');
if (nextComment === -1) {
nextComment = rawCss.length;
}
css += rawCss.substring(0, nextComment);
rawCss = rawCss.substring(nextComment);
if (rawCss.length) {
//were inside a comment
let endComment = rawCss.indexOf('*/');
if (endComment === -1) endComment = rawCss.length;
rawCss = rawCss.substring(endComment + 2);
}
}
function trim(s) {
return s.replace(/^\s+|\s+$/gm, '');
}
const styles = css
.split(';')
.map(trim)
.filter((s) => s.length);
style = {};
for (const s of styles) {
const parts = s.split(':').map(trim);
if (s.indexOf('\n') !== -1) {
errorMessage += 'Missing semicolon: ' + s.split('\n')[0];
} else if (parts.length !== 2) {
errorMessage += 'Syntax error: ' + s;
} else {
const nameParts = parts[0].split('-');
for (let i = 1; i < nameParts.length; i++) {
nameParts[i] = nameParts[i][0].toUpperCase() + nameParts[i].substring(1);
}
style[nameParts.join('')] = parts[1];
}
}
if (errorMessage) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'css-parse-waring', {
message: 'Error in CSS Style<br>' + errorMessage
});
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'css-parse-waring');
style && this.setStyle(style);
this.customCssStyles = style;
}
},
setChildIndex(index) {
this.childIndex = index;
this.flagOutputDirty('childIndex');
},
updateChildIndices() {
let indexOffset = 0;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.name === 'For Each' || child.name === 'Component Children') {
indexOffset--;
}
child.setChildIndex && child.setChildIndex(i + indexOffset);
}
},
updateChildrenCount() {
let count = 0;
this.children.forEach((child) => {
if (child?.model?.type === 'For Each') {
count += child.model.children.length;
} else {
count++;
}
});
this.childrenCount = count;
this.flagOutputDirty('childrenCount');
},
addChild(child, index) {
if (index === undefined) {
index = this.children.length;
}
child.parent = this;
this.children.splice(index, 0, child);
this.cachedChildren = undefined;
this.scheduleUpdateChildCountAndIndicies();
this.forceUpdate();
},
removeChild(child) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
child.parent = undefined;
this.cachedChildren = undefined;
this.scheduleUpdateChildCountAndIndicies();
this.forceUpdate();
}
},
contains(node) {
//breadth first
const index = this.children.indexOf(node);
if (index !== -1) return true;
return this.children.some((child) => child.contains && child.contains(node));
},
scheduleUpdateChildCountAndIndicies() {
if (this.updateChildIndiciesScheduled) return;
this.updateChildIndiciesScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
this.updateChildIndices();
if (hasChildCountOutput) {
this.updateChildrenCount();
}
this.updateChildIndiciesScheduled = false;
});
},
getChildren() {
return this.children;
},
isChild(child) {
return this.children.indexOf(child) !== -1;
},
getChildRoot() {
return this;
},
forceUpdate() {
if (this.forceUpdateScheduled === true) return;
this.forceUpdateScheduled = true;
this.context.eventEmitter.once('frameEnd', () => {
this.forceUpdateScheduled = false;
if (this.renderedAtFrame === this.context.frameNumber) {
return;
}
this.reactComponentRef && this.reactComponentRef.setState({});
});
this.context.scheduleUpdate();
},
_resetReactVirtualDOM() {
//reset the react key to force a full re-render
//this can be required since we're editing the DOM tree, without React knowing
//and can confuse React in certain cases
this.reactKey = 'key' + reactKeyCounter;
reactKeyCounter++;
const parent = this.getVisualParentNode();
if (parent) {
parent.cachedChildren = undefined;
parent.forceUpdate();
}
},
/** Added for SSR Support */
triggerDidMount() {
if (this.wantsToBeMounted && !this.didCallTriggerDidMount) {
this.didCallTriggerDidMount = true;
if (this.hasOutput('didMount')) {
this.sendSignalOnOutput('didMount');
}
// HACK: This is requried for the Page Router.
if (this.props.didMount) {
this.props.didMount();
}
// HACK: Repeater... same as above
if (this.didMount) {
this.didMount();
}
}
this.children.forEach((child) => {
// TODO: Repeater is missing triggerDidMount
child.triggerDidMount && child.triggerDidMount();
});
},
render() {
if (!this.wantsToBeMounted) {
return;
}
//these props will only be sent when this component
//is added to the react tree. Further updates will only call
//render on the NoodlReactComponent, so make sure these props
//don't need to change over the lifetime of this node
return React.createElement(NoodlReactComponent, {
key: this.reactKey,
noodlNode: this,
ref: (ref) => {
this.reactComponentRef = ref;
this.boundingBoxObserver.setTarget(ReactDOM.findDOMNode(ref));
}
});
},
renderChildren() {
if (!this.cachedChildren) {
let c = this.children.map((child) => child.render());
let children = [];
flattenArray(children, c);
if (children.length === 0) {
children = null;
} else if (children.length === 1) {
//some components expects a single child only, and won't handle arrays
//and React.Children.only() throws an error on an array with only one child
children = children[0];
}
this.cachedChildren = children;
}
return this.cachedChildren;
},
setStyle(newStyles, styleTag) {
//this method will try to set css styles directly on the
//raw DOM elements, circumventing React.
//However, there's some layout and align attributes that are dependent
//on each other and need to go through additional processing.
//This function will detect those and trigger everything to run through the
//React component when necessary.
//TODO: move all these checks to the inputs themselves
//so the set-method of e.g. marginLeft can do the check and either
//set the style directly on the dom node, or trigger a react render
const styleObject = styleTag ? this.props.styles[styleTag] : this.style;
for (const p in newStyles) {
styleObject[p] = newStyles[p];
}
const domElement = this.getDOMElement();
if (!domElement) return;
let forceUpdate = false;
if (!styleTag) {
if (newStyles.hasOwnProperty('opacity')) {
//opacity change between zero and non-zero can toggle pointer events
//to be treated differently, so make sure to force update during those transitions
//note: properties in domElement.style are all strings
forceUpdate =
newStyles.hasOwnProperty('opacity') &&
((domElement.style.opacity === '0' && newStyles.opacity > 0) ||
(domElement.style.opacity !== '0' && newStyles.opacity === 0));
}
if (newStyles.transform) {
let transform = newStyles.transform;
const parent = this.getVisualParentNode();
//three ways we can be position absolute:
//1. user explicitly set this node to absolute
//2. parent has no layout, which translate into no flexDirection
//3. there is no parent, meaning we're a root in the root component
if (this.style.position === 'absolute' || !parent || !parent.style.flexDirection) {
if (this.props.alignX === 'center' && !(domElement.style.marginLeft && domElement.style.marginRight))
transform = 'translateX(-50%) ' + transform;
if (this.props.alignY === 'center' && !(domElement.style.marginTop && domElement.style.marginBottom))
transform = 'translateY(-50%) ' + transform;
}
newStyles.transform = transform;
}
const marginsChanged =
newStyles.hasOwnProperty('marginLeft') ||
newStyles.hasOwnProperty('marginRight') ||
newStyles.hasOwnProperty('marginTop') ||
newStyles.hasOwnProperty('marginBottom');
const sizeInPercent =
(this.props.width && this.props.width[this.props.width.length - 1] === '%') ||
(this.props.height && this.props.height[this.props.height.length - 1] === '%');
if (sizeInPercent && marginsChanged) {
forceUpdate = true;
}
if (newStyles.position || newStyles.flexDirection || newStyles.clip) {
forceUpdate = true;
}
}
if (forceUpdate) {
this.forceUpdate();
} else {
setStylesOnDOMNode(domElement, newStyles, styleTag);
}
},
removeStyle(styles, styleTag) {
const styleObject = styleTag ? this.props.styles[styleTag] : this.style;
for (const p of styles) {
delete styleObject[p];
}
const domElement = this.getDOMElement();
let forceUpdate = false;
if (!styleTag && domElement) {
const forceUpdateAttributes = {
marginTop: true,
marginBottom: true,
marginLeft: true,
marginRight: true
};
for (const p of styles) {
if (forceUpdateAttributes[p]) forceUpdate = true;
}
}
if (domElement) {
//deleting styles is done by setting them to an empty string
const newStyles = {};
for (const p of styles) {
newStyles[p] = '';
}
setStylesOnDOMNode(domElement, newStyles, styleTag);
}
if (forceUpdate) {
this.forceUpdate();
}
},
getStyle(style) {
return this.style[style];
},
getRef() {
return this.reactComponentRef;
},
getDOMElement() {
const ref = this.getRef();
if (!ref) return;
return ReactDOM.findDOMNode(ref);
},
getVisualParentNode() {
if (this.parent) return this.parent;
//we're a root
let component = this.nodeScope.componentOwner;
while (!component.parent && component.parentNodeScope) {
component = component.parentNodeScope.componentOwner;
}
return component ? component.parent : undefined;
},
setVariant(variant) {
//stop any state transitions that are currently running
this._stopStateTransitions();
this.variant = variant;
const parameters = {};
//apply base variant parameters
variant && mergeDeep(parameters, variant.parameters);
//apply base node parameters
mergeDeep(parameters, this.model.parameters);
//and then states, if any
if (this.currentVisualStates) {
const stateParameters = this.getParametersForStates(this.currentVisualStates);
mergeDeep(parameters, stateParameters);
}
const parametersToSet = Object.keys(parameters).filter((p) => !this._hasInputBeenSetFromAConnection(p));
for (const inputName of parametersToSet) {
this.registerInputIfNeeded(inputName);
if (this.hasInput(inputName)) {
this.queueInput(inputName, parameters[inputName]);
}
}
},
getParameter(name) {
if (this.model.parameters.hasOwnProperty(name)) {
return this.model.parameters[name];
} else if (this.variant && this.variant.parameters.hasOwnProperty(name)) {
return this.variant.parameters[name];
} else {
return this.context.getDefaultValueForInput(this.model.type, name);
}
},
getParametersForStates(states) {
const params = {};
//1. Get the parameters from the variant
//2. Then override with all local values from the node's neutral state (so a color in neutral will override all states from the variant)
//3. then apply the node specific state parameters
//1. Apply variant states
if (this.variant) {
for (const state of states) {
if (this.variant.stateParameters && this.variant.stateParameters.hasOwnProperty(state)) {
mergeDeep(params, this.variant.stateParameters[state]);
}
}
}
//2. Override with local values from the nodes neutral state
for (const param in params) {
if (this.model.parameters.hasOwnProperty(param)) {
if (isObject(params[param])) {
mergeDeep(params[param], this.model.parameters[param]);
} else {
params[param] = this.model.parameters[param];
}
}
}
// mergeDeep(params, this.model.parameters);
//3. Apply node specific state paramters
if (this.model.stateParameters) {
for (const state of states) {
if (this.model.stateParameters.hasOwnProperty(state)) {
mergeDeep(params, this.model.stateParameters[state]);
}
}
}
return params;
},
_getNewState(prevStates, newStates) {
const addedStates = newStates.filter((value) => !(prevStates || []).includes(value));
const newState = addedStates.length ? addedStates[0] : 'neutral';
return newState === '' ? 'neutral' : newState;
},
_getDefaultTransition(state) {
if (
this.model.defaultStateTransitions &&
this.model.defaultStateTransitions[state] &&
this.model.defaultStateTransitions[state].curve
) {
return this.model.defaultStateTransitions[state];
} else if (
this.variant &&
this.variant.defaultStateTransitions &&
this.variant.defaultStateTransitions[state] &&
this.variant.defaultStateTransitions[state].curve
) {
return this.variant.defaultStateTransitions[state];
}
},
_getStateTransition(state) {
let transitions = {};
if (this.model.stateTransitions && this.model.stateTransitions[state]) {
Object.assign(transitions, this.model.stateTransitions[state]);
}
if (this.variant && this.variant.stateTransitions && this.variant.stateTransitions[state]) {
Object.assign(transitions, this.variant.stateTransitions[state]);
}
return transitions;
},
setVisualStates(newStates) {
if (!this.model) {
//this node has probably been generated by a router or similar, and is an internal node without a model
//those nodes can't have visual states
return;
}
const statesAreEqual =
this.currentVisualStates &&
newStates.length === this.currentVisualStates.length &&
newStates.every((val, index) => val === this.currentVisualStates[index]);
if (statesAreEqual) return;
const prevStateParams = this.currentVisualStates ? this.getParametersForStates(this.currentVisualStates) : {};
const newStateParams = this.getParametersForStates(newStates);
const newState = this._getNewState(this.currentVisualStates, newStates);
this.currentVisualStates = newStates;
const newValues = {};
//all params that were in the old states, but not in the new states, needs to be reset back to original state
for (const param in prevStateParams) {
if (!newStateParams.hasOwnProperty(param) && !this._hasInputBeenSetFromAConnection(param)) {
const value = this.getParameter(param);
if (value !== undefined) {
newValues[param] = this.getParameter(param);
}
}
}
for (const param in newStateParams) {
if (!this._hasInputBeenSetFromAConnection(param) && newStateParams[param] !== undefined) {
newValues[param] = newStateParams[param];
}
}
const defaultTransition = this._getDefaultTransition(newState);
const stateTransition = this._getStateTransition(newState);
for (const param in newValues) {
if (stateTransition[param] && stateTransition[param].curve) {
transitionParameter(this, param, newValues[param], stateTransition[param]);
} else if (!stateTransition[param] && defaultTransition) {
transitionParameter(this, param, newValues[param], defaultTransition);
} else {
//stop any running transition
if (this._transitions && this._transitions[param]) {
this._transitions[param].stop();
delete this._transitions[param];
}
this.queueInput(param, newValues[param]);
}
}
},
_getVisualStates() {
return this.currentVisualStates || [];
},
_stopStateTransitions() {
if (!this._transitions) return;
for (const name in this._transitions) {
this._transitions[name].stop();
delete this._transitions[name];
}
}
}
};
if (useVariants) {
ReactComponentNode.inputs.variant = {
displayName: 'Variant',
group: 'General',
type: {
name: 'string',
allowConnectionsOnly: true
},
set(variantName) {
if (this.variant && this.variant.name === variantName) return;
const variant = this.context.variants.getVariant(this.model.type, variantName);
variant && this.setVariant(variant);
}
};
}
if (def.mountedInput !== false) {
ReactComponentNode.inputs.mounted = {
displayName: 'Mounted',
index: 9999,
type: 'boolean',
group: 'General',
default: true,
set(value) {
value = value ? true : false;
if (this.wantsToBeMounted !== value) {
this.wantsToBeMounted = value;
//either we have a direct parent, or we're a root and need to tell
//the parent of the component instance instead
const parent = this.getVisualParentNode();
if (parent) {
parent.cachedChildren = undefined;
parent.forceUpdate();
}
}
}
};
}
const hasChildCountOutput = ReactComponentNode.allowChildren || ReactComponentNode.displayName;
if (hasChildCountOutput) {
ReactComponentNode.outputs.childrenCount = {
displayName: 'Children Count',
type: 'number',
get() {
return this.childrenCount;
}
};
}
//regular inputs
for (const name in inputs) {
ReactComponentNode.inputs[name] = inputs[name];
}
//inputs that set react props
for (const inputName in inputProps) {
const input = inputProps[inputName];
if (input.type === 'node') {
input.type = 'reference';
input.set = function (value) {
const props = input.propPath ? this.props[input.propPath] : this.props;
if (value !== undefined) {
props[inputName] = value.render();
} else {
delete props[inputName];
}
this.forceUpdate();
};
} else {
if (input.type === 'signal') {
console.error(`Error: Signals not supported as a react prop. node: '${def.name}' input: '${inputName}'`);
} else {
defineRegularInputProp(input, inputName);
}
}
ReactComponentNode.inputs[inputName] = input;
}
//inputs that set a css attribute
for (const name in inputCss) {
const input = inputCss[name];
const styleTargetName = input.targetStyleProperty || name;
if (input.type.units) {
input.set = function (value) {
if (typeof value !== 'object' && input.type.defaultUnit) {
value = { value, unit: input.type.defaultUnit };
}
if (typeof value === 'object' && value.value !== undefined) {
//this is a value with a unit
this.setStyle({ [styleTargetName]: value.value + value.unit }, input.styleTag);
} else if (value !== undefined) {
//value without a unit. One example is line height, that can be unitless
this.setStyle({ [styleTargetName]: value }, input.styleTag);
} else {
//value is undefined, so just reset the style
this.removeStyle([styleTargetName], input.styleTag);
}
if (input.onChange) {
input.onChange.call(this, value);
}
};
} else {
input.set = function (value) {
if (value !== undefined) {
this.setStyle({ [styleTargetName]: value }, input.styleTag);
} else {
this.removeStyle([styleTargetName], input.styleTag);
}
if (input.onChange) {
input.onChange.call(this, value);
}
};
}
ReactComponentNode.inputs[name] = input;
}
//regular outputs
for (const name in outputs) {
ReactComponentNode.outputs[name] = outputs[name];
}
//outputs triggered by react props
for (const name in outputProps) {
const output = outputProps[name];
if (output.type !== 'signal') {
output.get = function () {
return this.outputPropValues[name];
};
}
ReactComponentNode.outputs[name] = output;
}
for (const name in methods) {
ReactComponentNode.methods[name] = methods[name];
}
return {
node: ReactComponentNode,
setup: def.setup
};
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
export { createNodeFromReactComponent };