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:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,435 @@
import React, { useEffect } from 'react';
import FontLoader from '../../fontloader';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
// --------------------------------------------------------------------------------------
// Button
// --------------------------------------------------------------------------------------
function Button(props) {
// On mount
useEffect(() => {
props.focusChanged && props.focusChanged(false);
props.hoverChanged && props.hoverChanged(false);
props.pressedChanged && props.pressedChanged(false);
}, []);
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.textStyle !== undefined) {
// Apply text style
style = Object.assign({}, props.textStyle, style);
}
if (props.boxShadowEnabled) {
style.boxShadow = `${props.boxShadowInset ? 'inset ' : ''}${props.boxShadowOffsetX} ${props.boxShadowOffsetY} ${
props.boxShadowBlurRadius
} ${props.boxShadowSpreadRadius} ${props.boxShadowColor}`;
}
let className = 'ndl-controls-button';
if (props.className) className = className + ' ' + props.className;
return (
<button
className={className}
disabled={!props.enabled}
{...Utils.controlEvents(props)}
type={props.buttonType}
style={style}
onClick={props.onClick}
>
{props.label}
{props.children}
</button>
);
}
var ButtonNode = {
name: 'Button',
docs: 'https://docs.noodl.net/nodes/visual/button',
allowChildren: true,
noodlNodeAsProp: true,
initialize() {
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
},
getReactComponent() {
return Button;
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
},
// Text style
textStyle: {
index: 20,
type: 'textStyle',
group: 'Text',
displayName: 'Text Style',
default: 'None',
set(value) {
this.props.textStyle = this.context.styles.getTextStyle(value);
this.forceUpdate();
}
},
fontFamily: {
index: 21,
type: 'font',
group: 'Text',
displayName: 'Font Family',
set(value) {
if (value) {
let family = value;
if (family.split('.').length > 1) {
family = family.replace(/\.[^/.]+$/, '');
family = family.split('/').pop();
}
this.setStyle({ fontFamily: family });
} else {
this.removeStyle(['fontFamily']);
}
if (this.props.textStyle) {
this.forceUpdate();
}
}
}
/* textAlignX: {
group: 'Text Alignment',
index: 13,
displayName: 'Text Horizontal Align',
type: {
name: 'enum',
enums: [
{label: 'left', value: 'left'},
{label: 'center', value: 'center'},
{label: 'right', value: 'right'}
],
alignComp: 'justify'
},
default: 'left',
set(value) {
switch(value) {
case 'left': this.setStyle({textAlign: 'left', justifyContent: 'flex-start'}); break;
case 'center': this.setStyle({textAlign: 'center', justifyContent: 'center'}); break;
case 'right': this.setStyle({textAlign: 'right', justifyContent: 'flex-end'}); break;
}
}
},
textAlignY: {
group: 'Text Alignment',
index: 14,
displayName: 'Text Vertical Align',
type: {
name: 'enum',
enums: [
{label: 'Top', value: 'top'},
{label: 'Center', value: 'center'},
{label: 'Bottom', value: 'bottom'}
],
alignComp: 'vertical'
},
default: 'top',
set(value) {
switch(value) {
case 'top': this.setStyle({alignItems: 'flex-start'}); break;
case 'center': this.setStyle({alignItems: 'center'}); break;
case 'bottom': this.setStyle({alignItems: 'flex-end'}); break;
}
}
}*/
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
}
},
inputCss: {
fontSize: {
index: 21,
group: 'Text',
displayName: 'Font Size',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
onChange() {
if (this.props.textStyle) {
this.forceUpdate();
}
}
},
color: {
index: 24,
group: 'Text',
displayName: 'Color',
type: 'color',
default: '#FFFFFF'
},
backgroundColor: {
index: 100,
displayName: 'Background Color',
group: 'Style',
type: 'color',
default: '#000000'
},
// Padding
paddingLeft: {
index: 64,
group: 'Margin and padding',
default: 20,
applyDefault: false,
displayName: 'Pad Left',
type: { name: 'number', units: ['px'], defaultUnit: 'px', marginPaddingComp: 'padding-left' }
},
paddingRight: {
index: 65,
group: 'Margin and padding',
default: 20,
applyDefault: false,
displayName: 'Pad Right',
type: { name: 'number', units: ['px'], defaultUnit: 'px', marginPaddingComp: 'padding-right' }
},
paddingTop: {
index: 66,
group: 'Margin and padding',
displayName: 'Pad Top',
default: 5,
applyDefault: false,
type: { name: 'number', units: ['px'], defaultUnit: 'px', marginPaddingComp: 'padding-top' }
},
paddingBottom: {
index: 67,
group: 'Margin and padding',
displayName: 'Pad Bottom',
default: 5,
applyDefault: false,
type: { name: 'number', units: ['px'], defaultUnit: 'px', marginPaddingComp: 'padding-bottom' }
},
// Border styles
borderRadius: {
index: 202,
displayName: 'Border Radius',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 0,
applyDefault: false
},
borderStyle: {
index: 203,
displayName: 'Border Style',
group: 'Style',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
},
default: 'none',
applyDefault: false
},
borderWidth: {
index: 204,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 0,
applyDefault: false
},
borderColor: {
index: 205,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: '#000000'
}
},
inputProps: {
label: {
type: 'string',
displayName: 'Label',
group: 'General',
default: 'Label'
},
buttonType: {
type: {
name: 'enum',
enums: [
{ label: 'Button', value: 'button' },
{ label: 'Submit', value: 'submit' }
]
},
displayName: 'Type',
default: 'button',
group: 'General'
},
// Box shadow
boxShadowEnabled: {
index: 250,
group: 'Box Shadow',
displayName: 'Shadow Enabled',
type: 'boolean',
default: false
},
boxShadowOffsetX: {
index: 251,
group: 'Box Shadow',
displayName: 'Offset X',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowOffsetY: {
index: 252,
group: 'Box Shadow',
displayName: 'Offset Y',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowBlurRadius: {
index: 253,
group: 'Box Shadow',
displayName: 'Blur Radius',
default: 5,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowSpreadRadius: {
index: 254,
group: 'Box Shadow',
displayName: 'Spread Radius',
default: 2,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowInset: {
index: 255,
group: 'Box Shadow',
displayName: 'Inset',
type: 'boolean',
default: false
},
boxShadowColor: {
index: 256,
group: 'Box Shadow',
displayName: 'Shadow Color',
type: 'color',
default: 'rgba(0,0,0,0.2)'
}
},
outputProps: {
// Click
onClick: {
displayName: 'Click',
group: 'Events',
type: 'signal'
}
},
dynamicports: [
{
condition: 'boxShadowEnabled = true',
inputs: [
'boxShadowOffsetX',
'boxShadowOffsetY',
'boxShadowInset',
'boxShadowBlurRadius',
'boxShadowSpreadRadius',
'boxShadowColor'
]
}
],
methods: {}
};
NodeSharedPortDefinitions.addDimensions(ButtonNode, {
defaultSizeMode: 'contentSize',
contentLabel: 'Content',
useDimensionConstraints: false
});
NodeSharedPortDefinitions.addAlignInputs(ButtonNode);
NodeSharedPortDefinitions.addTransformInputs(ButtonNode);
//NodeSharedPortDefinitions.addPaddingInputs(ButtonNode);
NodeSharedPortDefinitions.addMarginInputs(ButtonNode);
NodeSharedPortDefinitions.addSharedVisualInputs(ButtonNode);
Utils.addControlEventsAndStates(ButtonNode);
ButtonNode = createNodeFromReactComponent(ButtonNode);
ButtonNode.setup = function (context, graphModel) {
graphModel.on('nodeAdded.Button', function (node) {
if (node.parameters.fontFamily && node.parameters.fontFamily.split('.').length > 1) {
FontLoader.instance.loadFont(node.parameters.fontFamily);
}
node.on('parameterUpdated', function (event) {
if (event.name === 'fontFamily' && event.value) {
if (event.value.split('.').length > 1) {
FontLoader.instance.loadFont(event.value);
}
}
});
});
};
export default ButtonNode;

View File

@@ -0,0 +1,329 @@
import React, { useEffect, useState } from 'react';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
function _styleTemplate(_class, props) {
return `
.${_class}:checked {
background-color: ${props.checkedBackgroundColor};
}
`;
}
// --------------------------------------------------------------------------------------
// CheckBox
// --------------------------------------------------------------------------------------
function CheckBox(props) {
const [checked, setChecked] = useState(props.checked);
// Report initial values when mounted
useEffect(() => {
setChecked(!!props.checked);
}, []);
useEffect(() => {
setChecked(!!props.checked);
}, [props.checked]);
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.boxShadowEnabled) {
style.boxShadow = `${props.boxShadowInset ? 'inset ' : ''}${props.boxShadowOffsetX} ${props.boxShadowOffsetY} ${
props.boxShadowBlurRadius
} ${props.boxShadowSpreadRadius} ${props.boxShadowColor}`;
}
const tagProps = { id: props.id, style: style };
Utils.updateStylesForClass(
'ndl-controls-checkbox-' + props._nodeId,
{ checkedBackgroundColor: props.checkedBackgroundColor },
_styleTemplate
);
let className = 'ndl-controls-checkbox-' + props._nodeId + ' ndl-controls-checkbox';
if (props.className) className = className + ' ' + props.className;
return (
<input
className={className}
type="checkbox"
{...tagProps}
{...Utils.controlEvents(props)}
checked={checked}
disabled={!props.enabled}
onChange={(e) => {
setChecked(e.target.checked);
props.checkedChanged && props.checkedChanged(e.target.checked);
}}
></input>
);
}
var CheckBoxNode = {
name: 'Checkbox',
displayName: 'Checkbox',
docs: 'https://docs.noodl.net/nodes/visual/checkbox',
allowChildren: false,
noodlNodeAsProp: true,
initialize() {
this.props.sizeMode = 'explicit';
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
this.props.checked = this._internal.checked = false;
this.props._nodeId = this.id;
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
this.props.checkedChanged = (checked) => {
const changed = this._internal.checked !== checked;
this._internal.checked = checked;
if (changed) {
this.flagOutputDirty('checked');
this.sendSignalOnOutput('onChange');
}
};
},
getReactComponent() {
return CheckBox;
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
},
checked: {
type: 'boolean',
displayName: 'Checked',
group: 'General',
default: false,
set: function (value) {
value = !!value;
const changed = value !== this._internal.checked;
this.props.checked = this._internal.checked = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('checked');
}
}
}
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
},
checked: {
type: 'boolean',
displayName: 'Checked',
group: 'States',
getter: function () {
return this._internal.checked;
}
},
// Change
onChange: {
displayName: 'Changed',
group: 'Events',
type: 'signal'
}
},
inputProps: {
width: {
index: 11,
group: 'Dimensions',
displayName: 'Width',
type: {
name: 'number',
units: ['%', 'px', 'vw'],
defaultUnit: 'px'
},
default: 32
},
height: {
index: 12,
group: 'Dimensions',
displayName: 'Height',
type: {
name: 'number',
units: ['%', 'px', 'vh'],
defaultUnit: 'px'
},
default: 32
},
// Styles
checkedBackgroundColor: {
displayName: 'Background color',
group: 'Checked Style',
type: { name: 'color', allowEditOnly: true },
default: '#000000'
},
// Box shadow
boxShadowEnabled: {
index: 250,
group: 'Box Shadow',
displayName: 'Shadow Enabled',
type: 'boolean',
default: false
},
boxShadowOffsetX: {
index: 251,
group: 'Box Shadow',
displayName: 'Offset X',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowOffsetY: {
index: 252,
group: 'Box Shadow',
displayName: 'Offset Y',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowBlurRadius: {
index: 253,
group: 'Box Shadow',
displayName: 'Blur Radius',
default: 5,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowSpreadRadius: {
index: 254,
group: 'Box Shadow',
displayName: 'Spread Radius',
default: 2,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowInset: {
index: 255,
group: 'Box Shadow',
displayName: 'Inset',
type: 'boolean',
default: false
},
boxShadowColor: {
index: 256,
group: 'Box Shadow',
displayName: 'Shadow Color',
type: 'color',
default: 'rgba(0,0,0,0.2)'
}
},
inputCss: {
backgroundColor: {
index: 201,
displayName: 'Background Color',
group: 'Style',
type: 'color'
},
// Border styles
borderRadius: {
index: 202,
displayName: 'Border Radius',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 2,
applyDefault: false
},
borderStyle: {
index: 203,
displayName: 'Border Style',
group: 'Style',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
},
default: 'solid',
applyDefault: false
},
borderWidth: {
index: 204,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 1,
applyDefault: false
},
borderColor: {
index: 205,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: '#000000'
}
},
outputProps: {},
methods: {}
};
NodeSharedPortDefinitions.addAlignInputs(CheckBoxNode);
NodeSharedPortDefinitions.addTransformInputs(CheckBoxNode);
NodeSharedPortDefinitions.addMarginInputs(CheckBoxNode);
NodeSharedPortDefinitions.addSharedVisualInputs(CheckBoxNode);
Utils.addControlEventsAndStates(CheckBoxNode);
CheckBoxNode = createNodeFromReactComponent(CheckBoxNode);
export default CheckBoxNode;

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { flexDirectionValues } from '../../constants/flex';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
function FieldSet(props) {
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
let className = 'ndl-controls-fieldset';
if (props.className) className = className + ' ' + props.className;
return (
<fieldset className={className} style={style}>
{props.children}
</fieldset>
);
}
var FieldSetNode = {
name: 'Field Set',
docs: 'https://docs.noodl.net/nodes/visual/fieldset',
allowChildren: true,
noodlNodeAsProp: true,
deprecated: true,
initialize() {},
defaultCss: {
display: 'flex',
position: 'relative',
flexDirection: 'column'
},
getReactComponent() {
return FieldSet;
},
inputs: {
flexDirection: {
//don't rename for backwards compat
index: 11,
displayName: 'Layout',
group: 'Layout',
type: {
name: 'enum',
enums: [
{ label: 'Vertical', value: 'column' },
{ label: 'Horizontal', value: 'row' }
]
},
default: 'column',
set(value) {
this.props.layout = value;
if (value !== 'none') {
this.setStyle({ flexDirection: value });
} else {
this.removeStyle(['flexDirection']);
}
if (this.context.editorConnection) {
// Send warning if the value is wrong
if (value !== 'none' && !flexDirectionValues.includes(value)) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'layout-warning', {
message: 'Invalid Layout value has to be a valid flex-direction value.'
});
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'layout-warning');
}
}
this.forceUpdate();
}
}
},
inputProps: {},
outputProps: {}
};
NodeSharedPortDefinitions.addDimensions(FieldSetNode, { defaultSizeMode: 'contentSize', contentLabel: 'Content' });
NodeSharedPortDefinitions.addAlignInputs(FieldSetNode);
NodeSharedPortDefinitions.addTransformInputs(FieldSetNode);
NodeSharedPortDefinitions.addMarginInputs(FieldSetNode);
NodeSharedPortDefinitions.addPaddingInputs(FieldSetNode);
NodeSharedPortDefinitions.addSharedVisualInputs(FieldSetNode);
FieldSetNode = createNodeFromReactComponent(FieldSetNode);
export default FieldSetNode;

View File

@@ -0,0 +1,98 @@
import React from 'react';
import { flexDirectionValues } from '../../constants/flex';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
function Form(props) {
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
let className = 'ndl-controls-form';
if (props.className) className = className + ' ' + props.className;
return (
<form
className={className}
style={style}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit && props.onSubmit();
}}
>
{props.children}
</form>
);
}
var FormNode = {
name: 'Form',
docs: 'https://docs.noodl.net/nodes/visual/form',
allowChildren: true,
noodlNodeAsProp: true,
deprecated: true,
initialize() {},
defaultCss: {
display: 'flex',
position: 'relative',
flexDirection: 'column'
},
getReactComponent() {
return Form;
},
inputs: {
flexDirection: {
//don't rename for backwards compat
index: 11,
displayName: 'Layout',
group: 'Layout',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Vertical', value: 'column' },
{ label: 'Horizontal', value: 'row' }
]
},
default: 'column',
set(value) {
this.props.layout = value;
if (value !== 'none') {
this.setStyle({ flexDirection: value });
} else {
this.removeStyle(['flexDirection']);
}
if (this.context.editorConnection) {
// Send warning if the value is wrong
if (value !== 'none' && !flexDirectionValues.includes(value)) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'layout-warning', {
message: 'Invalid Layout value has to be a valid flex-direction value.'
});
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'layout-warning');
}
}
this.forceUpdate();
}
}
},
inputProps: {},
outputProps: {
onSubmit: { type: 'signal', displayName: 'Submit', group: 'Events' }
}
};
NodeSharedPortDefinitions.addDimensions(FormNode, { defaultSizeMode: 'contentSize', contentLabel: 'Content' });
NodeSharedPortDefinitions.addAlignInputs(FormNode);
NodeSharedPortDefinitions.addTransformInputs(FormNode);
NodeSharedPortDefinitions.addMarginInputs(FormNode);
NodeSharedPortDefinitions.addPaddingInputs(FormNode);
NodeSharedPortDefinitions.addSharedVisualInputs(FormNode);
FormNode = createNodeFromReactComponent(FormNode);
export default FormNode;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
function Label(props) {
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.textStyle !== undefined) {
// Apply text style
style = Object.assign({}, props.textStyle, style);
}
const tagProps = {
for: props.for,
onClick: props.onClick
};
let className = 'ndl-controls-label';
if (props.className) className = className + ' ' + props.className;
return (
<label className={className} style={style} {...tagProps}>
{props.text}
</label>
);
}
const LabelNode = {
name: 'Label',
displayName: 'Label',
docs: 'https://docs.noodl.net/nodes/visual/label',
allowChildren: true,
noodlNodeAsProp: true,
deprecated: true,
getReactComponent() {
return Label;
},
defaultCss: {
position: 'relative',
display: 'flex'
},
inputProps: {
for: { type: 'string', displayName: 'For', group: 'General' },
text: { type: 'string', displayName: 'Text', group: 'General' }
}
};
NodeSharedPortDefinitions.addDimensions(LabelNode, { defaultSizeMode: 'contentSize', contentLabel: 'Content' });
NodeSharedPortDefinitions.addAlignInputs(LabelNode);
NodeSharedPortDefinitions.addTextStyleInputs(LabelNode);
NodeSharedPortDefinitions.addTransformInputs(LabelNode);
//NodeSharedPortDefinitions.addPaddingInputs(LabelNode);
NodeSharedPortDefinitions.addMarginInputs(LabelNode);
NodeSharedPortDefinitions.addSharedVisualInputs(LabelNode);
NodeSharedPortDefinitions.addBorderInputs(LabelNode);
export default createNodeFromReactComponent(LabelNode);

View File

@@ -0,0 +1,413 @@
import React, { useEffect, useState } from 'react';
import FontLoader from '../../fontloader';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
function Options(props) {
const [value, setValue] = useState(props.value);
// Must update value output on both "mount" and when it's changed
useEffect(() => {
setValue(props.value);
}, []);
useEffect(() => {
setValue(props.value);
}, [props.value]);
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.textStyle !== undefined) {
// Apply text style
style = Object.assign({}, props.textStyle, style);
}
if (props.boxShadowEnabled) {
style.boxShadow = `${props.boxShadowInset ? 'inset ' : ''}${props.boxShadowOffsetX} ${props.boxShadowOffsetY} ${
props.boxShadowBlurRadius
} ${props.boxShadowSpreadRadius} ${props.boxShadowColor}`;
}
// Hide label if there is no selected value, of if value is not in the items array
const selectedIndex =
value === undefined || value === ''
? -1
: props.items === undefined
? -1
: props.items.findIndex((i) => i.Value === value);
const tagProps = { id: props.id, style: style, onClick: props.onClick };
let className = 'ndl-controls-select';
if (props.className) className = className + ' ' + props.className;
return (
<select
className={className}
ref={(el) => {
if (el) el.selectedIndex = selectedIndex;
}}
{...tagProps}
disabled={!props.enabled}
value={value}
{...Utils.controlEvents(props)}
onChange={(e) => {
setValue(e.target.value);
props.valueChanged && props.valueChanged(e.target.value);
}}
>
{props.items !== undefined
? props.items.map((i) => (
<option
value={i.Value}
disabled={i.Disabled === 'true' || i.Disabled === true ? true : undefined}
selected={i.Value === value}
>
{i.Label}
</option>
))
: null}
</select>
);
}
var OptionsNode = {
name: 'Options',
displayName: 'Options',
docs: 'https://docs.noodl.net/nodes/visual/options',
allowChildren: false,
noodlNodeAsProp: true,
initialize: function () {
this._itemsChanged = () => {
this.forceUpdate();
};
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
this.props.valueChanged = (value) => {
const changed = this._internal.value !== value;
this._internal.value = value;
if (changed) {
this.flagOutputDirty('value');
this.sendSignalOnOutput('onChange');
}
};
},
getReactComponent() {
return Options;
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
},
items: {
type: 'array',
displayName: 'Items',
group: 'General',
set: function (newValue) {
if (this._internal.items !== newValue && this._internal.items !== undefined) {
this._internal.items.off('change', this._itemsChanged);
}
this._internal.items = newValue;
this._internal.items.on('change', this._itemsChanged);
this.props.items = this._internal.items;
}
},
value: {
type: '*',
displayName: 'Value',
group: 'General',
set: function (value) {
if (value !== undefined && typeof value !== 'string') {
if (value.toString !== undefined) value = value.toString();
else return;
}
const changed = value !== this._internal.value;
this.props.value = this._internal.value = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('value');
}
}
},
// Text style
textStyle: {
index: 20,
type: 'textStyle',
group: 'Text',
displayName: 'Text Style',
default: 'None',
set(value) {
this.props.textStyle = this.context.styles.getTextStyle(value);
this.forceUpdate();
}
},
fontFamily: {
index: 21,
type: 'font',
group: 'Text',
displayName: 'Font Family',
set(value) {
if (value) {
let family = value;
if (family.split('.').length > 1) {
family = family.replace(/\.[^/.]+$/, '');
family = family.split('/').pop();
}
this.setStyle({ fontFamily: family });
} else {
this.removeStyle(['fontFamily']);
}
if (this.props.textStyle) {
this.forceUpdate();
}
}
}
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
},
value: {
type: 'string',
displayName: 'Value',
group: 'States',
getter: function () {
return this._internal.value;
}
},
onChange: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
}
},
inputCss: {
fontSize: {
index: 21,
group: 'Text',
displayName: 'Font Size',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
onChange() {
if (this.props.textStyle) {
this.forceUpdate();
}
}
},
color: {
index: 24,
group: 'Text',
displayName: 'Color',
type: 'color'
},
backgroundColor: {
index: 100,
displayName: 'Background Color',
group: 'Style',
type: 'color',
default: 'transparent'
},
// Border styles
borderRadius: {
index: 202,
displayName: 'Border Radius',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 0,
applyDefault: false
},
borderStyle: {
index: 203,
displayName: 'Border Style',
group: 'Style',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
},
default: 'solid',
applyDefault: false
},
borderWidth: {
index: 204,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 1,
applyDefault: false
},
borderColor: {
index: 205,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: '#000000'
}
},
inputProps: {
// Box shadow
boxShadowEnabled: {
index: 250,
group: 'Box Shadow',
displayName: 'Shadow Enabled',
type: 'boolean',
default: false
},
boxShadowOffsetX: {
index: 251,
group: 'Box Shadow',
displayName: 'Offset X',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowOffsetY: {
index: 252,
group: 'Box Shadow',
displayName: 'Offset Y',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowBlurRadius: {
index: 253,
group: 'Box Shadow',
displayName: 'Blur Radius',
default: 5,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowSpreadRadius: {
index: 254,
group: 'Box Shadow',
displayName: 'Spread Radius',
default: 2,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowInset: {
index: 255,
group: 'Box Shadow',
displayName: 'Inset',
type: 'boolean',
default: false
},
boxShadowColor: {
index: 256,
group: 'Box Shadow',
displayName: 'Shadow Color',
type: 'color',
default: 'rgba(0,0,0,0.2)'
}
},
outputProps: {},
dynamicports: [
{
condition: 'boxShadowEnabled = true',
inputs: [
'boxShadowOffsetX',
'boxShadowOffsetY',
'boxShadowInset',
'boxShadowBlurRadius',
'boxShadowSpreadRadius',
'boxShadowColor'
]
}
],
methods: {}
};
NodeSharedPortDefinitions.addDimensions(OptionsNode, { defaultSizeMode: 'contentSize', contentLabel: 'Content' });
NodeSharedPortDefinitions.addAlignInputs(OptionsNode);
NodeSharedPortDefinitions.addTransformInputs(OptionsNode);
NodeSharedPortDefinitions.addPaddingInputs(OptionsNode);
NodeSharedPortDefinitions.addMarginInputs(OptionsNode);
NodeSharedPortDefinitions.addSharedVisualInputs(OptionsNode);
Utils.addControlEventsAndStates(OptionsNode);
OptionsNode = createNodeFromReactComponent(OptionsNode);
OptionsNode.setup = function (context, graphModel) {
graphModel.on('nodeAdded.Options', function (node) {
if (node.parameters.fontFamily && node.parameters.fontFamily.split('.').length > 1) {
FontLoader.instance.loadFont(node.parameters.fontFamily);
}
node.on('parameterUpdated', function (event) {
if (event.name === 'fontFamily' && event.value) {
if (event.value.split('.').length > 1) {
FontLoader.instance.loadFont(event.value);
}
}
});
});
};
export default OptionsNode;

View File

@@ -0,0 +1,299 @@
import React, { useContext } from 'react';
import RadioButtonContext from '../../contexts/radiobuttoncontext';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
function _styleTemplate(_class, props) {
return `
.${_class}:checked {
background-color: ${props.checkedBackgroundColor};
}
`;
}
function RadioButton(props) {
const radioButtonGroup = useContext(RadioButtonContext);
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
if (props.boxShadowEnabled) {
style.boxShadow = `${props.boxShadowInset ? 'inset ' : ''}${props.boxShadowOffsetX} ${props.boxShadowOffsetY} ${
props.boxShadowBlurRadius
} ${props.boxShadowSpreadRadius} ${props.boxShadowColor}`;
}
const tagProps = { id: props.id, style: style };
props.checkedChanged && props.checkedChanged(radioButtonGroup ? radioButtonGroup.selected === props.value : false);
Utils.updateStylesForClass(
'ndl-controls-radiobutton-' + props._nodeId,
{ checkedBackgroundColor: props.checkedBackgroundColor },
_styleTemplate
);
let className = 'ndl-controls-radiobutton-' + props._nodeId + ' ndl-controls-radiobutton';
if (props.className) className = className + ' ' + props.className;
return (
<input
className={className}
{...Utils.controlEvents(props)}
type="radio"
name={radioButtonGroup ? radioButtonGroup.name : undefined}
{...tagProps}
disabled={!props.enabled}
checked={radioButtonGroup ? radioButtonGroup.selected === props.value : false}
onChange={(e) => {
radioButtonGroup && radioButtonGroup.checkedChanged && radioButtonGroup.checkedChanged(props.value);
}}
></input>
);
}
var RadioButtonNode = {
name: 'Radio Button',
displayName: 'Radio Button',
docs: 'https://docs.noodl.net/nodes/visual/radiobutton',
allowChildren: false,
noodlNodeAsProp: true,
initialize() {
this.props.sizeMode = 'explicit';
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
this.props._nodeId = this.id;
this._internal.checked = false;
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
this.props.checkedChanged = (checked) => {
const changed = this._internal.checked !== checked;
this._internal.checked = checked;
if (changed) {
this.flagOutputDirty('checked');
}
};
},
getReactComponent() {
return RadioButton;
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
}
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
},
checked: {
type: 'boolean',
displayName: 'Checked',
group: 'States',
getter: function () {
return this._internal.checked;
}
}
},
inputProps: {
value: { type: 'string', displayName: 'Value', group: 'General' },
width: {
index: 11,
group: 'Dimensions',
displayName: 'Width',
type: {
name: 'number',
units: ['%', 'px', 'vw'],
defaultUnit: 'px'
},
default: 32
},
height: {
index: 12,
group: 'Dimensions',
displayName: 'Height',
type: {
name: 'number',
units: ['%', 'px', 'vh'],
defaultUnit: 'px'
},
default: 32
},
// Styles
checkedBackgroundColor: {
displayName: 'Background color',
group: 'Checked Style',
type: { name: 'color', allowEditOnly: true },
default: '#000000'
},
// Box shadow
boxShadowEnabled: {
index: 250,
group: 'Box Shadow',
displayName: 'Shadow Enabled',
type: 'boolean',
default: false
},
boxShadowOffsetX: {
index: 251,
group: 'Box Shadow',
displayName: 'Offset X',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowOffsetY: {
index: 252,
group: 'Box Shadow',
displayName: 'Offset Y',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowBlurRadius: {
index: 253,
group: 'Box Shadow',
displayName: 'Blur Radius',
default: 5,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowSpreadRadius: {
index: 254,
group: 'Box Shadow',
displayName: 'Spread Radius',
default: 2,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowInset: {
index: 255,
group: 'Box Shadow',
displayName: 'Inset',
type: 'boolean',
default: false
},
boxShadowColor: {
index: 256,
group: 'Box Shadow',
displayName: 'Shadow Color',
type: 'color',
default: 'rgba(0,0,0,0.2)'
}
},
inputCss: {
backgroundColor: {
index: 201,
displayName: 'Background Color',
group: 'Style',
type: 'color'
},
// Border styles
borderRadius: {
index: 202,
displayName: 'Border Radius',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 16,
applyDefault: false
},
borderStyle: {
index: 203,
displayName: 'Border Style',
group: 'Style',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
},
default: 'solid',
applyDefault: false
},
borderWidth: {
index: 204,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 1,
applyDefault: false
},
borderColor: {
index: 205,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: '#000000'
}
},
outputProps: {},
methods: {}
};
NodeSharedPortDefinitions.addAlignInputs(RadioButtonNode);
NodeSharedPortDefinitions.addTransformInputs(RadioButtonNode);
NodeSharedPortDefinitions.addMarginInputs(RadioButtonNode);
NodeSharedPortDefinitions.addSharedVisualInputs(RadioButtonNode);
Utils.addControlEventsAndStates(RadioButtonNode);
RadioButtonNode = createNodeFromReactComponent(RadioButtonNode);
export default RadioButtonNode;

View File

@@ -0,0 +1,390 @@
import React, { useEffect, useState } from 'react';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
function _styleTemplate(_class, props) {
return `
.${_class}::-webkit-slider-thumb {
width: ${props.thumbWidth};
height: ${props.thumbHeight};
background: ${props.thumbColor};
border: 0;
border-radius: ${props.thumbRadius};
cursor: pointer;
-webkit-appearance: none;
margin-top:calc(${props.trackHeight}/2 - ${props.thumbHeight}/2);
}
.${_class}::-moz-range-thumb {
width: ${props.thumbWidth};
height: ${props.thumbHeight};
background: ${props.thumbColor};
border: none;
border-radius: ${props.thumbRadius};
cursor: pointer;
}
.${_class}::-ms-thumb {
width: ${props.thumbWidth};
height: ${props.thumbHeight};
background: ${props.thumbColor};
border: none;
border-radius: ${props.thumbRadius};
cursor: pointer;
margin-top: 0px;
}
.${_class}::-webkit-slider-runnable-track {
background: ${props.trackColor};
border: none;
width: 100%;
height: ${props.trackHeight};
cursor: pointer;
margin-top:0px;
}
.${_class}:focus::-webkit-slider-runnable-track {
background: ${props.trackColor};
}
.${_class}::-moz-range-track {
background: ${props.trackColor};
border: none;
width: 100%;
height: ${props.trackHeight};
cursor: pointer;
}
.${_class}::-ms-track {
background: transparent;
border:none;
color: transparent;
width: 100%;
height: ${props.trackHeight};
cursor: pointer;
}
.${_class}::-ms-fill-lower {
background: ${props.trackColor};
border: none;
}
.${_class}::-ms-fill-upper {
background: ${props.trackColor};
border: none;
}
`;
}
// --------------------------------------------------------------------------------------
// Range
// --------------------------------------------------------------------------------------
function Range(props) {
const [value, setValue] = useState(props.value);
// Report initial values when mounted
useEffect(() => {
setValue(props.value);
}, []);
useEffect(() => {
setValue(props.value);
}, [props.value]);
var style = { ...props.style };
Layout.size(style, props);
Layout.align(style, props);
const tagProps = { id: props.id, min: props.min, max: props.max, step: props.step, style: style };
Utils.updateStylesForClass('ndl-controls-range-' + props._nodeId, props, _styleTemplate);
let className = 'ndl-controls-range-' + props._nodeId + ' ndl-controls-range';
if (props.className) className = className + ' ' + props.className;
return (
<input
className={className}
{...Utils.controlEvents(props)}
type="range"
{...tagProps}
value={value}
disabled={!props.enabled}
onChange={(e) => {
setValue(e.target.value);
props.valueChanged && props.valueChanged(e.target.value);
}}
></input>
);
}
var RangeNode = {
name: 'Range',
docs: 'https://docs.noodl.net/nodes/visual/range',
allowChildren: false,
noodlNodeAsProp: true,
initialize() {
this.props.sizeMode = 'explicit';
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
this._internal.value = this.props.value = this.props.min;
this.props._nodeId = this.id;
this.props.valueChanged = (value) => {
value = typeof value === 'string' ? parseFloat(value) : value;
const valueChanged = this._internal.value !== value;
this._internal.value = value;
this._updateValuePercent(value);
if (valueChanged) {
this.flagOutputDirty('value');
this.sendSignalOnOutput('onChange');
}
};
this.props.valueChanged(this.props.value); // Update initial values
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
},
getReactComponent() {
return Range;
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
},
value: {
type: 'string',
displayName: 'Value',
group: 'General',
set: function (value) {
const changed = value !== this._internal.value;
this.props.value = this._internal.value = value;
this._updateValuePercent(value);
if (changed) {
this.forceUpdate();
this.flagOutputDirty('value');
}
}
}
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
},
value: {
type: 'number',
displayName: 'Value',
group: 'States',
getter: function () {
return this._internal.value;
}
},
valuePercent: {
type: 'number',
displayName: 'Value Percent',
group: 'States',
getter: function () {
return this._internal.valuePercent;
}
},
onChange: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
}
},
inputProps: {
min: { type: 'number', displayName: 'Min', group: 'General', default: 0 },
max: { type: 'number', displayName: 'Max', group: 'General', default: 100 },
step: { type: 'number', displayName: 'Step', group: 'General', default: 1 },
width: {
index: 11,
group: 'Dimensions',
displayName: 'Width',
type: {
name: 'number',
units: ['%', 'px', 'vw'],
defaultUnit: '%'
},
default: 100
},
height: {
index: 12,
group: 'Dimensions',
displayName: 'Height',
type: {
name: 'number',
units: ['%', 'px', 'vh'],
defaultUnit: '%'
},
default: 100
},
// Styles
thumbWidth: {
group: 'Thumb Style',
displayName: 'Width',
type: {
name: 'number',
units: ['px', 'vw', '%'],
defaultUnit: 'px',
allowEditOnly: true
},
default: 16
},
thumbHeight: {
group: 'Thumb Style',
displayName: 'Height',
type: {
name: 'number',
units: ['px', 'vh', '%'],
defaultUnit: 'px',
allowEditOnly: true
},
default: 16
},
thumbRadius: {
group: 'Thumb Style',
displayName: 'Radius',
type: {
name: 'number',
units: ['px', '%'],
defaultUnit: 'px',
allowEditOnly: true
},
default: 8
},
thumbColor: {
group: 'Thumb Style',
displayName: 'Color',
type: { name: 'color', allowEditOnly: true },
default: '#000000'
},
trackHeight: {
group: 'Track Style',
displayName: 'Height',
type: {
name: 'number',
units: ['px', 'vh', '%'],
defaultUnit: 'px',
allowEditOnly: true
},
default: 6
},
trackColor: {
group: 'Track Style',
displayName: 'Color',
type: { name: 'color', allowEditOnly: true },
default: '#f0f0f0'
}
},
inputCss: {
backgroundColor: {
index: 201,
displayName: 'Background Color',
group: 'Style',
type: 'color'
},
// Border styles
borderRadius: {
index: 202,
displayName: 'Border Radius',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 1,
applyDefault: false
},
borderStyle: {
index: 203,
displayName: 'Border Style',
group: 'Style',
type: {
name: 'enum',
enums: [
{ label: 'None', value: 'none' },
{ label: 'Solid', value: 'solid' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dashed', value: 'dashed' }
]
},
default: 'none',
applyDefault: false
},
borderWidth: {
index: 204,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 1,
applyDefault: false
},
borderColor: {
index: 205,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: '#000000'
}
},
outputProps: {},
methods: {
_updateValuePercent(value) {
const min = this.props.min;
const max = this.props.max;
const valuePercent = Math.floor(((value - min) / (max - min)) * 100);
const valuePercentChanged = this._internal.valuePercentChanged !== valuePercent;
this._internal.valuePercent = valuePercent;
valuePercentChanged && this.flagOutputDirty('valuePercent');
}
}
};
NodeSharedPortDefinitions.addAlignInputs(RangeNode);
NodeSharedPortDefinitions.addTransformInputs(RangeNode);
NodeSharedPortDefinitions.addMarginInputs(RangeNode);
NodeSharedPortDefinitions.addSharedVisualInputs(RangeNode);
Utils.addControlEventsAndStates(RangeNode);
RangeNode = createNodeFromReactComponent(RangeNode);
export default RangeNode;

View File

@@ -0,0 +1,461 @@
import React from 'react';
import FontLoader from '../../fontloader';
import guid from '../../guid';
import Layout from '../../layout';
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
import { createNodeFromReactComponent } from '../../react-component-node';
import Utils from './utils';
//this stops a text field from being unfocused by the clickHandler in the viewer that handles focus globally.
//The specific case is when a mouseDown is registered in the input, but the mouseUp is outside.
//It'll trigger a focus change that'll blur the input field, which is annyoing when you're selecting text
function preventGlobalFocusChange(e) {
e.stopPropagation();
window.removeEventListener('click', preventGlobalFocusChange, true);
}
class TextFieldComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: props.startValue
};
this.ref = React.createRef();
}
setText(value) {
this.setState({ value });
this.props.onTextChanged && this.props.onTextChanged(value);
}
componentDidMount() {
//plumbing for the focused signals
this.ref.current.noodlNode = this.props.noodlNode;
this.setText(this.props.startValue);
}
render() {
const style = { ...this.props.textStyle, ...this.props.style };
Layout.size(style, this.props);
Layout.align(style, this.props);
if (style.opacity === 0) {
style.pointerEvents = 'none';
}
const props = this.props;
const sharedProps = {
id: props.id,
value: this.state.value,
...Utils.controlEvents(props),
disabled: !props.enabled,
style,
className: props.className,
placeholder: props.placeholder,
onChange: (e) => this.onChange(e)
};
if (this.props.type !== 'textArea') {
return (
<input
ref={this.ref}
type={this.props.type}
{...sharedProps}
onKeyDown={(e) => this.onKeyDown(e)}
onMouseDown={() => window.addEventListener('click', preventGlobalFocusChange, true)}
/>
);
} else {
sharedProps.style.resize = 'none'; //disable user resizing
return <textarea ref={this.ref} {...sharedProps} onKeyDown={(e) => this.onKeyDown(e)} />;
}
}
onKeyDown(e) {
if (e.key === 'Enter' || e.which === 13) {
this.props.onEnter && this.props.onEnter();
}
}
onChange(event) {
const value = event.target.value;
this.setText(value);
}
focus() {
this.ref.current && this.ref.current.focus();
}
blur() {
this.ref.current && this.ref.current.blur();
}
}
const TextInput = {
name: 'Text Input',
docs: 'https://docs.noodl.net/nodes/visual/text-input',
allowChildren: false,
noodlNodeAsProp: true,
getReactComponent() {
return TextFieldComponent;
},
defaultCss: {
outline: 'none',
borderStyle: 'solid',
padding: 0
},
initialize() {
this.props.startValue = '';
this.props.id = this._internal.controlId = 'input-' + guid();
this.props.enabled = this._internal.enabled = true;
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
},
inputProps: {
type: {
displayName: 'Type',
group: 'Text',
index: 19,
type: {
name: 'enum',
enums: [
{ label: 'Text', value: 'text' },
{ label: 'Text Area', value: 'textArea' },
{ label: 'Email', value: 'email' },
{ label: 'Number', value: 'number' },
{ label: 'Password', value: 'password' },
{ label: 'URL', value: 'url' }
]
},
default: 'text'
},
placeholder: {
index: 22,
group: 'Text',
displayName: 'Placeholder',
default: 'Type here...',
type: {
name: 'string'
}
},
/* disabled: {
group: 'Text',
index: 23,
displayName: 'Disabled',
propPath: 'dom',
default: false,
type: 'boolean'
}*/
// Box shadow
boxShadowEnabled: {
index: 250,
group: 'Box Shadow',
displayName: 'Shadow Enabled',
type: 'boolean',
default: false
},
boxShadowOffsetX: {
index: 251,
group: 'Box Shadow',
displayName: 'Offset X',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowOffsetY: {
index: 252,
group: 'Box Shadow',
displayName: 'Offset Y',
default: 0,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowBlurRadius: {
index: 253,
group: 'Box Shadow',
displayName: 'Blur Radius',
default: 5,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowSpreadRadius: {
index: 254,
group: 'Box Shadow',
displayName: 'Spread Radius',
default: 2,
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
}
},
boxShadowInset: {
index: 255,
group: 'Box Shadow',
displayName: 'Inset',
type: 'boolean',
default: false
},
boxShadowColor: {
index: 256,
group: 'Box Shadow',
displayName: 'Shadow Color',
type: 'color',
default: 'rgba(0,0,0,0.2)'
}
},
inputs: {
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'General',
default: true,
set: function (value) {
value = !!value;
const changed = value !== this._internal.enabled;
this.props.enabled = this._internal.enabled = value;
if (changed) {
this.forceUpdate();
this.flagOutputDirty('enabled');
}
}
},
set: {
group: 'Actions',
displayName: 'Set',
type: 'signal',
valueChangedToTrue() {
this.scheduleAfterInputsHaveUpdated(() => {
this.setText(this._internal.text);
});
}
},
startValue: {
index: 18,
displayName: 'Text',
type: 'string',
group: 'Text',
set(value) {
if (this._internal.text === value) return;
this._internal.text = value;
if (this.isInputConnected('set') === false) {
this.setText(value);
}
}
},
textStyle: {
index: 19,
type: 'textStyle',
group: 'Text',
displayName: 'Text Style',
default: 'None',
set(value) {
this.props.textStyle = this.context.styles.getTextStyle(value);
this.forceUpdate();
}
},
fontFamily: {
index: 20,
type: 'font',
group: 'Text',
displayName: 'Font Family',
set(value) {
if (value) {
let family = value;
if (family.split('.').length > 1) {
family = family.replace(/\.[^/.]+$/, '');
family = family.split('/').pop();
}
this.setStyle({ fontFamily: family });
} else {
this.removeStyle(['fontFamily']);
}
if (this.props.textStyle) {
this.forceUpdate();
}
}
},
clear: {
type: 'signal',
group: 'Actions',
displayName: 'Clear',
valueChangedToTrue() {
this.setText('');
}
},
focus: {
type: 'signal',
group: 'Actions',
displayName: 'Focus',
valueChangedToTrue() {
this.context.setNodeFocused(this, true);
}
},
blur: {
type: 'signal',
group: 'Actions',
displayName: 'Blur',
valueChangedToTrue() {
this.context.setNodeFocused(this, false);
}
}
},
inputCss: {
fontSize: {
index: 21,
group: 'Text',
displayName: 'Font Size',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
onChange() {
if (this.props.textStyle) {
this.forceUpdate();
}
}
},
color: {
index: 99,
type: 'color',
displayName: 'Font Color',
group: 'Style'
},
backgroundColor: {
index: 100,
displayName: 'Background Color',
group: 'Style',
type: 'color',
default: 'transparent'
},
borderColor: {
index: 101,
displayName: 'Border Color',
group: 'Style',
type: 'color',
default: 'black'
},
borderWidth: {
index: 102,
displayName: 'Border Width',
group: 'Style',
type: {
name: 'number',
units: ['px'],
defaultUnit: 'px'
},
default: 0
}
},
outputs: {
controlId: {
type: 'string',
displayName: 'Control Id',
group: 'General',
getter: function () {
return this._internal.controlId;
}
},
enabled: {
type: 'boolean',
displayName: 'Enabled',
group: 'States',
getter: function () {
return this._internal.enabled;
}
}
},
outputProps: {
// Value
onTextChanged: {
group: 'Value',
displayName: 'Text',
type: 'string'
},
// Events
onEnter: {
group: 'Events',
displayName: 'On Enter',
type: 'signal'
}
},
dynamicports: [
{
condition: 'boxShadowEnabled = true',
inputs: [
'boxShadowOffsetX',
'boxShadowOffsetY',
'boxShadowInset',
'boxShadowBlurRadius',
'boxShadowSpreadRadius',
'boxShadowColor'
]
}
],
methods: {
_focus() {
if (!this.innerReactComponentRef) return;
this.innerReactComponentRef.focus();
},
_blur() {
if (!this.innerReactComponentRef) return;
this.innerReactComponentRef.blur();
},
setText(text) {
this.props.startValue = text;
if (this.innerReactComponentRef) {
//the text component is mounted, and will signal the onTextChanged output
this.innerReactComponentRef.setText(text);
} else if (this.outputPropValues['onTextChanged'] !== text) {
//text component isn't mounted, set the output manually
this.outputPropValues['onTextChanged'] = text;
this.flagOutputDirty('onTextChanged');
}
}
}
};
NodeSharedPortDefinitions.addDimensions(TextInput, { defaultSizeMode: 'contentSize', contentLabel: 'Text' });
NodeSharedPortDefinitions.addAlignInputs(TextInput);
NodeSharedPortDefinitions.addTransformInputs(TextInput);
NodeSharedPortDefinitions.addPaddingInputs(TextInput);
NodeSharedPortDefinitions.addMarginInputs(TextInput);
NodeSharedPortDefinitions.addSharedVisualInputs(TextInput);
Utils.addControlEventsAndStates(TextInput);
const definition = createNodeFromReactComponent(TextInput);
definition.setup = function (context, graphModel) {
graphModel.on('nodeAdded.Text Input', function (node) {
if (node.parameters.fontFamily && node.parameters.fontFamily.split('.').length > 1) {
FontLoader.instance.loadFont(node.parameters.fontFamily);
}
node.on('parameterUpdated', function (event) {
if (event.name === 'fontFamily' && event.value) {
if (event.value.split('.').length > 1) {
FontLoader.instance.loadFont(event.value);
}
}
});
});
};
export default definition;

View File

@@ -0,0 +1,273 @@
import PointerListeners from '../../pointerlisteners';
function _shallowCompare(o1, o2) {
for (var p in o1) {
if (o1.hasOwnProperty(p)) {
if (o1[p] !== o2[p]) {
return false;
}
}
}
for (var p in o2) {
if (o2.hasOwnProperty(p)) {
if (o1[p] !== o2[p]) {
return false;
}
}
}
return true;
}
const _styleSheets = {};
function updateStylesForClass(_class, props, _styleTemplate) {
if (_styleSheets[_class]) {
// Check if props have changed
if (!_shallowCompare(props, _styleSheets[_class].props)) {
_styleSheets[_class].style.innerHTML = _styleTemplate(_class, props);
_styleSheets[_class].props = Object.assign({}, props);
}
} else {
// Create a new style sheet if none exists
var style = document.createElement('style');
style.innerHTML = _styleTemplate(_class, props);
document.head.appendChild(style);
_styleSheets[_class] = { style, props: Object.assign({}, props) };
}
}
function addInputCss(definition, inputs) {
if (!definition.inputCss) {
definition.inputCss = {};
}
if (!definition.defaultCss) {
definition.defaultCss = {};
}
for (const name in inputs) {
definition.inputCss[name] = inputs[name];
if (inputs[name].hasOwnProperty('default') && inputs[name].applyDefault !== false) {
definition.defaultCss[name] = inputs[name].default;
}
}
}
function mergeAttribute(definition, attribute, values) {
if (!definition[attribute]) {
definition[attribute] = {};
}
for (const name in values) {
definition[attribute][name] = values[name];
}
}
function addInputs(definition, values) {
mergeAttribute(definition, 'inputs', values);
}
function addInputProps(definition, values) {
mergeAttribute(definition, 'inputProps', values);
}
function addDynamicInputPorts(definition, condition, inputs) {
if (!definition.dynamicports) {
definition.dynamicports = [];
}
definition.dynamicports.push({ condition, inputs });
}
function addOutputProps(definition, values) {
mergeAttribute(definition, 'outputProps', values);
}
function addControlEventsAndStates(definition) {
addInputProps(definition, {
blockTouch: {
index: 450,
displayName: 'Block Pointer Events',
type: 'boolean'
}
});
addOutputProps(definition, {
// Focus
focusState: {
displayName: 'Focused',
group: 'States',
type: 'boolean',
props: {
onFocus() {
this.outputPropValues.focusState = true;
this.flagOutputDirty('focusState');
this.hasOutput('onFocus') && this.sendSignalOnOutput('onFocus');
},
onBlur() {
this.outputPropValues.focusState = false;
this.flagOutputDirty('focusState');
this.hasOutput('onBlur') && this.sendSignalOnOutput('onBlur');
}
}
},
onFocus: {
displayName: 'Focused',
group: 'Events',
type: 'signal',
props: {
onFocus() {
this.outputPropValues.focusState = true;
this.flagOutputDirty('focusState');
this.sendSignalOnOutput('onFocus');
}
}
},
onBlur: {
displayName: 'Blurred',
group: 'Events',
type: 'signal',
props: {
onBlur() {
this.outputPropValues.focusState = false;
this.flagOutputDirty('focusState');
this.sendSignalOnOutput('onBlur');
}
}
},
// Hover
hoverState: {
displayName: 'Hover',
group: 'States',
type: 'boolean',
props: {
onMouseOver() {
this.outputPropValues.hoverState = true;
this.flagOutputDirty('hoverState');
this.hasOutput('hoverStart') && this.sendSignalOnOutput('hoverStart');
},
onMouseLeave() {
this.outputPropValues.hoverState = false;
this.flagOutputDirty('hoverState');
this.hasOutput('hoverEnd') && this.sendSignalOnOutput('hoverEnd');
}
}
},
hoverStart: {
displayName: 'Hover Start',
group: 'Events',
type: 'signal',
props: {
onMouseOver() {
this.outputPropValues.hoverState = true;
this.flagOutputDirty('hoverState');
this.sendSignalOnOutput('hoverStart');
}
}
},
hoverEnd: {
displayName: 'Hover End',
group: 'Events',
type: 'signal',
props: {
onMouseLeave() {
this.outputPropValues.hoverState = false;
this.flagOutputDirty('hoverState');
this.sendSignalOnOutput('hoverEnd');
}
}
},
// Pressed
pressedState: {
displayName: 'Pressed',
group: 'States',
type: 'boolean',
props: {
onMouseDown() {
this.outputPropValues.pressedState = true;
this.flagOutputDirty('pressedState');
this.hasOutput('pointerDown') && this.sendSignalOnOutput('pointerDown');
},
onTouchStart() {
this.outputPropValues.pressedState = true;
this.flagOutputDirty('pressedState');
this.hasOutput('pointerDown') && this.sendSignalOnOutput('pointerDown');
},
onMouseUp() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.hasOutput('pointerUp') && this.sendSignalOnOutput('pointerUp');
},
onTouchEnd() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.hasOutput('pointerUp') && this.sendSignalOnOutput('pointerUp');
},
onTouchCancel() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.hasOutput('pointerUp') && this.sendSignalOnOutput('pointerUp');
}
}
},
pointerDown: {
displayName: 'Pointer Down',
group: 'Events',
type: 'signal',
props: {
onMouseDown() {
this.outputPropValues.pressedState = true;
this.flagOutputDirty('pressedState');
this.sendSignalOnOutput('pointerDown');
},
onTouchStart() {
this.outputPropValues.pressedState = true;
this.flagOutputDirty('pressedState');
this.sendSignalOnOutput('pointerDown');
}
}
},
pointerUp: {
displayName: 'Pointer Up',
group: 'Events',
type: 'signal',
props: {
onMouseUp() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.sendSignalOnOutput('pointerUp');
},
onTouchEnd() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.sendSignalOnOutput('pointerUp');
},
onTouchCancel() {
this.outputPropValues.pressedState = false;
this.flagOutputDirty('pressedState');
this.sendSignalOnOutput('pointerUp');
}
}
}
});
}
function controlEvents(props) {
return Object.assign(
{},
{
onFocus: props.onFocus,
onBlur: props.onBlur
},
PointerListeners(props)
);
}
export default {
updateStylesForClass,
addControlEventsAndStates,
controlEvents
};

View File

@@ -0,0 +1,543 @@
'use strict';
const EaseCurves = require('../../easecurves'),
BezierEasing = require('bezier-easing');
function SubAnimation(args) {
this.name = args.name;
this.startValue = 0;
this.endValue = 0;
this.currentValue = undefined;
this.startMode = 'implicit';
this.ease = args.ease;
this.node = args.node;
this.hasSampledStartValue = false;
var self = this;
this.animation = args.node.context.timerScheduler.createTimer({
startValue: 0,
endValue: 0,
onRunning: function (t) {
var value = self.ease(this.startValue, this.endValue, t);
self.setCurrentValue(value);
}
});
this.animation.startValue = 0;
this.animation.endValue = 0;
}
Object.defineProperties(SubAnimation.prototype, {
setCurrentValue: {
value: function (value) {
this.currentValue = value;
this.node.flagOutputDirty(this.name);
}
},
play: {
value: function (start, end) {
if (start === undefined) {
console.log('Animation warning, start value is undefined');
start = 0;
}
if (end === undefined) {
console.error('Animation error, start:', start, 'end:', end);
return;
}
var animation = this.animation;
animation.startValue = start;
this.setCurrentValue(start);
animation.endValue = end;
animation.duration = this.node._internal.duration;
animation.start();
}
},
playToEnd: {
value: function () {
if (this.hasConnections() === false) {
return;
}
this.updateStartValue();
this.play(this.getTargetsCurrentValue(), this.endValue);
}
},
playToStart: {
value: function () {
if (this.hasConnections() === false) {
return;
}
this.updateStartValue();
this.play(this.getTargetsCurrentValue(), this.startValue);
}
},
replayToEnd: {
value: function () {
if (this.hasConnections() === false) {
return;
}
this.updateStartValue(); //in case animation doesn't have an explicit start value set
this.play(this.startValue, this.endValue);
}
},
replayToStart: {
value: function () {
if (this.hasConnections() === false) {
return;
}
this.play(this.endValue, this.startValue);
}
},
hasConnections: {
value: function () {
return this.node.getOutput(this.name).hasConnections();
}
},
getTargetsCurrentValue: {
value: function () {
var valueConnections = this.node.getOutput(this.name).connections;
//TODO: this will only work for the first connection
const value = valueConnections[0].node.getInputValue(valueConnections[0].inputPortName);
return value instanceof Object && value.hasOwnProperty('value') ? value.value : value;
}
},
updateStartValue: {
value: function () {
if (this.startMode !== 'implicit' || this.hasSampledStartValue) {
return;
}
this.hasSampledStartValue = true;
this.startValue = this.getTargetsCurrentValue();
}
},
stop: {
value: function () {
this.animation.stop();
this.setCurrentValue(undefined);
}
},
jumpToStart: {
value: function () {
this.animation.stop();
this.setCurrentValue(this.startValue);
}
},
jumpToEnd: {
value: function () {
this.animation.stop();
this.setCurrentValue(this.endValue);
}
}
});
var easeEnum = [
{ value: 'easeOut', label: 'Ease Out' },
{ value: 'easeIn', label: 'Ease In' },
{ value: 'linear', label: 'Linear' },
{ value: 'easeInOut', label: 'Ease In Out' },
{ value: 'cubicBezier', label: 'Cubic Bezier' }
];
var defaultDuration = 300;
var AnimationNode = {
name: 'Animation',
docs: 'https://docs.noodl.net/nodes/animation/animation',
shortDesc: 'Node that can animate any number of values, with different types of easing curves.',
category: 'Animation',
deprecated: true,
initialize: function () {
var internal = this._internal;
internal.duration = defaultDuration;
internal.ease = EaseCurves.easeOut;
internal._isPlayingToEnd = false;
internal.animations = [];
internal.cubicBezierPoints = [0, 0, 0, 0];
internal.cubicBezierFunction = undefined;
var self = this;
internal.animation = this.context.timerScheduler.createTimer({
onFinish: function () {
if (internal._isPlayingToEnd === false) {
self.sendSignalOnOutput('hasReachedStart');
} else {
self.sendSignalOnOutput('hasReachedEnd');
}
}
});
},
inputs: {
duration: {
index: 0,
type: 'number',
displayName: 'Duration (ms)',
group: 'Animation Properties',
default: defaultDuration,
set: function (value) {
this._internal.duration = value;
}
},
easingCurve: {
index: 10,
type: {
name: 'enum',
enums: easeEnum
},
group: 'Animation Properties',
displayName: 'Easing Curve',
default: 'easeOut',
set: function (value) {
var easeCurve;
if (value === 'cubicBezier') {
this.updateCubicBezierFunction();
easeCurve = this._internal.cubicBezierFunction;
} else {
easeCurve = EaseCurves[value];
}
this._internal.ease = easeCurve;
}
},
playToEnd: {
index: 20,
group: 'Play',
displayName: 'To End',
editorName: 'Play To End',
valueChangedToTrue: function () {
this._internal._isPlayingToEnd = true;
var animation = this._internal.animation,
animations = this._internal.animations,
self = this;
this.scheduleAfterInputsHaveUpdated(function () {
animation.duration = self._internal.duration;
animation.start();
for (var i = 0; i < animations.length; i++) {
animations[i].ease = self._internal.ease;
animations[i].playToEnd();
}
});
}
},
playToStart: {
index: 21,
group: 'Play',
displayName: 'To Start',
editorName: 'Play To Start',
valueChangedToTrue: function () {
this._internal._isPlayingToEnd = false;
var animation = this._internal.animation,
animations = this._internal.animations,
self = this;
this.scheduleAfterInputsHaveUpdated(function () {
animation.duration = self._internal.duration;
animation.start();
for (var i = 0; i < animations.length; i++) {
animations[i].ease = self._internal.ease;
animations[i].playToStart();
}
});
}
},
replayToEnd: {
index: 22,
group: 'Play',
displayName: 'From Start To End',
editorName: 'Play From Start To End',
valueChangedToTrue: function () {
var animation = this._internal.animation,
animations = this._internal.animations,
self = this;
this._internal._isPlayingToEnd = true;
this.scheduleAfterInputsHaveUpdated(function () {
animation.duration = self._internal.duration;
animation.start();
for (var i = 0; i < animations.length; i++) {
animations[i].ease = self._internal.ease;
animations[i].replayToEnd();
}
});
}
},
replayToStart: {
index: 23,
group: 'Play',
displayName: 'From End To Start',
editorName: 'Play From End To Start',
valueChangedToTrue: function () {
this._internal._isPlayingToEnd = false;
var animation = this._internal.animation,
animations = this._internal.animations,
self = this;
this.scheduleAfterInputsHaveUpdated(function () {
animation.duration = self._internal.duration;
animation.start();
for (var i = 0; i < animations.length; i++) {
animations[i].ease = self._internal.ease;
animations[i].replayToStart();
}
});
}
},
stop: {
index: 60,
group: 'Instant Actions',
displayName: 'Stop',
valueChangedToTrue: function () {
var animation = this._internal.animation,
animations = this._internal.animations;
animation.stop();
for (var i = 0; i < animations.length; i++) {
animations[i].stop();
}
}
},
jumpToStart: {
index: 61,
group: 'Instant Actions',
displayName: 'Jump To Start',
valueChangedToTrue: function () {
var animations = this._internal.animations;
for (var i = 0; i < animations.length; i++) {
animations[i].jumpToStart();
}
this.sendSignalOnOutput('hasReachedStart');
}
},
jumpToEnd: {
index: 62,
group: 'Instant Actions',
displayName: 'Jump To End',
valueChangedToTrue: function () {
var animations = this._internal.animations;
for (var i = 0; i < animations.length; i++) {
animations[i].jumpToEnd();
}
this.sendSignalOnOutput('hasReachedEnd');
}
},
cubicBezierP1X: {
displayName: 'P1 X',
group: 'Cubic Bezier',
type: {
name: 'number'
},
index: 11,
set: function (value) {
this._internal.cubicBezierPoints[0] = Math.min(1, Math.max(0, value));
this.updateCubicBezierFunction();
}
},
cubicBezierP1Y: {
displayName: 'P1 Y',
group: 'Cubic Bezier',
type: {
name: 'number'
},
index: 12,
set: function (value) {
this._internal.cubicBezierPoints[1] = value;
this.updateCubicBezierFunction();
}
},
cubicBezierP2X: {
displayName: 'P2 X',
group: 'Cubic Bezier',
type: {
name: 'number'
},
index: 13,
set: function (value) {
this._internal.cubicBezierPoints[2] = Math.min(1, Math.max(0, value));
this.updateCubicBezierFunction();
}
},
cubicBezierP2Y: {
displayName: 'P2 Y',
group: 'Cubic Bezier',
type: {
name: 'number'
},
index: 14,
set: function (value) {
this._internal.cubicBezierPoints[3] = value;
this.updateCubicBezierFunction();
}
}
},
outputs: {
hasReachedStart: {
type: 'signal',
group: 'Signals',
displayName: 'Has Reached Start'
},
hasReachedEnd: {
type: 'signal',
group: 'Signals',
displayName: 'Has Reached End'
}
},
dynamicports: [
{
condition: 'easingCurve = cubicBezier',
inputs: ['cubicBezierP1X', 'cubicBezierP1Y', 'cubicBezierP2X', 'cubicBezierP2Y']
},
//animation outputs
{
name: 'expand/basic',
indexStep: 100,
template: [
{
name: '{{portname}}.startMode',
type: {
name: 'enum',
enums: [
{
value: 'explicit',
label: 'Explicit'
},
{
value: 'implicit',
label: 'Implicit'
}
],
allowEditOnly: true
},
plug: 'input',
group: '{{portname}} Animation',
displayName: 'Start Mode',
default: 'implicit',
index: 1000
},
{
name: '{{portname}}.endValue',
type: 'number',
plug: 'input',
group: '{{portname}} Animation',
displayName: 'End Value',
editorName: 'End Value | {{portname}} ',
default: 0,
index: 1002
}
]
},
{
name: 'expand/basic',
condition: "'{{portname}}.startMode' = explicit",
indexStep: 100,
template: [
{
name: '{{portname}}.startValue',
plug: 'input',
type: 'number',
displayName: 'Start Value',
editorName: 'Start Value | {{portname}}',
group: '{{portname}} Animation',
default: 0,
index: 1001
}
]
}
],
panels: [
{
name: 'PortEditor',
title: 'Animations',
plug: 'output',
type: { name: 'number' },
group: 'Animation Values'
}
],
prototypeExtensions: {
updateCubicBezierFunction: {
value: function () {
var points = this._internal.cubicBezierPoints;
var cubicBezierEase = BezierEasing(points);
this._internal.cubicBezierFunction = function (start, end, t) {
return EaseCurves.linear(start, end, cubicBezierEase.get(t));
};
this._internal.ease = this._internal.cubicBezierFunction;
}
},
_registerAnimationGroup: {
value: function (name) {
var subAnimation = new SubAnimation({
node: this,
ease: this._internal.ease,
name: name
});
this._internal.animations.push(subAnimation);
var inputs = {};
inputs[name + '.' + 'startMode'] = {
set: function (value) {
subAnimation.startMode = value;
}
};
inputs[name + '.' + 'startValue'] = {
default: 0,
set: function (value) {
subAnimation.startValue = value;
}
};
inputs[name + '.' + 'endValue'] = {
default: 0,
set: function (value) {
subAnimation.endValue = value;
}
};
this.registerInputs(inputs);
this.registerOutput(name, {
getter: function () {
return subAnimation.currentValue;
}
});
}
},
registerInputIfNeeded: {
value: function (name) {
if (this.hasInput(name)) {
return;
}
var dotIndex = name.indexOf('.'),
animationName = name.substr(0, dotIndex);
if (this.hasOutput(animationName)) {
return;
}
this._registerAnimationGroup(animationName);
}
},
registerOutputIfNeeded: {
value: function (name) {
if (this.hasOutput(name)) {
return;
}
this._registerAnimationGroup(name);
}
}
}
};
module.exports = {
node: AnimationNode
};

View File

@@ -0,0 +1,256 @@
'use strict';
const { Node } = require('@noodl/runtime');
const Model = require('@noodl/runtime/src/model');
const ComponentState = {
name: 'Component State',
displayNodeName: 'Component Object',
category: 'Component Utilities',
color: 'component',
docs: 'https://docs.noodl.net/nodes/component-utilities/component-object',
deprecated: true,
initialize: function () {
this._internal.inputValues = {};
this._internal.onModelChangedCallback = (args) => {
if (this.isInputConnected('fetch') !== false) return;
if (this.hasOutput('value-' + args.name)) {
this.flagOutputDirty('value-' + args.name);
}
if (this.hasOutput('changed-' + args.name)) {
this.sendSignalOnOutput('changed-' + args.name);
}
this.sendSignalOnOutput('changed');
};
const model = Model.get('componentState' + this.nodeScope.componentOwner.getInstanceId());
this._internal.model = model;
model.on('change', this._internal.onModelChangedCallback);
// if(this.isInputConnected('fetch') === false)
// this.fetch();
},
getInspectInfo() {
const data = this._internal.model.data;
return Object.keys(data).map((key) => {
return { type: 'text', value: key + ': ' + data[key] };
});
},
inputs: {
properties: {
type: {
name: 'stringlist',
allowEditOnly: true
},
displayName: 'Properties',
group: 'Properties',
set(value) {}
},
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue() {
this.scheduleStore();
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue() {
this.scheduleFetch();
}
}
},
outputs: {
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
stored: {
type: 'signal',
displayName: 'Stored',
group: 'Events'
}
},
methods: {
scheduleStore() {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
this.sendSignalOnOutput('stored');
});
},
scheduleFetch() {
if (this.hasScheduledFetch) return;
this.hasScheduledFetch = true;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledFetch = false;
this.fetch();
});
},
fetch() {
for (var key in this._internal.model.data) {
if (this.hasOutput('value-' + key)) {
this.flagOutputDirty('value-' + key);
if (this.hasOutput('changed-' + key)) {
this.sendSignalOnOutput('changed-' + key);
}
}
}
this.sendSignalOnOutput('fetched');
},
_onNodeDeleted() {
Node.prototype._onNodeDeleted.call(this);
this._internal.model.off('change', this._internal.onModelChangedCallback);
},
registerOutputIfNeeded(name) {
if (this.hasOutput(name)) {
return;
}
const split = name.split('-');
const propertyName = split[split.length - 1];
this.registerOutput(name, {
get() {
return this._internal.model.get(propertyName, { resolve: true });
}
});
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
const split = name.split('-');
const propertyName = split[split.length - 1];
if (name.startsWith('value-')) {
this.registerInput(name, {
set(value) {
this._internal.inputValues[propertyName] = value;
if (this.isInputConnected('store') === false)
// Lazy set
this.scheduleStore();
}
});
}
/* else if (name.startsWith('start-value-')) {
this.registerInput(name, {
set(value) {
this._internal.model.set(propertyName, value)
}
});
}
else if (name.startsWith('type-')) {
this.registerInput(name, {
set(value) {}
});
}*/
}
}
};
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [];
// Add value outputs
if (parameters.properties) {
var properties = parameters.properties.split(',');
for (var i in properties) {
var p = properties[i];
ports.push({
type: {
name: '*',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: 'value-' + p,
displayName: p
});
/* ports.push({
type: {
name: parameters['type-' + p] || 'string',
allowEditOnly: true
},
plug: 'input',
group: 'Start Values',
name: 'start-value-' + p,
displayName: p
});
ports.push({
type: {
name: 'enum',
enums: [
{ label: 'Number', value: 'number' },
{ label: 'String', value: 'string' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Color', value: 'color' },
{ label: 'Image', value: 'image' }
],
allowEditOnly: true
},
default: 'string',
plug: 'input',
group: 'Types',
displayName: p,
name: 'type-' + p,
});*/
ports.push({
type: 'signal',
plug: 'output',
group: 'Changed Events',
displayName: p + ' Changed',
name: 'changed-' + p
});
}
}
editorConnection.sendDynamicPorts(nodeId, ports, {
detectRenamed: {
plug: 'input/output'
}
});
}
module.exports = {
node: ComponentState,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.Component State', (node) => {
updatePorts(node.id, node.parameters, context.editorConnection);
node.on('parameterUpdated', (event) => {
if (event.name === 'properties' || event.name.startsWith('type-')) {
updatePorts(node.id, node.parameters, context.editorConnection);
}
});
});
}
};

View File

@@ -0,0 +1,332 @@
'use strict';
const { Node } = require('@noodl/runtime');
var Model = require('@noodl/runtime/src/model'),
Collection = require('@noodl/runtime/src/collection');
var CollectionNode = {
name: 'Collection',
docs: 'https://docs.noodl.net/nodes/data/array',
displayNodeName: 'Array',
shortDesc: 'A collection of models, mainly used together with a For Each Node.',
category: 'Data',
usePortAsLabel: 'collectionId',
color: 'data',
deprecated: true, // Use new array node
initialize: function () {
var _this = this;
var collectionChangedScheduled = false;
this._internal.collectionChangedCallback = function () {
if (_this.isInputConnected('fetch') === true) return; // Ignore if we have explicit fetch connection
//this can be called multiple times when adding/removing more than one item
//so optimize by only updating outputs once
if (collectionChangedScheduled) return;
collectionChangedScheduled = true;
_this.scheduleAfterInputsHaveUpdated(function () {
_this.sendSignalOnOutput('changed');
_this.flagOutputDirty('count');
collectionChangedScheduled = false;
});
};
// When the source collection has changed, simply copy items into this collection
this._internal.sourceCollectionChangedCallback = function () {
if (_this.isInputConnected('store') === true) return; // Ignore if we have explicit store connection
_this.scheduleCopyItems();
};
},
getInspectInfo() {
if (this._internal.collection) {
return 'Count: ' + this._internal.collection.size();
}
},
inputs: {
collectionId: {
type: {
name: 'string',
identifierOf: 'CollectionName',
identifierDisplayName: 'Array Ids'
},
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Collection) value = value.getId(); // Can be passed as collection as well
this._internal.collectionId = value; // Wait to fetch data
if (this.isInputConnected('fetch') === false) this.setCollectionID(value);
else {
this.flagOutputDirty('id');
}
}
},
items: {
type: 'array',
group: 'General',
displayName: 'Items',
set: function (value) {
var _this = this;
if (value === undefined) return;
if (value === this._internal.collection) return;
this._internal.pendingSourceCollection = value;
if (this.isInputConnected('store') === false) {
// Don't auto copy if we have connections to store
this.scheduleAfterInputsHaveUpdated(function () {
_this.setSourceCollection(value);
});
}
}
},
modifyId: {
type: { name: 'string', allowConnectionsOnly: true },
displayName: 'Item Id',
group: 'Modify',
set: function (value) {
this._internal.modifyId = value;
}
},
/* modifyModel: {
type: {name:'object',
allowConnectionsOnly:true},
displayName:'Item',
group:'Modify',
set:function(value) {
if(!(value instanceof Model)) return;
this._internal.modifyId = value.getId();
},
}, */
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleStore();
}
},
add: {
displayName: 'Add',
group: 'Modify',
valueChangedToTrue: function () {
var _this = this;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
if (internal.modifyId === undefined) return;
if (internal.collection === undefined && this.isInputConnected('fetch') === false)
_this.setCollection(Collection.get()); // Create a new empty collection if we don't have one yet
if (internal.collection === undefined) return;
var model = Model.get(internal.modifyId);
internal.collection.add(model);
_this.sendSignalOnOutput('modified');
});
}
},
remove: {
displayName: 'Remove',
group: 'Modify',
valueChangedToTrue: function () {
var _this = this;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
if (internal.modifyId === undefined) return;
if (internal.collection === undefined && this.isInputConnected('fetch') === false)
_this.setCollection(Collection.get()); // Create a new empty collection if we don't have one yet
if (internal.collection === undefined) return;
var model = Model.get(internal.modifyId);
internal.collection.remove(model);
_this.sendSignalOnOutput('modified');
});
}
},
clear: {
displayName: 'Clear',
group: 'Modify',
valueChangedToTrue: function () {
var _this = this;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
if (internal.collection === undefined && this.isInputConnected('fetch') === false)
_this.setCollection(Collection.get()); // Create a new empty collection if we don't have one yet
if (internal.collection === undefined) return;
internal.collection.set([]);
_this.sendSignalOnOutput('modified');
_this.sendSignalOnOutput('count');
});
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleSetCollection();
}
},
new: {
displayName: 'New',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleNew();
}
}
},
outputs: {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.collection ? this._internal.collection.getId() : this._internal.collectionId;
}
},
items: {
type: 'array',
displayName: 'Items',
group: 'General',
getter: function () {
return this._internal.collection;
}
},
count: {
type: 'number',
displayName: 'Count',
group: 'General',
getter: function () {
return this._internal.collection ? this._internal.collection.size() : 0;
}
},
modified: {
group: 'Events',
type: 'signal',
displayName: 'Modified'
},
changed: {
group: 'Events',
type: 'signal',
displayName: 'Changed'
},
stored: {
group: 'Events',
type: 'signal',
displayName: 'Stored'
},
fetched: {
group: 'Events',
type: 'signal',
displayName: 'Fetched'
},
created: {
group: 'Events',
type: 'signal',
displayName: 'Created'
}
},
prototypeExtensions: {
setCollectionID: function (id) {
this.setCollection(Collection.get(id));
},
setCollection: function (collection) {
if (this._internal.collection)
// Remove old listener if existing
this._internal.collection.off('change', this._internal.collectionChangedCallback);
this._internal.collection = collection;
this.flagOutputDirty('id');
collection.on('change', this._internal.collectionChangedCallback);
this.flagOutputDirty('items');
this.flagOutputDirty('count');
},
setSourceCollection: function (collection) {
var internal = this._internal;
if (internal.sourceCollection && internal.sourceCollection instanceof Collection)
// Remove old listener if existing
internal.sourceCollection.off('change', internal.sourceCollectionChangedCallback);
internal.sourceCollection = collection;
if (internal.sourceCollection instanceof Collection)
internal.sourceCollection.on('change', internal.sourceCollectionChangedCallback);
this._copySourceItems();
},
scheduleSetCollection: function () {
var _this = this;
if (this.hasScheduledSetCollection) return;
this.hasScheduledSetCollection = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this.hasScheduledSetCollection = false;
_this.setCollectionID(_this._internal.collectionId);
_this.sendSignalOnOutput('fetched');
});
},
scheduleStore: function () {
var _this = this;
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
_this.hasScheduledStore = false;
_this.setSourceCollection(internal.pendingSourceCollection);
_this.sendSignalOnOutput('stored');
});
},
_copySourceItems: function () {
var internal = this._internal;
if (internal.collection === undefined && this.isInputConnected('fetch') === false)
this.setCollection(Collection.get());
internal.collection && internal.collection.set(internal.sourceCollection);
},
scheduleCopyItems: function () {
var _this = this;
var internal = this._internal;
if (this.hasScheduledCopyItems) return;
this.hasScheduledCopyItems = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this.hasScheduledCopyItems = false;
_this._copySourceItems();
});
},
scheduleNew: function () {
var _this = this;
if (this.hasScheduledNew) return;
this.hasScheduledNew = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
_this.hasScheduledNew = false;
_this.setCollection(Collection.get());
// If we have a source collection, copy items
if (internal.sourceCollection) internal.collection.set(internal.sourceCollection);
_this.sendSignalOnOutput('created');
});
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
if (this._internal.collection)
// Remove old listener if existing
this._internal.collection.off('change', this._internal.collectionChangedCallback);
}
}
};
module.exports = {
node: CollectionNode
};

View File

@@ -0,0 +1,717 @@
'use strict';
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
const Model = require('@noodl/runtime/src/model'),
Collection = require('@noodl/runtime/src/collection'),
CloudStore = require('@noodl/runtime/src/api/cloudstore'),
JavascriptNodeParser = require('@noodl/runtime/src/javascriptnodeparser');
function _convertFilterOp(filter, options) {
const keys = Object.keys(filter);
if (keys.length === 0) return {};
if (keys.length !== 1) return options.error('Filter must only have one key found ' + keys.join(','));
const res = {};
const key = keys[0];
if (filter['and'] !== undefined && Array.isArray(filter['and'])) {
res['$and'] = filter['and'].map((f) => _convertFilterOp(f, options));
} else if (filter['or'] !== undefined && Array.isArray(filter['or'])) {
res['$or'] = filter['or'].map((f) => _convertFilterOp(f, options));
} else if (filter['idEqualTo'] !== undefined) {
res['objectId'] = { $eq: filter['idEqualTo'] };
} else if (filter['idContainedIn'] !== undefined) {
res['objectId'] = { $in: filter['idContainedIn'] };
} else if (filter['relatedTo'] !== undefined) {
var modelId = filter['relatedTo']['id'];
if (modelId === undefined) return options.error('Must provide id in relatedTo filter');
var relationKey = filter['relatedTo']['key'];
if (relationKey === undefined) return options.error('Must provide key in relatedTo filter');
var m = Model.get(modelId);
res['$relatedTo'] = {
object: {
__type: 'Pointer',
objectId: modelId,
className: m._class
},
key: relationKey
};
} else if (typeof filter[key] === 'object') {
const opAndValue = filter[key];
if (opAndValue['equalTo'] !== undefined) res[key] = { $eq: opAndValue['equalTo'] };
else if (opAndValue['notEqualTo'] !== undefined) res[key] = { $ne: opAndValue['notEqualTo'] };
else if (opAndValue['lessThan'] !== undefined) res[key] = { $lt: opAndValue['lessThan'] };
else if (opAndValue['greaterThan'] !== undefined) res[key] = { $gt: opAndValue['greaterThan'] };
else if (opAndValue['lessThanOrEqualTo'] !== undefined) res[key] = { $lte: opAndValue['lessThanOrEqualTo'] };
else if (opAndValue['greaterThanOrEqualTo'] !== undefined) res[key] = { $gte: opAndValue['greaterThanOrEqualTo'] };
else if (opAndValue['exists'] !== undefined) res[key] = { $exists: opAndValue['exists'] };
else if (opAndValue['containedIn'] !== undefined) res[key] = { $in: opAndValue['containedIn'] };
else if (opAndValue['notContainedIn'] !== undefined) res[key] = { $nin: opAndValue['notContainedIn'] };
else if (opAndValue['pointsTo'] !== undefined) {
var m = Model.get(opAndValue['pointsTo']);
if (CloudStore._collections[options.collectionName])
var schema = CloudStore._collections[options.collectionName].schema;
var targetClass =
schema && schema.properties && schema.properties[key] ? schema.properties[key].targetClass : undefined;
var type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
if (type === 'Relation') {
res[key] = {
__type: 'Pointer',
objectId: opAndValue['pointsTo'],
className: targetClass
};
} else {
if (Array.isArray(opAndValue['pointsTo']))
res[key] = {
$in: opAndValue['pointsTo'].map((v) => {
return { __type: 'Pointer', objectId: v, className: targetClass };
})
};
else
res[key] = {
$eq: {
__type: 'Pointer',
objectId: opAndValue['pointsTo'],
className: targetClass
}
};
}
} else if (opAndValue['matchesRegex'] !== undefined) {
res[key] = {
$regex: opAndValue['matchesRegex'],
$options: opAndValue['options']
};
} else if (opAndValue['text'] !== undefined && opAndValue['text']['search'] !== undefined) {
var _v = opAndValue['text']['search'];
if (typeof _v === 'string') res[key] = { $text: { $search: { $term: _v, $caseSensitive: false } } };
else
res[key] = {
$text: {
$search: {
$term: _v.term,
$language: _v.language,
$caseSensitive: _v.caseSensitive,
$diacriticSensitive: _v.diacriticSensitive
}
}
};
}
} else {
options.error('Unrecognized filter keys ' + keys.join(','));
}
return res;
}
var DbCollectionNode = {
name: 'DbCollection',
docs: 'https://docs.noodl.net/nodes/cloud-services/collection',
displayNodeName: 'Query Collection',
shortDesc: 'A database collection.',
category: 'Cloud Services',
usePortAsLabel: 'collectionName',
color: 'data',
deprecated: true, // Use Query Records
initialize: function () {
var _this = this;
var collectionChangedScheduled = false;
this._internal.collectionChangedCallback = function () {
//this can be called multiple times when adding/removing more than one item
//so optimize by only updating outputs once
if (collectionChangedScheduled) return;
collectionChangedScheduled = true;
_this.scheduleAfterInputsHaveUpdated(function () {
_this.sendSignalOnOutput('modified');
_this.flagOutputDirty('count');
_this.flagOutputDirty('firstItemId');
// _this.flagOutputDirty('firstItem');
collectionChangedScheduled = false;
});
};
this._internal.storageSettings = {};
},
inputs: {},
outputs: {
id: {
type: 'string',
displayName: 'Name',
group: 'General',
getter: function () {
return this._internal.name;
}
},
items: {
type: 'array',
displayName: 'Result',
group: 'General',
getter: function () {
return this._internal.collection;
}
},
firstItemId: {
type: 'string',
displayName: 'First Item Id',
group: 'General',
getter: function () {
if (this._internal.collection) {
var firstItem = this._internal.collection.get(0);
if (firstItem !== undefined) return firstItem.getId();
}
}
},
/* firstItem: {
type: 'object',
displayName: 'First Item',
group: 'General',
getter: function () {
if (this._internal.collection) {
return this._internal.collection.get(0);
}
}
}, */
count: {
type: 'number',
displayName: 'Count',
group: 'General',
getter: function () {
return this._internal.collection ? this._internal.collection.size() : 0;
}
},
modified: {
group: 'Events',
type: 'signal',
displayName: 'Modified'
},
fetched: {
group: 'Events',
type: 'signal',
displayName: 'Fetched'
},
failure: {
group: 'Events',
type: 'signal',
displayName: 'Failure'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Events',
getter: function () {
return this._internal.error;
}
}
},
prototypeExtensions: {
setCollectionName: function (name) {
this._internal.name = name;
// this.invalidateCollection();
this.flagOutputDirty('id');
},
setCollection: function (collection) {
this.bindCollection(collection);
this.flagOutputDirty('firstItemId');
// this.flagOutputDirty('firstItem');
this.flagOutputDirty('items');
this.flagOutputDirty('count');
},
unbindCurrentCollection: function () {
var collection = this._internal.collection;
if (!collection) return;
collection.off('change', this._internal.collectionChangedCallback);
this._internal.collection = undefined;
},
bindCollection: function (collection) {
this.unbindCurrentCollection();
this._internal.collection = collection;
collection && collection.on('change', this._internal.collectionChangedCallback);
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
this.unbindCurrentCollection();
},
setError: function (err) {
this._internal.err = err;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
},
fetch: function () {
var internal = this._internal;
if (this.context.editorConnection) {
if (this._internal.name === undefined) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection', {
message: 'No collection specified for query'
});
} else {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'query-collection');
}
}
if (internal.fetchScheduled) return;
internal.fetchScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
internal.fetchScheduled = false;
const _c = Collection.get();
const f = this.getStorageFilter();
CloudStore.instance.query({
collection: this._internal.name,
where: f.where,
sort: f.sort,
limit: this.getStorageLimit(),
skip: this.getStorageSkip(),
success: (results) => {
if (results !== undefined) {
_c.set(
results.map((i) => {
var m = CloudStore._fromJSON(i, this._internal.name);
// Remove from collection if model is deleted
m.on('delete', () => _c.remove(m));
return m;
})
);
}
this.setCollection(_c);
this.sendSignalOnOutput('fetched');
},
error: (err) => {
this.setCollection(_c);
this.setError(err || 'Failed to fetch.');
}
});
});
},
getStorageFilter: function () {
const storageSettings = this._internal.storageSettings;
if (storageSettings['storageFilterType'] === undefined || storageSettings['storageFilterType'] === 'simple') {
// Create simple filter
if (storageSettings['storageFilter']) {
const filters = storageSettings['storageFilter'].split(',');
var _filters = [];
filters.forEach(function (f) {
const _filter = {};
var op = '$' + (storageSettings['storageFilterOp-' + f] || 'eq');
_filter[f] = {};
_filter[f][op] = storageSettings['storageFilterValue-' + f];
_filters.push(_filter);
});
var _where = _filters.length > 1 ? { $and: _filters } : _filters[0];
}
if (storageSettings['storageSort']) {
const sort = storageSettings['storageSort'].split(',');
var _sort = [];
sort.forEach(function (s) {
_sort.push((storageSettings['storageSort-' + s] === 'descending' ? '-' : '') + s);
});
}
return {
where: _where,
sort: _sort
};
} else if (storageSettings['storageFilterType'] === 'json') {
// JSON filter
if (!this._internal.filterFunc) {
try {
var filterCode = storageSettings['storageJSONFilter'];
// Parse out variables
filterCode = filterCode.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
this._internal.filterVariables = filterCode.match(/\$[A-Za-z0-9]+/g) || [];
var args = ['filter', 'where', 'sort', 'Inputs']
.concat(this._internal.filterVariables)
.concat([filterCode]);
this._internal.filterFunc = Function.apply(null, args);
} catch (e) {
this._internal.filterFunc = undefined;
console.log('Error while parsing filter script: ' + e);
}
}
if (!this._internal.filterFunc) return;
var _filter = {},
_sort = [],
_this = this;
// Collect filter variables
var _filterCb = function (f) {
_filter = _convertFilterOp(f, {
collectionName: _this._internal.name,
error: function (err) {
_this.context.editorConnection.sendWarning(
_this.nodeScope.componentOwner.name,
_this.id,
'query-collection-filter',
{
message: err
}
);
}
});
};
var _sortCb = function (s) {
_sort = s;
};
// Extract inputs
const inputs = {};
for (let key in storageSettings) {
if (key.startsWith('storageFilterValue-'))
inputs[key.substring('storageFilterValue-'.length)] = storageSettings[key];
}
var filterFuncArgs = [_filterCb, _filterCb, _sortCb, inputs]; // One for filter, one for where
this._internal.filterVariables.forEach((v) => {
filterFuncArgs.push(storageSettings['storageFilterValue-' + v.substring(1)]);
});
// Run the code to get the filter
try {
this._internal.filterFunc.apply(this, filterFuncArgs);
} catch (e) {
console.log('Error while running filter script: ' + e);
}
return { where: _filter, sort: _sort };
}
},
getStorageLimit: function () {
const storageSettings = this._internal.storageSettings;
if (!storageSettings['storageEnableLimit']) return;
else return storageSettings['storageLimit'] || 10;
},
getStorageSkip: function () {
const storageSettings = this._internal.storageSettings;
if (!storageSettings['storageEnableLimit']) return;
else return storageSettings['storageSkip'] || 0;
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name)
});
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
const dynamicSignals = {
storageFetch: this.fetch.bind(this)
};
if (dynamicSignals[name])
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: dynamicSignals[name]
})
});
const dynamicSetters = {
collectionName: this.setCollectionName.bind(this)
};
if (dynamicSetters[name])
return this.registerInput(name, {
set: dynamicSetters[name]
});
this.registerInput(name, {
set: userInputSetter.bind(this, name)
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.storageSettings[name];
}
function userInputSetter(name, value) {
/* jshint validthis:true */
this._internal.storageSettings[name] = value;
}
const _defaultJSONQuery =
'// Write your query script here, check out the reference documentation for examples\n' + 'where({ })\n';
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
var ports = [];
ports.push({
name: 'collectionName',
type: {
name: 'enum',
enums:
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: [],
allowEditOnly: true
},
displayName: 'Collecton Name',
plug: 'input',
group: 'General'
});
ports.push({
name: 'storageFilterType',
type: {
name: 'enum',
allowEditOnly: true,
enums: [
{ value: 'simple', label: 'Simple' },
{ value: 'json', label: 'Advanced' }
]
},
displayName: 'Filter',
default: 'simple',
plug: 'input',
group: 'General'
});
// Limit
ports.push({
type: 'boolean',
plug: 'input',
group: 'Limit',
name: 'storageEnableLimit',
displayName: 'Use limit'
});
if (parameters['storageEnableLimit']) {
ports.push({
type: 'number',
default: 10,
plug: 'input',
group: 'Limit',
name: 'storageLimit',
displayName: 'Limit'
});
ports.push({
type: 'number',
default: 0,
plug: 'input',
group: 'Limit',
name: 'storageSkip',
displayName: 'Skip'
});
}
ports.push({
type: 'signal',
plug: 'input',
group: 'Storage',
name: 'storageFetch',
displayName: 'Fetch'
});
// Simple query
if (parameters['storageFilterType'] === undefined || parameters['storageFilterType'] === 'simple') {
ports.push({
type: { name: 'stringlist', allowEditOnly: true },
plug: 'input',
group: 'Filter',
name: 'storageFilter',
displayName: 'Filter'
});
const filterOps = {
string: [
{ value: 'eq', label: 'Equals' },
{ value: 'ne', label: 'Not Equals' }
],
boolean: [
{ value: 'eq', label: 'Equals' },
{ value: 'ne', label: 'Not Equals' }
],
number: [
{ value: 'eq', label: 'Equals' },
{ value: 'ne', label: 'Not Equals' },
{ value: 'lt', label: 'Less than' },
{ value: 'gt', label: 'Greater than' },
{ value: 'gte', label: 'Greater than or equal' },
{ value: 'lte', label: 'Less than or equal' }
]
};
if (parameters['storageFilter']) {
var filters = parameters['storageFilter'].split(',');
filters.forEach((f) => {
// Type
ports.push({
type: {
name: 'enum',
enums: [
{ value: 'string', label: 'String' },
{ value: 'number', label: 'Number' },
{ value: 'boolean', label: 'Boolean' }
]
},
default: 'string',
plug: 'input',
group: f + ' filter',
displayName: 'Type',
editorName: f + ' filter | Type',
name: 'storageFilterType-' + f
});
var type = parameters['storageFilterType-' + f];
// String filter type
ports.push({
type: { name: 'enum', enums: filterOps[type || 'string'] },
default: 'eq',
plug: 'input',
group: f + ' filter',
displayName: 'Op',
editorName: f + ' filter| Op',
name: 'storageFilterOp-' + f
});
ports.push({
type: type || 'string',
plug: 'input',
group: f + ' filter',
displayName: 'Value',
editorName: f + ' Filter Value',
name: 'storageFilterValue-' + f
});
});
}
ports.push({
type: { name: 'stringlist', allowEditOnly: true },
plug: 'input',
group: 'Sort',
name: 'storageSort',
displayName: 'Sort'
});
// Sorting inputs
if (parameters['storageSort']) {
var filters = parameters['storageSort'].split(',');
filters.forEach((f) => {
ports.push({
type: {
name: 'enum',
enums: [
{ value: 'ascending', label: 'Ascending' },
{ value: 'descending', label: 'Descending' }
]
},
default: 'ascending',
plug: 'input',
group: f + ' sort',
displayName: 'Sort',
editorName: f + ' sorting',
name: 'storageSort-' + f
});
});
}
}
// JSON query
else if (parameters['storageFilterType'] === 'json') {
ports.push({
type: { name: 'string', allowEditOnly: true, codeeditor: 'javascript' },
plug: 'input',
group: 'Filter',
name: 'storageJSONFilter',
default: _defaultJSONQuery,
displayName: 'Filter'
});
var filter = parameters['storageJSONFilter'];
if (filter) {
filter = filter.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ''); // Remove comments
var variables = filter.match(/\$[A-Za-z0-9]+/g);
if (variables) {
const unique = {};
variables.forEach((v) => {
unique[v] = true;
});
Object.keys(unique).forEach((p) => {
ports.push({
name: 'storageFilterValue-' + p.substring(1),
displayName: p.substring(1),
group: 'Filter Values',
plug: 'input',
type: { name: '*', allowConnectionsOnly: true }
});
});
}
// Support variables with the "Inputs."" syntax
JavascriptNodeParser.parseAndAddPortsFromScript(filter, ports, {
inputPrefix: 'storageFilterValue-',
inputGroup: 'Filter Values',
inputType: { name: '*', allowConnectionsOnly: true },
skipOutputs: true
});
}
}
editorConnection.sendDynamicPorts(nodeId, ports);
}
module.exports = {
node: DbCollectionNode,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
node.on('parameterUpdated', function (event) {
if (event.name.startsWith('storage')) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
}
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, data);
});
graphModel.on('metadataChanged.cloudservices', function (data) {
CloudStore.instance._initCloudServices();
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.DbCollection', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('DbCollection')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,797 @@
'use strict';
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
const isEqual = require('lodash.isequal');
var Model = require('@noodl/runtime/src/model');
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
var modelPortsHash = {},
previousProperties = {};
var ModelNodeDefinition = {
name: 'DbModel',
docs: 'https://docs.noodl.net/nodes/cloud-services/model',
displayNodeName: 'Model',
shortDesc: 'Database model',
category: 'Cloud Services',
usePortAsLabel: '$ndlCollectionName',
color: 'data',
deprecated: true, // Use record node
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
internal.relationModelIds = {};
var _this = this;
this._internal.onModelChangedCallback = function (args) {
if (_this.isInputConnected('fetch')) return;
if (_this.hasOutput(args.name)) _this.flagOutputDirty(args.name);
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
_this.sendSignalOnOutput('changed');
};
},
getInspectInfo() {
const model = this._internal.model;
if (!model) return '[No Model]';
return [
{ type: 'text', value: 'Id: ' + model.getId() },
{ type: 'value', value: model.data }
];
},
outputs: {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
},
saved: {
type: 'signal',
displayName: 'Saved',
group: 'Events'
},
stored: {
type: 'signal',
displayName: 'Stored',
group: 'Events'
},
created: {
type: 'signal',
displayName: 'Created',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
deleted: {
type: 'signal',
displayName: 'Deleted',
group: 'Events'
},
failure: {
type: 'signal',
displayName: 'Failure',
group: 'Events'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Events',
getter: function () {
return this._internal.error;
}
}
},
inputs: {
modelId: {
type: { name: 'string', allowConnectionsOnly: true },
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
this._internal.modelId = value; // Wait to fetch data
if (this.isInputConnected('fetch') === false) this.setModelID(value);
else {
this.flagOutputDirty('id');
}
}
},
properties: {
type: { name: 'stringlist', allowEditOnly: true },
displayName: 'Properties',
group: 'Properties',
set: function (value) {}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleFetch();
}
},
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleStore();
}
},
save: {
displayName: 'Save',
group: 'Actions',
valueChangedToTrue: function () {
this.storageSave();
}
},
delete: {
displayName: 'Delete',
group: 'Actions',
valueChangedToTrue: function () {
this.storageDelete();
}
},
new: {
displayName: 'New',
group: 'Actions',
valueChangedToTrue: function () {
this.storageNew();
}
},
insert: {
displayName: 'Insert',
group: 'Actions',
valueChangedToTrue: function () {
this.storageInsert();
// this.storageSave();
}
}
},
methods: {
setCollectionID: function (id) {
this._internal.collectionId = id;
this.clearWarnings();
},
setModelID: function (id) {
var model = Model.get(id);
// this._internal.modelIsNew = false;
this.setModel(model);
},
setModel: function (model) {
if (this._internal.model)
// Remove old listener if existing
this._internal.model.off('change', this._internal.onModelChangedCallback);
this._internal.model = model;
this.flagOutputDirty('id');
model.on('change', this._internal.onModelChangedCallback);
// We have a new model, mark all outputs as dirty
for (var key in model.data) {
if (this.hasOutput(key)) this.flagOutputDirty(key);
}
this.sendSignalOnOutput('fetched');
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
},
scheduleOnce: function (type, cb) {
const _this = this;
const _type = 'hasScheduled' + type;
if (this._internal[_type]) return;
this._internal[_type] = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this._internal[_type] = false;
cb();
});
},
_hasChangesPending: function () {
const internal = this._internal;
var model = internal.model;
for (var key in internal.inputValues) {
if (isEqual(model.data[key], internal.inputValues[key])) return true;
}
return false;
},
scheduleFetch: function () {
var _this = this;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('Fetch', function () {
if (internal.modelId === undefined || internal.modelId === '') return; // Don't do fetch if no id
CloudStore.instance.fetch({
collection: internal.collectionId,
objectId: internal.modelId, // Get the objectId part of the model id
success: function (response) {
var model = CloudStore._fromJSON(response, internal.collectionId);
if (internal.model !== model) {
// Check if we need to change model
if (internal.model)
// Remove old listener if existing
internal.model.off('change', internal.onModelChangedCallback);
internal.model = model;
model.on('change', internal.onModelChangedCallback);
}
_this.flagOutputDirty('id');
delete response.objectId;
for (var key in response) {
// model.set(key,response[key]);
if (_this.hasOutput(key)) _this.flagOutputDirty(key);
}
_this.sendSignalOnOutput('fetched');
},
error: function (err) {
_this.setError(err || 'Failed to fetch.');
}
});
});
},
scheduleStore: function () {
var _this = this;
var internal = this._internal;
if (!internal.model) return;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('Store', function () {
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
_this.sendSignalOnOutput('stored');
});
},
storageSave: function () {
const _this = this;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
//console.log('dbmodel save scheduled')
this.scheduleOnce('StorageSave', function () {
if (!internal.model) return;
var model = internal.model;
//console.log('dbmodel save hasChanges='+_this._hasChangesPending())
//if(!_this._internal.modelIsNew && !_this._hasChangesPending()) return; // No need to save, no changes pending
for (var i in internal.inputValues) {
model.set(i, internal.inputValues[i], { resolve: true });
}
CloudStore.instance.save({
collection: internal.collectionId,
objectId: model.getId(), // Get the objectId part of the model id
data: model.data,
success: function (response) {
for (var key in response) {
model.set(key, response[key]);
}
// _this._internal.modelIsNew = false; // If the model was a new model, it is now saved
_this.sendSignalOnOutput('saved');
},
error: function (err) {
_this.setError(err || 'Failed to save.');
}
});
});
},
storageDelete: function () {
const _this = this;
if (!this._internal.model) return;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('StorageDelete', function () {
CloudStore.instance.delete({
collection: internal.collectionId,
objectId: internal.model.getId(), // Get the objectId part of the model id,
success: function () {
internal.model.notify('delete'); // Notify that this model has been deleted
_this.sendSignalOnOutput('deleted');
},
error: function (err) {
_this.setError(err || 'Failed to delete.');
}
});
});
},
storageInsert: function () {
const _this = this;
const internal = this._internal;
if (!this.checkWarningsBeforeCloudOp()) return;
this.scheduleOnce('StorageInsert', function () {
var _data = _this._getModelInitData();
CloudStore.instance.create({
collection: internal.collectionId,
data: _data,
success: function (data) {
// Successfully created
const m = CloudStore._fromJSON(data, internal.collectionId);
_this.setModel(m);
_this.sendSignalOnOutput('created');
_this.sendSignalOnOutput('saved');
},
error: function (err) {
_this.setError(err || 'Failed to insert.');
}
});
});
},
checkWarningsBeforeCloudOp() {
//clear all errors first
this.clearWarnings();
if (!this._internal.collectionId) {
this.setError('No collection name specified');
return false;
}
return true;
},
setError: function (err) {
this._internal.error = err;
this.flagOutputDirty('error');
this.sendSignalOnOutput('failure');
if (this.context.editorConnection) {
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning', {
message: err,
showGlobally: true
});
}
},
clearWarnings() {
if (this.context.editorConnection) {
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'storage-op-warning');
}
},
onRelationAdd: function (key) {
const _this = this;
const internal = this._internal;
this.scheduleOnce('StorageAddRelation', function () {
if (!internal.model) return;
var model = internal.model;
var targetModelId = internal.relationModelIds[key];
if (targetModelId === undefined) return;
CloudStore.instance.addRelation({
collection: internal.collectionId,
objectId: model.getId(),
key: key,
targetObjectId: targetModelId,
targetClass: Model.get(targetModelId)._class,
success: function (response) {
for (var _key in response) {
model.set(_key, response[_key]);
}
// Successfully added relation
_this.sendSignalOnOutput('$relation-added-' + key);
},
error: function (err) {
_this.setError(err || 'Failed to add relation.');
}
});
});
},
onRelationRemove: function (key) {
const _this = this;
const internal = this._internal;
this.scheduleOnce('StorageRemoveRelation', function () {
if (!internal.model) return;
var model = internal.model;
var targetModelId = internal.relationModelIds[key];
if (targetModelId === undefined) return;
CloudStore.instance.removeRelation({
collection: internal.collectionId,
objectId: model.getId(),
key: key,
targetObjectId: targetModelId,
targetClass: Model.get(targetModelId)._class,
success: function (response) {
for (var _key in response) {
model.set(_key, response[_key]);
}
// Successfully removed relation
_this.sendSignalOnOutput('$relation-removed-' + key);
},
error: function (err) {
_this.setError(err || 'Failed to remove relation.');
}
});
});
},
setRelationModelId: function (key, modelId) {
this._internal.relationModelIds[key] = modelId;
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
if (name.startsWith('$relation-added-'))
return this.registerOutput(name, {
getter: function () {
/** No needed for signals */
}
});
if (name.startsWith('$relation-removed-'))
return this.registerOutput(name, {
getter: function () {
/** No needed for signals */
}
});
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name)
});
},
_getModelInitData: function () {
var internal = this._internal;
var _data = {};
// First copy values from inputs
for (var i in internal.inputValues) {
_data[i] = internal.inputValues[i];
}
// Then run initialize code
if (this._internal.modelInitCode) {
try {
var initCode = new Function('initialize', this._internal.modelInitCode);
initCode(function (data) {
for (var key in data) {
if (typeof data[key] === 'function') _data[key] = data[key]();
else _data[key] = data[key];
}
});
} catch (e) {
console.log('Error while initializing model: ' + e);
}
}
return _data;
},
setModelInitCode: function (code) {
this._internal.modelInitCode = code;
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
// Relation inputs
if (name.startsWith('$relation-add-'))
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: this.onRelationAdd.bind(this, name.substring('$relation-add-'.length))
})
});
if (name.startsWith('$relation-remove-'))
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: this.onRelationRemove.bind(this, name.substring('$relation-remove-'.length))
})
});
if (name.startsWith('$relation-modelid-'))
return this.registerInput(name, {
set: this.setRelationModelId.bind(this, name.substring('$relation-modelid-'.length))
});
const dynamicSignals = {};
if (dynamicSignals[name])
return this.registerInput(name, {
set: EdgeTriggeredInput.createSetter({
valueChangedToTrue: dynamicSignals[name]
})
});
const dynamicSetters = {
$ndlCollectionName: this.setCollectionID.bind(this),
$ndlModelInitCode: this.setModelInitCode.bind(this)
// '$ndlModelValidationCode':this.setModelValidationCode.bind(this),
};
if (dynamicSetters[name])
return this.registerInput(name, {
set: dynamicSetters[name]
});
this.registerInput(name, {
set: userInputSetter.bind(this, name)
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
}
function userInputSetter(name, value) {
//console.log('dbmodel setter:',name,value)
/* jshint validthis:true */
this._internal.inputValues[name] = value;
}
function detectRename(before, after) {
if (!before || !after) return;
if (before.length !== after.length) return; // Must be of same length
var res = {};
for (var i = 0; i < before.length; i++) {
if (after.indexOf(before[i]) === -1) {
if (res.before) return; // Can only be one from before that is missing
res.before = before[i];
}
if (before.indexOf(after[i]) === -1) {
if (res.after) return; // Only one can be missing,otherwise we cannot match
res.after = after[i];
}
}
return res.before && res.after ? res : undefined;
}
const defaultStorageInitCode =
'initialize({\n' +
'\t// Here you can initialize new models\n' +
"\t//myProperty:'Some init value',\n" +
"\t//anotherProperty:function() { return 'Some other value' }\n" +
'})\n';
/*const defaultStorageValidateCode = "validation({\n" +
"\t// Here you add validation specifications for your model properties.\n" +
"\t//myProperty: { required:true, length:4 },\n" +
"\t//anotherProperty: function(value) {\n" +
"\t//\tif(value !== 'someValue) return 'Error message'\n" +
"\t//}\n" +
"})\n";*/
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
var ports = [];
// Add value outputs
var properties = parameters.properties;
if (properties) {
properties = properties ? properties.split(',') : undefined;
for (var i in properties) {
var p = properties[i];
ports.push({
type: {
name: '*',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: p
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Events',
displayName: 'Changed ' + p,
name: 'changed-' + p
});
}
var propertyRenamed = detectRename(previousProperties[nodeId], properties);
previousProperties[nodeId] = properties;
if (propertyRenamed) {
var renamed = {
plug: 'input/output',
patterns: ['{{*}}'],
before: propertyRenamed.before,
after: propertyRenamed.after
};
}
}
ports.push({
name: '$ndlCollectionName',
displayName: 'Class',
group: 'General',
type: {
name: 'enum',
enums:
dbCollections !== undefined
? dbCollections.map((c) => {
return { value: c.name, label: c.name };
})
: [],
allowEditOnly: true
},
plug: 'input'
});
if (parameters.$ndlCollectionName && dbCollections) {
// Fetch ports from collection keys
var c = dbCollections.find((c) => c.name === parameters.$ndlCollectionName);
if (c && c.schema && c.schema.properties) {
var props = c.schema.properties;
for (var key in props) {
var p = props[key];
if (ports.find((_p) => _p.name === key)) continue;
if (p.type === 'Relation') {
// Ports for adding / removing relation
ports.push({
type: 'signal',
plug: 'input',
group: key + ' Relation',
name: '$relation-add-' + key,
displayName: 'Add',
editorName: key + ' | Add'
});
ports.push({
type: 'signal',
plug: 'input',
group: key + ' Relation',
name: '$relation-remove-' + key,
displayName: 'Remove',
editorName: key + ' | Remove'
});
ports.push({
type: { name: 'string', allowConnectionsOnly: true },
plug: 'input',
group: key + ' Relation',
name: '$relation-modelid-' + key,
displayName: 'Model Id',
editorName: key + ' | Model Id'
});
ports.push({
type: 'signal',
plug: 'output',
group: key + ' Relation',
name: '$relation-removed-' + key,
displayName: 'Removed',
editorName: key + ' | Removed'
});
ports.push({
type: 'signal',
plug: 'output',
group: key + ' Relation',
name: '$relation-added-' + key,
displayName: 'Added',
editorName: key + ' | Added'
});
} else {
// Other schema type ports
ports.push({
type: {
name: '*',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: key
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Events',
displayName: 'Changed ' + key,
name: 'changed-' + key
});
}
}
}
}
// Storage ports
ports.push({
name: '$ndlModelInitCode',
displayName: 'Initialize',
group: 'Scripts',
type: {
name: 'string',
allowEditOnly: true,
codeeditor: 'javascript'
},
default: defaultStorageInitCode,
plug: 'input'
});
/* ports.push({
name:'$ndlModelValidationCode',
displayName: "Validate",
group: "Storage scripts",
"type": {
name: "string",
allowEditOnly: true,
codeeditor: "javascript"
},
default: defaultStorageValidateCode,
plug:'input'
}) */
var hash = JSON.stringify(ports);
if (modelPortsHash[nodeId] !== hash) {
// Make sure we don't resend the same port data
modelPortsHash[nodeId] = hash;
editorConnection.sendDynamicPorts(nodeId, ports, { renamed: renamed });
}
}
module.exports = {
node: ModelNodeDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
function _managePortsForNode(node) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
node.on('parameterUpdated', function (event) {
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
});
graphModel.on('metadataChanged.dbCollections', function (data) {
CloudStore.invalidateCollections();
updatePorts(node.id, node.parameters, context.editorConnection, data);
});
}
graphModel.on('editorImportComplete', () => {
graphModel.on('nodeAdded.DbModel', function (node) {
_managePortsForNode(node);
});
for (const node of graphModel.getNodesWithType('DbModel')) {
_managePortsForNode(node);
}
});
}
};

View File

@@ -0,0 +1,371 @@
'use strict';
const { Node } = require('@noodl/runtime');
var Model = require('@noodl/runtime/src/model');
//var previousProperties = {};
var ModelNodeDefinition = {
name: 'Model',
docs: 'https://docs.noodl.net/nodes/data/object',
displayNodeName: 'Object',
shortDesc:
'Stores any amount of properties and can be used standalone or together with Collections and For Each nodes.',
category: 'Data',
usePortAsLabel: 'modelId',
color: 'data',
deprecated: true, // Use new model node
initialize: function () {
var internal = this._internal;
internal.inputValues = {};
var _this = this;
this._internal.onModelChangedCallback = function (args) {
if (_this.isInputConnected('fetch') === true) return;
if (_this.hasOutput(args.name)) _this.flagOutputDirty(args.name);
if (_this.hasOutput('changed-' + args.name)) _this.sendSignalOnOutput('changed-' + args.name);
_this.sendSignalOnOutput('changed');
};
},
getInspectInfo() {
const model = this._internal.model;
if (!model) return '[No Object]';
const modelInfo = [{ type: 'text', value: 'Id: ' + model.getId() }];
const data = this._internal.model.data;
return modelInfo.concat(
Object.keys(data).map((key) => {
return { type: 'text', value: key + ': ' + data[key] };
})
);
},
outputs: {
id: {
type: 'string',
displayName: 'Id',
group: 'General',
getter: function () {
return this._internal.model ? this._internal.model.getId() : this._internal.modelId;
}
},
/* currentModel: {
type: 'object',
displayName: 'Object',
group: 'General',
getter: function () {
return this._internal.model;
}
},*/
stored: {
type: 'signal',
displayName: 'Stored',
group: 'Events'
},
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
created: {
type: 'signal',
displayName: 'Created',
group: 'Events'
}
},
inputs: {
modelId: {
type: {
name: 'string',
identifierOf: 'ModelName',
identifierDisplayName: 'Object Ids'
},
displayName: 'Id',
group: 'General',
set: function (value) {
if (value instanceof Model) value = value.getId(); // Can be passed as model as well
this._internal.modelId = value; // Wait to fetch data
if (this.isInputConnected('fetch') === false) this.setModelID(value);
else {
this.flagOutputDirty('id');
}
}
},
/* model:{
type:'object',
displayName:'Object',
group: 'General',
set: function (value) {
if(value === undefined) return;
if(value === this._internal.model) return;
if(!(value instanceof Model) && typeof value === 'object') {
// This is a regular JS object, convert to model
value = Model.create(value);
}
this._internal.modelId = value.getId();
if(this.isInputConnected('fetch') === false)
this.setModelID(this._internal.modelId);
else {
this.flagOutputDirty('id');
}
}
},*/
properties: {
type: { name: 'stringlist', allowEditOnly: true },
displayName: 'Properties',
group: 'Properties',
set: function (value) {}
},
new: {
displayName: 'New',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleNew();
}
},
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleStore();
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleSetModel();
}
},
clear: {
displayName: 'Clear',
group: 'Actions',
valueChangedToTrue: function () {
var internal = this._internal;
if (!internal.model) return;
for (var i in internal.inputValues) {
internal.model.set(i, undefined, { resolve: true });
}
}
}
},
prototypeExtensions: {
scheduleStore: function () {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
if (!internal.model) return;
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
this.sendSignalOnOutput('stored');
});
},
scheduleNew: function () {
if (this.hasScheduledNew) return;
this.hasScheduledNew = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledNew = false;
const newModel = Model.get();
for (var i in internal.inputValues) {
newModel.set(i, internal.inputValues[i], { resolve: true });
}
this.setModel(newModel);
this.sendSignalOnOutput('created');
this.sendSignalOnOutput('stored');
});
},
scheduleSetModel: function () {
if (this.hasScheduledSetModel) return;
this.hasScheduledSetModel = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledSetModel = false;
this.setModelID(this._internal.modelId);
});
},
setModelID: function (id) {
var model = Model.get(id);
this.setModel(model);
this.sendSignalOnOutput('fetched');
},
setModel: function (model) {
if (this._internal.model)
// Remove old listener if existing
this._internal.model.off('change', this._internal.onModelChangedCallback);
this._internal.model = model;
this.flagOutputDirty('id');
model.on('change', this._internal.onModelChangedCallback);
// We have a new model, mark all outputs as dirty
for (var key in model.data) {
if (this.hasOutput(key)) this.flagOutputDirty(key);
}
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
if (this._internal.model) this._internal.model.off('change', this._internal.onModelChangedCallback);
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
return;
}
this.registerOutput(name, {
getter: userOutputGetter.bind(this, name)
});
},
registerInputIfNeeded: function (name) {
var _this = this;
if (this.hasInput(name)) {
return;
}
this.registerInput(name, {
set: userInputSetter.bind(this, name)
});
}
}
};
function userOutputGetter(name) {
/* jshint validthis:true */
return this._internal.model ? this._internal.model.get(name, { resolve: true }) : undefined;
}
function userInputSetter(name, value) {
/* jshint validthis:true */
this._internal.inputValues[name] = value;
// Store on change if no connection to store or new
if (this.isInputConnected('store') === false && this.isInputConnected('new') === false) {
const model = this._internal.model;
const valueChanged = model ? model.get(name) !== value : true;
if (valueChanged) {
this.scheduleStore();
}
}
}
/*function detectRename(before, after) {
if (!before || !after) return;
if (before.length !== after.length) return; // Must be of same length
var res = {}
for (var i = 0; i < before.length; i++) {
if (after.indexOf(before[i]) === -1) {
if (res.before) return; // Can only be one from before that is missing
res.before = before[i];
}
if (before.indexOf(after[i]) === -1) {
if (res.after) return; // Only one can be missing,otherwise we cannot match
res.after = after[i];
}
}
return (res.before && res.after) ? res : undefined;
}*/
/*const defaultStorageInitCode = "initialize({\n"+
"\t// Here you can initialize new models\n"+
"\tmyProperty:'Some init value',\n"+
"\tanotherProperty:function() { return 'Some other value')\n"+
"})\n";
const defaultStorageValidateCode = "validation({\n"+
"\t// Here you add validation specifications for your model properties.\n"+
"\tmyProperty: { required:true, length:4 },\n"+
"\tanotherProperty: function(value) {\n"+
"\t\tif(value !== 'someValue) return 'Error message'\n"+
"\t}\n"+
"})\n";*/
function updatePorts(nodeId, parameters, editorConnection) {
var ports = [];
// Add value outputs
var properties = parameters.properties;
if (properties) {
properties = properties ? properties.split(',') : undefined;
for (var i in properties) {
var p = properties[i];
ports.push({
type: {
name: '*',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: p
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Changed Events',
displayName: p + ' Changed',
name: 'changed-' + p
});
}
/* var propertyRenamed = detectRename(previousProperties[nodeId], properties);
previousProperties[nodeId] = properties;
if (propertyRenamed) {
var renamed = {
plug: 'input/output',
patterns: ['{{*}}'],
before: propertyRenamed.before,
after: propertyRenamed.after
};
}*/
}
editorConnection.sendDynamicPorts(nodeId, ports, {
detectRenamed: {
plug: 'input/output'
}
});
}
module.exports = {
node: ModelNodeDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.Model', function (node) {
updatePorts(node.id, node.parameters, context.editorConnection);
node.on('parameterUpdated', function (event) {
updatePorts(node.id, node.parameters, context.editorConnection);
});
});
}
};

View File

@@ -0,0 +1,142 @@
'use strict';
const { Node } = require('@noodl/runtime');
var Model = require('@noodl/runtime/src/model');
var VariableNodeDefinition = {
name: 'Variable',
docs: 'https://docs.noodl.net/nodes/data/variable',
category: 'Data',
usePortAsLabel: 'name',
color: 'data',
deprecated: true, // use newvariable instead
initialize: function () {
var _this = this;
var internal = this._internal;
this._internal.onModelChangedCallback = function (args) {
if (!_this.isInputConnected('fetch') && args.name === internal.name) {
_this.sendSignalOnOutput('changed');
_this.flagOutputDirty('value');
}
};
internal.variablesModel = Model.get('--ndl--global-variables');
internal.variablesModel.on('change', this._internal.onModelChangedCallback);
},
getInspectInfo() {
if (this._internal.name) {
return this._internal.variablesModel.get(this._internal.name);
}
return '[No value set]';
},
outputs: {
name: {
type: 'string',
displayName: 'Name',
group: 'General',
getter: function () {
return this._internal.name;
}
},
stored: {
type: 'signal',
displayName: 'Stored',
group: 'Events'
},
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
value: {
type: '*',
displayName: 'Value',
group: 'General',
getter: function () {
var internal = this._internal;
if (!internal.name) return;
return internal.variablesModel.get(internal.name);
}
}
},
inputs: {
name: {
type: {
name: 'string',
identifierOf: 'VariableName',
identifierDisplayName: 'Variable names'
},
displayName: 'Name',
group: 'General',
set: function (value) {
if (this.isInputConnected('fetch') === false) this.setVariableName(value);
else {
this._internal.name = value; // Wait to fetch data
this.flagOutputDirty('name');
}
}
},
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleStore();
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.setVariableName(this._internal.name);
}
},
value: {
type: '*',
displayName: 'Value',
group: 'General',
set: function (value) {
this._internal.value = value;
if (this.isInputConnected('store') === false) {
this.scheduleStore();
}
}
}
},
prototypeExtensions: {
scheduleStore: function () {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(function () {
this.hasScheduledStore = false;
internal.variablesModel.set(internal.name, internal.value);
this.sendSignalOnOutput('stored');
});
},
setVariableName: function (name) {
this._internal.name = name;
this.flagOutputDirty('name');
this.flagOutputDirty('value');
this.sendSignalOnOutput('fetched');
},
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
this._internal.variablesModel.off('change', this._internal.onModelChangedCallback);
}
}
};
module.exports = {
node: VariableNodeDefinition
};

View File

@@ -0,0 +1,82 @@
'use strict';
const { Node } = require('@noodl/runtime');
const GlobalsNode = {
name: 'Globals',
shortDesc: 'A node used to communicate values across the project.',
category: 'Utilities',
color: 'component',
deprecated: true, // use variable instead
initialize: function () {
this._internal.listeners = [];
},
panels: [
{
name: 'PortEditor',
context: ['select', 'connectTo', 'connectFrom'],
title: 'Globals',
plug: 'input/output',
type: {
name: '*'
}
}
],
prototypeExtensions: {
_onNodeDeleted: function () {
Node.prototype._onNodeDeleted.call(this);
var globalsEmitter = this.context.globalsEventEmitter;
for (var i = 0; i < this._internal.listeners.length; i++) {
var listener = this._internal.listeners[i];
globalsEmitter.removeListener(listener.name, listener.listener);
}
this._internal.listeners = [];
},
_newOutputValueReceived: {
value: function (name) {
this._cachedInputValues[name] = this.context.globalValues[name];
this.flagOutputDirty(name);
}
},
registerInputIfNeeded: {
value: function (name) {
var self = this;
if (this.hasInput(name)) {
return;
}
this.registerInput(name, {
set: self.context.setGlobalValue.bind(self.context, name)
});
}
},
registerOutputIfNeeded: {
value: function (name) {
if (this.hasOutput(name)) {
return;
}
var newOutputValueReceivedCallback = this._newOutputValueReceived.bind(this, name);
var globalsEmitter = this.context.globalsEventEmitter;
this._internal.listeners.push({
name: name,
listener: newOutputValueReceivedCallback
});
globalsEmitter.on(name, newOutputValueReceivedCallback);
this.registerOutput(name, {
getter: function () {
return this.context.globalValues[name];
}
});
}
}
}
};
module.exports = {
node: GlobalsNode
};

View File

@@ -0,0 +1,64 @@
'use strict';
const GyroscopeNode = {
name: 'Gyroscope',
docs: 'https://docs.noodl.net/nodes/sensors/device-orientation',
shortDesc: 'The orientation of a device. Works on phones, tablets and other devices with the required sensors.',
displayNodeName: 'Device Orientation',
category: 'Sensors',
deprecated: true,
initialize: function () {
this._internal.alpha = 0;
this._internal.beta = 0;
this._internal.gamma = 0;
var onEvent = onDeviceOrientation.bind(this);
window.addEventListener('deviceorientation', onEvent);
this.context.eventEmitter.once('applicationDataReloaded', function () {
window.removeEventListener('deviceorientation', onEvent);
});
},
outputs: {
rotationX: {
type: 'number',
displayName: 'Rotation X',
getter: function () {
return -this._internal.beta;
}
},
rotationY: {
type: 'number',
displayName: 'Rotation Y',
getter: function () {
return this._internal.gamma;
}
},
rotationZ: {
type: 'number',
displayName: 'Rotation Z',
getter: function () {
return -this._internal.alpha;
}
}
}
};
function onDeviceOrientation(event) {
/* jshint validthis:true */
if (event.alpha !== this._internal.alpha) {
this._internal.alpha = event.alpha;
this.flagOutputDirty('rotationZ');
}
if (event.beta !== this._internal.beta) {
this._internal.beta = event.beta;
this.flagOutputDirty('rotationX');
}
if (event.gamma !== this._internal.gamma) {
this._internal.gamma = event.gamma;
this.flagOutputDirty('rotationY');
}
}
module.exports = {
node: GyroscopeNode
};

View File

@@ -0,0 +1,112 @@
'use strict';
const EaseCurves = require('../../easecurves');
const NumberBlend = {
name: 'Number Blend',
docs: 'https://docs.noodl.net/nodes/interpolation/number-blend',
shortDesc: 'Computes a result output based on blending (linearly interpolating) between the inputs.',
category: 'Interpolation',
deprecated: true,
initialize: function () {
var internal = this._internal;
internal.inputs = [];
internal.blendValue = 0;
internal.result = 0;
internal.clamp = false;
},
getInspectInfo() {
return this._internal.result;
},
prototypeExtensions: {
updateResult: function () {
var inputs = this._internal.inputs;
if (inputs.length === 0) {
return 0;
}
var index = Math.floor(this._internal.blendValue),
t = this._internal.blendValue - index;
if (index >= inputs.length - 1) {
if (this._internal.clamp) {
index = inputs.length - 1;
t = 0;
} else {
t += index - (inputs.length - 1);
index = inputs.length - 1;
}
} else if (index <= 0) {
if (this._internal.clamp) {
index = 0;
t = 0;
} else {
t += index;
index = 0;
}
}
if (t === 0 || inputs.length === 1) {
this._internal.result = inputs[index];
} else if (index === inputs.length - 1 && t > 0) {
this._internal.result = EaseCurves.linear(inputs[index - 1], inputs[index], t + 1);
} else {
this._internal.result = EaseCurves.linear(inputs[index], inputs[index + 1], t);
}
this.flagOutputDirty('result');
}
},
numberedInputs: {
input: {
type: 'number',
displayPrefix: 'Number',
createSetter(index) {
return function (value) {
const inputs = this._internal.inputs;
if (inputs[index] === value) {
return;
}
inputs[index] = value || 0;
this.updateResult();
};
}
}
},
inputs: {
blendValue: {
type: 'number',
displayName: 'Blend Value',
default: 0,
set: function (value) {
this._internal.blendValue = value;
this.updateResult();
}
},
clamp: {
type: 'boolean',
displayName: 'Clamp',
default: false,
set: function (value) {
this._internal.clamp = value ? true : false;
this.updateResult();
}
}
},
outputs: {
result: {
type: 'number',
displayName: 'Result',
getter: function () {
return this._internal.result;
}
}
}
};
module.exports = {
node: NumberBlend
};

View File

@@ -0,0 +1,307 @@
'use strict';
const { Node } = require('@noodl/runtime');
const Model = require('@noodl/runtime/src/model');
const EventEmitter = require('events').EventEmitter;
const graphEventEmitter = new EventEmitter();
graphEventEmitter.setMaxListeners(1000000);
const ParentComponentState = {
name: 'Parent Component State',
displayNodeName: 'Parent Component Object',
category: 'Component Utilities',
color: 'component',
docs: 'https://docs.noodl.net/nodes/component-utilities/parent-component-object',
deprecated: true,
initialize() {
this._internal.inputValues = {};
this._internal.onModelChangedCallback = (args) => {
if (this.isInputConnected('fetch') === true) return;
if (this.hasOutput(args.name)) {
this.flagOutputDirty(args.name);
}
if (this.hasOutput('changed-' + args.name)) {
this.sendSignalOnOutput('changed-' + args.name);
}
this.sendSignalOnOutput('changed');
};
//TODO: don't listen for delta updates when running deployed
this.onComponentStateNodesChanged = () => {
const id = this.findParentComponentStateModelId();
if (this._internal.modelId !== id) {
this._internal.modelId = id;
if (this.isInputConnected('fetch') === false) {
this.setModelId(this._internal.modelId);
}
}
};
graphEventEmitter.on('componentStateNodesChanged', this.onComponentStateNodesChanged);
this.updateComponentState();
},
//to search up the tree the root nodes in this component must have been initialized
//we also need the connections to be setup so we can use isInputConnected
//nodeScopeDidInitialize takes care of that
nodeScopeDidInitialize() {
//FIXME: temporary hack. Our parent's node scope might not have finished created yet
//so just wait until after this update. It'll make the parent component state
//have a delay in propagating outputs which can cause subtle bugs.
//The fix is to call this code when the entire node tree has been created,
//before running updating the next update.
if (!this._internal.modelId) {
this.context.scheduleAfterUpdate(() => {
this.updateComponentState();
});
}
},
getInspectInfo() {
const model = this._internal.model;
if (!model) return 'No parent component state found';
const modelInfo = [{ type: 'text', value: this._internal.parentComponentName }];
const data = this._internal.model.data;
return modelInfo.concat(
Object.keys(data).map((key) => {
return { type: 'text', value: key + ': ' + data[key] };
})
);
},
inputs: {
properties: {
type: {
name: 'stringlist',
allowEditOnly: true
},
displayName: 'Properties',
group: 'Properties',
set(value) {}
},
store: {
displayName: 'Set',
group: 'Actions',
valueChangedToTrue() {
this.scheduleStore();
}
},
fetch: {
displayName: 'Fetch',
group: 'Actions',
valueChangedToTrue: function () {
this.setModelId(this._internal.modelId);
}
}
},
outputs: {
changed: {
type: 'signal',
displayName: 'Changed',
group: 'Events'
},
fetched: {
type: 'signal',
displayName: 'Fetched',
group: 'Events'
},
stored: {
type: 'signal',
displayName: 'Stored',
group: 'Events'
}
},
methods: {
updateComponentState() {
this._internal.modelId = this.findParentComponentStateModelId();
if (this.isInputConnected('fetch') === false) {
this.setModelId(this._internal.modelId);
}
},
findParentComponentStateModelId() {
function getParentComponent(component) {
let parent;
if (component.getRoots().length > 0) {
//visual
const root = component.getRoots()[0];
if (root.getVisualParentNode) {
//regular visual node
if (root.getVisualParentNode()) {
parent = root.getVisualParentNode().nodeScope.componentOwner;
}
} else if (root.parentNodeScope) {
//component instance node
parent = component.parentNodeScope.componentOwner;
}
} else if (component.parentNodeScope) {
parent = component.parentNodeScope.componentOwner;
}
//check that a parent exists and that the component is different
if (parent && parent.nodeScope && parent.nodeScope.componentOwner !== component) {
//check if parent has a Component State node
if (parent.nodeScope.getNodesWithType('Component State').length > 0) {
return parent;
}
//if not, continue searching up the tree
return getParentComponent(parent);
}
}
const parent = getParentComponent(this.nodeScope.componentOwner);
if (!parent) return;
this._internal.parentComponentName = parent.name;
return 'componentState' + parent.getInstanceId();
},
setModelId(id) {
this._internal.model && this._internal.model.off('change', this._internal.onModelChangedCallback);
this._internal.model = undefined;
if (!id) return;
const model = Model.get(id);
this._internal.model = model;
model.on('change', this._internal.onModelChangedCallback);
for (var key in model.data) {
if (this.hasOutput(key)) {
this.flagOutputDirty(key);
}
if (this.hasOutput('changed-' + key)) {
this.sendSignalOnOutput('changed-' + key);
}
}
this.sendSignalOnOutput('changed');
this.sendSignalOnOutput('fetched');
},
scheduleStore() {
if (this.hasScheduledStore) return;
this.hasScheduledStore = true;
var internal = this._internal;
this.scheduleAfterInputsHaveUpdated(() => {
this.hasScheduledStore = false;
if (!internal.model) return;
for (var i in internal.inputValues) {
internal.model.set(i, internal.inputValues[i], { resolve: true });
}
this.sendSignalOnOutput('stored');
});
},
_onNodeDeleted() {
Node.prototype._onNodeDeleted.call(this);
graphEventEmitter.off('componentStateNodesChanged', this.onComponentStateNodesChanged);
this._internal.model && this._internal.model.off('change', this._internal.onModelChangedCallback);
},
registerOutputIfNeeded(name) {
if (this.hasOutput(name)) {
return;
}
this.registerOutput(name, {
get() {
if (!this._internal.model) return undefined;
return this._internal.model.get(name, { resolve: true });
}
});
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
this.registerInput(name, {
set(value) {
this._internal.inputValues[name] = value;
if (this.isInputConnected('store') === false)
// Lazy set
this.scheduleStore();
}
});
}
}
};
function updatePorts(nodeId, parameters, editorConnection) {
const ports = [];
// Add value outputs
var properties = parameters.properties && parameters.properties.split(',');
for (var i in properties) {
var p = properties[i];
ports.push({
type: {
name: '*', //parameters['type-' + p] || 'string',
allowConnectionsOnly: true
},
plug: 'input/output',
group: 'Properties',
name: p,
displayName: p
});
ports.push({
type: 'signal',
plug: 'output',
group: 'Changed Events',
displayName: p + ' Changed',
name: 'changed-' + p
});
}
editorConnection.sendDynamicPorts(nodeId, ports, {
detectRenamed: {
plug: 'input/output'
}
});
}
module.exports = {
node: ParentComponentState,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
graphModel.on('nodeAdded.Parent Component State', (node) => {
updatePorts(node.id, node.parameters, context.editorConnection);
node.on('parameterUpdated', (event) => {
updatePorts(node.id, node.parameters, context.editorConnection);
});
});
//TODO: handle additional delta update event:
// - visual parent changed
//this are the same events that'll create and delete the Comopent State instance node
//it might not have had a chance to run yet if we're first in the event list, so
//use a setTimeout
graphModel.on('nodeAdded.Component State', (node) => {
setTimeout(() => {
graphEventEmitter.emit('componentStateNodesChanged');
}, 0);
});
graphModel.on('nodeRemoved.Component State', (node) => {
setTimeout(() => {
graphEventEmitter.emit('componentStateNodesChanged');
});
});
}
};

View File

@@ -0,0 +1,149 @@
'use strict';
const ScriptDownloadDefinition = {
name: 'Script Downloader',
docs: 'https://docs.noodl.net/nodes/javascript/script-downloader',
shortDesc: 'Script Downloader allows you load external Javascript libraries. ',
category: 'Javascript',
color: 'javascript',
deprecated: true,
initialize: function () {
var internal = this._internal;
internal.loaded = false;
internal.scripts = [];
internal.loadedScripts = {};
internal.startLoad = true;
},
inputs: {
startLoad: {
type: 'boolean',
default: true,
displayName: 'Load on start',
group: 'General',
set: function (value) {
this._internal.startLoad = value;
}
},
load: {
displayName: 'Load',
group: 'Actions',
valueChangedToTrue: function () {
this.scheduleUpdateScripts();
}
}
},
outputs: {
loaded: {
type: 'signal',
displayName: 'Loaded'
}
},
numberedInputs: {
input: {
displayPrefix: 'Script',
group: 'External scripts',
type: 'string',
index: 3008,
createSetter: function (index) {
return function (value) {
value = value.toString();
this._internal.scripts[index] = value;
if (!this._internal.loadStarted) {
this._internal.loadStarted = true;
this.scheduleAfterInputsHaveUpdated(function () {
this._internal.loadStarted = false;
if (!this._internal.startLoad) {
return;
}
this.updateScripts();
});
}
};
}
}
},
methods: {
removeLoadTerminator: function () {
var terminatorId = 'sentinel_' + this.id;
var elem = document.getElementById(terminatorId);
if (elem && elem.parentNode) {
elem.parentNode.removeChild(elem);
}
},
scheduleUpdateScripts: function () {
const _this = this;
if (!this._internal.updateScriptsScheduled) {
this._internal.updateScriptsScheduled = true;
this.scheduleAfterInputsHaveUpdated(function () {
_this._internal.updateScriptsScheduled = false;
_this.updateScripts();
});
}
},
updateScripts: function () {
var terminatorId = 'sentinel_' + this.id;
this.removeLoadTerminator();
var scripts = this._internal.scripts;
scripts = scripts.filter(function (script) {
return script !== '';
});
var scriptElements = document.head.getElementsByTagName('script');
var scriptsInHead = {};
for (var i = 0; i < scriptElements.length; i++) {
var script = scriptElements[i];
if (script.src !== undefined && script.src !== '') {
scriptsInHead[script.src] = script;
}
}
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i].trim();
if (!this._internal.loadedScripts.hasOwnProperty(script)) {
if (scriptsInHead.hasOwnProperty(script)) {
continue;
}
var scriptObj = document.createElement('script');
var _this = this;
scriptObj.src = script;
scriptObj.async = false;
document.head.appendChild(scriptObj);
}
}
var self = this;
var onLoadedScript = document.createElement('script');
onLoadedScript.onload = onLoadedScript.onreadystatechange = function (script) {
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
self._internal.loaded = true;
self.sendSignalOnOutput('loaded');
self.removeLoadTerminator();
}
};
// Since all scripts are downloaded synchronously, this will be loaded last and
// signals that the others are done loading.
onLoadedScript.id = terminatorId;
onLoadedScript.src = 'load_terminator.js';
onLoadedScript.async = false;
document.head.appendChild(onLoadedScript);
}
}
};
module.exports = {
node: ScriptDownloadDefinition,
setup: function (context, graphModel) {
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
return;
}
}
};

View File

@@ -0,0 +1,66 @@
'use strict';
const { EdgeTriggeredInput } = require('@noodl/runtime');
const SignalToIndexNode = {
name: 'Signal To Index',
docs: 'https://docs.noodl.net/nodes/logic/signal-to-index',
shortDesc: 'Maps signal inputs to their index value.',
category: 'Logic',
deprecated: true,
initialize: function () {
this._internal.currentIndex = 0;
},
getInspectInfo() {
return 'Index: ' + this._internal.currentIndex;
},
numberedInputs: {
input: {
type: 'boolean',
displayPrefix: 'Signal',
createSetter: function (index) {
return EdgeTriggeredInput.createSetter({
valueChangedToTrue: this.onValueChangedToTrue.bind(this, index)
});
},
selectors: {
startIndex: {
displayName: 'Start Index',
set: function (index) {
this._internal.currentIndex = index;
this.flagOutputDirty('index');
}
}
}
}
},
outputs: {
index: {
displayName: 'Index',
type: 'number',
getter: function () {
return this._internal.currentIndex;
}
},
signalTriggered: {
displayName: 'Signal Triggered',
type: 'signal'
}
},
prototypeExtensions: {
onValueChangedToTrue: function (index) {
this.sendSignalOnOutput('signalTriggered');
if (this._internal.currentIndex === index) {
return;
}
this._internal.currentIndex = index;
this.flagOutputDirty('index');
}
}
};
module.exports = {
node: SignalToIndexNode
};

View File

@@ -0,0 +1,68 @@
'use strict';
const StringSelectorNode = {
name: 'String Selector',
displayNodeName: 'Index To String',
shortDesc: 'Choose between multiple strings.',
category: 'Utilities',
deprecated: true,
initialize: function () {
this._internal.inputs = [];
this._internal.currentSelectedIndex = 0;
this._internal.indexChanged = false;
},
getInspectInfo() {
return this._internal.inputs[this._internal.currentSelectedIndex];
},
numberedInputs: {
input: {
type: 'string',
displayPrefix: 'String for ',
group: 'Inputs',
createSetter: function (index) {
return function (value) {
value = value ? value.toString() : '';
this._internal.inputs[index] = value;
if (this._internal.currentSelectedIndex === index) {
this.flagOutputDirty('currentValue');
}
};
}
}
},
inputs: {
index: {
type: {
name: 'number'
},
displayName: 'Index',
default: 0,
set: function (value) {
value = value | 0;
this._internal.currentSelectedIndex = value;
this.flagOutputDirty('currentValue');
this.sendSignalOnOutput('indexChanged');
}
}
},
outputs: {
currentValue: {
type: 'string',
displayName: 'Current Value',
group: 'Value',
getter: function () {
return this._internal.inputs[this._internal.currentSelectedIndex];
}
},
indexChanged: {
type: 'signal',
displayName: 'Index Changed',
group: 'Signals'
}
}
};
module.exports = {
node: StringSelectorNode
};

View File

@@ -0,0 +1,171 @@
'use strict';
var EaseCurves = require('../../easecurves');
var defaultDuration = 300;
var TransitionNode = {
name: 'Transition',
docs: 'https://docs.noodl.net/nodes/animation/transition',
shortDesc: 'This node can interpolate smooothely for the current value to a target value.',
category: 'Animation',
deprecated: true,
initialize: function () {
var self = this,
_internal = this._internal;
_internal.currentNumber = 0;
_internal.numberInitialized = false;
_internal.animationStarted = false;
_internal.setCurrentNumberEnabled = false;
_internal.overrideValue = 0;
_internal._animation = this.context.timerScheduler.createTimer({
duration: defaultDuration,
startValue: 0,
endValue: 0,
ease: EaseCurves.easeOut,
onStart: function () {
_internal.animationStarted = true;
},
onRunning: function (t) {
_internal.currentNumber = this.ease(this.startValue, this.endValue, t);
self.flagOutputDirty('currentValue');
},
onFinish: function () {
self.sendSignalOnOutput('atTargetValue');
}
});
},
inputs: {
targetValue: {
type: {
name: 'number'
},
displayName: 'Target Value',
group: 'Target Value',
default: undefined, //default is undefined so transition initializes to the first input value
set: function (value) {
if (value === true) {
value = 1;
} else if (value === false) {
value = 0;
}
value = Number(value);
if (isNaN(value)) {
//bail out on NaN values
return;
}
var internal = this._internal;
if (internal.numberInitialized === false) {
internal.currentNumber = value;
internal.numberInitialized = true;
internal._animation.endValue = value;
this.flagOutputDirty('currentValue');
return;
} else if (value === internal._animation.endValue) {
//same as previous value
return;
}
internal._animation.startValue = internal.currentNumber;
internal._animation.endValue = value;
internal._animation.start();
}
},
'overrideCurrentValue.value': {
type: {
name: 'number'
},
group: 'Override Value',
displayName: 'Override Value',
editorName: 'Value|Override Value',
set: function (value) {
this._internal.overrideValue = value;
}
},
'overrideCurrentValue.do': {
group: 'Override Value',
displayName: 'Do',
editorName: 'Do|Override Value',
valueChangedToTrue: function () {
setCurrentNumber.call(this, this._internal.overrideValue);
}
},
duration: {
type: 'number',
group: 'Parameters',
displayName: 'Duration',
default: defaultDuration,
set: function (value) {
this._internal._animation.duration = value;
}
},
delay: {
type: 'number',
group: 'Parameters',
displayName: 'Delay',
default: 0,
set: function (value) {
this._internal._animation.delay = value;
}
},
easingCurve: {
type: {
name: 'enum',
enums: [
{ value: 'easeOut', label: 'Ease Out' },
{ value: 'easeIn', label: 'Ease In' },
{ value: 'linear', label: 'Linear' },
{ value: 'easeInOut', label: 'Ease In Out' }
]
},
default: 'easeOut',
displayName: 'Easing Curve',
group: 'Parameters',
set: function (value) {
this._internal._animation.ease = EaseCurves[value];
}
}
},
outputs: {
currentValue: {
type: 'number',
displayName: 'Current Value',
group: 'Current State',
getter: function () {
return this._internal.currentNumber;
}
},
atTargetValue: {
type: 'signal',
displayName: 'At Target Value',
group: 'Signals'
}
}
};
function setCurrentNumber(value) {
/* jshint validthis:true */
var animation = this._internal._animation;
animation.stop();
animation.startValue = value;
//wait until all values are set until checking if animation needs to be started
this.scheduleAfterInputsHaveUpdated(function () {
if (animation.endValue !== value) {
animation.start();
}
});
this._internal.currentNumber = value;
this.flagOutputDirty('currentValue');
}
module.exports = {
node: TransitionNode
};