mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-09 02:23:30 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user