mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-12 23:32:55 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
86
packages/noodl-viewer-react/src/nodes/controls/button.ts
Normal file
86
packages/noodl-viewer-react/src/nodes/controls/button.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Button } from '../../components/controls/Button';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
const ButtonNode = {
|
||||
name: 'net.noodl.controls.button',
|
||||
displayName: 'Button',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/button',
|
||||
allowChildren: true,
|
||||
noodlNodeAsProp: true,
|
||||
usePortAsLabel: 'label',
|
||||
portLabelTruncationMode: 'length',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'label'
|
||||
},
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
initialize() {
|
||||
this.props.layout = 'row'; //Used to tell child nodes what layout to expect
|
||||
},
|
||||
getReactComponent() {
|
||||
return Button;
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
index: 100,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
default: '#000000',
|
||||
allowVisualStates: true
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
onClick: {
|
||||
displayName: 'Click',
|
||||
group: 'Events',
|
||||
type: 'signal'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(ButtonNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Content'
|
||||
});
|
||||
NodeSharedPortDefinitions.addTextStyleInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(ButtonNode, {
|
||||
defaults: {
|
||||
paddingTop: 5,
|
||||
paddingRight: 20,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 20
|
||||
}
|
||||
});
|
||||
NodeSharedPortDefinitions.addMarginInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addLabelInputs(ButtonNode, {
|
||||
defaults: { useLabel: true }
|
||||
});
|
||||
NodeSharedPortDefinitions.addIconInputs(ButtonNode, {
|
||||
enableIconPlacement: true,
|
||||
defaults: { useIcon: false }
|
||||
});
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(ButtonNode);
|
||||
NodeSharedPortDefinitions.addShadowInputs(ButtonNode);
|
||||
|
||||
Utils.addControlEventsAndStates(ButtonNode);
|
||||
|
||||
export default createNodeFromReactComponent(ButtonNode);
|
||||
178
packages/noodl-viewer-react/src/nodes/controls/checkbox.ts
Normal file
178
packages/noodl-viewer-react/src/nodes/controls/checkbox.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Checkbox } from '../../components/controls/Checkbox';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
const CheckBoxNode = {
|
||||
name: 'net.noodl.controls.checkbox',
|
||||
displayName: 'Checkbox',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/checkbox',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'label'
|
||||
},
|
||||
usePortAsLabel: 'label',
|
||||
portLabelTruncationMode: 'length',
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
initialize() {
|
||||
this.props.sizeMode = 'explicit';
|
||||
this.props.id = 'input-' + guid();
|
||||
this.props.checked = this._internal.checked = false;
|
||||
|
||||
this.props.checkedChanged = (checked) => {
|
||||
const changed = this._internal.checked !== checked;
|
||||
this._internal.checked = checked;
|
||||
if (changed) {
|
||||
this.flagOutputDirty('checked');
|
||||
this.sendSignalOnOutput('onChange');
|
||||
this._updateVisualState();
|
||||
}
|
||||
};
|
||||
},
|
||||
getReactComponent() {
|
||||
return Checkbox;
|
||||
},
|
||||
inputs: {
|
||||
checked: {
|
||||
type: 'boolean',
|
||||
displayName: 'Checked',
|
||||
group: 'General',
|
||||
default: false,
|
||||
index: 100,
|
||||
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');
|
||||
this._updateVisualState();
|
||||
}
|
||||
}
|
||||
},
|
||||
check: {
|
||||
type: 'signal',
|
||||
displayName: 'Check',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
if (this._internal.checked === true) return;
|
||||
|
||||
this.props.checked = this._internal.checked = true;
|
||||
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('checked');
|
||||
this._updateVisualState();
|
||||
}
|
||||
},
|
||||
uncheck: {
|
||||
type: 'signal',
|
||||
displayName: 'Uncheck',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
if (this._internal.checked === false) return;
|
||||
|
||||
this.props.checked = this._internal.checked = false;
|
||||
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('checked');
|
||||
this._updateVisualState();
|
||||
}
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
index: 201,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
default: 'transparent',
|
||||
applyDefault: false,
|
||||
allowVisualStates: true,
|
||||
styleTag: 'checkbox'
|
||||
},
|
||||
width: {
|
||||
index: 11,
|
||||
group: 'Dimensions',
|
||||
displayName: 'Width',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vw', 'vh'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 32,
|
||||
allowVisualStates: true,
|
||||
styleTag: 'checkbox'
|
||||
},
|
||||
height: {
|
||||
index: 12,
|
||||
group: 'Dimensions',
|
||||
displayName: 'Height',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vw', 'vh'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 32,
|
||||
allowVisualStates: true,
|
||||
styleTag: 'checkbox'
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
checked: {
|
||||
type: 'boolean',
|
||||
displayName: 'Checked',
|
||||
group: 'States',
|
||||
getter: function () {
|
||||
return this._internal.checked;
|
||||
}
|
||||
},
|
||||
onChange: {
|
||||
displayName: 'Changed',
|
||||
group: 'Events',
|
||||
type: 'signal'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addAlignInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addIconInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addLabelInputs(CheckBoxNode, {
|
||||
enableSpacing: true,
|
||||
styleTag: 'label'
|
||||
});
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(CheckBoxNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(CheckBoxNode, {
|
||||
defaults: {
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderColor: '#000000',
|
||||
borderRadius: 3
|
||||
},
|
||||
styleTag: 'checkbox'
|
||||
});
|
||||
NodeSharedPortDefinitions.addShadowInputs(CheckBoxNode, {
|
||||
styleTag: 'checkbox'
|
||||
});
|
||||
Utils.addControlEventsAndStates(CheckBoxNode, { checked: true });
|
||||
|
||||
export default createNodeFromReactComponent(CheckBoxNode);
|
||||
171
packages/noodl-viewer-react/src/nodes/controls/options.ts
Normal file
171
packages/noodl-viewer-react/src/nodes/controls/options.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Select } from '../../components/controls/Select';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
const OptionsNode = {
|
||||
name: 'net.noodl.controls.options',
|
||||
displayName: 'Dropdown',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/dropdown',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
usePortAsLabel: 'label',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'label'
|
||||
},
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Text Style',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
initialize: function () {
|
||||
this._itemsChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
this.props.id = 'input-' + guid();
|
||||
|
||||
this.props.valueChanged = (value) => {
|
||||
const changed = this._internal.value !== value;
|
||||
this._internal.value = value;
|
||||
if (changed) {
|
||||
this.flagOutputDirty('value');
|
||||
this.sendSignalOnOutput('onChange');
|
||||
}
|
||||
};
|
||||
},
|
||||
getReactComponent() {
|
||||
return Select;
|
||||
},
|
||||
inputs: {
|
||||
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;
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value !== undefined && typeof value !== 'string') {
|
||||
if (value?.toString !== undefined) value = value.toString();
|
||||
else return;
|
||||
}
|
||||
|
||||
// // if value is passed in before the items, then items is undefined
|
||||
// if (this._internal.items) {
|
||||
// //make sure this is a valid value that exists in the dropdown. If it doesn't, deselect all options
|
||||
// value = this._internal.items.find((i) => i.Value === value) ? value : undefined;
|
||||
// }
|
||||
|
||||
const changed = value !== this._internal.value;
|
||||
this.props.value = this._internal.value = value;
|
||||
|
||||
if (changed) {
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('value');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
placeholder: {
|
||||
displayName: 'Placeholder',
|
||||
type: 'string',
|
||||
group: 'Placeholder'
|
||||
},
|
||||
placeholderOpacity: {
|
||||
group: 'Placeholder',
|
||||
displayName: 'Placeholder opacity',
|
||||
type: 'number',
|
||||
default: 0.5
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
value: {
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'States',
|
||||
getter: function () {
|
||||
return this._internal.value;
|
||||
}
|
||||
},
|
||||
onChange: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
index: 100,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
default: 'transparent',
|
||||
styleTag: 'inputWrapper',
|
||||
allowVisualStates: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(OptionsNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Content'
|
||||
});
|
||||
NodeSharedPortDefinitions.addAlignInputs(OptionsNode);
|
||||
NodeSharedPortDefinitions.addTextStyleInputs(OptionsNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(OptionsNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(OptionsNode, {
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
NodeSharedPortDefinitions.addMarginInputs(OptionsNode);
|
||||
NodeSharedPortDefinitions.addIconInputs(OptionsNode, {
|
||||
enableIconPlacement: true,
|
||||
defaults: { useIcon: false, iconColor: '#000000' }
|
||||
});
|
||||
NodeSharedPortDefinitions.addLabelInputs(OptionsNode, {
|
||||
enableSpacing: true,
|
||||
styleTag: 'label'
|
||||
});
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(OptionsNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(OptionsNode, {
|
||||
defaults: {
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderColor: '#000000',
|
||||
borderRadius: 5
|
||||
},
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
|
||||
NodeSharedPortDefinitions.addShadowInputs(OptionsNode, {
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
Utils.addControlEventsAndStates(OptionsNode);
|
||||
|
||||
export default createNodeFromReactComponent(OptionsNode);
|
||||
164
packages/noodl-viewer-react/src/nodes/controls/radiobutton.ts
Normal file
164
packages/noodl-viewer-react/src/nodes/controls/radiobutton.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { RadioButton } from '../../components/controls/RadioButton';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
const RadioButtonNode = {
|
||||
name: 'net.noodl.controls.radiobutton',
|
||||
displayName: 'Radio Button',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/radio-button',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
usePortAsLabel: 'label',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'label'
|
||||
},
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Fill Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
initialize() {
|
||||
this.props.sizeMode = 'explicit';
|
||||
this.props.id = 'input-' + guid();
|
||||
|
||||
this._internal.checked = false;
|
||||
|
||||
this.props.checkedChanged = (checked) => {
|
||||
const changed = this._internal.checked !== checked;
|
||||
this._internal.checked = checked;
|
||||
if (changed) {
|
||||
this.flagOutputDirty('checked');
|
||||
this._updateVisualState();
|
||||
}
|
||||
};
|
||||
|
||||
this.props.styles.fill = {};
|
||||
},
|
||||
getReactComponent() {
|
||||
return RadioButton;
|
||||
},
|
||||
inputs: {
|
||||
fillColor: {
|
||||
index: 19,
|
||||
displayName: 'Fill Color',
|
||||
group: 'Fill Style',
|
||||
type: 'color',
|
||||
allowVisualStates: true,
|
||||
styleTag: 'fill',
|
||||
set(value) {
|
||||
this.setStyle({ backgroundColor: value }, 'fill');
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
value: {
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
index: 100
|
||||
},
|
||||
fillSpacing: {
|
||||
displayName: 'Fill Spacing',
|
||||
group: 'Fill Style',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vw', 'vh'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
allowVisualStates: true,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
width: {
|
||||
index: 11,
|
||||
group: 'Dimensions',
|
||||
displayName: 'Width',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', '%', 'vw', 'vh'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 32,
|
||||
allowVisualStates: true,
|
||||
styleTag: 'radio',
|
||||
onChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
height: {
|
||||
index: 12,
|
||||
group: 'Dimensions',
|
||||
displayName: 'Height',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', '%'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 32,
|
||||
allowVisualStates: true,
|
||||
styleTag: 'radio',
|
||||
onChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
index: 201,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
allowVisualStates: true,
|
||||
styleTag: 'radio',
|
||||
default: 'transparent',
|
||||
applyDefault: false
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
checked: {
|
||||
type: 'boolean',
|
||||
displayName: 'Checked',
|
||||
group: 'States',
|
||||
get() {
|
||||
return this._internal.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addAlignInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addIconInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addLabelInputs(RadioButtonNode, {
|
||||
enableSpacing: true,
|
||||
styleTag: 'label'
|
||||
});
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(RadioButtonNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(RadioButtonNode, {
|
||||
defaults: {
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderRadius: 16
|
||||
},
|
||||
styleTag: 'radio'
|
||||
});
|
||||
NodeSharedPortDefinitions.addShadowInputs(RadioButtonNode, {
|
||||
styleTag: 'radio'
|
||||
});
|
||||
Utils.addControlEventsAndStates(RadioButtonNode, { checked: true });
|
||||
|
||||
export default createNodeFromReactComponent(RadioButtonNode);
|
||||
@@ -0,0 +1,125 @@
|
||||
import { RadioButtonGroup } from '../../components/controls/RadioButtonGroup';
|
||||
import { flexDirectionValues } from '../../constants/flex';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
let RadioButtonGroupNode = {
|
||||
name: 'Radio Button Group',
|
||||
displayName: 'Radio Button Group',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/radio-button-group',
|
||||
allowChildren: true,
|
||||
noodlNodeAsProp: true,
|
||||
useVariants: false,
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Style', 'Actions', 'Events', 'Mounted', 'States']
|
||||
},
|
||||
initialize() {
|
||||
this.props.name = 'radio-' + guid();
|
||||
|
||||
this.props.valueChanged = (value) => {
|
||||
const changed = this._internal.value !== value;
|
||||
this._internal.value = value;
|
||||
this.props.value = value;
|
||||
if (changed) {
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('value');
|
||||
this.sendSignalOnOutput('onChange');
|
||||
}
|
||||
};
|
||||
},
|
||||
getReactComponent() {
|
||||
return RadioButtonGroup;
|
||||
},
|
||||
defaultCss: {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
value: {
|
||||
index: 20,
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (typeof value !== 'string' && value.toString !== undefined) value = value.toString();
|
||||
if (typeof value !== 'string') return;
|
||||
|
||||
const changed = value !== this._internal.value;
|
||||
this.props.value = this._internal.value = value;
|
||||
|
||||
if (changed) {
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('value');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
value: {
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'States',
|
||||
getter: function () {
|
||||
return this._internal.value;
|
||||
}
|
||||
},
|
||||
onChange: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputProps: {},
|
||||
outputProps: {}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(RadioButtonGroupNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Content'
|
||||
});
|
||||
NodeSharedPortDefinitions.addAlignInputs(RadioButtonGroupNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(RadioButtonGroupNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(RadioButtonGroupNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(RadioButtonGroupNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(RadioButtonGroupNode);
|
||||
|
||||
RadioButtonGroupNode = createNodeFromReactComponent(RadioButtonGroupNode);
|
||||
export default RadioButtonGroupNode;
|
||||
498
packages/noodl-viewer-react/src/nodes/controls/slider.ts
Normal file
498
packages/noodl-viewer-react/src/nodes/controls/slider.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import { Slider } from '../../components/controls/Slider';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
const thumbPopout = { group: 'thumb-styles', label: 'Thumb Styles' };
|
||||
const trackPopout = { group: 'track-styles', label: 'Track Styles' };
|
||||
|
||||
const RangeNode = {
|
||||
name: 'net.noodl.controls.range',
|
||||
displayNodeName: 'Slider',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/slider',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
initialize() {
|
||||
this.props.sizeMode = 'contentHeight';
|
||||
this.props.id = 'input-' + guid();
|
||||
|
||||
this.props.value = this.props.min;
|
||||
this._internal.outputValue = 0;
|
||||
|
||||
this.props._nodeId = this.id;
|
||||
|
||||
//this is used by the range to communicate with the node whenever the range changes value
|
||||
this.props.updateOutputValue = (value: string | number) => {
|
||||
value = typeof value === 'string' ? parseFloat(value) : value;
|
||||
|
||||
const valueChanged = this._internal.outputValue !== value;
|
||||
if (valueChanged) {
|
||||
this._internal.outputValue = value;
|
||||
this.flagOutputDirty('value');
|
||||
this._updateOutputValuePercent(value);
|
||||
this.sendSignalOnOutput('onChange');
|
||||
}
|
||||
|
||||
// On first mount, output the initial percentage.
|
||||
if (!this._internal.valuePercent) {
|
||||
this._updateOutputValuePercent(value);
|
||||
}
|
||||
};
|
||||
|
||||
this.props.updateOutputValue(this.props.value); // Set the value output to an initial value
|
||||
},
|
||||
getReactComponent() {
|
||||
return Slider;
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: 'string',
|
||||
displayName: 'Value',
|
||||
group: 'General',
|
||||
index: 100,
|
||||
set(value) {
|
||||
this._setInputValue(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
value: {
|
||||
type: 'number',
|
||||
displayName: 'Value',
|
||||
group: 'States',
|
||||
get() {
|
||||
return this._internal.outputValue;
|
||||
}
|
||||
},
|
||||
valuePercent: {
|
||||
type: 'number',
|
||||
displayName: 'Value Percent',
|
||||
group: 'States',
|
||||
get() {
|
||||
return this._internal.valuePercent;
|
||||
}
|
||||
},
|
||||
onChange: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
min: {
|
||||
type: 'number',
|
||||
displayName: 'Min',
|
||||
group: 'General',
|
||||
default: 0,
|
||||
index: 100,
|
||||
onChange() {
|
||||
this._setInputValue(this.props.value);
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: 'number',
|
||||
displayName: 'Max',
|
||||
group: 'General',
|
||||
default: 100,
|
||||
index: 100,
|
||||
onChange() {
|
||||
this._setInputValue(this.props.value);
|
||||
}
|
||||
},
|
||||
step: {
|
||||
type: 'number',
|
||||
displayName: 'Step',
|
||||
group: 'General',
|
||||
default: 1,
|
||||
index: 100
|
||||
},
|
||||
width: {
|
||||
index: 11,
|
||||
group: 'Dimensions',
|
||||
displayName: 'Width',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['%', 'px', 'vw'],
|
||||
defaultUnit: '%'
|
||||
},
|
||||
default: 100,
|
||||
allowVisualStates: true
|
||||
},
|
||||
// Styles
|
||||
thumbWidth: {
|
||||
group: 'Thumb Style',
|
||||
displayName: 'Width',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vw', '%'],
|
||||
defaultUnit: 'px',
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 16,
|
||||
popout: thumbPopout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
thumbHeight: {
|
||||
group: 'Thumb Style',
|
||||
displayName: 'Height',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vh', '%'],
|
||||
defaultUnit: 'px',
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 16,
|
||||
popout: thumbPopout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
thumbColor: {
|
||||
group: 'Thumb Style',
|
||||
displayName: 'Color',
|
||||
type: { name: 'color', allowEditOnly: true },
|
||||
default: '#000000',
|
||||
popout: thumbPopout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
trackHeight: {
|
||||
group: 'Track Style',
|
||||
displayName: 'Height',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', 'vh', '%'],
|
||||
defaultUnit: 'px',
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 6,
|
||||
popout: trackPopout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
trackColor: {
|
||||
group: 'Track Style',
|
||||
displayName: 'Inactive Color',
|
||||
type: { name: 'color', allowEditOnly: true },
|
||||
default: '#f0f0f0',
|
||||
popout: trackPopout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
trackActiveColor: {
|
||||
group: 'Track Style',
|
||||
displayName: 'Active Color',
|
||||
type: { name: 'color', allowEditOnly: true },
|
||||
default: '#f0f0f0',
|
||||
popout: trackPopout,
|
||||
allowVisualStates: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_updateOutputValuePercent(value: number) {
|
||||
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');
|
||||
},
|
||||
_setInputValue(newValue) {
|
||||
//make sure value never goes out of range
|
||||
const value = Math.max(this.props.min, Math.min(this.props.max, newValue || 0));
|
||||
|
||||
const changed = value !== this.props.value;
|
||||
|
||||
if (changed) {
|
||||
this.props.value = value;
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Add borders
|
||||
function addBorderInputs(definition, opts) {
|
||||
opts = opts || {};
|
||||
const defaults = opts.defaults || {};
|
||||
const popout = opts.popout;
|
||||
|
||||
defaults.borderStyle = 'none';
|
||||
defaults.borderWidth = 0;
|
||||
defaults.borderColor = '#000000';
|
||||
|
||||
const prefixLabel = opts.propPrefix[0].toUpperCase() + opts.propPrefix.slice(1);
|
||||
|
||||
function defineBorderTab(definition, suffix, tabName, indexOffset) {
|
||||
const styleName = opts.propPrefix + `Border${suffix}Style`;
|
||||
const widthName = opts.propPrefix + `Border${suffix}Width`;
|
||||
const colorName = opts.propPrefix + `Border${suffix}Color`;
|
||||
|
||||
let orNotSet = defaults.borderStyle !== 'none' ? 'OR borderStyle NOT SET' : '';
|
||||
|
||||
if (suffix) {
|
||||
if (defaults[styleName] && defaults[styleName] !== 'none') orNotSet += `OR ${styleName} NOT SET`;
|
||||
NodeSharedPortDefinitions.addDynamicInputPorts(
|
||||
definition,
|
||||
`${styleName} = solid OR ${styleName} = dashed OR ${styleName} = dotted OR borderStyle = solid OR borderStyle = dashed OR borderStyle = dotted ${orNotSet}`,
|
||||
[`${widthName}`, `${colorName}`]
|
||||
);
|
||||
} else {
|
||||
NodeSharedPortDefinitions.addDynamicInputPorts(
|
||||
definition,
|
||||
`${styleName} = solid OR ${styleName} = dashed OR ${styleName} = dotted ${orNotSet}`,
|
||||
[`${widthName}`, `${colorName}`]
|
||||
);
|
||||
}
|
||||
|
||||
const tab = {
|
||||
group: opts.propPrefix + '-border-styles',
|
||||
tab: tabName,
|
||||
label: suffix
|
||||
};
|
||||
|
||||
const editorName = (name) => `${prefixLabel} ${name} ${suffix ? '(' + suffix + ')' : ''}`;
|
||||
const index = 202 + indexOffset * 4;
|
||||
|
||||
const groupName = prefixLabel + ' Border Style';
|
||||
|
||||
NodeSharedPortDefinitions.addInputProps(definition, {
|
||||
[styleName]: {
|
||||
index: index + 1,
|
||||
displayName: 'Border Style',
|
||||
editorName: editorName('Border Style'),
|
||||
group: groupName,
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: 'Solid', value: 'solid' },
|
||||
{ label: 'Dotted', value: 'dotted' },
|
||||
{ label: 'Dashed', value: 'dashed' }
|
||||
]
|
||||
},
|
||||
default: defaults[`border${suffix}Style`],
|
||||
tab,
|
||||
popout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
[widthName]: {
|
||||
index: index + 2,
|
||||
displayName: 'Border Width',
|
||||
editorName: editorName('Border Width'),
|
||||
group: groupName,
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: defaults[`border${suffix}Width`],
|
||||
tab,
|
||||
popout,
|
||||
allowVisualStates: true
|
||||
},
|
||||
[colorName]: {
|
||||
index: index + 3,
|
||||
displayName: 'Border Color',
|
||||
editorName: editorName('Border Color'),
|
||||
group: groupName,
|
||||
type: 'color',
|
||||
default: defaults[`border${suffix}Color`],
|
||||
tab,
|
||||
popout,
|
||||
allowVisualStates: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defineBorderTab(definition, '', 'borders-all', 0);
|
||||
defineBorderTab(definition, 'Left', 'borders-left', 1);
|
||||
defineBorderTab(definition, 'Top', 'borders-top', 2);
|
||||
defineBorderTab(definition, 'Right', 'borders-right', 3);
|
||||
defineBorderTab(definition, 'Bottom', 'borders-bottom', 4);
|
||||
}
|
||||
|
||||
function addBorderRadius(definition, opts) {
|
||||
opts = opts || {};
|
||||
const defaults = opts.defaults || {};
|
||||
const popout = opts.popout;
|
||||
|
||||
if (!defaults.borderRadius) defaults.borderRadius = 0;
|
||||
|
||||
const prefixLabel = opts.propPrefix[0].toUpperCase() + opts.propPrefix.slice(1);
|
||||
|
||||
function defineCornerTab(definition, suffix, tabName, indexOffset) {
|
||||
const editorName = (name) => `${prefixLabel} ${name} ${suffix ? '(' + suffix + ')' : ''}`;
|
||||
|
||||
const tab = {
|
||||
group: opts.propPrefix + '-corners',
|
||||
tab: tabName,
|
||||
label: suffix
|
||||
};
|
||||
const radiusName = `Border${suffix}Radius`;
|
||||
|
||||
NodeSharedPortDefinitions.addInputProps(definition, {
|
||||
[opts.propPrefix + radiusName]: {
|
||||
index: 240 + indexOffset,
|
||||
displayName: 'Corner Radius',
|
||||
editorName: editorName('Corner Radius'),
|
||||
group: prefixLabel + ' Corner Radius',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', '%'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: defaults[`border${suffix}Radius`],
|
||||
tab,
|
||||
popout
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defineCornerTab(definition, '', 'corners-all', 0);
|
||||
defineCornerTab(definition, 'TopLeft', 'corners-top-left', 1);
|
||||
defineCornerTab(definition, 'TopRight', 'corners-top-right', 2);
|
||||
defineCornerTab(definition, 'BottomRight', 'corners-bottom-right', 3);
|
||||
defineCornerTab(definition, 'BottomLeft', 'corners-bottom-left', 4);
|
||||
}
|
||||
|
||||
function addShadowInputs(definition, opts) {
|
||||
opts = opts || {};
|
||||
const popout = opts.popout;
|
||||
const prefix = opts.propPrefix;
|
||||
|
||||
NodeSharedPortDefinitions.addDynamicInputPorts(definition, `${prefix}BoxShadowEnabled = true`, [
|
||||
`${prefix}BoxShadowOffsetX`,
|
||||
`${prefix}BoxShadowOffsetY`,
|
||||
`${prefix}BoxShadowInset`,
|
||||
`${prefix}BoxShadowBlurRadius`,
|
||||
`${prefix}BoxShadowSpreadRadius`,
|
||||
`${prefix}BoxShadowColor`
|
||||
]);
|
||||
|
||||
const prefixLabel = opts.propPrefix[0].toUpperCase() + opts.propPrefix.slice(1);
|
||||
const editorName = (name) => `${prefixLabel} ${name}`;
|
||||
|
||||
NodeSharedPortDefinitions.addInputProps(definition, {
|
||||
[`${prefix}BoxShadowEnabled`]: {
|
||||
index: 250,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Shadow Enabled',
|
||||
editorName: editorName('Shadow Enabled'),
|
||||
type: 'boolean',
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowOffsetX`]: {
|
||||
index: 251,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Offset X',
|
||||
editorName: editorName('Offset X'),
|
||||
default: 0,
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowOffsetY`]: {
|
||||
index: 252,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Offset Y',
|
||||
editorName: editorName('Offset Y'),
|
||||
default: 0,
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowBlurRadius`]: {
|
||||
index: 253,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Blur Radius',
|
||||
editorName: editorName('Blur Radius'),
|
||||
default: 5,
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowSpreadRadius`]: {
|
||||
index: 254,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Spread Radius',
|
||||
editorName: editorName('Spread Radius'),
|
||||
default: 2,
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowInset`]: {
|
||||
index: 255,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Inset',
|
||||
editorName: editorName('Inset'),
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
},
|
||||
[`${prefix}BoxShadowColor`]: {
|
||||
index: 256,
|
||||
group: opts.group || 'Box Shadow',
|
||||
displayName: 'Shadow Color',
|
||||
editorName: editorName('Shadow Color'),
|
||||
type: 'color',
|
||||
default: '#00000033',
|
||||
allowVisualStates: true,
|
||||
popout
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NodeSharedPortDefinitions.addAlignInputs(RangeNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(RangeNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(RangeNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(RangeNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(RangeNode);
|
||||
addBorderInputs(RangeNode, { propPrefix: 'track', popout: trackPopout });
|
||||
addBorderRadius(RangeNode, { propPrefix: 'track', popout: trackPopout });
|
||||
addShadowInputs(RangeNode, {
|
||||
propPrefix: 'track',
|
||||
popout: trackPopout,
|
||||
group: 'Track Box Shadow'
|
||||
});
|
||||
|
||||
addBorderInputs(RangeNode, { propPrefix: 'thumb', popout: thumbPopout });
|
||||
addBorderRadius(RangeNode, { propPrefix: 'thumb', popout: thumbPopout });
|
||||
addShadowInputs(RangeNode, {
|
||||
propPrefix: 'thumb',
|
||||
popout: thumbPopout,
|
||||
group: 'Thumb Box Shadow'
|
||||
});
|
||||
|
||||
Utils.addControlEventsAndStates(RangeNode);
|
||||
|
||||
export default createNodeFromReactComponent(RangeNode);
|
||||
281
packages/noodl-viewer-react/src/nodes/controls/text-input.ts
Normal file
281
packages/noodl-viewer-react/src/nodes/controls/text-input.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { TextInput } from '../../components/controls/TextInput';
|
||||
import guid from '../../guid';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import Utils from './utils';
|
||||
|
||||
function _styleTemplate(className, props) {
|
||||
return `
|
||||
.${className}::placeholder {
|
||||
opacity: ${props.placeholderOpacity};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const TextInputNode = {
|
||||
name: 'net.noodl.controls.textinput',
|
||||
displayName: 'Text Input',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/text-input',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
usePortAsLabel: 'label',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'label'
|
||||
},
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Text',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Text Style',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
getReactComponent() {
|
||||
return TextInput;
|
||||
},
|
||||
initialize() {
|
||||
this.props.startValue = '';
|
||||
this.props.id = this._internal.controlId = 'input-' + guid();
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
maxLength: {
|
||||
group: 'Text',
|
||||
displayName: 'Max length',
|
||||
type: 'number',
|
||||
index: 24
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
placeHolderOpacity: {
|
||||
index: 23,
|
||||
group: 'Text',
|
||||
displayName: 'Placeholder opacity',
|
||||
type: 'number',
|
||||
default: 0.5,
|
||||
set(value) {
|
||||
const className = this._internal.controlId;
|
||||
Utils.updateStylesForClass(className, { placeholderOpacity: value }, _styleTemplate);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
clear: {
|
||||
type: 'signal',
|
||||
group: 'Actions',
|
||||
displayName: 'Clear',
|
||||
valueChangedToTrue() {
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
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' }, 'input');
|
||||
break;
|
||||
case 'center':
|
||||
this.setStyle({ textAlign: 'center' }, 'input');
|
||||
break;
|
||||
case 'right':
|
||||
this.setStyle({ textAlign: 'right' }, 'input');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
index: 100,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
default: 'transparent',
|
||||
allowVisualStates: true,
|
||||
styleTag: 'inputWrapper'
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
// Value
|
||||
onTextChanged: {
|
||||
group: 'General',
|
||||
displayName: 'Text',
|
||||
type: 'string',
|
||||
index: 1,
|
||||
onChange() {
|
||||
this.sendSignalOnOutput('textChanged');
|
||||
}
|
||||
},
|
||||
|
||||
// Events
|
||||
onEnter: {
|
||||
group: 'Events',
|
||||
displayName: 'On Enter',
|
||||
type: 'signal'
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
textChanged: {
|
||||
displayName: 'Text Changed',
|
||||
type: 'signal',
|
||||
group: 'General',
|
||||
index: 2
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_focus() {
|
||||
if (!this.innerReactComponentRef) return;
|
||||
this.innerReactComponentRef.focus();
|
||||
},
|
||||
_blur() {
|
||||
if (!this.innerReactComponentRef) return;
|
||||
this.innerReactComponentRef.blur();
|
||||
},
|
||||
clear() {
|
||||
this.props.startValue = '';
|
||||
if (this.innerReactComponentRef) {
|
||||
this.innerReactComponentRef.setText('');
|
||||
}
|
||||
},
|
||||
setText(text) {
|
||||
this.props.startValue = text;
|
||||
if (this.innerReactComponentRef) {
|
||||
//the text component is currently mounted, and will signal the onTextChanged output
|
||||
if (this.innerReactComponentRef.hasFocus() === false) {
|
||||
this.innerReactComponentRef.setText(text);
|
||||
}
|
||||
} else if (this.outputPropValues['onTextChanged'] !== text) {
|
||||
//text component isn't mounted yet, set the output manually
|
||||
this.outputPropValues['onTextChanged'] = text;
|
||||
this.flagOutputDirty('onTextChanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(TextInputNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Text'
|
||||
});
|
||||
NodeSharedPortDefinitions.addIconInputs(TextInputNode, {
|
||||
enableIconPlacement: true,
|
||||
defaults: { useIcon: false, iconColor: '#000000' }
|
||||
});
|
||||
NodeSharedPortDefinitions.addLabelInputs(TextInputNode, {
|
||||
enableSpacing: true,
|
||||
styleTag: 'label',
|
||||
displayName: 'Label'
|
||||
});
|
||||
|
||||
NodeSharedPortDefinitions.addTextStyleInputs(TextInputNode, {
|
||||
styleTag: 'input',
|
||||
portPrefix: '',
|
||||
portIndex: 18,
|
||||
popout: {
|
||||
group: 'input-text-style',
|
||||
label: 'Text Style',
|
||||
parentGroup: 'Text'
|
||||
}
|
||||
});
|
||||
|
||||
NodeSharedPortDefinitions.addAlignInputs(TextInputNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(TextInputNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(TextInputNode, {
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
NodeSharedPortDefinitions.addMarginInputs(TextInputNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(TextInputNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(TextInputNode, {
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
NodeSharedPortDefinitions.addShadowInputs(TextInputNode, {
|
||||
styleTag: 'inputWrapper'
|
||||
});
|
||||
Utils.addControlEventsAndStates(TextInputNode);
|
||||
|
||||
export default createNodeFromReactComponent(TextInputNode);
|
||||
309
packages/noodl-viewer-react/src/nodes/controls/utils.ts
Normal file
309
packages/noodl-viewer-react/src/nodes/controls/utils.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import PointerListeners from '../../pointerlisteners';
|
||||
|
||||
function _shallowCompare(o1, o2) {
|
||||
let p;
|
||||
for (p in o1) {
|
||||
if (o1.hasOwnProperty(p)) {
|
||||
if (o1[p] !== o2[p]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (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
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = _styleTemplate(_class, props);
|
||||
document.head.appendChild(style);
|
||||
|
||||
_styleSheets[_class] = { style, props: Object.assign({}, props) };
|
||||
}
|
||||
}
|
||||
|
||||
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 addOutputProps(definition, values) {
|
||||
mergeAttribute(definition, 'outputProps', values);
|
||||
}
|
||||
|
||||
function addOutputs(definition, values) {
|
||||
mergeAttribute(definition, 'outputs', values);
|
||||
}
|
||||
|
||||
function addControlEventsAndStates(definition, args?) {
|
||||
args = args || {};
|
||||
|
||||
definition.visualStates = [
|
||||
{ name: 'neutral', label: 'Neutral' },
|
||||
{ name: 'hover', label: 'Hover' },
|
||||
{ name: 'pressed', label: 'Pressed' },
|
||||
{ name: 'focused', label: 'Focused' },
|
||||
{ name: 'disabled', label: 'Disabled' }
|
||||
];
|
||||
|
||||
if (args.checked) {
|
||||
definition.visualStates.splice(3, 0, { name: 'checked', label: 'Checked' });
|
||||
}
|
||||
|
||||
addInputs(definition, {
|
||||
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._updateVisualState();
|
||||
this.forceUpdate();
|
||||
this.flagOutputDirty('enabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addInputProps(definition, {
|
||||
blockTouch: {
|
||||
index: 450,
|
||||
displayName: 'Block Pointer Events',
|
||||
type: 'boolean',
|
||||
group: 'Pointer Events'
|
||||
}
|
||||
});
|
||||
|
||||
definition.methods._updateVisualState = function () {
|
||||
const states = [];
|
||||
|
||||
//make sure they are in the order they should be applied
|
||||
if (this._internal.enabled) {
|
||||
if (this.outputPropValues.hoverState) states.push('hover');
|
||||
if (this.outputPropValues.pressedState) states.push('pressed');
|
||||
if (this.outputPropValues.focusState) states.push('focused');
|
||||
}
|
||||
|
||||
if (args.checked && this._internal.checked) states.push('checked');
|
||||
if (!this._internal.enabled) states.push('disabled');
|
||||
|
||||
this.setVisualStates(states);
|
||||
};
|
||||
|
||||
addOutputProps(definition, {
|
||||
// Focus
|
||||
focusState: {
|
||||
displayName: 'Focused',
|
||||
group: 'States',
|
||||
type: 'boolean',
|
||||
props: {
|
||||
onFocus() {
|
||||
this.outputPropValues.focusState = true;
|
||||
this.flagOutputDirty('focusState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onBlur() {
|
||||
this.outputPropValues.focusState = false;
|
||||
this.flagOutputDirty('focusState');
|
||||
this._updateVisualState();
|
||||
}
|
||||
}
|
||||
},
|
||||
onFocus: {
|
||||
displayName: 'Focused',
|
||||
group: 'Focus Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onFocus() {
|
||||
this.sendSignalOnOutput('onFocus');
|
||||
}
|
||||
}
|
||||
},
|
||||
onBlur: {
|
||||
displayName: 'Blurred',
|
||||
group: 'Focus Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onBlur() {
|
||||
this.sendSignalOnOutput('onBlur');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Hover
|
||||
hoverState: {
|
||||
displayName: 'Hover',
|
||||
group: 'States',
|
||||
type: 'boolean',
|
||||
props: {
|
||||
onMouseOver() {
|
||||
this.outputPropValues.hoverState = true;
|
||||
this.flagOutputDirty('hoverState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onMouseLeave() {
|
||||
this.outputPropValues.hoverState = false;
|
||||
this.flagOutputDirty('hoverState');
|
||||
this._updateVisualState();
|
||||
}
|
||||
}
|
||||
},
|
||||
hoverStart: {
|
||||
displayName: 'Hover Start',
|
||||
group: 'Pointer Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onMouseOver() {
|
||||
this.sendSignalOnOutput('hoverStart');
|
||||
}
|
||||
}
|
||||
},
|
||||
hoverEnd: {
|
||||
displayName: 'Hover End',
|
||||
group: 'Pointer Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onMouseLeave() {
|
||||
this.sendSignalOnOutput('hoverEnd');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Pressed
|
||||
pressedState: {
|
||||
displayName: 'Pressed',
|
||||
group: 'States',
|
||||
type: 'boolean',
|
||||
props: {
|
||||
onMouseDown() {
|
||||
this.outputPropValues.pressedState = true;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onTouchStart() {
|
||||
this.outputPropValues.pressedState = true;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onMouseUp() {
|
||||
this.outputPropValues.pressedState = false;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.outputPropValues.pressedState = false;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onTouchCancel() {
|
||||
this.outputPropValues.pressedState = false;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
},
|
||||
onMouseLeave() {
|
||||
this.outputPropValues.pressedState = false;
|
||||
this.flagOutputDirty('pressedState');
|
||||
this._updateVisualState();
|
||||
}
|
||||
}
|
||||
},
|
||||
pointerDown: {
|
||||
displayName: 'Pointer Down',
|
||||
group: 'Pointer Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onMouseDown() {
|
||||
this.sendSignalOnOutput('pointerDown');
|
||||
},
|
||||
onTouchStart() {
|
||||
this.sendSignalOnOutput('pointerDown');
|
||||
}
|
||||
}
|
||||
},
|
||||
pointerUp: {
|
||||
displayName: 'Pointer Up',
|
||||
group: 'Pointer Events',
|
||||
type: 'signal',
|
||||
props: {
|
||||
onMouseUp() {
|
||||
this.sendSignalOnOutput('pointerUp');
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.sendSignalOnOutput('pointerUp');
|
||||
},
|
||||
onTouchCancel() {
|
||||
this.sendSignalOnOutput('pointerUp');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addOutputs(definition, {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
displayName: 'Enabled',
|
||||
group: 'States',
|
||||
getter: function () {
|
||||
return this._internal.enabled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const oldInit = definition.initialize;
|
||||
definition.initialize = function () {
|
||||
oldInit && oldInit.call(this);
|
||||
this.props.enabled = this._internal.enabled = true;
|
||||
this.outputPropValues.hoverState = this.outputPropValues.focusState = this.outputPropValues.pressedState = false;
|
||||
};
|
||||
}
|
||||
|
||||
function controlEvents(props) {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
onFocus: props.onFocus,
|
||||
onBlur: props.onBlur
|
||||
},
|
||||
PointerListeners(props)
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
updateStylesForClass,
|
||||
addControlEventsAndStates,
|
||||
controlEvents
|
||||
};
|
||||
149
packages/noodl-viewer-react/src/nodes/navigation/closepopup.js
Normal file
149
packages/noodl-viewer-react/src/nodes/navigation/closepopup.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const { EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
|
||||
const ClosePopupNode = {
|
||||
name: 'NavigationClosePopup',
|
||||
displayNodeName: 'Close Popup',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/popups/close-popup',
|
||||
initialize: function () {
|
||||
this._internal.resultValues = {};
|
||||
},
|
||||
inputs: {
|
||||
results: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Results',
|
||||
set: function (value) {
|
||||
this._internal.results = value;
|
||||
}
|
||||
},
|
||||
closeActions: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Close Actions',
|
||||
set: function (value) {
|
||||
this._internal.closeActions = value;
|
||||
}
|
||||
},
|
||||
close: {
|
||||
type: 'Signal',
|
||||
displayName: 'Close',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setResultValue: function (key, value) {
|
||||
this._internal.resultValues[key] = value;
|
||||
},
|
||||
_setCloseCallback: function (cb) {
|
||||
this._internal.closeCallback = cb;
|
||||
},
|
||||
scheduleClose: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledClose) {
|
||||
internal.hasScheduledClose = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
internal.hasScheduledClose = false;
|
||||
_this.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
if (this._internal.closeCallback)
|
||||
this._internal.closeCallback(this._internal.closeAction, this._internal.resultValues);
|
||||
},
|
||||
closeActionTriggered: function (name) {
|
||||
this._internal.closeAction = name;
|
||||
this.scheduleClose();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('result-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setResultValue.bind(this, name.substring('result-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('closeAction-'))
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: this.closeActionTriggered.bind(this, name)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ClosePopupNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
// Add results inputs
|
||||
var results = node.parameters['results'];
|
||||
if (results) {
|
||||
results = results ? results.split(',') : undefined;
|
||||
for (var i in results) {
|
||||
var p = results[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Results',
|
||||
name: 'result-' + p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add close actions
|
||||
var closeActions = node.parameters['closeActions'];
|
||||
if (closeActions) {
|
||||
closeActions = closeActions ? closeActions.split(',') : undefined;
|
||||
for (var i in closeActions) {
|
||||
var p = closeActions[i];
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'input',
|
||||
group: 'Close Actions',
|
||||
name: 'closeAction-' + p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'results' || event.name === 'closeActions') {
|
||||
_updatePorts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.NavigationClosePopup', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('NavigationClosePopup')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
const NavigateBack = {
|
||||
name: 'PageStackNavigateBack',
|
||||
displayNodeName: 'Pop Component Stack',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/component-stack/pop-component',
|
||||
inputs: {
|
||||
navigate: {
|
||||
displayName: 'Navigate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNavigate();
|
||||
}
|
||||
},
|
||||
results: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Results',
|
||||
set: function (value) {
|
||||
this._internal.results = value;
|
||||
}
|
||||
},
|
||||
backActions: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Back Actions',
|
||||
set: function (value) {
|
||||
this._internal.backActions = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
initialize: function () {
|
||||
this._internal.resultValues = {};
|
||||
},
|
||||
methods: {
|
||||
scheduleNavigate: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledNavigate) {
|
||||
internal.hasScheduledNavigate = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
internal.hasScheduledNavigate = false;
|
||||
_this.navigate();
|
||||
});
|
||||
}
|
||||
},
|
||||
_setBackCallback(cb) {
|
||||
this._internal.backCallback = cb;
|
||||
},
|
||||
navigate() {
|
||||
if (this._internal.backCallback === undefined) return;
|
||||
|
||||
this._internal.backCallback({
|
||||
backAction: this._internal.backAction,
|
||||
results: this._internal.resultValues
|
||||
});
|
||||
},
|
||||
setResultValue: function (key, value) {
|
||||
this._internal.resultValues[key] = value;
|
||||
},
|
||||
backActionTriggered: function (name) {
|
||||
this._internal.backAction = name;
|
||||
this.scheduleNavigate();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('result-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setResultValue.bind(this, name.substring('result-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('backAction-'))
|
||||
return this.registerInput(name, {
|
||||
set: _createSignal({
|
||||
valueChangedToTrue: this.backActionTriggered.bind(this, name)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
// Add results inputs
|
||||
var results = node.parameters.results;
|
||||
if (results) {
|
||||
results = results ? results.split(',') : undefined;
|
||||
for (var i in results) {
|
||||
var p = results[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Results',
|
||||
name: 'result-' + p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add back actions
|
||||
var backActions = node.parameters.backActions;
|
||||
if (backActions) {
|
||||
backActions = backActions ? backActions.split(',') : undefined;
|
||||
for (var i in backActions) {
|
||||
var p = backActions[i];
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'input',
|
||||
group: 'Back Actions',
|
||||
name: 'backAction-' + p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'results' || event.name === 'backActions') {
|
||||
_updatePorts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.PageStackNavigateBack', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('PageStackNavigateBack')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: NavigateBack,
|
||||
setup: setup
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
'use strict';
|
||||
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
|
||||
const NavigateToPathNode = {
|
||||
name: 'PageStackNavigateToPath',
|
||||
displayNodeName: 'Navigate To Path',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/navigate-to-path',
|
||||
initialize() {
|
||||
const internal = this._internal;
|
||||
internal.params = {};
|
||||
internal.query = {};
|
||||
internal.openInNewTab = false;
|
||||
},
|
||||
inputs: {
|
||||
path: {
|
||||
type: { name: 'string' },
|
||||
displayName: 'Path',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
this._internal.path = value;
|
||||
}
|
||||
},
|
||||
queryNames: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Query',
|
||||
group: 'Query',
|
||||
set(value) {
|
||||
this._internal.queryNames = value;
|
||||
}
|
||||
},
|
||||
openInNewTab: {
|
||||
index: 10,
|
||||
displayName: 'Open in new tab',
|
||||
group: 'General',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
set(value) {
|
||||
this._internal.openInNewTab = !!value;
|
||||
}
|
||||
},
|
||||
navigate: {
|
||||
displayName: 'Navigate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleNavigate();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
scheduleNavigate() {
|
||||
var internal = this._internal;
|
||||
|
||||
if (!internal.hasScheduledNavigate) {
|
||||
internal.hasScheduledNavigate = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.hasScheduledNavigate = false;
|
||||
this.navigate();
|
||||
});
|
||||
}
|
||||
},
|
||||
navigate() {
|
||||
var internal = this._internal;
|
||||
|
||||
var formattedPath = internal.path;
|
||||
if (formattedPath === undefined) return;
|
||||
|
||||
var matches = internal.path.match(/\{[A-Za-z0-9_]*\}/g);
|
||||
var inputs = [];
|
||||
if (matches) {
|
||||
inputs = matches.map(function (name) {
|
||||
return name.replace('{', '').replace('}', '');
|
||||
});
|
||||
}
|
||||
|
||||
inputs.forEach(function (name) {
|
||||
var v = internal.params[name];
|
||||
formattedPath = formattedPath.replace('{' + name + '}', v !== undefined ? v : '');
|
||||
});
|
||||
|
||||
var urlPath, hashPath;
|
||||
var navigationPathType = NoodlRuntime.instance.getProjectSettings()['navigationPathType'];
|
||||
if (navigationPathType === undefined || navigationPathType === 'hash') hashPath = formattedPath;
|
||||
else urlPath = formattedPath;
|
||||
|
||||
var query = [];
|
||||
if (internal.queryNames !== undefined) {
|
||||
internal.queryNames.split(',').forEach((q) => {
|
||||
if (internal.query[q] !== undefined) {
|
||||
query.push(q + '=' + internal.query[q]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var compiledUrl =
|
||||
(urlPath !== undefined ? urlPath : '') +
|
||||
(query.length >= 1 ? '?' + query.join('&') : '') +
|
||||
(hashPath !== undefined ? '#' + hashPath : '');
|
||||
|
||||
if (this._internal.openInNewTab) {
|
||||
window.open(compiledUrl, '_blank');
|
||||
} else {
|
||||
window.history.pushState({}, '', compiledUrl);
|
||||
dispatchEvent(new PopStateEvent('popstate', {}));
|
||||
}
|
||||
},
|
||||
setParam(name, value) {
|
||||
this._internal.params[name] = value;
|
||||
},
|
||||
setQuery(name, value) {
|
||||
this._internal.query[name] = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('p-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setParam.bind(this, name.substring('p-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('q-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setQuery.bind(this, name.substring('q-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: NavigateToPathNode,
|
||||
setup: function setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
if (node.parameters['path'] !== undefined) {
|
||||
var inputs = node.parameters['path'].match(/\{[A-Za-z0-9_]*\}/g) || [];
|
||||
var portsNames = inputs.map(function (def) {
|
||||
return def.replace('{', '').replace('}', '');
|
||||
});
|
||||
|
||||
var ports = portsNames
|
||||
//get unique names
|
||||
.filter(function (value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
})
|
||||
//and map names to ports
|
||||
.map(function (name) {
|
||||
return {
|
||||
name: 'p-' + name,
|
||||
displayName: name,
|
||||
group: 'Parameter',
|
||||
type: '*',
|
||||
plug: 'input'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (node.parameters['queryNames'] !== undefined) {
|
||||
node.parameters['queryNames'].split(',').forEach((q) => {
|
||||
ports.push({
|
||||
name: 'q-' + q,
|
||||
displayName: q,
|
||||
group: 'Query',
|
||||
plug: 'input',
|
||||
type: '*'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.PageStackNavigateToPath', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('PageStackNavigateToPath')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
324
packages/noodl-viewer-react/src/nodes/navigation/navigate.js
Normal file
324
packages/noodl-viewer-react/src/nodes/navigation/navigate.js
Normal file
@@ -0,0 +1,324 @@
|
||||
const Transitions = require('./transitions');
|
||||
const NavigationHandler = require('./navigation-handler');
|
||||
|
||||
const Navigate = {
|
||||
name: 'PageStackNavigate',
|
||||
displayNodeName: 'Push Component To Stack',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/component-stack/push-component',
|
||||
initialize: function () {
|
||||
this._internal.transitionParams = {};
|
||||
this._internal.pageParams = {};
|
||||
this._internal.backResults = {};
|
||||
},
|
||||
inputs: {
|
||||
stack: {
|
||||
type: { name: 'string', identifierOf: 'PackStack' },
|
||||
displayName: 'Stack',
|
||||
group: 'General',
|
||||
default: 'Main',
|
||||
set: function (value) {
|
||||
this._internal.stack = value;
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Push', value: 'push' },
|
||||
{ label: 'Replace', value: 'replace' }
|
||||
]
|
||||
},
|
||||
displayName: 'Mode',
|
||||
default: 'push',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.navigationMode = value;
|
||||
}
|
||||
},
|
||||
navigate: {
|
||||
displayName: 'Navigate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNavigate();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
navigated: {
|
||||
type: 'signal',
|
||||
displayName: 'Navigated',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleNavigate: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledNavigate) {
|
||||
internal.hasScheduledNavigate = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
internal.hasScheduledNavigate = false;
|
||||
_this.navigate();
|
||||
});
|
||||
}
|
||||
},
|
||||
navigate() {
|
||||
if (this._internal.navigationMode === 'push' || this._internal.navigationMode === undefined) {
|
||||
NavigationHandler.instance.navigate(this._internal.stack, {
|
||||
target: this._internal.target,
|
||||
transition: { ...{ type: this._internal.transition }, ...this._internal.transitionParams },
|
||||
params: this._internal.pageParams,
|
||||
backCallback: (action, results) => {
|
||||
this._internal.backResults = results;
|
||||
|
||||
for (var key in results) {
|
||||
if (this.hasOutput('backResult-' + key)) this.flagOutputDirty('backResult-' + key);
|
||||
}
|
||||
|
||||
if (action !== undefined) this.sendSignalOnOutput(action);
|
||||
},
|
||||
hasNavigated: () => {
|
||||
this.sendSignalOnOutput('navigated');
|
||||
}
|
||||
});
|
||||
} else if (this._internal.navigationMode === 'replace') {
|
||||
NavigationHandler.instance.replace(this._internal.stack, {
|
||||
target: this._internal.target,
|
||||
params: this._internal.pageParams,
|
||||
hasNavigated: () => {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.sendSignalOnOutput('navigated');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
setTransitionParam: function (param, value) {
|
||||
this._internal.transitionParams[param] = value;
|
||||
},
|
||||
setPageParam: function (param, value) {
|
||||
this._internal.pageParams[param] = value;
|
||||
},
|
||||
getBackResult: function (param, value) {
|
||||
return this._internal.backResults[param];
|
||||
},
|
||||
setTargetPageId: function (pageId) {
|
||||
this._internal.target = pageId;
|
||||
},
|
||||
setTransition: function (value) {
|
||||
this._internal.transition = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'target')
|
||||
return this.registerInput(name, {
|
||||
set: this.setTargetPageId.bind(this)
|
||||
});
|
||||
else if (name === 'transition')
|
||||
return this.registerInput(name, {
|
||||
set: this.setTransition.bind(this)
|
||||
});
|
||||
else if (name.startsWith('tr-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setTransitionParam.bind(this, name.substring('tr-'.length))
|
||||
});
|
||||
else if (name.startsWith('pm-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setPageParam.bind(this, name.substring('pm-'.length))
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('backResult-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getBackResult.bind(this, name.substring('backResult-'.length))
|
||||
});
|
||||
else if (name.startsWith('backAction-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: function () {
|
||||
/** No needed for signals */
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
// Only push mode have transition
|
||||
if (node.parameters['mode'] === 'push' || node.parameters['mode'] === undefined) {
|
||||
ports.push({
|
||||
name: 'transition',
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: Object.keys(Transitions) },
|
||||
default: 'Push',
|
||||
displayName: 'Transition',
|
||||
group: 'Transition'
|
||||
});
|
||||
|
||||
var transition = node.parameters['transition'] || 'Push';
|
||||
if (Transitions[transition]) ports = ports.concat(Transitions[transition].ports(node.parameters));
|
||||
}
|
||||
|
||||
// if(node.parameters['stack'] !== undefined) {
|
||||
var pageStacks = graphModel.getNodesWithType('Page Stack');
|
||||
var pageStack = pageStacks.find(
|
||||
(ps) => (ps.parameters['name'] || 'Main') === (node.parameters['stack'] || 'Main')
|
||||
);
|
||||
|
||||
if (pageStack !== undefined) {
|
||||
var pages = pageStack.parameters['pages'];
|
||||
if (pages !== undefined && pages.length > 0) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: pages.map((p) => ({ label: p.label, value: p.id })), allowEditOnly: true },
|
||||
group: 'General',
|
||||
displayName: 'Target Page',
|
||||
name: 'target',
|
||||
default: pages[0].id
|
||||
});
|
||||
|
||||
// See if there is a target page with component
|
||||
var targetPageId = node.parameters['target'] || pages[0].id;
|
||||
var targetComponentName = pageStack.parameters['pageComp-' + targetPageId];
|
||||
if (targetComponentName !== undefined) {
|
||||
const component = graphModel.components[targetComponentName];
|
||||
|
||||
if (component !== undefined) {
|
||||
// Make all inputs of the component to inputs of this navigation node
|
||||
for (var inputName in component.inputPorts) {
|
||||
ports.push({
|
||||
name: 'pm-' + inputName,
|
||||
displayName: inputName,
|
||||
type: '*',
|
||||
plug: 'input',
|
||||
group: 'Parameters'
|
||||
});
|
||||
}
|
||||
|
||||
// Find all navigate back nodes and compile the back actions as
|
||||
// outputs of this node
|
||||
for (const backNode of component.getNodesWithType('PageStackNavigateBack')) {
|
||||
if (backNode.parameters['backActions'] !== undefined) {
|
||||
backNode.parameters['backActions'].split(',').forEach((a) => {
|
||||
if (ports.find((_p) => _p.name === 'backAction-' + a)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'backAction-' + a,
|
||||
displayName: a,
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Back Actions'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (backNode.parameters['results']) {
|
||||
backNode.parameters['results'].split(',').forEach((p) => {
|
||||
if (ports.find((_p) => _p.name === 'backResult-' + p)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'backResult-' + p,
|
||||
displayName: p,
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
group: 'Back Results'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
function _trackTargetComponent() {
|
||||
var pageStacks = graphModel.getNodesWithType('Page Stack');
|
||||
var pageStack = pageStacks.find((ps) => ps.parameters['name'] === node.parameters['stack']);
|
||||
if (pageStack === undefined) return;
|
||||
|
||||
var pages = pageStack.parameters['pages'];
|
||||
if (pages === undefined || pages.length === 0) return;
|
||||
|
||||
var targetCompoment = pageStack.parameters['pageComp-' + (node.parameters['target'] || pages[0].id)];
|
||||
if (targetCompoment === undefined) return;
|
||||
var c = graphModel.components[targetCompoment];
|
||||
if (c === undefined) return;
|
||||
|
||||
c.on('inputPortAdded', _updatePorts);
|
||||
c.on('inputPortRemoved', _updatePorts);
|
||||
|
||||
// Also track all back navigate for changes
|
||||
for (const _n of c.getNodesWithType('PageStackNavigateBack')) {
|
||||
_n.on('parameterUpdated', _updatePorts);
|
||||
}
|
||||
|
||||
// Track back navigate added and removed
|
||||
c.on('nodeAdded', (_n) => {
|
||||
if (_n.type === 'PageStackNavigateBack') {
|
||||
_n.on('parameterUpdated', _updatePorts);
|
||||
_updatePorts();
|
||||
}
|
||||
});
|
||||
|
||||
c.on('nodeWasRemoved', (_n) => {
|
||||
if (_n.type === 'PageStackNavigateBack') _updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
_trackTargetComponent();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
if (ev.name === 'target') {
|
||||
_trackTargetComponent();
|
||||
_updatePorts();
|
||||
} else if (ev.name === 'stack' || ev.name === 'mode' || ev.name === 'transition' || ev.name.startsWith('tr-')) _updatePorts();
|
||||
});
|
||||
|
||||
// Track all page stacks for changes (if there are any changes to the pages of the name of a stack we might have to update)
|
||||
function _trackPageStack(node) {
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
if (ev.name === 'pages' || ev.name === 'name') _updatePorts();
|
||||
});
|
||||
}
|
||||
graphModel.on('nodeAdded.Page Stack', _trackPageStack);
|
||||
graphModel.on('nodeWasRemoved.Page Stack', _updatePorts);
|
||||
|
||||
for (const node of graphModel.getNodesWithType('Page Stack')) {
|
||||
_trackPageStack(node);
|
||||
}
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.PageStackNavigate', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('PageStackNavigate')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: Navigate,
|
||||
setup: setup
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
class NavigationHandler {
|
||||
constructor() {
|
||||
this._pageStacks = {};
|
||||
this._navigationQueue = [];
|
||||
}
|
||||
|
||||
_performNavigation(name, args, type) {
|
||||
name = name || 'Main';
|
||||
if (this._pageStacks[name]) {
|
||||
for (const pageStack of this._pageStacks[name]) {
|
||||
type === 'navigate' ? pageStack.navigate(args) : pageStack.replace(args);
|
||||
}
|
||||
} else {
|
||||
this._navigationQueue.push({ name, args, type });
|
||||
}
|
||||
}
|
||||
|
||||
navigate(name, args) {
|
||||
name = name || 'Main';
|
||||
this._performNavigation(name, args, 'navigate');
|
||||
}
|
||||
|
||||
replace(name, args) {
|
||||
name = name || 'Main';
|
||||
this._performNavigation(name, args, 'replace');
|
||||
}
|
||||
|
||||
registerPageStack(name, pageStack) {
|
||||
name = name || 'Main';
|
||||
if (!this._pageStacks[name]) {
|
||||
this._pageStacks[name] = [];
|
||||
}
|
||||
|
||||
this._pageStacks[name].push(pageStack);
|
||||
|
||||
let hasReset = false;
|
||||
let hasNavigated = false;
|
||||
|
||||
let i = 0;
|
||||
while (i < this._navigationQueue.length) {
|
||||
const e = this._navigationQueue[i];
|
||||
if (e.name === name) {
|
||||
if (e.type === 'navigate') {
|
||||
if (!hasReset) {
|
||||
//we need to reset to the start page before doing the first navigation
|
||||
pageStack.reset();
|
||||
hasReset = true;
|
||||
}
|
||||
pageStack.navigate(e.args);
|
||||
} else {
|
||||
pageStack.replace(e.args);
|
||||
}
|
||||
|
||||
hasNavigated = true;
|
||||
this._navigationQueue.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNavigated) {
|
||||
pageStack.reset(); //no navigation has happened, call reset() so the start page is created
|
||||
}
|
||||
}
|
||||
|
||||
deregisterPageStack(name, pageStack) {
|
||||
name = name || 'Main';
|
||||
|
||||
if (!this._pageStacks[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._pageStacks[name].indexOf(pageStack);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pageStacks[name].splice(index, 1);
|
||||
if (this._pageStacks[name].length === 0) {
|
||||
delete this._pageStacks[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationHandler.instance = new NavigationHandler();
|
||||
|
||||
module.exports = NavigationHandler;
|
||||
@@ -0,0 +1,720 @@
|
||||
import ASyncQueue from '../../async-queue';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const { useEffect } = require('react');
|
||||
|
||||
const guid = require('../../guid');
|
||||
|
||||
const NavigationHandler = require('./navigation-handler');
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
|
||||
const Transitions = require('./transitions');
|
||||
|
||||
function PageStackReactComponent(props) {
|
||||
const { didMount, willUnmount, style, children } = props;
|
||||
|
||||
useEffect(() => {
|
||||
didMount();
|
||||
return () => {
|
||||
willUnmount();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div style={style}>{children}</div>;
|
||||
}
|
||||
|
||||
const PageStack = {
|
||||
name: 'Page Stack',
|
||||
displayNodeName: 'Component Stack',
|
||||
category: 'Visuals',
|
||||
docs: 'https://docs.noodl.net/nodes/component-stack/component-stack-node',
|
||||
useVariants: false,
|
||||
initialize() {
|
||||
this._internal.stack = [];
|
||||
|
||||
this._internal.topPageName = '';
|
||||
this._internal.stackDepth = 0;
|
||||
|
||||
this._internal.pageInfo = {};
|
||||
|
||||
this._internal.asyncQueue = new ASyncQueue();
|
||||
|
||||
this.onScheduleReset = () => {
|
||||
this.scheduleReset();
|
||||
};
|
||||
|
||||
this.props.didMount = () => {
|
||||
this._internal.isMounted = true;
|
||||
|
||||
// Listen to push state events and update stack
|
||||
if (window.history && window.history.pushState) {
|
||||
//this is event is manually sent on push, so covers both pop and push state
|
||||
window.addEventListener('popstate', this.onScheduleReset);
|
||||
} else {
|
||||
// Only hash support
|
||||
window.addEventListener('hashchange', this.onScheduleReset);
|
||||
}
|
||||
|
||||
this._registerPageStack();
|
||||
};
|
||||
|
||||
this.props.willUnmount = () => {
|
||||
this._internal.isMounted = false;
|
||||
|
||||
window.removeEventListener('popstate', this.onScheduleReset);
|
||||
window.removeEventListener('hashchange', this.onScheduleReset);
|
||||
|
||||
this._deregisterPageStack();
|
||||
};
|
||||
},
|
||||
getInspectInfo() {
|
||||
if (this._internal.stack.length === 0) {
|
||||
return 'No active page';
|
||||
}
|
||||
|
||||
const info = [{ type: 'text', value: 'Active Components:' }];
|
||||
|
||||
return info.concat(
|
||||
this._internal.stack.map((p, i) => ({
|
||||
type: 'text',
|
||||
value: '- ' + this._internal.pages.find((pi) => pi.id === p.pageId).label
|
||||
}))
|
||||
);
|
||||
},
|
||||
defaultCss: {
|
||||
width: '100%',
|
||||
flex: '1 1 100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
getReactComponent() {
|
||||
return PageStackReactComponent;
|
||||
},
|
||||
inputs: {
|
||||
name: {
|
||||
type: { name: 'string', identifierOf: 'PackStack' },
|
||||
displayName: 'Name',
|
||||
group: 'General',
|
||||
default: 'Main',
|
||||
set: function (value) {
|
||||
this._deregisterPageStack();
|
||||
this._internal.name = value;
|
||||
|
||||
if (this._internal.isMounted) {
|
||||
this._registerPageStack();
|
||||
}
|
||||
}
|
||||
},
|
||||
/* startPage: {
|
||||
type: 'component',
|
||||
displayName: 'Start Page',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.startPage = value;
|
||||
this.scheduleReset();
|
||||
}
|
||||
},*/
|
||||
useRoutes: {
|
||||
type: 'boolean',
|
||||
displayName: 'Use Routes',
|
||||
group: 'General',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.useRoutes = !!value;
|
||||
}
|
||||
},
|
||||
clip: {
|
||||
displayName: 'Clip Content',
|
||||
type: 'boolean',
|
||||
group: 'Layout',
|
||||
default: true,
|
||||
set(value) {
|
||||
if (value) {
|
||||
this.setStyle({ overflow: 'hidden' });
|
||||
} else {
|
||||
this.removeStyle(['overflow']);
|
||||
}
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
type: 'proplist',
|
||||
displayName: 'Components',
|
||||
group: 'Components',
|
||||
set: function (value) {
|
||||
this._internal.pages = value;
|
||||
if (this._internal.isMounted) {
|
||||
this.scheduleReset();
|
||||
}
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
type: 'signal',
|
||||
displayName: 'Reset',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleReset();
|
||||
}
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
default: 'transparent',
|
||||
applyDefault: false
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
topPageName: {
|
||||
type: 'string',
|
||||
displayName: 'Top Component Name',
|
||||
get() {
|
||||
return this._internal.topPageName;
|
||||
}
|
||||
},
|
||||
stackDepth: {
|
||||
type: 'number',
|
||||
displayName: 'Stack Depth',
|
||||
get() {
|
||||
return this._internal.stackDepth;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_registerPageStack() {
|
||||
NavigationHandler.instance.registerPageStack(this._internal.name, this);
|
||||
},
|
||||
_deregisterPageStack() {
|
||||
NavigationHandler.instance.deregisterPageStack(this._internal.name, this);
|
||||
},
|
||||
_pageNameForId(id) {
|
||||
if (this._internal.pages === undefined) return;
|
||||
const page = this._internal.pages.find((p) => p.id === id);
|
||||
if (page === undefined) return;
|
||||
|
||||
return page.label;
|
||||
},
|
||||
setPageOutputs(outputs) {
|
||||
for (const prop in outputs) {
|
||||
this._internal[prop] = outputs[prop];
|
||||
this.flagOutputDirty(prop);
|
||||
}
|
||||
},
|
||||
scheduleReset() {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledReset) {
|
||||
internal.hasScheduledReset = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.hasScheduledReset = false;
|
||||
this.reset();
|
||||
});
|
||||
}
|
||||
},
|
||||
createPageContainer() {
|
||||
const group = this.nodeScope.createPrimitiveNode('Group');
|
||||
group.setStyle({ flex: '1 0 100%' });
|
||||
return group;
|
||||
},
|
||||
reset() {
|
||||
this._internal.asyncQueue.enqueue(this.resetAsync.bind(this));
|
||||
},
|
||||
async resetAsync() {
|
||||
var children = this.getChildren();
|
||||
for (var i in children) {
|
||||
var c = children[i];
|
||||
this.removeChild(c);
|
||||
this.nodeScope.deleteNode(c);
|
||||
}
|
||||
|
||||
if (this._internal.pages === undefined || this._internal.pages.length === 0) return;
|
||||
|
||||
var startPageId,
|
||||
params = {};
|
||||
var pageFromUrl = this.matchPageFromUrl();
|
||||
if (pageFromUrl !== undefined) {
|
||||
// We have an url matching a page, use that page as start page
|
||||
startPageId = pageFromUrl.pageId;
|
||||
|
||||
params = Object.assign({}, pageFromUrl.query, pageFromUrl.params);
|
||||
} else {
|
||||
var startPageId = this._internal.startPageId;
|
||||
if (startPageId === undefined) startPageId = this._internal.pages[0].id;
|
||||
}
|
||||
|
||||
var pageInfo = this._internal.pageInfo[startPageId];
|
||||
|
||||
if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page
|
||||
|
||||
var content = await this.nodeScope.createNode(pageInfo.component, guid());
|
||||
|
||||
for (var key in params) {
|
||||
content.setInputValue(key, params[key]);
|
||||
}
|
||||
|
||||
const group = this.createPageContainer();
|
||||
group.addChild(content);
|
||||
|
||||
this.addChild(group);
|
||||
this._internal.stack = [
|
||||
{
|
||||
from: null,
|
||||
page: group,
|
||||
pageId: startPageId,
|
||||
pageInfo: pageInfo,
|
||||
params: params,
|
||||
componentName: this._internal.startPage
|
||||
}
|
||||
];
|
||||
|
||||
this.setPageOutputs({
|
||||
topPageName: this._pageNameForId(startPageId),
|
||||
stackDepth: this._internal.stack.length
|
||||
});
|
||||
},
|
||||
getRelativeURL() {
|
||||
var top = this._internal.stack[this._internal.stack.length - 1];
|
||||
if (top === undefined) return;
|
||||
|
||||
var urlPath = top.pageInfo.path;
|
||||
if (urlPath === undefined) {
|
||||
var pageItem = this._internal.pages.find((p) => p.id == top.pageId);
|
||||
if (pageItem === undefined) return;
|
||||
|
||||
urlPath = pageItem.label.replace(/\s+/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
// First add matching parameters to path
|
||||
var paramsInPath = urlPath.match(/{([^}]+)}/g);
|
||||
|
||||
var params = Object.assign({}, top.params);
|
||||
if (paramsInPath) {
|
||||
for (var param of paramsInPath) {
|
||||
var key = param.replace(/[{}]/g, '');
|
||||
if (top.params[key] !== undefined) {
|
||||
urlPath = urlPath.replace(param, encodeURIComponent(params[key]));
|
||||
delete params[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add other paramters as query
|
||||
var query = [];
|
||||
for (var key in params) {
|
||||
query.push({
|
||||
name: key,
|
||||
value: params[key]
|
||||
});
|
||||
}
|
||||
|
||||
if (urlPath.startsWith('/')) urlPath = urlPath.substring(1);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
query: query
|
||||
};
|
||||
},
|
||||
getNavigationAbsoluteURL() {
|
||||
var parent = this.parent;
|
||||
|
||||
while (parent !== undefined && typeof parent.getNavigationAbsoluteURL !== 'function') {
|
||||
parent = parent.getVisualParentNode();
|
||||
}
|
||||
|
||||
if (parent === undefined) {
|
||||
var parentUrl = { path: '', query: [] };
|
||||
} else {
|
||||
var parentUrl = parent.getNavigationAbsoluteURL();
|
||||
}
|
||||
|
||||
var thisUrl = this.getRelativeURL();
|
||||
if (thisUrl === undefined) return parentUrl;
|
||||
else {
|
||||
return {
|
||||
path: parentUrl.path + (parentUrl.path.endsWith('/') ? '' : '/') + thisUrl.path,
|
||||
query: parentUrl.query.concat(thisUrl.query)
|
||||
};
|
||||
}
|
||||
},
|
||||
_getLocationPath: function () {
|
||||
var navigationPathType = NoodlRuntime.instance.getProjectSettings()['navigationPathType'];
|
||||
if (navigationPathType === undefined || navigationPathType === 'hash') {
|
||||
// Use hash as path
|
||||
var hash = location.hash;
|
||||
if (hash) {
|
||||
if (hash[0] === '#') hash = hash.substring(1);
|
||||
if (hash[0] === '/') hash = hash.substring(1);
|
||||
}
|
||||
return hash;
|
||||
} else {
|
||||
// Use url as path
|
||||
var path = location.pathname;
|
||||
if (path) {
|
||||
if (path[0] === '/') path = path.substring(1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
},
|
||||
_getSearchParams: function () {
|
||||
var match,
|
||||
pl = /\+/g, // Regex for replacing addition symbol with a space
|
||||
search = /([^&=]+)=?([^&]*)/g,
|
||||
decode = function (s) {
|
||||
return decodeURIComponent(s.replace(pl, ' '));
|
||||
},
|
||||
query = window.location.search.substring(1);
|
||||
|
||||
var urlParams = {};
|
||||
while ((match = search.exec(query))) urlParams[decode(match[1])] = decode(match[2]);
|
||||
|
||||
return urlParams;
|
||||
},
|
||||
getNavigationRemainingPath() {
|
||||
return this._internal.remainingNavigationPath;
|
||||
},
|
||||
matchPageFromUrl(url) {
|
||||
if (!this._internal.useRoutes) return;
|
||||
if (this._internal.pages === undefined || this._internal.pages.length === 0) return;
|
||||
|
||||
// Attempt to find relative path from closest navigation parent
|
||||
var parent = this.parent;
|
||||
while (parent !== undefined && typeof parent.getNavigationRemainingPath !== 'function') {
|
||||
parent = parent.getVisualParentNode();
|
||||
}
|
||||
|
||||
if (parent === undefined) {
|
||||
var urlPath = this._getLocationPath();
|
||||
if (urlPath[0] === '/') urlPath = urlPath.substring(1);
|
||||
var pathParts = urlPath.split('/');
|
||||
} else {
|
||||
var pathParts = parent.getNavigationRemainingPath();
|
||||
}
|
||||
if (pathParts === undefined) return;
|
||||
|
||||
var urlQuery = this._getSearchParams();
|
||||
|
||||
function _matchPathParts(path, pattern) {
|
||||
var params = {};
|
||||
for (var i = 0; i < pattern.length; i++) {
|
||||
if (path[i] === undefined) return;
|
||||
|
||||
var _p = pattern[i];
|
||||
if (_p[0] === '{' && _p[_p.length - 1] === '}') {
|
||||
// This is a param, collect it
|
||||
params[_p.substring(1, _p.length - 1)] = decodeURIComponent(path[i]);
|
||||
} else if (_p !== path[i]) return;
|
||||
}
|
||||
return {
|
||||
params: params,
|
||||
remainingPathParts: path.splice(pattern.length)
|
||||
};
|
||||
}
|
||||
|
||||
for (var page of this._internal.pages) {
|
||||
var pageInfo = this._internal.pageInfo[page.id];
|
||||
if (pageInfo === undefined) continue;
|
||||
|
||||
var pagePattern = pageInfo.path;
|
||||
if (pagePattern === undefined) {
|
||||
pagePattern = page.label.replace(/\s+/g, '-').toLowerCase();
|
||||
}
|
||||
if (pagePattern[0] === '/') pagePattern = pagePattern.substring(1);
|
||||
|
||||
let match = _matchPathParts(pathParts, pagePattern.split('/'));
|
||||
if (match) {
|
||||
// This page is a match
|
||||
this._internal.remainingNavigationPath = match.remainingPathParts;
|
||||
return {
|
||||
pageId: page.id,
|
||||
params: match.params,
|
||||
query: urlQuery
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
_updateUrlWithTopPage() {
|
||||
// Push the state to the browser url
|
||||
if (this._internal.useRoutes && window.history !== undefined) {
|
||||
var url = this.getNavigationAbsoluteURL();
|
||||
|
||||
var urlPath, hashPath;
|
||||
var navigationPathType = NoodlRuntime.instance.getProjectSettings()['navigationPathType'];
|
||||
if (navigationPathType === undefined || navigationPathType === 'hash') hashPath = url.path;
|
||||
else urlPath = url.path;
|
||||
|
||||
var query = url.query.map((q) => q.name + '=' + q.value);
|
||||
var compiledUrl =
|
||||
(urlPath !== undefined ? urlPath : '') +
|
||||
(query.length >= 1 ? '?' + query.join('&') : '') +
|
||||
(hashPath !== undefined ? '#' + hashPath : '');
|
||||
|
||||
this._internal.remainingNavigationPath = undefined; // Reset remaining nav path
|
||||
// console.log(compiledUrl);
|
||||
window.history.pushState({}, '', compiledUrl);
|
||||
}
|
||||
},
|
||||
replace(args) {
|
||||
this._internal.asyncQueue.enqueue(this.replaceAsync.bind(this, args));
|
||||
},
|
||||
async replaceAsync(args) {
|
||||
if (this._internal.pages === undefined || this._internal.pages.length === 0) return;
|
||||
|
||||
if (this._internal.isTransitioning) return;
|
||||
|
||||
var pageId = args.target || this._internal.pages[0].id;
|
||||
var pageInfo = this._internal.pageInfo[pageId];
|
||||
if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page
|
||||
|
||||
// Remove all current pages in the stack
|
||||
var children = this.getChildren();
|
||||
for (var i in children) {
|
||||
var c = children[i];
|
||||
this.removeChild(c);
|
||||
this.nodeScope.deleteNode(c);
|
||||
}
|
||||
|
||||
const group = this.createPageContainer();
|
||||
|
||||
// Create the page content
|
||||
const content = await this.nodeScope.createNode(pageInfo.component, guid());
|
||||
for (var key in args.params) {
|
||||
content.setInputValue(key, args.params[key]);
|
||||
}
|
||||
group.addChild(content);
|
||||
|
||||
this.addChild(group);
|
||||
|
||||
// Replace stack
|
||||
this._internal.stack = [
|
||||
{
|
||||
from: null,
|
||||
page: group,
|
||||
pageId: pageId,
|
||||
pageInfo: pageInfo,
|
||||
params: args.params,
|
||||
componentName: args.target
|
||||
}
|
||||
];
|
||||
|
||||
this.setPageOutputs({
|
||||
topPageName: this._pageNameForId(pageId),
|
||||
stackDepth: this._internal.stack.length
|
||||
});
|
||||
|
||||
this._updateUrlWithTopPage();
|
||||
|
||||
args.hasNavigated && args.hasNavigated();
|
||||
},
|
||||
navigate(args) {
|
||||
this._internal.asyncQueue.enqueue(this.navigateAsync.bind(this, args));
|
||||
},
|
||||
async navigateAsync(args) {
|
||||
if (this._internal.pages === undefined || this._internal.pages.length === 0) return;
|
||||
|
||||
if (this._internal.isTransitioning) return;
|
||||
|
||||
var pageId = args.target || this._internal.pages[0].id;
|
||||
var pageInfo = this._internal.pageInfo[pageId];
|
||||
if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page
|
||||
|
||||
// Create the container group
|
||||
const group = this.createPageContainer();
|
||||
group.setInputValue('position', 'absolute');
|
||||
|
||||
// Create the page content
|
||||
const content = await this.nodeScope.createNode(pageInfo.component, guid());
|
||||
for (var key in args.params) {
|
||||
content.setInputValue(key, args.params[key]);
|
||||
}
|
||||
group.addChild(content);
|
||||
|
||||
// Connect navigate back nodes
|
||||
var navigateBackNodes = content.nodeScope.getNodesWithType('PageStackNavigateBack');
|
||||
if (navigateBackNodes && navigateBackNodes.length > 0) {
|
||||
for (var j = 0; j < navigateBackNodes.length; j++) {
|
||||
navigateBackNodes[j]._setBackCallback(this.back.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new top
|
||||
var top = this._internal.stack[this._internal.stack.length - 1];
|
||||
var newTop = {
|
||||
from: top.page,
|
||||
page: group,
|
||||
pageInfo: pageInfo,
|
||||
pageId: pageId,
|
||||
params: args.params,
|
||||
transition: new Transitions[args.transition.type || 'Push'](top.page, group, args.transition),
|
||||
backCallback: args.backCallback,
|
||||
componentName: args.target
|
||||
};
|
||||
this._internal.stack.push(newTop);
|
||||
this.setPageOutputs({
|
||||
topPageName: this._pageNameForId(args.target),
|
||||
stackDepth: this._internal.stack.length
|
||||
});
|
||||
this._updateUrlWithTopPage();
|
||||
|
||||
newTop.transition.forward(0);
|
||||
|
||||
this._internal.isTransitioning = true;
|
||||
newTop.transition.start({
|
||||
end: () => {
|
||||
this._internal.isTransitioning = false;
|
||||
|
||||
// Transition has completed, remove the previous top from the stack
|
||||
this.removeChild(top.page);
|
||||
group.setInputValue('position', 'relative');
|
||||
}
|
||||
});
|
||||
|
||||
this.addChild(group);
|
||||
|
||||
args.hasNavigated && args.hasNavigated();
|
||||
},
|
||||
back(args) {
|
||||
if (this._internal.stack.length <= 1) return;
|
||||
if (this._internal.isTransitioning) return;
|
||||
|
||||
var top = this._internal.stack[this._internal.stack.length - 1];
|
||||
|
||||
top.page.setInputValue('position', 'absolute');
|
||||
// Insert the destination in the stack again
|
||||
this.addChild(top.from, 0);
|
||||
top.backCallback && top.backCallback(args.backAction, args.results);
|
||||
|
||||
this.setPageOutputs({
|
||||
topPageName: this._pageNameForId(this._internal.stack[this._internal.stack.length - 2].pageId),
|
||||
stackDepth: this._internal.stack.length - 1
|
||||
});
|
||||
|
||||
this._internal.isTransitioning = true;
|
||||
top.transition.start({
|
||||
end: () => {
|
||||
this._internal.isTransitioning = false;
|
||||
|
||||
top.page.setInputValue('position', 'relative');
|
||||
this.removeChild(top.page);
|
||||
this.nodeScope.deleteNode(top.page);
|
||||
this._internal.stack.pop();
|
||||
|
||||
this._updateUrlWithTopPage();
|
||||
},
|
||||
back: true
|
||||
});
|
||||
},
|
||||
setPageComponent(pageId, component) {
|
||||
var internal = this._internal;
|
||||
if (!internal.pageInfo[pageId]) internal.pageInfo[pageId] = {};
|
||||
internal.pageInfo[pageId].component = component;
|
||||
|
||||
// this.scheduleRefresh();
|
||||
},
|
||||
setPagePath(pageId, path) {
|
||||
var internal = this._internal;
|
||||
if (!internal.pageInfo[pageId]) internal.pageInfo[pageId] = {};
|
||||
internal.pageInfo[pageId].path = path;
|
||||
|
||||
// this.scheduleRefresh();
|
||||
},
|
||||
setStartPage(pageId) {
|
||||
this._internal.startPageId = pageId;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('pageComp-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setPageComponent.bind(this, name.substring('pageComp-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('pagePath-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setPagePath.bind(this, name.substring('pagePath-'.length))
|
||||
});
|
||||
|
||||
if (name === 'startPage')
|
||||
return this.registerInput(name, {
|
||||
set: this.setStartPage.bind(this)
|
||||
});
|
||||
}
|
||||
},
|
||||
setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
if (node.parameters['pages'] !== undefined && node.parameters['pages'].length > 0) {
|
||||
node.parameters['pages'].forEach((p) => {
|
||||
// Component for page
|
||||
ports.push({
|
||||
name: 'pageComp-' + p.id,
|
||||
displayName: 'Component',
|
||||
editorName: p.label + ' | Component',
|
||||
plug: 'input',
|
||||
type: 'component',
|
||||
parent: 'pages',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Path for page
|
||||
if (node.parameters['useRoutes'] === true) {
|
||||
ports.push({
|
||||
name: 'pagePath-' + p.id,
|
||||
displayName: 'Path',
|
||||
editorName: p.label + ' | Path',
|
||||
plug: 'input',
|
||||
type: 'string',
|
||||
default: p.label.replace(/\s+/g, '-').toLowerCase(),
|
||||
parent: 'pages',
|
||||
parentItemId: p.id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: node.parameters['pages'].map((p) => ({
|
||||
label: p.label,
|
||||
value: p.id
|
||||
})),
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'General',
|
||||
displayName: 'Start Page',
|
||||
name: 'startPage',
|
||||
default: node.parameters['pages'][0].id
|
||||
});
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
if (ev.name === 'pages' || ev.name === 'useRoutes') _updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.Page Stack', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('Page Stack')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default createNodeFromReactComponent(PageStack);
|
||||
103
packages/noodl-viewer-react/src/nodes/navigation/page-inputs.js
Normal file
103
packages/noodl-viewer-react/src/nodes/navigation/page-inputs.js
Normal file
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
const PageInputsNode = {
|
||||
name: 'PageInputs',
|
||||
displayNodeName: 'Page Inputs',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/page-inputs',
|
||||
color: 'component',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.params = {};
|
||||
},
|
||||
inputs: {
|
||||
pathParams: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Path Parameters'
|
||||
},
|
||||
queryParams: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
group: 'Query Parameters'
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
_setPageParams: function (params) {
|
||||
for (var key in params) {
|
||||
this._internal.params[key] = params[key];
|
||||
if (this.hasOutput('pm-' + key)) this.flagOutputDirty('pm-' + key);
|
||||
}
|
||||
},
|
||||
getPageParam: function (name) {
|
||||
return this._internal.params[name];
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('pm-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getPageParam.bind(this, name.substring('pm-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: PageInputsNode
|
||||
/* setup: function setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = []
|
||||
|
||||
const uniqueNames = {}
|
||||
|
||||
if(node.parameters['pathParams'] !== undefined) {
|
||||
node.parameters['pathParams'].split(',').forEach((p) => {
|
||||
uniqueNames[p] = true
|
||||
})
|
||||
}
|
||||
|
||||
if(node.parameters['queryParams'] !== undefined) {
|
||||
node.parameters['queryParams'].split(',').forEach((p) => {
|
||||
uniqueNames[p] = true
|
||||
})
|
||||
}
|
||||
|
||||
Object.keys(uniqueNames).forEach((outputName) => {
|
||||
ports.push({
|
||||
name: 'pm-' + outputName,
|
||||
displayName: outputName,
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
group: 'Parameters'
|
||||
})
|
||||
})
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
// Track page node in this component, update if there are any changes
|
||||
_updatePorts();
|
||||
node.on("parameterUpdated", function (event) {
|
||||
_updatePorts();
|
||||
})
|
||||
}
|
||||
|
||||
graphModel.on("editorImportComplete", ()=> {
|
||||
graphModel.on("nodeAdded.PageInputs", function (node) {
|
||||
_managePortsForNode(node)
|
||||
})
|
||||
|
||||
for(const node of graphModel.getNodesWithType('PageInputs')) {
|
||||
_managePortsForNode(node)
|
||||
}
|
||||
})
|
||||
}*/
|
||||
};
|
||||
220
packages/noodl-viewer-react/src/nodes/navigation/page.js
Normal file
220
packages/noodl-viewer-react/src/nodes/navigation/page.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { META_TAGS, Page } from '../../components/navigation/Page';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const PageNode = {
|
||||
name: 'Page',
|
||||
displayNodeName: 'Page',
|
||||
category: 'Visuals',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/page',
|
||||
useVariants: false,
|
||||
mountedInput: false,
|
||||
allowAsExportRoot: false,
|
||||
singleton: true,
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Mounted']
|
||||
},
|
||||
initialize: function () {
|
||||
this.props.layout = 'column'; //this allows the children to know what type of layout type they're in
|
||||
if (this.isInputConnected('onPageReady')) {
|
||||
this.nodeScope.context.eventEmitter.emit('SSR_PageLoading', this.id);
|
||||
}
|
||||
},
|
||||
defaultCss: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
alignItems: 'flex-start',
|
||||
flex: '1 1',
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
getReactComponent() {
|
||||
return Page;
|
||||
},
|
||||
inputs: {
|
||||
// TODO: Enable with SSR
|
||||
// onPageReady: {
|
||||
// displayName: 'Page Ready',
|
||||
// type: 'signal',
|
||||
// valueChangedToTrue() {
|
||||
// this.nodeScope.context.eventEmitter.emit('SSR_PageReady', this.id);
|
||||
// }
|
||||
// },
|
||||
sitemapIncluded: {
|
||||
index: 80001,
|
||||
displayName: 'Included',
|
||||
group: 'Experimental Sitemap',
|
||||
default: true,
|
||||
type: {
|
||||
name: 'boolean',
|
||||
allowEditOnly: true
|
||||
}
|
||||
},
|
||||
sitemapChangefreq: {
|
||||
index: 80002,
|
||||
displayName: 'Change Freq',
|
||||
group: 'Experimental Sitemap',
|
||||
default: 'weekly',
|
||||
type: {
|
||||
name: 'enum',
|
||||
allowEditOnly: true,
|
||||
enums: [
|
||||
{ label: 'always', value: 'always' },
|
||||
{ label: 'hourly', value: 'hourly' },
|
||||
{ label: 'daily', value: 'daily' },
|
||||
{ label: 'weekly', value: 'weekly' },
|
||||
{ label: 'monthly', value: 'monthly' },
|
||||
{ label: 'yearly', value: 'yearly' },
|
||||
{ label: 'never', value: 'never' }
|
||||
]
|
||||
}
|
||||
},
|
||||
sitemapPriority: {
|
||||
index: 80003,
|
||||
displayName: 'Priority',
|
||||
group: 'Experimental Sitemap',
|
||||
default: 0.5,
|
||||
type: {
|
||||
name: 'number',
|
||||
allowEditOnly: true
|
||||
}
|
||||
}
|
||||
// NOTE: Hide this for now, this is going to be important for SSR
|
||||
// sitemapScript: {
|
||||
// index: 80004,
|
||||
// displayName: 'Script',
|
||||
// group: 'Sitemap',
|
||||
// type: {
|
||||
// name: 'string',
|
||||
// allowEditOnly: true,
|
||||
// codeeditor: 'javascript'
|
||||
// }
|
||||
// }
|
||||
},
|
||||
inputProps: META_TAGS.reduce((result, x, index) => {
|
||||
result[x.key] = {
|
||||
index: 80000 + index,
|
||||
displayName: x.displayName,
|
||||
editorName: x.editorName || x.displayName,
|
||||
propPath: 'metatags',
|
||||
group: x.group,
|
||||
popout: x.popout,
|
||||
type: x.type || 'string'
|
||||
};
|
||||
return result;
|
||||
}, {}),
|
||||
methods: {
|
||||
getUrlPath: function () {
|
||||
return this._internal.urlPath;
|
||||
},
|
||||
getTitle: function () {
|
||||
return this._internal.title;
|
||||
},
|
||||
/* setRouter:function(value) {
|
||||
this._internal.router = value
|
||||
},*/
|
||||
setTitle: function (value) {
|
||||
this._internal.title = value;
|
||||
},
|
||||
setUrlPath: function (value) {
|
||||
this._internal.urlPath = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* if (name === 'router') return this.registerInput(name, {
|
||||
set: this.setRouter.bind(this)
|
||||
})*/
|
||||
|
||||
if (name === 'title')
|
||||
return this.registerInput(name, {
|
||||
set: this.setTitle.bind(this)
|
||||
});
|
||||
|
||||
if (name === 'urlPath')
|
||||
return this.registerInput(name, {
|
||||
set: this.setUrlPath.bind(this)
|
||||
});
|
||||
}
|
||||
},
|
||||
setup(context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
// Show router selector if more that one
|
||||
/* var routers = graphModel.getNodesWithType('Router')
|
||||
if(routers.length > 1) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: routers.map((r) => ({ label: (r.parameters['name'] || 'Main'), value: (r.parameters['name'] || 'Main') })), allowEditOnly: true },
|
||||
group: 'General',
|
||||
displayName: 'Router',
|
||||
name: 'router',
|
||||
default:'Main'
|
||||
})
|
||||
}*/
|
||||
|
||||
// Title and urlpath ports
|
||||
const titleParts = node.component.name.split('/');
|
||||
ports.push({
|
||||
name: 'title',
|
||||
displayName: 'Title',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
plug: 'input',
|
||||
default: titleParts[titleParts.length - 1]
|
||||
});
|
||||
|
||||
const title = node.parameters['title'] || titleParts[titleParts.length - 1];
|
||||
const defaultUrlPath = title.replace(/\s+/g, '-').toLowerCase();
|
||||
ports.push({
|
||||
name: 'urlPath',
|
||||
displayName: 'Url Path',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
plug: 'input',
|
||||
default: defaultUrlPath
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
if (ev.name === 'title') _updatePorts();
|
||||
});
|
||||
|
||||
// graphModel.on("nodeAdded.Router", _updatePorts)
|
||||
// graphModel.on("nodeWasRemoved.Router", _updatePorts)
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.Page', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
graphModel.on('componentRenamed', function (component) {
|
||||
const page = graphModel.getNodesWithType('Page').filter((x) => component.roots.includes(x.id));
|
||||
if (page.length > 0) {
|
||||
_managePortsForNode(page[0]);
|
||||
}
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('Page')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//NodeSharedPortDefinitions.addSharedVisualInputs(PageNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(PageNode);
|
||||
|
||||
export default createNodeFromReactComponent(PageNode);
|
||||
@@ -0,0 +1,134 @@
|
||||
import NoodlRuntime from '@noodl/runtime';
|
||||
|
||||
import type { NodeConstructor } from '../../../typings/global';
|
||||
|
||||
export type NavigateArgs = {
|
||||
target: string;
|
||||
params: Record<string, string | number | boolean>;
|
||||
openInNewTab?: boolean;
|
||||
hasNavigated: () => void;
|
||||
};
|
||||
|
||||
export type ComponentPageInfo = {
|
||||
path: string;
|
||||
title: string;
|
||||
component: string;
|
||||
};
|
||||
|
||||
export class RouterHandler {
|
||||
static instance = new RouterHandler();
|
||||
|
||||
_routers: Record<string, any>;
|
||||
_navigationQueue: any[];
|
||||
|
||||
constructor() {
|
||||
this._routers = {};
|
||||
this._navigationQueue = [];
|
||||
}
|
||||
|
||||
navigate(name: string, args: NavigateArgs) {
|
||||
//add a 1ms timeout so other nodes have a chance to run before the page is destroyed.
|
||||
setTimeout(() => {
|
||||
const routerNames = Object.keys(this._routers);
|
||||
if (routerNames.length === 1) {
|
||||
name = routerNames[0];
|
||||
}
|
||||
|
||||
if (this._routers[name]) {
|
||||
for (const router of this._routers[name]) {
|
||||
router.navigate(args);
|
||||
}
|
||||
} else {
|
||||
this._navigationQueue.push({ name, args });
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
|
||||
registerRouter(name: string, router: NodeConstructor) {
|
||||
name = name || 'Main';
|
||||
if (!this._routers[name]) {
|
||||
this._routers[name] = [];
|
||||
}
|
||||
|
||||
this._routers[name].push(router);
|
||||
|
||||
let hasNavigated = false;
|
||||
|
||||
let i = 0;
|
||||
while (i < this._navigationQueue.length) {
|
||||
const e = this._navigationQueue[i];
|
||||
if (e.name === name) {
|
||||
router.navigate(e.args);
|
||||
hasNavigated = true;
|
||||
this._navigationQueue.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNavigated) {
|
||||
router.reset(); //no navigation has happened, call reset() so the start page is created
|
||||
}
|
||||
}
|
||||
|
||||
deregisterRouter(name: string, router: NodeConstructor) {
|
||||
name = name || 'Main';
|
||||
|
||||
if (!this._routers[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._routers[name].indexOf(router);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._routers[name].splice(index, 1);
|
||||
if (this._routers[name].length === 0) {
|
||||
delete this._routers[name];
|
||||
}
|
||||
}
|
||||
|
||||
getPagesForRouter(name) {
|
||||
const routerIndex = NoodlRuntime.instance.graphModel.routerIndex;
|
||||
const routers = routerIndex.routers;
|
||||
|
||||
if (routers === undefined || routers.length === 0) return [];
|
||||
|
||||
const matchingRouters = name === undefined ? [routers[0]] : routers.filter((r) => r.name === name);
|
||||
|
||||
const pageComponents = new Set();
|
||||
matchingRouters.forEach((r) => {
|
||||
const pages = r.pages;
|
||||
if (pages !== undefined && pages.routes !== undefined) {
|
||||
pages.routes.forEach((r) => {
|
||||
pageComponents.add(r);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(pageComponents)
|
||||
.map((c) => this.getPageInfoForComponent(String(c)))
|
||||
.filter((item) => !!item);
|
||||
}
|
||||
|
||||
getPageInfoForComponent(componentName: string): ComponentPageInfo | undefined {
|
||||
const routerIndex = NoodlRuntime.instance.graphModel.routerIndex;
|
||||
const page = routerIndex.pages.find((p) => p.component === componentName);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Occurs when the Router navigates to a new page.
|
||||
*
|
||||
* @param routerName
|
||||
* @param page
|
||||
*/
|
||||
onNavigated(routerName: string, page: ComponentPageInfo) {
|
||||
// @ts-expect-error window.Noodl is not defined
|
||||
window.Noodl.Events.emit('NoodlApp_Navigated', {
|
||||
routerName,
|
||||
...page
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
const { RouterHandler } = require('./router-handler');
|
||||
|
||||
const RouterNavigate = {
|
||||
name: 'RouterNavigate',
|
||||
displayNodeName: 'Navigate',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/navigate',
|
||||
initialize: function () {
|
||||
this._internal.pageParams = {};
|
||||
this._internal.openInNewTab = false;
|
||||
},
|
||||
inputs: {
|
||||
navigate: {
|
||||
displayName: 'Navigate',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNavigate();
|
||||
}
|
||||
},
|
||||
openInNewTab: {
|
||||
index: 10,
|
||||
displayName: 'Open in new tab',
|
||||
group: 'General',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
set(value) {
|
||||
this._internal.openInNewTab = !!value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
navigated: {
|
||||
type: 'signal',
|
||||
displayName: 'Navigated',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleNavigate: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledNavigate) {
|
||||
internal.hasScheduledNavigate = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.hasScheduledNavigate = false;
|
||||
this.navigate();
|
||||
});
|
||||
}
|
||||
},
|
||||
navigate() {
|
||||
RouterHandler.instance.navigate(this._internal.router, {
|
||||
target: this._internal.target,
|
||||
params: this._internal.pageParams,
|
||||
openInNewTab: this._internal.openInNewTab,
|
||||
hasNavigated: () => {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.sendSignalOnOutput('navigated');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setPageParam: function (param, value) {
|
||||
this._internal.pageParams[param] = value;
|
||||
},
|
||||
setTargetPage: function (page) {
|
||||
this._internal.target = page;
|
||||
},
|
||||
setRouter: function (value) {
|
||||
this._internal.router = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'target') {
|
||||
return this.registerInput(name, {
|
||||
set: this.setTargetPage.bind(this)
|
||||
});
|
||||
} else if (name === 'router') {
|
||||
return this.registerInput(name, {
|
||||
set: this.setRouter.bind(this)
|
||||
});
|
||||
} else if (name.startsWith('pm-')) {
|
||||
return this.registerInput(name, {
|
||||
set: this.setPageParam.bind(this, name.substring('pm-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: RouterNavigate
|
||||
};
|
||||
628
packages/noodl-viewer-react/src/nodes/navigation/router.tsx
Normal file
628
packages/noodl-viewer-react/src/nodes/navigation/router.tsx
Normal file
@@ -0,0 +1,628 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import NoodlRuntime from '@noodl/runtime';
|
||||
|
||||
import ASyncQueue from '../../async-queue';
|
||||
import guid from '../../guid';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import { Noodl, Slot } from '../../types';
|
||||
import { ComponentPageInfo, NavigateArgs, RouterHandler } from './router-handler';
|
||||
|
||||
export interface RouterReactComponentProps extends Noodl.ReactProps {
|
||||
didMount: () => void;
|
||||
willUnmount: () => void;
|
||||
|
||||
children: Slot;
|
||||
}
|
||||
|
||||
function RouterReactComponent(props: RouterReactComponentProps) {
|
||||
const { didMount, willUnmount, children, style } = props;
|
||||
|
||||
useEffect(() => {
|
||||
didMount();
|
||||
return () => {
|
||||
willUnmount();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={props.className} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _trimUrlPart(url) {
|
||||
if (url[0] === '/') url = url.substring(1);
|
||||
if (url[url.length - 1] === '/') url = url.substring(0, url.length - 1);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getBaseUrlLength(url: string): number {
|
||||
// If the URL is a full URL, then we only want to get the pathname.
|
||||
// Otherwise we just return the url length which should be the pathname.
|
||||
if (!url.startsWith('/')) {
|
||||
try {
|
||||
// Lets say the baseUrl is:
|
||||
// "https://collar-zippy-overcome.sandbox.noodl.app/my-folder"
|
||||
// Then we want to remove "/my-folder" from the substring
|
||||
return new URL(url).pathname.length;
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
return url.length;
|
||||
}
|
||||
|
||||
const RouterNode = {
|
||||
name: 'Router',
|
||||
displayNodeName: 'Page Router',
|
||||
category: 'Visuals',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/page-router',
|
||||
useVariants: false,
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Actions', 'Events', 'Mounted']
|
||||
},
|
||||
initialize: function () {
|
||||
this._internal.asyncQueue = new ASyncQueue();
|
||||
|
||||
this.onScheduleReset = () => {
|
||||
this.scheduleReset();
|
||||
};
|
||||
|
||||
this.props.didMount = () => {
|
||||
this._internal.isMounted = true;
|
||||
|
||||
// SSR Support
|
||||
if (typeof window !== 'undefined') {
|
||||
// Listen to push state events and update stack
|
||||
if (window.history && window.history.pushState) {
|
||||
//this is event is manually sent on push, so covers both pop and push state
|
||||
window.addEventListener('popstate', this.onScheduleReset);
|
||||
} else {
|
||||
// Only hash support
|
||||
window.addEventListener('hashchange', this.onScheduleReset);
|
||||
}
|
||||
}
|
||||
|
||||
this._registerRouter();
|
||||
};
|
||||
|
||||
this.props.willUnmount = () => {
|
||||
this._internal.isMounted = false;
|
||||
|
||||
window.removeEventListener('popstate', this.onScheduleReset);
|
||||
window.removeEventListener('hashchange', this.onScheduleReset);
|
||||
|
||||
this._deregisterRouter();
|
||||
};
|
||||
|
||||
this.props.layout = 'column';
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.currentUrl;
|
||||
},
|
||||
defaultCss: {
|
||||
flex: '1 1',
|
||||
alignSelf: 'stretch',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
getReactComponent() {
|
||||
return RouterReactComponent;
|
||||
},
|
||||
inputs: {
|
||||
name: {
|
||||
type: 'string',
|
||||
displayName: 'Name',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._deregisterRouter();
|
||||
this._internal.name = value;
|
||||
|
||||
if (this._internal.isMounted) {
|
||||
this._registerRouter();
|
||||
}
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
type: { name: 'pages', allowEditOnly: true },
|
||||
displayName: 'Pages',
|
||||
group: 'Pages',
|
||||
set: function (value) {
|
||||
this._internal.pages = value;
|
||||
if (this._internal.isMounted) {
|
||||
this.scheduleReset();
|
||||
}
|
||||
}
|
||||
},
|
||||
urlPath: {
|
||||
type: 'string',
|
||||
displayName: 'Url path',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.urlPath = value;
|
||||
}
|
||||
},
|
||||
clip: {
|
||||
displayName: 'Clip Behavior',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ value: 'contentHeight', label: 'Expand to content size' },
|
||||
{ value: 'scroll', label: 'Scroll' },
|
||||
{ value: 'clip', label: 'Clip content' }
|
||||
]
|
||||
},
|
||||
group: 'Layout',
|
||||
default: 'contentHeight',
|
||||
set(value) {
|
||||
switch (value) {
|
||||
case 'scroll':
|
||||
this.setStyle({ overflow: 'auto' });
|
||||
break;
|
||||
case 'clip':
|
||||
this.setStyle({ overflow: 'hidden' });
|
||||
break;
|
||||
default:
|
||||
this.removeStyle(['overflow']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
type: 'signal',
|
||||
displayName: 'Reset',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleReset();
|
||||
}
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
default: 'transparent',
|
||||
applyDefault: false
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentPageTitle: {
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
displayName: 'Current Page Title',
|
||||
getter: function () {
|
||||
return this._internal.currentPage !== undefined ? this._internal.currentPage.title : undefined;
|
||||
}
|
||||
},
|
||||
currentPageComponent: {
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
displayName: 'Current Page Component',
|
||||
getter: function () {
|
||||
return this._internal.currentPage !== undefined ? this._internal.currentPage.component : undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_registerRouter() {
|
||||
RouterHandler.instance.registerRouter(this._internal.name, this);
|
||||
},
|
||||
_deregisterRouter() {
|
||||
RouterHandler.instance.deregisterRouter(this._internal.name, this);
|
||||
},
|
||||
setPageOutputs(outputs) {
|
||||
for (const prop in outputs) {
|
||||
this._internal[prop] = outputs[prop];
|
||||
this.flagOutputDirty(prop);
|
||||
}
|
||||
},
|
||||
scheduleReset() {
|
||||
const internal = this._internal;
|
||||
if (!internal.hasScheduledReset) {
|
||||
internal.hasScheduledReset = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
internal.hasScheduledReset = false;
|
||||
this.reset();
|
||||
});
|
||||
}
|
||||
},
|
||||
createPageContainer() {
|
||||
const group = this.nodeScope.createPrimitiveNode('Group');
|
||||
group.setStyle({ flex: '1 0 100%' });
|
||||
return group;
|
||||
},
|
||||
reset() {
|
||||
this._internal.asyncQueue.enqueue(this.resetAsync.bind(this));
|
||||
},
|
||||
scrollToTop() {
|
||||
const dom = this.getDOMElement();
|
||||
if (dom) {
|
||||
dom.scrollTop = 0;
|
||||
|
||||
if (NoodlRuntime.instance.getProjectSettings().bodyScroll) {
|
||||
//Automatically scroll the page router into view in case it's currently outside the viewport
|
||||
//Might want to add an option to disable this
|
||||
dom.scrollIntoView();
|
||||
}
|
||||
}
|
||||
},
|
||||
async resetAsync() {
|
||||
let component: string;
|
||||
let params = {};
|
||||
|
||||
const matchFromUrl = this.matchPageFromUrl();
|
||||
if (matchFromUrl) {
|
||||
if (!matchFromUrl.page) {
|
||||
// Use the start page
|
||||
component = this._internal.pages !== undefined ? this._internal.pages.startPage : undefined;
|
||||
params = Object.assign({}, matchFromUrl.params, matchFromUrl.query);
|
||||
} else {
|
||||
// Use the matching page
|
||||
component = matchFromUrl.page.component;
|
||||
params = Object.assign({}, matchFromUrl.params, matchFromUrl.query);
|
||||
}
|
||||
} else {
|
||||
// TODO: Clean up matchPageFromUrl, in this case it returns undefined.
|
||||
// remainingNavigationPath is undefined, since it have to run
|
||||
// matchPageFromUrl to get the path. So it feels like it needs
|
||||
// some bigger refactoring to make it understandable.
|
||||
component = this._internal.pages.startPage;
|
||||
params = {};
|
||||
}
|
||||
|
||||
if (component === undefined) return; // No component specified for page
|
||||
|
||||
const currentPage = RouterHandler.instance.getPageInfoForComponent(component);
|
||||
|
||||
if (this._internal.currentPage === currentPage) {
|
||||
//already at the correct page, keep the current page
|
||||
//update page inputs if they have changed
|
||||
//TODO: fix if a parameter goes from a value to undefined, the old value will still exist in the connection from previous navigation
|
||||
if (!shallowObjectsEqual(this._internal.currentParams, params)) {
|
||||
this._internal.currentParams = params;
|
||||
this._updatePageInputs(this._internal.currentPageComponent.nodeScope, params);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollToTop();
|
||||
|
||||
// Reset the current page for this router
|
||||
// First remove all children
|
||||
const children = this.getChildren();
|
||||
for (const i in children) {
|
||||
const c = children[i];
|
||||
this.removeChild(c);
|
||||
this.nodeScope.deleteNode(c);
|
||||
}
|
||||
|
||||
const content = await this.nodeScope.createNode(component, guid());
|
||||
this._internal.currentPageComponent = content;
|
||||
|
||||
// Find the root page node
|
||||
const pageNodes = content.nodeScope.getNodesWithType('Page');
|
||||
if (pageNodes === undefined || pageNodes.length !== 1) {
|
||||
return; // Should be only one page root
|
||||
}
|
||||
|
||||
this._internal.currentPage = RouterHandler.instance.getPageInfoForComponent(component);
|
||||
this._internal.currentParams = params;
|
||||
|
||||
this.flagOutputDirty('currentPageTitle');
|
||||
this.flagOutputDirty('currentPageComponent');
|
||||
// @ts-expect-error Noodl is not defined
|
||||
Noodl.SEO.setTitle(this._internal.currentPage.title);
|
||||
|
||||
this._updatePageInputs(this._internal.currentPageComponent.nodeScope, params);
|
||||
|
||||
const group = this.createPageContainer();
|
||||
group.addChild(content);
|
||||
|
||||
this.addChild(group);
|
||||
|
||||
/* this.setPageOutputs({
|
||||
currentUrl: pageInfo.path,
|
||||
currentTitle: pageInfo.title
|
||||
});*/
|
||||
},
|
||||
_updatePageInputs(nodeScope, params) {
|
||||
for (const pageInputNode of nodeScope.getNodesWithType('PageInputs')) {
|
||||
pageInputNode._setPageParams(params);
|
||||
}
|
||||
},
|
||||
getRelativeURL(targetPage: ComponentPageInfo, pageParams: NavigateArgs['params']) {
|
||||
if (!targetPage) return;
|
||||
|
||||
let urlPath = targetPage.path;
|
||||
if (urlPath === undefined) return;
|
||||
|
||||
// First add matching parameters to path
|
||||
const paramsInPath = urlPath.match(/{([^}]+)}/g);
|
||||
|
||||
const params = Object.assign({}, pageParams);
|
||||
if (paramsInPath) {
|
||||
for (const param of paramsInPath) {
|
||||
const key = param.replace(/[{}]/g, '');
|
||||
if (pageParams[key] !== undefined) {
|
||||
urlPath = urlPath.replace(param, encodeURIComponent(params[key]));
|
||||
delete params[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add other paramters as query
|
||||
const query = [];
|
||||
for (const key in params) {
|
||||
query.push({
|
||||
name: key,
|
||||
value: encodeURIComponent(params[key])
|
||||
});
|
||||
}
|
||||
|
||||
urlPath = _trimUrlPart(urlPath);
|
||||
|
||||
// Prepend this routers url path if there is one
|
||||
if (this._internal.urlPath !== undefined) urlPath = _trimUrlPart(this._internal.urlPath) + '/' + urlPath;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
query: query
|
||||
};
|
||||
},
|
||||
getNavigationAbsoluteURL(targetPage: ComponentPageInfo, pageParams: NavigateArgs['params']) {
|
||||
let parent = this.parent;
|
||||
let parentUrl = { path: '', query: [] };
|
||||
|
||||
while (parent !== undefined && typeof parent.getNavigationAbsoluteURL !== 'function') {
|
||||
parent = parent.getVisualParentNode();
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parentUrl = parent.getNavigationAbsoluteURL(parent._internal.currentPage, parent._internal.currentParams);
|
||||
}
|
||||
|
||||
const thisUrl = this.getRelativeURL(targetPage, pageParams);
|
||||
if (thisUrl) {
|
||||
const haveForwardSlash = parentUrl.path.endsWith('/') || thisUrl.path.startsWith('/');
|
||||
return {
|
||||
path: parentUrl.path + (haveForwardSlash ? '' : '/') + thisUrl.path,
|
||||
query: parentUrl.query.concat(thisUrl.query)
|
||||
};
|
||||
}
|
||||
|
||||
return parentUrl;
|
||||
},
|
||||
_getLocationPath: function () {
|
||||
const navigationPathType = NoodlRuntime.instance.getProjectSettings()['navigationPathType'];
|
||||
if (navigationPathType === undefined || navigationPathType === 'hash') {
|
||||
// Use hash as path
|
||||
let hash = location.hash;
|
||||
if (hash) {
|
||||
if (hash[0] === '#') hash = hash.substring(1);
|
||||
if (hash[0] === '/') hash = hash.substring(1);
|
||||
}
|
||||
return decodeURI(hash);
|
||||
} else {
|
||||
// Use url as path
|
||||
let path = location.pathname;
|
||||
if (path) {
|
||||
if (path[0] === '/') {
|
||||
// @ts-expect-error missing Noodl typings
|
||||
const baseUrl = Noodl.Env['BaseUrl'];
|
||||
if (baseUrl) {
|
||||
const pathnameLength = getBaseUrlLength(baseUrl);
|
||||
path = path.substring(pathnameLength);
|
||||
} else {
|
||||
path = path.substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return decodeURI(path);
|
||||
}
|
||||
},
|
||||
_getSearchParams: function () {
|
||||
// Regex for replacing addition symbol with a space
|
||||
const pl = /\+/g;
|
||||
const search = /([^&=]+)=?([^&]*)/g;
|
||||
const decode = function (s) {
|
||||
return decodeURIComponent(s.replace(pl, ' '));
|
||||
};
|
||||
const query = location.search.substring(1);
|
||||
|
||||
let match: RegExpExecArray;
|
||||
|
||||
const urlParams = {};
|
||||
while ((match = search.exec(query))) urlParams[decode(match[1])] = decode(match[2]);
|
||||
|
||||
return urlParams;
|
||||
},
|
||||
getNavigationRemainingPath() {
|
||||
return this._internal.remainingNavigationPath;
|
||||
},
|
||||
matchPageFromUrl() {
|
||||
// Attempt to find relative path from closest navigation parent
|
||||
let parent = this.parent;
|
||||
while (parent !== undefined && typeof parent.getNavigationRemainingPath !== 'function') {
|
||||
parent = parent.getVisualParentNode();
|
||||
}
|
||||
|
||||
let pathParts = undefined;
|
||||
|
||||
// Either use current browser location if we have no parent, or use remaining path
|
||||
// from parent
|
||||
if (parent === undefined) {
|
||||
let urlPath = this._getLocationPath();
|
||||
if (urlPath[0] === '/') urlPath = urlPath.substring(1);
|
||||
pathParts = urlPath.split('/');
|
||||
} else {
|
||||
pathParts = parent.getNavigationRemainingPath();
|
||||
}
|
||||
if (pathParts === undefined) return;
|
||||
|
||||
const urlQuery = this._getSearchParams();
|
||||
|
||||
function _matchPathParts(path, pattern) {
|
||||
const params = {};
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const _p = pattern[i];
|
||||
if (_p[0] === '{' && _p[_p.length - 1] === '}') {
|
||||
// This is a param, collect it
|
||||
if (path[i] !== undefined) {
|
||||
params[_p.substring(1, _p.length - 1)] = decodeURIComponent(path[i]);
|
||||
}
|
||||
} else if (path[i] === undefined || _p !== path[i]) return;
|
||||
}
|
||||
return {
|
||||
params: params,
|
||||
remainingPathParts: path.slice().splice(pattern.length) // Make copy
|
||||
};
|
||||
}
|
||||
|
||||
const pages = RouterHandler.instance.getPagesForRouter(this._internal.name);
|
||||
if (pages === undefined || pages.length === 0) return;
|
||||
|
||||
let matchedPage,
|
||||
bestMatchLength = 9999;
|
||||
for (const pageInfo of pages) {
|
||||
let pagePattern = pageInfo.path;
|
||||
if (pagePattern === undefined) continue;
|
||||
pagePattern = _trimUrlPart(pagePattern);
|
||||
|
||||
// Prepend this routers url path if there is one
|
||||
if (this._internal.urlPath !== undefined)
|
||||
pagePattern = _trimUrlPart(this._internal.urlPath) + '/' + pagePattern;
|
||||
|
||||
const pagePatternParts = pagePattern.split('/');
|
||||
const match = _matchPathParts(pathParts, pagePatternParts);
|
||||
|
||||
const dist = Math.abs(pagePatternParts.length - pathParts.length);
|
||||
if (match && bestMatchLength > dist) {
|
||||
// This page is a match
|
||||
matchedPage = { match, pageInfo };
|
||||
bestMatchLength = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPage) {
|
||||
this._internal.remainingNavigationPath = matchedPage.match.remainingPathParts;
|
||||
return {
|
||||
page: matchedPage.pageInfo,
|
||||
params: matchedPage.match.params,
|
||||
query: urlQuery
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
page: undefined, // no matched page
|
||||
params: {},
|
||||
query: urlQuery
|
||||
};
|
||||
}
|
||||
},
|
||||
_getCompleteUrlToPage(page: ComponentPageInfo, pageParams: NavigateArgs['params']) {
|
||||
const url = this.getNavigationAbsoluteURL(page, pageParams);
|
||||
|
||||
let urlPath, hashPath;
|
||||
const navigationPathType = NoodlRuntime.instance.getProjectSettings()['navigationPathType'];
|
||||
if (navigationPathType === undefined || navigationPathType === 'hash') {
|
||||
hashPath = url.path;
|
||||
} else {
|
||||
urlPath = url.path;
|
||||
}
|
||||
|
||||
const query = url.query.map((q) => q.name + '=' + q.value);
|
||||
const compiledUrl =
|
||||
(urlPath !== undefined ? urlPath : '') +
|
||||
(query.length >= 1 ? '?' + query.join('&') : '') +
|
||||
(hashPath !== undefined ? '#' + hashPath : '');
|
||||
|
||||
return compiledUrl;
|
||||
},
|
||||
_updateUrlWithTopPage() {
|
||||
// Push the state to the browser url
|
||||
if (window.history !== undefined) {
|
||||
this._internal.remainingNavigationPath = undefined; // Reset remaining nav path
|
||||
|
||||
const url = this._getCompleteUrlToPage(this._internal.currentPage, this._internal.currentParams);
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
},
|
||||
navigate(args) {
|
||||
this._internal.asyncQueue.enqueue(this.navigateAsync.bind(this, args));
|
||||
},
|
||||
async navigateAsync(args: NavigateArgs) {
|
||||
if (args.target === undefined) return;
|
||||
|
||||
const newPage = RouterHandler.instance.getPageInfoForComponent(args.target);
|
||||
if (!newPage) return; //TODO: send error to editor, "invalid page component name"
|
||||
|
||||
if (args.openInNewTab) {
|
||||
const url = this._getCompleteUrlToPage(newPage, args.params);
|
||||
window.open(url, '_blank');
|
||||
args.hasNavigated && args.hasNavigated();
|
||||
} else {
|
||||
await this._navigateInCurrentWindow(newPage, args);
|
||||
}
|
||||
},
|
||||
async _navigateInCurrentWindow(newPage: ComponentPageInfo, args: NavigateArgs) {
|
||||
this.scrollToTop();
|
||||
|
||||
// Remove all current pages in the stack
|
||||
const children = this.getChildren();
|
||||
for (const i in children) {
|
||||
const c = children[i];
|
||||
this.removeChild(c);
|
||||
this.nodeScope.deleteNode(c);
|
||||
}
|
||||
|
||||
const group = this.createPageContainer();
|
||||
|
||||
// Create the page content
|
||||
const content = await this.nodeScope.createNode(args.target, guid());
|
||||
|
||||
this._internal.currentPage = newPage;
|
||||
this._internal.currentParams = args.params;
|
||||
|
||||
this.flagOutputDirty('currentPageTitle');
|
||||
this.flagOutputDirty('currentPageComponent');
|
||||
// @ts-expect-error Noodl is not defined
|
||||
Noodl.SEO.setTitle(this._internal.currentPage.title);
|
||||
|
||||
const pageInputNodes = content.nodeScope.getNodesWithType('PageInputs');
|
||||
if (pageInputNodes !== undefined && pageInputNodes.length > 0) {
|
||||
pageInputNodes.forEach((node) => {
|
||||
node._setPageParams(args.params);
|
||||
});
|
||||
}
|
||||
|
||||
group.addChild(content);
|
||||
this.addChild(group);
|
||||
|
||||
this._updateUrlWithTopPage();
|
||||
|
||||
args.hasNavigated && args.hasNavigated();
|
||||
RouterHandler.instance.onNavigated(this._internal.name, newPage);
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function shallowObjectsEqual(object1, object2) {
|
||||
const keys1 = Object.keys(object1 || {});
|
||||
const keys2 = Object.keys(object2 || {});
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return keys1.every((key) => object1[key] === object2[key]);
|
||||
}
|
||||
|
||||
export default createNodeFromReactComponent(RouterNode);
|
||||
204
packages/noodl-viewer-react/src/nodes/navigation/showpopup.js
Normal file
204
packages/noodl-viewer-react/src/nodes/navigation/showpopup.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const ShowPopupNode = {
|
||||
name: 'NavigationShowPopup',
|
||||
displayNodeName: 'Show Popup',
|
||||
category: 'Navigation',
|
||||
docs: 'https://docs.noodl.net/nodes/popups/show-popup',
|
||||
initialize: function () {
|
||||
this._internal.popupParams = {};
|
||||
this._internal.closeResults = {};
|
||||
},
|
||||
inputs: {
|
||||
target: {
|
||||
type: 'component',
|
||||
displayName: 'Target',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.target = value;
|
||||
}
|
||||
},
|
||||
show: {
|
||||
type: 'signal',
|
||||
displayName: 'Show',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleShow();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
Closed: {
|
||||
type: 'signal'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setPopupParam: function (param, value) {
|
||||
this._internal.popupParams[param] = value;
|
||||
},
|
||||
getCloseResult: function (param) {
|
||||
return this._internal.closeResults[param];
|
||||
},
|
||||
scheduleShow: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledShow) {
|
||||
internal.hasScheduledShow = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
internal.hasScheduledShow = false;
|
||||
_this.show();
|
||||
});
|
||||
}
|
||||
},
|
||||
show: function () {
|
||||
if (this._internal.target == undefined) return;
|
||||
|
||||
this.context.showPopup(this._internal.target, this._internal.popupParams, {
|
||||
senderNode: this.nodeScope.componentOwner,
|
||||
onClosePopup: (action, results) => {
|
||||
this._internal.closeResults = results;
|
||||
|
||||
for (var key in results) {
|
||||
if (this.hasOutput('closeResult-' + key)) this.flagOutputDirty('closeResult-' + key);
|
||||
}
|
||||
|
||||
if (!action) this.sendSignalOnOutput('Closed');
|
||||
else this.sendSignalOnOutput(action);
|
||||
}
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('popupParam-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setPopupParam.bind(this, name.substring('popupParam-'.length))
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('closeResult-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getCloseResult.bind(this, name.substring('closeResult-'.length))
|
||||
});
|
||||
|
||||
if (name.startsWith('closeAction-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: function () {
|
||||
/** No needed for signals */
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ShowPopupNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
var targetComponentName = node.parameters['target'];
|
||||
if (targetComponentName !== undefined) {
|
||||
var c = graphModel.components[targetComponentName];
|
||||
if (c) {
|
||||
for (var inputName in c.inputPorts) {
|
||||
var o = c.inputPorts[inputName];
|
||||
ports.push({
|
||||
name: 'popupParam-' + inputName,
|
||||
displayName: inputName,
|
||||
type: o.type || '*',
|
||||
plug: 'input',
|
||||
group: 'Params'
|
||||
});
|
||||
}
|
||||
|
||||
for (const _n of c.getNodesWithType('NavigationClosePopup')) {
|
||||
if (_n.parameters['closeActions'] !== undefined) {
|
||||
_n.parameters['closeActions'].split(',').forEach((a) => {
|
||||
if (ports.find((p) => p.name === a)) return;
|
||||
|
||||
ports.push({
|
||||
name: 'closeAction-' + a,
|
||||
displayName: a,
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Close Actions'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (_n.parameters['results'] !== undefined) {
|
||||
_n.parameters['results'].split(',').forEach((p) => {
|
||||
ports.push({
|
||||
name: 'closeResult-' + p,
|
||||
displayName: p,
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
group: 'Close Results'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
function _trackTargetComponent(name) {
|
||||
if (name === undefined) return;
|
||||
var c = graphModel.components[name];
|
||||
if (c === undefined) return;
|
||||
|
||||
c.on('inputPortAdded', _updatePorts);
|
||||
c.on('inputPortRemoved', _updatePorts);
|
||||
|
||||
// Also track all close popups for changes
|
||||
for (const _n of c.getNodesWithType('NavigationClosePopup')) {
|
||||
_n.on('parameterUpdated', _updatePorts);
|
||||
}
|
||||
|
||||
// Track close popup added and removed
|
||||
c.on('nodeAdded', (_n) => {
|
||||
if (_n.type === 'NavigationClosePopup') {
|
||||
_n.on('parameterUpdated', _updatePorts);
|
||||
_updatePorts();
|
||||
}
|
||||
});
|
||||
|
||||
c.on('nodeWasRemoved', (_n) => {
|
||||
if (_n.type === 'NavigationClosePopup') _updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
_trackTargetComponent(node.parameters['target']);
|
||||
|
||||
// Track parameter updated
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'target') {
|
||||
_updatePorts();
|
||||
_trackTargetComponent(node.parameters['target']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.NavigationShowPopup', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('NavigationShowPopup')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
None: require('./transitions/none-transition'),
|
||||
Push: require('./transitions/push-transition'),
|
||||
Popup: require('./transitions/popup-transition')
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const Transition = require('./transition');
|
||||
|
||||
class NoneTransition extends Transition {
|
||||
constructor(from, to, params) {
|
||||
super();
|
||||
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
|
||||
this.timing = { dur: 0, delay: 0 };
|
||||
}
|
||||
|
||||
update(t) {}
|
||||
|
||||
forward(t) {}
|
||||
|
||||
back(t) {}
|
||||
|
||||
static ports() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NoneTransition;
|
||||
@@ -0,0 +1,121 @@
|
||||
const Transition = require('./transition');
|
||||
const BezierEasing = require('bezier-easing');
|
||||
|
||||
class PopupTransition extends Transition {
|
||||
constructor(from, to, params) {
|
||||
super();
|
||||
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
|
||||
this.timing = params.timing || { curve: [0.0, 0.0, 0.58, 1.0], dur: 300, delay: 0 };
|
||||
this.distance = params.shift || { value: 25, unit: '%' };
|
||||
if (typeof this.distance === 'number') this.distance = { value: this.distance, unit: '%' };
|
||||
this.direction = params.direction || 'Right';
|
||||
|
||||
this.timing.curve[0] = Math.min(1, Math.max(0, this.timing.curve[0]));
|
||||
this.timing.curve[2] = Math.min(1, Math.max(0, this.timing.curve[2]));
|
||||
this.ease = BezierEasing.apply(null, this.timing.curve).get;
|
||||
|
||||
this.fadein = params.fadein === undefined ? false : params.fadein;
|
||||
|
||||
this.zoom = params.zoom || { value: 25, unit: '%' };
|
||||
if (typeof this.zoom === 'number') this.zoom = { value: this.zoom, unit: '%' };
|
||||
}
|
||||
|
||||
update(t) {
|
||||
if (this.direction === 'In' || this.direction === 'Out') {
|
||||
var zoom = this.zoom.value / 100;
|
||||
|
||||
zoom = this.direction === 'Out' ? -zoom : zoom;
|
||||
|
||||
this.to.setStyle({
|
||||
transform: 'scale(' + (1 - zoom * (1 - t)) + ')',
|
||||
opacity: this.crossfade ? t : 1
|
||||
});
|
||||
} else {
|
||||
var dist = this.distance.value;
|
||||
var unit = this.distance.unit;
|
||||
|
||||
const targets = {
|
||||
Up: { x: 0, y: -1 },
|
||||
Down: { x: 0, y: 1 },
|
||||
Left: { x: -1, y: 0 },
|
||||
Right: { x: 1, y: 0 }
|
||||
};
|
||||
const target = {
|
||||
x: targets[this.direction].x * dist,
|
||||
y: targets[this.direction].y * dist
|
||||
};
|
||||
|
||||
this.to.setStyle({
|
||||
transform: 'translate(' + target.x * (t - 1) + unit + ',' + target.y * (t - 1) + unit + ')',
|
||||
opacity: this.fadein ? t : 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
forward(t) {
|
||||
var _t = this.ease(t);
|
||||
this.update(_t);
|
||||
}
|
||||
|
||||
back(t) {
|
||||
var _t = this.ease(t);
|
||||
this.update(1 - _t);
|
||||
}
|
||||
|
||||
static ports(parameters) {
|
||||
var ports = [];
|
||||
|
||||
ports.push({
|
||||
name: 'tr-direction',
|
||||
displayName: 'Direction',
|
||||
group: 'Transition',
|
||||
type: { name: 'enum', enums: ['Right', 'Left', 'Up', 'Down', 'In', 'Out'] },
|
||||
default: 'Right',
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (parameters['tr-direction'] === 'In' || parameters['tr-direction'] === 'Out') {
|
||||
ports.push({
|
||||
name: 'tr-zoom',
|
||||
displayName: 'Zoom',
|
||||
group: 'Transition',
|
||||
type: { name: 'number', units: ['%'] },
|
||||
default: { value: 25, unit: '%' },
|
||||
plug: 'input'
|
||||
});
|
||||
} else {
|
||||
ports.push({
|
||||
name: 'tr-shift',
|
||||
displayName: 'Shift Distance',
|
||||
group: 'Transition',
|
||||
type: { name: 'number', units: ['%', 'px'] },
|
||||
default: { value: 25, unit: '%' },
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'tr-fadein',
|
||||
displayName: 'Fade In',
|
||||
group: 'Transition',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'tr-timing',
|
||||
displayName: 'Timing',
|
||||
group: 'Transition',
|
||||
type: 'curve',
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
return ports;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PopupTransition;
|
||||
@@ -0,0 +1,186 @@
|
||||
const Transition = require('./transition');
|
||||
const BezierEasing = require('bezier-easing');
|
||||
class PushTransition extends Transition {
|
||||
constructor(from, to, params) {
|
||||
super();
|
||||
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
|
||||
this.timing = params.timing || { curve: [0.0, 0.0, 0.58, 1.0], dur: 300, delay: 0 };
|
||||
this.distance = params.shift || { value: 25, unit: '%' };
|
||||
if (typeof this.distance === 'number') this.distance = { value: this.distance, unit: '%' };
|
||||
this.direction = params.direction || 'Left';
|
||||
|
||||
this.timing.curve[0] = Math.min(1, Math.max(0, this.timing.curve[0]));
|
||||
this.timing.curve[2] = Math.min(1, Math.max(0, this.timing.curve[2]));
|
||||
this.ease = BezierEasing.apply(null, this.timing.curve).get;
|
||||
|
||||
this.crossfade = params.crossfade === undefined ? false : params.crossfade;
|
||||
this.darkOverlay = params.darkoverlay === undefined ? true : params.darkoverlay;
|
||||
this.darkOverlayAmount = params.darkoverlayamount === undefined ? 0.5 : params.darkoverlayamount;
|
||||
|
||||
this.zoom = params.zoom || { value: 25, unit: '%' };
|
||||
if (typeof this.zoom === 'number') this.zoom = { value: this.zoom, unit: '%' };
|
||||
|
||||
if (this.darkOverlay) {
|
||||
this.darkOverlay = from.nodeScope.createPrimitiveNode('Group');
|
||||
this.darkOverlay.setInputValue('position', 'absolute');
|
||||
this.darkOverlay.setInputValue('sizeMode', 'explicit');
|
||||
this.darkOverlay.setInputValue('width', { value: 100, unit: '%' });
|
||||
this.darkOverlay.setInputValue('height', { value: 100, unit: '%' });
|
||||
this.darkOverlay.setInputValue('backgroundColor', '#000000');
|
||||
this.darkOverlay.setInputValue('opacity', 0);
|
||||
}
|
||||
}
|
||||
|
||||
update(t) {
|
||||
if (this.direction === 'In' || this.direction === 'Out') {
|
||||
var zoom = this.zoom.value / 100;
|
||||
|
||||
zoom = this.direction === 'Out' ? -zoom : zoom;
|
||||
|
||||
this.from.setStyle({
|
||||
transform: 'scale(' + (1 + zoom * t) + ')',
|
||||
opacity: this.crossfade ? 1 - t : 1
|
||||
});
|
||||
|
||||
this.to.setStyle({
|
||||
transform: 'scale(' + (1 - zoom * (1 - t)) + ')',
|
||||
opacity: this.crossfade ? t : 1
|
||||
});
|
||||
} else {
|
||||
var dist = this.distance.value;
|
||||
var unit = this.distance.unit;
|
||||
|
||||
const targets = {
|
||||
Up: { x: 0, y: -1 },
|
||||
Down: { x: 0, y: 1 },
|
||||
Left: { x: -1, y: 0 },
|
||||
Right: { x: 1, y: 0 }
|
||||
};
|
||||
|
||||
const from = {
|
||||
x: targets[this.direction].x * dist,
|
||||
y: targets[this.direction].y * dist
|
||||
};
|
||||
|
||||
const target = {
|
||||
x: targets[this.direction].x * 100,
|
||||
y: targets[this.direction].y * 100
|
||||
};
|
||||
|
||||
this.from.setStyle({
|
||||
transform: 'translate(' + from.x * t + unit + ',' + from.y * t + unit + ')',
|
||||
opacity: this.crossfade ? 1 - t : 1
|
||||
});
|
||||
|
||||
this.to.setStyle({
|
||||
transform: 'translate(' + target.x * (t - 1) + unit + ',' + target.y * (t - 1) + '%)',
|
||||
opacity: this.crossfade ? t : 1
|
||||
});
|
||||
}
|
||||
|
||||
if (this.darkOverlay) {
|
||||
this.darkOverlay.setStyle({
|
||||
opacity: t * this.darkOverlayAmount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
forward(t) {
|
||||
var _t = this.ease(t);
|
||||
this.update(_t);
|
||||
}
|
||||
|
||||
back(t) {
|
||||
var _t = this.ease(t);
|
||||
this.update(1 - _t);
|
||||
}
|
||||
|
||||
start(args) {
|
||||
super.start(args);
|
||||
if (this.darkOverlay) {
|
||||
this.from.addChild(this.darkOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
end(args) {
|
||||
if (this.darkOverlay) {
|
||||
this.from.removeChild(this.darkOverlay);
|
||||
}
|
||||
super.end(args);
|
||||
}
|
||||
|
||||
static ports(parameters) {
|
||||
const ports = [];
|
||||
|
||||
ports.push({
|
||||
name: 'tr-direction',
|
||||
displayName: 'Direction',
|
||||
group: 'Transition',
|
||||
type: { name: 'enum', enums: ['Right', 'Left', 'Up', 'Down', 'In', 'Out'] },
|
||||
default: 'Left',
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
if (parameters['tr-direction'] === 'In' || parameters['tr-direction'] === 'Out') {
|
||||
ports.push({
|
||||
name: 'tr-zoom',
|
||||
displayName: 'Zoom',
|
||||
group: 'Transition',
|
||||
type: { name: 'number', units: ['%'] },
|
||||
default: { value: 25, unit: '%' },
|
||||
plug: 'input'
|
||||
});
|
||||
} else {
|
||||
ports.push({
|
||||
name: 'tr-shift',
|
||||
displayName: 'Shift Distance',
|
||||
group: 'Transition',
|
||||
type: { name: 'number', units: ['%', 'px'] },
|
||||
default: { value: 25, unit: '%' },
|
||||
plug: 'input'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'tr-crossfade',
|
||||
displayName: 'Crossfade',
|
||||
group: 'Transition',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'tr-darkoverlay',
|
||||
displayName: 'Dark Overlay',
|
||||
group: 'Transition',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'tr-darkoverlayamount',
|
||||
displayName: 'Dark Overlay Amount',
|
||||
group: 'Transition',
|
||||
type: 'number',
|
||||
default: 0.5,
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
name: 'tr-timing',
|
||||
displayName: 'Timing',
|
||||
group: 'Transition',
|
||||
type: 'curve',
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
return ports;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PushTransition;
|
||||
@@ -0,0 +1,34 @@
|
||||
class Transition {
|
||||
constructor() {
|
||||
this._frame = this.frame.bind(this);
|
||||
}
|
||||
|
||||
start(args) {
|
||||
this.cb = args.end;
|
||||
|
||||
if (this.timing.delay + this.timing.dur === 0) {
|
||||
this.end();
|
||||
} else {
|
||||
this.transitionForward = !args.back;
|
||||
this.startTime = window.performance.now();
|
||||
requestAnimationFrame(this._frame);
|
||||
}
|
||||
}
|
||||
|
||||
frame() {
|
||||
var t = (window.performance.now() - (this.startTime + this.timing.delay)) / this.timing.dur;
|
||||
var _t = Math.max(0, Math.min(t, 1));
|
||||
|
||||
this.transitionForward ? this.forward(_t) : this.back(_t);
|
||||
|
||||
if (window.performance.now() <= this.startTime + this.timing.dur + this.timing.delay)
|
||||
requestAnimationFrame(this._frame);
|
||||
else this.end();
|
||||
}
|
||||
|
||||
end() {
|
||||
this.cb && this.cb();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Transition;
|
||||
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
var EaseCurves = require('../../easecurves');
|
||||
|
||||
var defaultDuration = 300;
|
||||
|
||||
var AnimateToValue = {
|
||||
name: 'net.noodl.animatetovalue',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/animate-to-value',
|
||||
displayName: 'Animate To Value',
|
||||
shortDesc: 'This node can interpolate smooothely from the current value to a target value.',
|
||||
category: 'Animation',
|
||||
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');
|
||||
}
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.currentNumber;
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: AnimateToValue
|
||||
};
|
||||
112
packages/noodl-viewer-react/src/nodes/std-library/colorblend.js
Normal file
112
packages/noodl-viewer-react/src/nodes/std-library/colorblend.js
Normal file
@@ -0,0 +1,112 @@
|
||||
'use strict';
|
||||
|
||||
const EaseCurves = require('../../easecurves');
|
||||
|
||||
function clamp(min, max, value) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function setRGB(result, hex) {
|
||||
for (var i = 0; i < 3; ++i) {
|
||||
var index = 1 + i * 2;
|
||||
result[i] = parseInt(hex.substring(index, index + 2), 16);
|
||||
}
|
||||
}
|
||||
|
||||
function componentToHex(c) {
|
||||
var hex = c.toString(16);
|
||||
return hex.length == 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
return '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
|
||||
}
|
||||
|
||||
//reusing these to reduce GC pressure
|
||||
let rgb0 = [0, 0, 0];
|
||||
let rgb1 = [0, 0, 0];
|
||||
let rgb2 = [0, 0, 0];
|
||||
|
||||
const ColorBlendNode = {
|
||||
name: 'Color Blend',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/color-blend',
|
||||
shortDesc:
|
||||
'Given any number of input colors this node can interpolate between these and give the result color as output.',
|
||||
category: 'Interpolation',
|
||||
getInspectInfo() {
|
||||
return [{ type: 'color', value: this._internal.resultColor }];
|
||||
},
|
||||
initialize() {
|
||||
const internal = this._internal;
|
||||
|
||||
internal.resultColor = '#000000';
|
||||
internal.blendValue = 0;
|
||||
internal.colors = [];
|
||||
},
|
||||
numberedInputs: {
|
||||
color: {
|
||||
type: 'color',
|
||||
displayPrefix: 'Color',
|
||||
createSetter(index) {
|
||||
return function (value) {
|
||||
this._internal.colors[index] = value;
|
||||
this.updateColor();
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
blendValue: {
|
||||
type: 'number',
|
||||
displayName: 'Blend Value',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal.blendValue = value;
|
||||
this.updateColor();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'color',
|
||||
displayName: 'Result',
|
||||
getter: function () {
|
||||
return this._internal.resultColor;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateColor() {
|
||||
var colors = this._internal.colors;
|
||||
if (colors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function getColor(index) {
|
||||
return colors[index] ? colors[index] : '#000000';
|
||||
}
|
||||
|
||||
var clampedBlendValue = clamp(0, colors.length - 1, this._internal.blendValue);
|
||||
var index = Math.floor(clampedBlendValue);
|
||||
var t = clampedBlendValue - index;
|
||||
|
||||
if (t === 0) {
|
||||
this._internal.resultColor = getColor(index);
|
||||
} else {
|
||||
setRGB(rgb0, getColor(index));
|
||||
setRGB(rgb1, getColor(index + 1));
|
||||
|
||||
rgb2[0] = Math.floor(EaseCurves.linear(rgb0[0], rgb1[0], t));
|
||||
rgb2[1] = Math.floor(EaseCurves.linear(rgb0[1], rgb1[1], t));
|
||||
rgb2[2] = Math.floor(EaseCurves.linear(rgb0[2], rgb1[2], t));
|
||||
this._internal.resultColor = rgbToHex(rgb2);
|
||||
}
|
||||
|
||||
this.flagOutputDirty('result');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ColorBlendNode
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
'use strict';
|
||||
|
||||
const Model = require('@noodl/runtime/src/model');
|
||||
|
||||
function extendSetComponentObjectProperties(def) {
|
||||
const SetComponentObjectProperties = {
|
||||
name: def.name,
|
||||
displayNodeName: def.displayName,
|
||||
category: 'Component Utilities',
|
||||
color: 'component',
|
||||
docs: def.docs,
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
type: {
|
||||
name: 'stringlist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Properties',
|
||||
group: 'Properties',
|
||||
set(value) {}
|
||||
},
|
||||
store: {
|
||||
type: 'signal',
|
||||
group: 'Actions',
|
||||
displayName: 'Do',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
stored: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Done'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getComponentObjectId: def.getComponentObjectId,
|
||||
scheduleStore() {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const model = Model.get(this.getComponentObjectId());
|
||||
this.hasScheduledStore = false;
|
||||
|
||||
const properties = this.model.parameters.properties || '';
|
||||
const validProperties = properties.split(',');
|
||||
|
||||
const keysToSet = Object.keys(internal.inputValues).filter((key) => validProperties.indexOf(key) !== -1);
|
||||
|
||||
for (const i of keysToSet) {
|
||||
model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
this.sendSignalOnOutput('stored');
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-')) {
|
||||
const propertyName = name.substring('prop-'.length);
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
this._internal.inputValues[propertyName] = value;
|
||||
}
|
||||
});
|
||||
} else if (name.startsWith('type-')) {
|
||||
this.registerInput(name, {
|
||||
set(value) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
var ports = [];
|
||||
|
||||
const _types = [
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'Any', value: '*' }
|
||||
];
|
||||
|
||||
// Add value outputs
|
||||
var properties = parameters.properties;
|
||||
if (properties) {
|
||||
properties = properties ? properties.split(',') : undefined;
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
// Property input
|
||||
ports.push({
|
||||
type: {
|
||||
name: parameters['type-' + p] === undefined ? '*' : parameters['type-' + p]
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Values',
|
||||
displayName: p,
|
||||
// editorName:p,
|
||||
name: 'prop-' + p
|
||||
});
|
||||
|
||||
// Property type
|
||||
ports.push({
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _types,
|
||||
allowEditOnly: true
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Property Types',
|
||||
displayName: p,
|
||||
default: '*',
|
||||
name: 'type-' + p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
node: SetComponentObjectProperties,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.' + def.name, (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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extendSetComponentObjectProperties
|
||||
};
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
const Model = require('@noodl/runtime/src/model');
|
||||
|
||||
const ComponentObject = {
|
||||
name: 'net.noodl.ComponentObject',
|
||||
displayNodeName: 'Component Object',
|
||||
category: 'Component Utilities',
|
||||
color: 'component',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/component-object',
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.dirtyValues = {};
|
||||
|
||||
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);
|
||||
},
|
||||
getInspectInfo() {
|
||||
return {
|
||||
type: 'value',
|
||||
value: this._internal.model.data
|
||||
};
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
type: {
|
||||
name: 'stringlist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Properties',
|
||||
group: 'Properties',
|
||||
set(value) {}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
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.dirtyValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
internal.dirtyValues = {};
|
||||
});
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
if (name.startsWith('value-')) {
|
||||
const propertyName = name.substring('value-'.length);
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
this._internal.inputValues[propertyName] = value;
|
||||
this._internal.dirtyValues[propertyName] = true;
|
||||
|
||||
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: ComponentObject,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.net.noodl.ComponentObject', (node) => {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
|
||||
node.on('parameterUpdated', (event) => {
|
||||
if (event.name === 'properties') {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
'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 ParentComponentObject = {
|
||||
name: 'net.noodl.ParentComponentObject',
|
||||
displayNodeName: 'Parent Component Object',
|
||||
category: 'Component Utilities',
|
||||
color: 'component',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/parent-component-object',
|
||||
initialize() {
|
||||
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');
|
||||
};
|
||||
|
||||
//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) {}
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
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('net.noodl.ComponentObject').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('value-' + key)) {
|
||||
this.flagOutputDirty('value-' + 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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
_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;
|
||||
}
|
||||
|
||||
const propertyName = name.substring('value-'.length);
|
||||
|
||||
this.registerOutput(name, {
|
||||
get() {
|
||||
if (!this._internal.model) return undefined;
|
||||
return this._internal.model.get(propertyName, { resolve: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('value-')) {
|
||||
const propertyName = name.substring('value-'.length);
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
this._internal.inputValues[propertyName] = value;
|
||||
|
||||
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: 'value-' + 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: ParentComponentObject,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.net.noodl.ParentComponentObject', (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.net.noodl.ComponentObject', (node) => {
|
||||
setTimeout(() => {
|
||||
graphEventEmitter.emit('componentStateNodesChanged');
|
||||
}, 0);
|
||||
});
|
||||
graphModel.on('nodeRemoved.net.noodl.ComponentObject', (node) => {
|
||||
setTimeout(() => {
|
||||
graphEventEmitter.emit('componentStateNodesChanged');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./base');
|
||||
|
||||
module.exports = Base.extendSetComponentObjectProperties({
|
||||
name: 'net.noodl.SetComponentObjectProperties',
|
||||
displayName: 'Set Component Object Properties',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/set-component-object-properties',
|
||||
getComponentObjectId: function () {
|
||||
return 'componentState' + this.nodeScope.componentOwner.getInstanceId();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
const Model = require('@noodl/runtime/src/model');
|
||||
|
||||
const Base = require('./base');
|
||||
|
||||
module.exports = Base.extendSetComponentObjectProperties({
|
||||
name: 'net.noodl.SetParentComponentObjectProperties',
|
||||
displayName: 'Set Parent Component Object Properties',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/set-parent-component-object-properties',
|
||||
getComponentObjectId: function () {
|
||||
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('net.noodl.ComponentObject').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();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,342 @@
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
|
||||
|
||||
('use strict');
|
||||
|
||||
/*var defaultBeforeCallScript = "// Add custom code to setup the parameters send to the function\n"+
|
||||
"// The parameters are found in the object called 'Parameters'\n";
|
||||
|
||||
var defaultAfterCallScript = "// Add custom code modfiy the result from the cloud function\n"+
|
||||
"// The result is found in the object called 'Result'\n";*/
|
||||
|
||||
function _makeRequest(path, options) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(xhr.response);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
options.success(json);
|
||||
} else options.error(json);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(options.method || 'GET', options.endpoint + path, true);
|
||||
|
||||
xhr.setRequestHeader('X-Parse-Application-Id', options.appId);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
// Check for current users
|
||||
var _cu = localStorage['Parse/' + options.appId + '/currentUser'];
|
||||
if (_cu !== undefined) {
|
||||
try {
|
||||
const currentUser = JSON.parse(_cu);
|
||||
xhr.setRequestHeader('X-Parse-Session-Token', currentUser.sessionToken);
|
||||
} catch (e) {
|
||||
// Failed to extract session token
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(JSON.stringify(options.content));
|
||||
}
|
||||
|
||||
var CloudFunctionNode = {
|
||||
name: 'Cloud Function',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
usePortAsLabel: 'functionName',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/cloud-function',
|
||||
deprecated: true,
|
||||
initialize: function () {
|
||||
this._internal.paramsValues = {};
|
||||
// this._internal.resultsValues = {};
|
||||
// this._internal.convertArraysAndObjects = true;
|
||||
},
|
||||
getInspectInfo() {
|
||||
const result = this._internal.lastCallResult;
|
||||
if (!result) return '[Not executed yet]';
|
||||
|
||||
return [{ type: 'value', value: result }];
|
||||
},
|
||||
inputs: {
|
||||
functionName: {
|
||||
type: 'string',
|
||||
displayName: 'Function Name',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.functionName = value;
|
||||
}
|
||||
},
|
||||
params: {
|
||||
group: 'Parameters',
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
set: function (value) {
|
||||
this._internal.params = value;
|
||||
}
|
||||
},
|
||||
/* results:{
|
||||
group:'Result',
|
||||
type:{name:'stringlist',allowEditOnly:true},
|
||||
set:function(value) {
|
||||
this._internal.results = value;
|
||||
}
|
||||
},
|
||||
beforeCallScript: {
|
||||
group:'Scripts',
|
||||
displayName:'Before Call',
|
||||
default:defaultBeforeCallScript,
|
||||
type:{name:'string',codeeditor:'javascript'},
|
||||
set: function(scriptCode) {
|
||||
try {
|
||||
if(scriptCode !== undefined) {
|
||||
var args = ['Parameters'].concat([scriptCode]);
|
||||
this._internal.scriptBeforeCallFunc = Function.apply(null,args);
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
this._internal.scriptBeforeCallFunc = undefined;
|
||||
console.log('Error while parsing script (before call): ' + e);
|
||||
}
|
||||
}
|
||||
},
|
||||
afterCallScript: {
|
||||
group:'Scripts',
|
||||
displayName:'After Call',
|
||||
default:defaultAfterCallScript,
|
||||
type:{name:'string',codeeditor:'javascript'},
|
||||
set: function(scriptCode) {
|
||||
try {
|
||||
if(scriptCode !== undefined) {
|
||||
var args = ['Result'].concat([scriptCode]);
|
||||
this._internal.scriptAfterCallFunc = Function.apply(null,args);
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
this._internal.scriptAfterCallFunc = undefined;
|
||||
console.log('Error while parsing script (after call): ' + e);
|
||||
}
|
||||
}
|
||||
},*/
|
||||
call: {
|
||||
type: 'signal',
|
||||
displayName: 'Call',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleCall();
|
||||
}
|
||||
}
|
||||
/* convertArraysAndObjects:{
|
||||
group:'Advanced',
|
||||
displayName:'Convert Objects',
|
||||
type:'boolean',
|
||||
default:true,
|
||||
set: function(value) {
|
||||
this._internal.convertArraysAndObjects = value;
|
||||
}
|
||||
}*/
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Signals'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Signals'
|
||||
},
|
||||
result: {
|
||||
type: '*',
|
||||
displayName: 'Result',
|
||||
group: 'Output',
|
||||
getter: function () {
|
||||
return this._internal.result;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/* getResultsValue:function(name) {
|
||||
return this._internal.resultsValues[name];
|
||||
},*/
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* if(name.startsWith('res-')) this.registerOutput(name, {
|
||||
getter: this.getResultsValue.bind(this, name.substring('res-'.length))
|
||||
});*/
|
||||
},
|
||||
setParamsValue: function (name, value) {
|
||||
this._internal.paramsValues[name] = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('pm-'))
|
||||
this.registerInput(name, {
|
||||
set: this.setParamsValue.bind(this, name.substring('pm-'.length))
|
||||
});
|
||||
},
|
||||
scheduleCall: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledCall) {
|
||||
internal.hasScheduledCall = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doCall.bind(this));
|
||||
}
|
||||
},
|
||||
doCall: function () {
|
||||
this._internal.hasScheduledCall = false;
|
||||
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
if (cloudServices === undefined) {
|
||||
console.log('No cloud services defined in this project.');
|
||||
this._internal.lastCallResult = {
|
||||
status: 'failure',
|
||||
res: 'No active cloud services in this project'
|
||||
};
|
||||
|
||||
this.sendSignalOnOutput('failure');
|
||||
return;
|
||||
}
|
||||
|
||||
var appId = cloudServices.appId;
|
||||
var endpoint = cloudServices.endpoint;
|
||||
// Run before call script
|
||||
/* if(this._internal.scriptBeforeCallFunc !== undefined) {
|
||||
this._internal.scriptBeforeCallFunc(this._internal.paramsValues);
|
||||
}*/
|
||||
|
||||
_makeRequest('/functions/' + encodeURIComponent(this._internal.functionName), {
|
||||
appId,
|
||||
endpoint,
|
||||
content: this._internal.paramsValues,
|
||||
method: 'POST',
|
||||
success: (res) => {
|
||||
var res = res.result; // Cloud functions always return "result"
|
||||
if (res === undefined) {
|
||||
this.sendSignalOnOutput('failure');
|
||||
return;
|
||||
}
|
||||
|
||||
// Run after call script
|
||||
/* if(this._internal.scriptAfterCallFunc !== undefined) {
|
||||
this._internal.scriptAfterCallFunc(res);
|
||||
}*/
|
||||
|
||||
this._internal.result = CloudStore._deserializeJSON(res);
|
||||
this.flagOutputDirty('result');
|
||||
|
||||
this._internal.lastCallResult = {
|
||||
status: 'success',
|
||||
result: this._internal.result
|
||||
};
|
||||
|
||||
// Deserialize values into Noodl arrays and objects
|
||||
/* if(this._internal.convertArraysAndObjects) {
|
||||
for(var key in res) {
|
||||
if(res[key] !== undefined)
|
||||
res[key] = CloudStore._fromJSON(res[key]);
|
||||
}
|
||||
}*/
|
||||
|
||||
/*this._internal.resultsValues = res;
|
||||
|
||||
for(var key in res) {
|
||||
if(this.hasOutput('res-'+key)) {
|
||||
this.flagOutputDirty('res-'+key);
|
||||
}
|
||||
}*/
|
||||
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (res) => {
|
||||
this._internal.lastCallResult = {
|
||||
status: 'failure',
|
||||
res
|
||||
};
|
||||
|
||||
this.sendSignalOnOutput('failure');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CloudFunctionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
// Add results outputs
|
||||
/* var results = node.parameters.results;
|
||||
if (results !== undefined) {
|
||||
results = results.split(',');
|
||||
for (var i in results) {
|
||||
var p = results[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*',
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Results',
|
||||
name: 'res-' + p,
|
||||
displayName: p
|
||||
});
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
// Add params inputs
|
||||
var params = node.parameters.params;
|
||||
if (params !== undefined) {
|
||||
params = params.split(',');
|
||||
for (var i in params) {
|
||||
var p = params[i];
|
||||
|
||||
ports.push({
|
||||
type: '*',
|
||||
plug: 'input',
|
||||
group: 'Parameters',
|
||||
name: 'pm-' + p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'params') {
|
||||
_updatePorts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.Cloud Function', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('Cloud Function')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
|
||||
|
||||
('use strict');
|
||||
|
||||
function _makeRequest(path, options) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(xhr.response);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
options.success(json);
|
||||
} else options.error(json);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(options.method || 'GET', options.endpoint + path, true);
|
||||
|
||||
xhr.setRequestHeader('X-Parse-Application-Id', options.appId);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
if (cloudServices && cloudServices.deployVersion) {
|
||||
xhr.setRequestHeader('x-noodl-cloud-version', cloudServices.deployVersion);
|
||||
}
|
||||
|
||||
// Check for current users
|
||||
var _cu = localStorage['Parse/' + options.appId + '/currentUser'];
|
||||
if (_cu !== undefined) {
|
||||
try {
|
||||
const currentUser = JSON.parse(_cu);
|
||||
xhr.setRequestHeader('X-Parse-Session-Token', currentUser.sessionToken);
|
||||
} catch (e) {
|
||||
// Failed to extract session token
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(JSON.stringify(options.content));
|
||||
}
|
||||
|
||||
var CloudFunctionNode = {
|
||||
name: 'CloudFunction2',
|
||||
displayName: 'Cloud Function',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
usePortAsLabel: 'function',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/cloud-function',
|
||||
initialize: function () {
|
||||
this._internal.paramsValues = {};
|
||||
this._internal.resultsValues = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
const result = this._internal.lastCallResult;
|
||||
if (!result) return '[Not executed yet]';
|
||||
|
||||
return [{ type: 'value', value: result }];
|
||||
},
|
||||
inputs: {
|
||||
call: {
|
||||
type: 'signal',
|
||||
displayName: 'Call',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleCall();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Signals'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Signals'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setError: function (err) {
|
||||
this._internal.error = err;
|
||||
this.flagOutputDirty('error');
|
||||
this.sendSignalOnOutput('failure');
|
||||
},
|
||||
getResultsValue: function (name) {
|
||||
return this._internal.resultsValues[name];
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
this.registerOutput(name, {
|
||||
getter: this.getResultsValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
},
|
||||
setParamsValue: function (name, value) {
|
||||
this._internal.paramsValues[name] = value;
|
||||
},
|
||||
setFunctionName: function (value) {
|
||||
this._internal.functionName = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'function')
|
||||
this.registerInput(name, {
|
||||
set: this.setFunctionName.bind(this)
|
||||
});
|
||||
|
||||
if (name.startsWith('in-'))
|
||||
this.registerInput(name, {
|
||||
set: this.setParamsValue.bind(this, name.substring('in-'.length))
|
||||
});
|
||||
},
|
||||
scheduleCall: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledCall) {
|
||||
internal.hasScheduledCall = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.doCall.bind(this));
|
||||
}
|
||||
},
|
||||
doCall: function () {
|
||||
this._internal.hasScheduledCall = false;
|
||||
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
if (this.context.editorConnection) {
|
||||
if (cloudServices === undefined || cloudServices.endpoint === undefined) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'cloud-function-2', {
|
||||
message: 'No cloud services defined in this project.'
|
||||
});
|
||||
} else if (this._internal.functionName === undefined) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'cloud-function-2', {
|
||||
message: 'No function specified'
|
||||
});
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'cloud-function-2');
|
||||
}
|
||||
}
|
||||
|
||||
const appId = cloudServices.appId;
|
||||
const endpoint = this.context.editorConnection.isRunningLocally()
|
||||
? `http://${window.location.hostname}:8577`
|
||||
: cloudServices.endpoint;
|
||||
|
||||
_makeRequest('/functions/' + encodeURIComponent(this._internal.functionName), {
|
||||
appId,
|
||||
endpoint,
|
||||
content: this._internal.paramsValues,
|
||||
method: 'POST',
|
||||
success: (res) => {
|
||||
if (res === undefined) {
|
||||
// No result, still success
|
||||
|
||||
this._internal.lastCallResult = {
|
||||
status: 'success',
|
||||
parameters: this._internal.paramsValues,
|
||||
results: 'empty'
|
||||
};
|
||||
} else {
|
||||
const results = res.result || {};
|
||||
for (let key in results) {
|
||||
this._internal.resultsValues[key] = results[key];
|
||||
if (this.hasOutput('out-' + key)) this.flagOutputDirty('out-' + key);
|
||||
}
|
||||
|
||||
this._internal.lastCallResult = {
|
||||
status: 'success',
|
||||
parameters: this._internal.paramsValues,
|
||||
results: this._internal.resultsValues
|
||||
};
|
||||
}
|
||||
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
const error = typeof e === 'string' ? e : e.error || 'Failed running cloud function.';
|
||||
this._internal.lastCallResult = {
|
||||
status: 'failure',
|
||||
error
|
||||
};
|
||||
|
||||
this.setError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CloudFunctionNode,
|
||||
setup: function (context, graphModel) {
|
||||
// Handled in editor adapter
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
const CollectionClearNode = {
|
||||
name: 'CollectionClear',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/clear-array',
|
||||
displayNodeName: 'Clear Array',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'collectionId',
|
||||
color: 'data',
|
||||
inputs: {
|
||||
collectionId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'CollectionName',
|
||||
identifierDisplayName: 'Array Ids'
|
||||
},
|
||||
displayName: 'Array Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Collection) value = value.getId(); // Can be passed as collection as well
|
||||
this.setCollectionID(value);
|
||||
}
|
||||
},
|
||||
clear: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const collection = this._internal.collection;
|
||||
|
||||
collection.set([]);
|
||||
this.sendSignalOnOutput('modified');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Done'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCollectionID: function (id) {
|
||||
this.setCollection(Collection.get(id));
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this._internal.collection = collection;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CollectionClearNode
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model'),
|
||||
Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
var CollectionInsertNode = {
|
||||
name: 'CollectionInsert',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/insert-into-array',
|
||||
displayNodeName: 'Insert Object Into Array',
|
||||
shortDesc: 'A collection of models, mainly used together with a For Each Node.',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'collectionId',
|
||||
color: 'data',
|
||||
initialize: function () {},
|
||||
inputs: {
|
||||
collectionId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'CollectionName',
|
||||
identifierDisplayName: 'Array Ids'
|
||||
},
|
||||
displayName: 'Array Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Collection) value = value.getId(); // Can be passed as collection as well
|
||||
this.setCollectionID(value);
|
||||
}
|
||||
},
|
||||
modifyId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Object Id',
|
||||
group: 'Modify',
|
||||
set: function (value) {
|
||||
this._internal.modifyId = value;
|
||||
}
|
||||
},
|
||||
add: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'insert-warning');
|
||||
}
|
||||
|
||||
if (internal.modifyId === undefined) {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'insert-warning', {
|
||||
showGlobally: true,
|
||||
message: 'No Object Id specified'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (internal.collection === undefined) {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'insert-warning', {
|
||||
showGlobally: true,
|
||||
message: 'No Array Id specified'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var model = Model.get(internal.modifyId);
|
||||
internal.collection.add(model);
|
||||
_this.sendSignalOnOutput('modified');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Done'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollectionID: function (id) {
|
||||
this.setCollection(Collection.get(id));
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this._internal.collection = collection;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CollectionInsertNode
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model'),
|
||||
Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
var CollectionNewNode = {
|
||||
name: 'CollectionNew',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/create-new-array',
|
||||
displayNodeName: 'Create New Array',
|
||||
shortDesc: 'A collection of models, mainly used together with a For Each Node.',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
initialize: function () {},
|
||||
inputs: {
|
||||
new: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleNew();
|
||||
}
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
group: 'General',
|
||||
displayName: 'Items',
|
||||
set: function (value) {
|
||||
this._internal.sourceCollection = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
displayName: 'Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection ? this._internal.collection.getId() : this._internal.collectionId;
|
||||
}
|
||||
},
|
||||
created: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Done'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollectionID: function (id) {
|
||||
this.setCollection(Collection.get(id));
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this._internal.collection = collection;
|
||||
this.flagOutputDirty('id');
|
||||
},
|
||||
scheduleNew: function () {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasScheduledNew) return;
|
||||
this.hasScheduledNew = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this.hasScheduledNew = false;
|
||||
|
||||
const collection = Collection.get();
|
||||
if (this._internal.sourceCollection !== undefined) collection.set(this._internal.sourceCollection);
|
||||
|
||||
_this.setCollection(collection);
|
||||
|
||||
_this.sendSignalOnOutput('created');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CollectionNewNode
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model'),
|
||||
Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
var CollectionRemoveNode = {
|
||||
name: 'CollectionRemove',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/remove-from-array',
|
||||
displayNodeName: 'Remove Object From Array',
|
||||
shortDesc: 'A collection of models, mainly used together with a For Each Node.',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'collectionId',
|
||||
color: 'data',
|
||||
initialize: function () {},
|
||||
inputs: {
|
||||
collectionId: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'CollectionName',
|
||||
identifierDisplayName: 'Array Ids'
|
||||
},
|
||||
displayName: 'Array Id',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
if (value instanceof Collection) value = value.getId(); // Can be passed as collection as well
|
||||
this.setCollectionID(value);
|
||||
}
|
||||
},
|
||||
modifyId: {
|
||||
type: { name: 'string', allowConnectionsOnly: true },
|
||||
displayName: 'Object Id',
|
||||
group: 'Modify',
|
||||
set: function (value) {
|
||||
this._internal.modifyId = value;
|
||||
}
|
||||
},
|
||||
remove: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (internal.modifyId === undefined) return;
|
||||
if (internal.collection === undefined) return;
|
||||
|
||||
var model = Model.get(internal.modifyId);
|
||||
internal.collection.remove(model);
|
||||
_this.sendSignalOnOutput('modified');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Done'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollectionID: function (id) {
|
||||
this.setCollection(Collection.get(id));
|
||||
},
|
||||
setCollection: function (collection) {
|
||||
this._internal.collection = collection;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CollectionRemoveNode
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model'),
|
||||
Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
var CollectionNode = {
|
||||
name: 'Collection2',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/array-node',
|
||||
displayNodeName: 'Array',
|
||||
shortDesc: 'A collection of models, mainly used together with a For Each Node.',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'collectionId',
|
||||
color: 'data',
|
||||
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() {
|
||||
const collection = this._internal.collection;
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[No Array]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Id: ' + collection.getId()
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleSetCollection();
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
changed: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Changed'
|
||||
},
|
||||
fetched: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Fetched'
|
||||
}
|
||||
},
|
||||
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);
|
||||
});
|
||||
},
|
||||
_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();
|
||||
});
|
||||
},
|
||||
_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,454 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
|
||||
const Collection = require('@noodl/runtime/src/collection'),
|
||||
Model = require('@noodl/runtime/src/model');
|
||||
|
||||
function applyFilter(item, filter) {
|
||||
for (var key in filter) {
|
||||
var op = filter[key];
|
||||
|
||||
//check neq first, it's the only operation where the key can be undefined
|
||||
if (op['$neq'] !== undefined) {
|
||||
if (!(item[key] != op['$neq'])) return false;
|
||||
} else if (item[key] === undefined) return false;
|
||||
// The key does not exist, always return false
|
||||
else if (op['$eq'] !== undefined && !(item[key] == op['$eq'])) return false;
|
||||
else if (op['$gt'] !== undefined && !(item[key] > op['$gt'])) return false;
|
||||
else if (op['$lt'] !== undefined && !(item[key] < op['$lt'])) return false;
|
||||
else if (op['$gte'] !== undefined && !(item[key] >= op['$gte'])) return false;
|
||||
else if (op['$lte'] !== undefined && !(item[key] <= op['$lte'])) return false;
|
||||
else if (op['$regex'] !== undefined) {
|
||||
// Test if string matches regex
|
||||
var a = item[key] + ''; // Convert to string
|
||||
var regex = new RegExp(op['$regex'], op['$case'] !== true ? 'i' : undefined);
|
||||
|
||||
if (!regex.test(a)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function sorter(a, b) {
|
||||
if (a instanceof Model) a = a.data;
|
||||
if (b instanceof Model) b = b.data;
|
||||
|
||||
for (var key in this) {
|
||||
var _a = a[key];
|
||||
var _b = b[key];
|
||||
if (_a !== _b) {
|
||||
if (typeof _a === 'string' && typeof _b === 'string') {
|
||||
if (this[key] === 1) {
|
||||
return _a > _b ? 1 : -1;
|
||||
} else return _a > _b ? -1 : 1;
|
||||
} else if (typeof _a === 'number' && typeof _b === 'number') {
|
||||
return this[key] === 1 ? _a - _b : _b - _a;
|
||||
} else {
|
||||
if (this[key] === 1) {
|
||||
return _a > _b ? 1 : -1;
|
||||
} else return _a > _b ? -1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
var FilterCollectionNode = {
|
||||
name: 'Filter Collection',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/array-filter',
|
||||
displayNodeName: 'Array Filter',
|
||||
shortDesc: 'Filter, sort and limit array',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
if (_this.isInputConnected('filter') === true) return;
|
||||
|
||||
_this.scheduleFilter();
|
||||
};
|
||||
|
||||
this._internal.enabled = true;
|
||||
this._internal.filterSettings = {};
|
||||
//this._internal.filteredCollection = Collection.get();
|
||||
},
|
||||
getInspectInfo() {
|
||||
const collection = this._internal.filteredCollection;
|
||||
|
||||
if (!collection) {
|
||||
return { type: 'text', value: '[Not executed yet]' };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Id: ' + collection.getId()
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
value: collection.items
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
set(value) {
|
||||
this.bindCollection(value);
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
group: 'General',
|
||||
displayName: 'Enabled',
|
||||
default: true,
|
||||
set: function (value) {
|
||||
this._internal.enabled = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
type: 'signal',
|
||||
group: 'Actions',
|
||||
displayName: 'Filter',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection;
|
||||
}
|
||||
},
|
||||
firstItemId: {
|
||||
type: 'string',
|
||||
displayName: 'First Item Id',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if (this._internal.filteredCollection !== undefined) {
|
||||
const firstItem = this._internal.filteredCollection.get(0);
|
||||
if (firstItem !== undefined) return firstItem.getId();
|
||||
}
|
||||
}
|
||||
},
|
||||
/* firstItem:{
|
||||
type: 'object',
|
||||
displayName: 'First Item',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
if(this._internal.filteredCollection !== undefined) {
|
||||
return this._internal.filteredCollection.get(0);
|
||||
}
|
||||
}
|
||||
}, */
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.filteredCollection ? this._internal.filteredCollection.size() : 0;
|
||||
}
|
||||
},
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Filtered'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
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();
|
||||
},
|
||||
getFilter: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
const options = ['case']; // List all supported options here
|
||||
|
||||
if (filterSettings['filterFilter']) {
|
||||
const filters = filterSettings['filterFilter'].split(',');
|
||||
var _filter = {};
|
||||
filters.forEach(function (f) {
|
||||
var op = '$' + (filterSettings['filterFilterOp-' + f] || 'eq');
|
||||
_filter[f] = {};
|
||||
_filter[f][op] = filterSettings['filterFilterValue-' + f];
|
||||
|
||||
options.forEach((o) => {
|
||||
var option = filterSettings['filterFilterOption-' + o + '-' + f];
|
||||
if (option) _filter[f]['$' + o] = option;
|
||||
});
|
||||
});
|
||||
return _filter;
|
||||
}
|
||||
},
|
||||
getSort: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (filterSettings['filterSort']) {
|
||||
const sort = filterSettings['filterSort'].split(',');
|
||||
var _sort = {};
|
||||
sort.forEach(function (s) {
|
||||
_sort[s] = filterSettings['filterSort-' + s] === 'descending' ? -1 : 1;
|
||||
});
|
||||
return _sort;
|
||||
}
|
||||
},
|
||||
getLimit: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterLimit'] || 10;
|
||||
},
|
||||
getSkip: function () {
|
||||
const filterSettings = this._internal.filterSettings;
|
||||
|
||||
if (!filterSettings['filterEnableLimit']) return;
|
||||
else return filterSettings['filterSkip'] || 0;
|
||||
},
|
||||
scheduleFilter: function () {
|
||||
if (this.collectionChangedScheduled) return;
|
||||
this.collectionChangedScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.collectionChangedScheduled = false;
|
||||
if (!this._internal.collection) return;
|
||||
|
||||
// Apply filter and write to output collection
|
||||
var filtered = [].concat(this._internal.collection.items); // Make sure we clone the array
|
||||
|
||||
if (this._internal.enabled) {
|
||||
var filter = this.getFilter();
|
||||
if (filter) filtered = filtered.filter((m) => applyFilter(m.data, filter));
|
||||
|
||||
var sort = this.getSort();
|
||||
if (sort) filtered.sort(sorter.bind(sort));
|
||||
|
||||
var skip = this.getSkip();
|
||||
if (skip) filtered = filtered.slice(skip, filtered.length);
|
||||
|
||||
var limit = this.getLimit();
|
||||
if (limit) filtered = filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
this._internal.filteredCollection = Collection.create(filtered);
|
||||
|
||||
this.sendSignalOnOutput('modified');
|
||||
this.flagOutputDirty('firstItemId');
|
||||
this.flagOutputDirty('items');
|
||||
// this.flagOutputDirty('firstItem');
|
||||
this.flagOutputDirty('count');
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
this._internal.filterSettings[name] = value;
|
||||
if (this.isInputConnected('filter') === false) this.scheduleFilter();
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
|
||||
var ports = [];
|
||||
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterEnableLimit',
|
||||
displayName: 'Use limit'
|
||||
});
|
||||
|
||||
if (parameters['filterEnableLimit']) {
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 10,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterLimit',
|
||||
displayName: 'Limit'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'number',
|
||||
default: 0,
|
||||
plug: 'input',
|
||||
group: 'Limit',
|
||||
name: 'filterSkip',
|
||||
displayName: 'Skip'
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Filter',
|
||||
name: 'filterFilter',
|
||||
displayName: 'Filter'
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
plug: 'input',
|
||||
group: 'Sort',
|
||||
name: 'filterSort',
|
||||
displayName: 'Sort'
|
||||
});
|
||||
|
||||
const filterOps = {
|
||||
string: [
|
||||
{ value: 'eq', label: 'Equals' },
|
||||
{ value: 'neq', label: 'Not Equals' },
|
||||
{ value: 'regex', label: 'Matches RegEx' }
|
||||
],
|
||||
boolean: [
|
||||
{ value: 'eq', label: 'Equals' },
|
||||
{ value: 'neq', label: 'Not Equals' }
|
||||
],
|
||||
number: [
|
||||
{ value: 'eq', label: 'Equals' },
|
||||
{ value: 'neq', 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['filterFilter']) {
|
||||
var filters = parameters['filterFilter'].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: 'filterFilterType-' + f
|
||||
});
|
||||
|
||||
var type = parameters['filterFilterType-' + 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: 'filterFilterOp-' + f
|
||||
});
|
||||
|
||||
// Case sensitivite option
|
||||
if (parameters['filterFilterOp-' + f] === 'regex') {
|
||||
ports.push({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Case sensitive',
|
||||
editorName: f + ' filter| Case',
|
||||
name: 'filterFilterOption-case-' + f
|
||||
});
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: type || 'string',
|
||||
plug: 'input',
|
||||
group: f + ' filter',
|
||||
displayName: 'Value',
|
||||
editorName: f + ' Filter Value',
|
||||
name: 'filterFilterValue-' + f
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters['filterSort']) {
|
||||
var filters = parameters['filterSort'].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: 'filterSort-' + f
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: FilterCollectionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Filter Collection', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name.startsWith('filter')) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('dbCollections'));
|
||||
}
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.dbCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,694 @@
|
||||
const { Node } = require('@noodl/runtime');
|
||||
const guid = require('../../../guid');
|
||||
const Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
const React = require('react');
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
const { useEffect } = React;
|
||||
|
||||
function ForEachComponent(props) {
|
||||
const { didMount, willUnmount } = props;
|
||||
|
||||
useEffect(() => {
|
||||
didMount();
|
||||
return () => {
|
||||
willUnmount();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultDynamicScript =
|
||||
"// Set the 'component' variable to the name of the desired component for this item.\n" +
|
||||
"// Component name must start with a '/'.\n" +
|
||||
"// A component in a sheet is referred to by '/#Sheet Name/Comopnent Name'.\n" +
|
||||
"// The data for each item is available in a variable called 'item'\n" +
|
||||
"component = '/MyComponent';";
|
||||
|
||||
const ForEachDefinition = {
|
||||
name: 'For Each',
|
||||
displayNodeName: 'Repeater',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/repeater',
|
||||
color: 'visual',
|
||||
category: 'Visual',
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'templateType = explicit OR templateType NOT SET',
|
||||
inputs: ['template']
|
||||
},
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'templateType = dynamic',
|
||||
inputs: ['templateScript']
|
||||
}
|
||||
],
|
||||
initialize() {
|
||||
this._internal.itemNodes = [];
|
||||
this._internal.itemOutputSignals = {};
|
||||
this._internal.itemOutputs = {};
|
||||
this._internal.collection = Collection.get(); // We keep an internal collection so we don't have to refresh all content if the input items collection changes
|
||||
this._internal.queuedOperations = [];
|
||||
this._internal.mountedOperations = [];
|
||||
|
||||
// Add an item
|
||||
this._internal.collection.on('add', async (args) => {
|
||||
if (!this._internal.target) return;
|
||||
|
||||
this._queueOperation(async () => {
|
||||
const baseIndex = this._internal.target.getChildren().indexOf(this) + 1;
|
||||
await this.addItem(args.item, baseIndex + args.index);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove an item
|
||||
this._internal.collection.on('remove', (args) => {
|
||||
this._queueOperation(() => {
|
||||
this.removeItem(args.item);
|
||||
});
|
||||
});
|
||||
|
||||
// On collection changed
|
||||
this._internal.onItemsCollectionChanged = () => {
|
||||
const repeaterDisabledWhenUnmounted = NoodlRuntime.instance.getProjectSettings().repeaterDisabledWhenUnmounted;
|
||||
|
||||
if (repeaterDisabledWhenUnmounted && !this.isMounted) {
|
||||
this._internal.mountedOperations.push(() => {
|
||||
this._internal.collection.set(this._internal.items);
|
||||
});
|
||||
} else {
|
||||
this._queueOperation(() => {
|
||||
this._internal.collection.set(this._internal.items);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.addDeleteListener(() => {
|
||||
this._deleteAllItemNodes();
|
||||
});
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
group: 'Data',
|
||||
displayName: 'Items',
|
||||
type: 'array',
|
||||
set: function (value) {
|
||||
if (!value) return;
|
||||
if (value === this._internal.items) return;
|
||||
this.bindCollection(value);
|
||||
//this.scheduleRefresh();
|
||||
}
|
||||
},
|
||||
templateType: {
|
||||
group: 'Appearance',
|
||||
displayName: 'Template Type',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Explicit', value: 'explicit' },
|
||||
{ label: 'Dynamic', value: 'dynamic' }
|
||||
]
|
||||
},
|
||||
default: 'explicit',
|
||||
set: function (value) {
|
||||
this._internal.templateType = value;
|
||||
this.scheduleRefresh();
|
||||
}
|
||||
},
|
||||
template: {
|
||||
type: 'component',
|
||||
displayName: 'Template',
|
||||
group: 'Appearance',
|
||||
set: function (value) {
|
||||
this._internal.template = value;
|
||||
this.scheduleRefresh();
|
||||
}
|
||||
},
|
||||
templateScript: {
|
||||
type: { name: 'string', codeeditor: 'javascript', allowEditOnly: true },
|
||||
displayName: 'Script',
|
||||
group: 'Appearance',
|
||||
default: defaultDynamicScript,
|
||||
set: function (value) {
|
||||
try {
|
||||
this._internal.templateFunction = new Function('item', 'var component;' + value + ';return component;');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'foreach-syntax-warning',
|
||||
{ message: '<strong>Syntax</strong>: ' + e.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
this.scheduleRefresh();
|
||||
}
|
||||
},
|
||||
refresh: {
|
||||
group: 'Appearance',
|
||||
displayName: 'Refresh',
|
||||
type: 'signal',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRefresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
itemActionItemId: {
|
||||
type: 'string',
|
||||
group: 'Actions',
|
||||
displayName: 'Item Id',
|
||||
getter: function () {
|
||||
return this._internal.itemActionItemId;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
updateTarget: function (targetId) {
|
||||
this._internal.target = targetId ? this.nodeScope.getNodeWithId(targetId) : undefined;
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
setNodeModel: function (nodeModel) {
|
||||
Node.prototype.setNodeModel.call(this, nodeModel);
|
||||
if (nodeModel.parent) {
|
||||
this.updateTarget(nodeModel.parent.id);
|
||||
}
|
||||
var self = this;
|
||||
nodeModel.on(
|
||||
'parentUpdated',
|
||||
function (newParent) {
|
||||
self.updateTarget(newParent ? newParent.id : undefined);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
scheduleRefresh: function () {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledRefresh) {
|
||||
internal.hasScheduledRefresh = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._queueOperation(() => {
|
||||
this.refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
unbindCurrentCollection: function () {
|
||||
var collection = this._internal.items;
|
||||
if (!collection) return;
|
||||
|
||||
Collection.instanceOf(collection) && collection.off('change', this._internal.onItemsCollectionChanged);
|
||||
this._internal.items = undefined;
|
||||
},
|
||||
bindCollection: function (collection) {
|
||||
var internal = this._internal;
|
||||
|
||||
this.unbindCurrentCollection();
|
||||
|
||||
Collection.instanceOf(collection) && collection.on('change', this._internal.onItemsCollectionChanged);
|
||||
|
||||
internal.items = collection;
|
||||
this.scheduleCopyItems();
|
||||
},
|
||||
getTemplateForModel: function (model) {
|
||||
var internal = this._internal;
|
||||
if (internal.templateType === undefined || internal.templateType === 'explicit') return internal.template;
|
||||
|
||||
if (!internal.templateFunction) return;
|
||||
try {
|
||||
var template = internal.templateFunction(model);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'foreach-dynamic-warning',
|
||||
{ message: '<strong>Dynamic template</strong>: ' + e.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//simple (and limited) way to support ./ and ../ at the start of component template names
|
||||
if (template) {
|
||||
if (template.startsWith('./')) {
|
||||
template = this.model.component.name + template.substring(1);
|
||||
}
|
||||
|
||||
if (template.startsWith('../')) {
|
||||
const pathParts = this.model.component.name.split('/');
|
||||
const parentPath = pathParts.slice(0, pathParts.length - 1).join('/');
|
||||
template = parentPath + template.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
},
|
||||
_mapInputs: function (itemNode, model) {
|
||||
if (this._internal.inputMapFunc !== undefined) {
|
||||
// We have a mapping function, run the function and use the mapped values
|
||||
// as inputs
|
||||
this._internal.inputMapFunc(function (mappings) {
|
||||
for (var key in mappings) {
|
||||
if (itemNode.hasInput(key)) {
|
||||
if (typeof mappings[key] === 'function') {
|
||||
itemNode.setInputValue(key, mappings[key](model));
|
||||
} else if (typeof mappings[key] === 'string') {
|
||||
itemNode.setInputValue(key, model.get(mappings[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, model);
|
||||
}
|
||||
},
|
||||
addItem: async function (model, index) {
|
||||
var internal = this._internal;
|
||||
|
||||
// Create a new component for this item
|
||||
var template = this.getTemplateForModel(model);
|
||||
if (!template) return;
|
||||
|
||||
var itemNode = await this.nodeScope.createNode(template, guid(), {
|
||||
_forEachModel: model,
|
||||
_forEachNode: this
|
||||
});
|
||||
|
||||
// Set input values for all model data, and track changes
|
||||
if (this._internal.inputMapFunc === undefined) {
|
||||
//set component inputs with values from model
|
||||
if (itemNode.hasInput('Id')) {
|
||||
itemNode.setInputValue('Id', model.getId());
|
||||
}
|
||||
if (itemNode.hasInput('id')) {
|
||||
itemNode.setInputValue('id', model.getId());
|
||||
}
|
||||
|
||||
for (var inputKey in itemNode._inputs) {
|
||||
if (model.data[inputKey] !== undefined) itemNode.setInputValue(inputKey, model.data[inputKey]);
|
||||
}
|
||||
|
||||
//listen to changes on model
|
||||
itemNode._forEachModelChangeListener = function (ev) {
|
||||
if (itemNode._inputs[ev.name]) itemNode.setInputValue(ev.name, ev.value);
|
||||
};
|
||||
model.on('change', itemNode._forEachModelChangeListener);
|
||||
|
||||
//listen to changes to the component inputs
|
||||
itemNode.componentModel.on(
|
||||
'inputPortAdded',
|
||||
(port) => {
|
||||
if (port.name === 'id') itemNode.setInputValue('id', model.getId());
|
||||
if (port.name === 'Id') itemNode.setInputValue('Id', model.getId());
|
||||
|
||||
if (model.data[port.name] !== undefined) {
|
||||
itemNode.setInputValue(port.name, model.data[port.name]);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
} else {
|
||||
// If there is a map script, then use it
|
||||
this._mapInputs(itemNode, model);
|
||||
itemNode._forEachModelChangeListener = () => this._mapInputs(itemNode, model);
|
||||
model.on('change', itemNode._forEachModelChangeListener);
|
||||
}
|
||||
|
||||
// Create connections for all item output signals that we should forward
|
||||
itemNode._internal.creatorCallbacks = {
|
||||
onOutputChanged: (name, value, oldValue) => {
|
||||
if ((oldValue === false || oldValue === undefined) && value === true && internal.itemOutputSignals[name]) {
|
||||
this.itemOutputSignalTriggered(name, model, itemNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Connect all model nodes of the component that have id type = instance
|
||||
/*var itemScopes = itemNode.nodeScope.getNodesWithType('Model')
|
||||
if(itemScopes && itemScopes.length>0) {
|
||||
for(var j = 0; j < itemScopes.length; j++) {
|
||||
itemScopes[j].hasInstanceIDType()&&itemScopes[j].setModel(model);
|
||||
}
|
||||
}*/
|
||||
|
||||
// If there is a for each actions node, signal that the item has been added
|
||||
var forEachActions = itemNode.nodeScope.getNodesWithType('For Each Actions');
|
||||
for (var j = 0; j < forEachActions.length; j++) {
|
||||
forEachActions[j].signalAdded();
|
||||
}
|
||||
|
||||
internal.itemNodes.push(itemNode);
|
||||
internal.target.addChild(itemNode, index);
|
||||
},
|
||||
removeItem: function (model) {
|
||||
var internal = this._internal;
|
||||
if (!internal.target) return;
|
||||
|
||||
function findChild() {
|
||||
var children = internal.target.getChildren();
|
||||
for (var i in children) {
|
||||
var c = children[i];
|
||||
if (c._forEachModel === model && !c._forEachRemoveInProgress) return c;
|
||||
}
|
||||
}
|
||||
var child = findChild();
|
||||
if (!child) return;
|
||||
|
||||
var forEachActions = child.nodeScope.getNodesWithType('For Each Actions');
|
||||
if (forEachActions && forEachActions.length > 0) {
|
||||
// Run a try remove on the for each actions, remove the child when completed
|
||||
child._forEachRemoveInProgress = true;
|
||||
forEachActions[0].tryRemove(() => this._deleteItem(child));
|
||||
} else {
|
||||
// There are no for each actions, just remove the item
|
||||
this._deleteItem(child);
|
||||
}
|
||||
|
||||
var idx = internal.itemNodes.indexOf(child);
|
||||
idx !== -1 && internal.itemNodes.splice(idx, 1);
|
||||
},
|
||||
_deleteItem(item) {
|
||||
item._forEachModel.off('change', item._forEachModelChangeListener);
|
||||
|
||||
item.model && item.model.removeListenersWithRef(this);
|
||||
item.componentModel && item.componentModel.removeListenersWithRef(this);
|
||||
|
||||
const parent = item.parent;
|
||||
if (item._deleted || !parent) return;
|
||||
|
||||
parent.removeChild(item);
|
||||
this.nodeScope.deleteNode(item);
|
||||
},
|
||||
_deleteAllItemNodes: function () {
|
||||
if (!this._internal.itemNodes) return;
|
||||
|
||||
for (const itemNode of this._internal.itemNodes) {
|
||||
this._deleteItem(itemNode);
|
||||
}
|
||||
|
||||
this._internal.itemNodes = [];
|
||||
},
|
||||
refresh: async function () {
|
||||
var internal = this._internal;
|
||||
internal.hasScheduledRefresh = false;
|
||||
if (!(internal.template || internal.templateFunction) || !internal.items) return;
|
||||
|
||||
this._deleteAllItemNodes();
|
||||
|
||||
//check if we have a target to add nodes to
|
||||
if (!internal.target) return;
|
||||
|
||||
// figure out our index in our target
|
||||
const baseIndex = this._internal.target.getChildren().indexOf(this) + 1;
|
||||
|
||||
// Iterate over all models and create items
|
||||
for (var i = 0; i < internal.collection.size(); i++) {
|
||||
var model = internal.collection.get(i);
|
||||
|
||||
await this.addItem(model, baseIndex + i);
|
||||
}
|
||||
},
|
||||
_queueOperation(op) {
|
||||
this._internal.queuedOperations.push(op);
|
||||
this._runQueueOperations();
|
||||
},
|
||||
async _runQueueOperations() {
|
||||
if (this.runningOperations) {
|
||||
return;
|
||||
}
|
||||
this.runningOperations = true;
|
||||
|
||||
const repeaterCreateComponentsAsync = NoodlRuntime.instance.getProjectSettings().repeaterCreateComponentsAsync;
|
||||
|
||||
if (repeaterCreateComponentsAsync) {
|
||||
//create items in chunks of roughly 25ms at a time
|
||||
//so basically trying to keep ~30 fps
|
||||
const runOps = async () => {
|
||||
const start = performance.now();
|
||||
|
||||
while (this._internal.queuedOperations.length && performance.now() - start < 25) {
|
||||
const op = this._internal.queuedOperations.shift();
|
||||
await op();
|
||||
}
|
||||
|
||||
if (this._internal.queuedOperations.length) {
|
||||
setTimeout(runOps, 0);
|
||||
} else {
|
||||
this.runningOperations = false;
|
||||
}
|
||||
};
|
||||
|
||||
runOps();
|
||||
} else {
|
||||
while (this._internal.queuedOperations.length) {
|
||||
const op = this._internal.queuedOperations.shift();
|
||||
await op();
|
||||
}
|
||||
|
||||
this.runningOperations = false;
|
||||
}
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this._internal.queuedOperations.length = 0; //delete all queued operations
|
||||
this.unbindCurrentCollection();
|
||||
},
|
||||
render() {
|
||||
return <ForEachComponent key={this.id} didMount={() => this.didMount()} willUnmount={() => this.willUnmount()} />;
|
||||
},
|
||||
didMount() {
|
||||
this.isMounted = true;
|
||||
|
||||
for (const op of this._internal.mountedOperations) {
|
||||
this._queueOperation(op);
|
||||
}
|
||||
this._internal.mountedOperations = [];
|
||||
},
|
||||
willUnmount() {
|
||||
this.isMounted = false;
|
||||
},
|
||||
getItemActionParameter: function (name) {
|
||||
if (!this._internal.itemActionParameters) return;
|
||||
return this._internal.itemActionParameters[name];
|
||||
},
|
||||
scheduleCopyItems: function () {
|
||||
if (this._internal.hasScheduledCopyItems) return;
|
||||
this._internal.hasScheduledCopyItems = true;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this._internal.hasScheduledCopyItems = false;
|
||||
|
||||
if (this._internal.items === undefined) return;
|
||||
|
||||
const repeaterDisabledWhenUnmounted = NoodlRuntime.instance.getProjectSettings().repeaterDisabledWhenUnmounted;
|
||||
|
||||
if (repeaterDisabledWhenUnmounted && !this.isMounted) {
|
||||
this._internal.mountedOperations.push(() => {
|
||||
this._internal.collection.set(this._internal.items);
|
||||
});
|
||||
} else {
|
||||
this._internal.collection.set(this._internal.items);
|
||||
}
|
||||
});
|
||||
},
|
||||
itemOutputSignalTriggered: function (name, model, itemNode) {
|
||||
this._internal.itemActionItemId = model.getId();
|
||||
this._internal.itemActionSignal = name;
|
||||
this.flagOutputDirty('itemActionItemId');
|
||||
|
||||
// Send signal and update item outputs after they have been correctly updated
|
||||
if (!this._internal.hasScheduledTriggerItemOutputSignal) {
|
||||
this._internal.hasScheduledTriggerItemOutputSignal = true;
|
||||
this.context.scheduleAfterUpdate(() => {
|
||||
this._internal.hasScheduledTriggerItemOutputSignal = false;
|
||||
for (var key in itemNode._outputs) {
|
||||
var _output = 'itemOutput-' + key;
|
||||
if (this.hasOutput(_output)) {
|
||||
this._internal.itemOutputs[key] = itemNode._outputs[key].value;
|
||||
this.flagOutputDirty(_output);
|
||||
}
|
||||
}
|
||||
this.sendSignalOnOutput('itemOutputSignal-' + this._internal.itemActionSignal);
|
||||
});
|
||||
}
|
||||
},
|
||||
getItemOutput: function (name) {
|
||||
return this._internal.itemOutputs[name];
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('itemOutputSignal-')) {
|
||||
this._internal.itemOutputSignals[name.substring('itemOutputSignal-'.length)] = true;
|
||||
this.registerOutput(name, {
|
||||
getter: function () {
|
||||
/** No needed for signals */
|
||||
}
|
||||
});
|
||||
} else if (name.startsWith('itemOutput-'))
|
||||
this.registerOutput(name, {
|
||||
getter: this.getItemOutput.bind(this, name.substring('itemOutput-'.length))
|
||||
});
|
||||
},
|
||||
setInputMappingScript: function (value) {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'foreach-inputmapping-warning'
|
||||
);
|
||||
}
|
||||
|
||||
this._internal.inputMappingScript = value;
|
||||
|
||||
if (this._internal.inputMappingScript) {
|
||||
try {
|
||||
this._internal.inputMapFunc = new Function('map', 'object', this._internal.inputMappingScript);
|
||||
} catch (e) {
|
||||
this._internal.inputMapFunc = undefined;
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'foreach-inputmapping-warning',
|
||||
{ message: '<strong>Input mapping</strong>: ' + e.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._internal.inputMapFunc = undefined;
|
||||
}
|
||||
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'inputMappingScript')
|
||||
return this.registerInput(name, {
|
||||
set: this.setInputMappingScript.bind(this)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _typeName(t) {
|
||||
if (typeof t === 'object') return t.name;
|
||||
else return t;
|
||||
}
|
||||
|
||||
const defaultMapCode =
|
||||
'// Here you add mappings between the properties of the item objects and the inputs of the components.\n' +
|
||||
"// 'myComponentInput': 'myObjectProperty',\n" +
|
||||
"// 'anotherComponentInput': function () { return object.get('someProperty') + ' ' + object.get('otherProp') }\n" +
|
||||
'// These are the default mappings based on the selected template component.\n' +
|
||||
'map({\n' +
|
||||
'{{#mappings}}' +
|
||||
'})\n';
|
||||
|
||||
module.exports = {
|
||||
ForEachComponent: ForEachComponent,
|
||||
node: ForEachDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _collectPortsInTemplateComponent() {
|
||||
var templateComponentName = node.parameters.template;
|
||||
if (templateComponentName === undefined) return;
|
||||
|
||||
var ports = [];
|
||||
var c = graphModel.components[templateComponentName];
|
||||
if (c === undefined) return;
|
||||
|
||||
// Collect item outputs and signals
|
||||
for (var outputName in c.outputPorts) {
|
||||
var o = c.outputPorts[outputName];
|
||||
if (_typeName(o.type) === 'signal') {
|
||||
ports.push({
|
||||
name: 'itemOutputSignal-' + outputName,
|
||||
displayName: outputName,
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Item Signals'
|
||||
});
|
||||
} else {
|
||||
ports.push({
|
||||
name: 'itemOutput-' + outputName,
|
||||
displayName: outputName,
|
||||
type: o.type,
|
||||
plug: 'output',
|
||||
group: 'Item Outputs'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collect default mappigs for template component inputs
|
||||
var defaultMappings = '';
|
||||
for (var inputName in c.inputPorts) {
|
||||
var o = c.inputPorts[inputName];
|
||||
if (_typeName(o.type) !== 'signal') {
|
||||
defaultMappings += "\t'" + inputName + "': '" + inputName + "',\n";
|
||||
}
|
||||
}
|
||||
|
||||
ports.push({
|
||||
name: 'inputMappingScript',
|
||||
type: { name: 'string', codeeditor: 'javascript' },
|
||||
displayName: 'Script',
|
||||
group: 'Input Mapping',
|
||||
default: defaultMapCode.replace('{{#mappings}}', defaultMappings),
|
||||
plug: 'input'
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'output',
|
||||
prefix: 'itemOutput'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _trackComponentOutputs(componentName) {
|
||||
if (componentName === undefined) return;
|
||||
var c = graphModel.components[componentName];
|
||||
if (c === undefined) return;
|
||||
|
||||
c.on('outputPortAdded', _collectPortsInTemplateComponent);
|
||||
c.on('outputPortRemoved', _collectPortsInTemplateComponent);
|
||||
c.on('outputPortTypesUpdated', _collectPortsInTemplateComponent);
|
||||
|
||||
c.on('inputPortTypesUpdated', _collectPortsInTemplateComponent);
|
||||
c.on('inputPortAdded', _collectPortsInTemplateComponent);
|
||||
c.on('inputPortRemoved', _collectPortsInTemplateComponent);
|
||||
}
|
||||
|
||||
_collectPortsInTemplateComponent();
|
||||
_trackComponentOutputs(node.parameters.template);
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'template') {
|
||||
_collectPortsInTemplateComponent();
|
||||
_trackComponentOutputs(node.parameters.template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.For Each', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('For Each')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
const { EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
|
||||
const ForEachActionsDefinition = {
|
||||
name: 'For Each Actions',
|
||||
docs: 'https://docs.noodl.net/nodes/ui-controls/repeater-item',
|
||||
displayNodeName: 'Repeater Item',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
inputs: {
|
||||
removeCompleted: {
|
||||
type: { name: 'boolean', allowConnectionsOnly: true },
|
||||
displayName: 'Remove Completed',
|
||||
group: 'Events',
|
||||
valueChangedToTrue: function () {
|
||||
this._internal.removeCompletedCallback && this._internal.removeCompletedCallback();
|
||||
}
|
||||
}
|
||||
/* itemActions:{
|
||||
type:{name:'stringlist',allowEditOnly:true},
|
||||
group:'Actions',
|
||||
set:function(value) {
|
||||
}
|
||||
},
|
||||
itemActionParameters:{
|
||||
type:{name:'stringlist',allowEditOnly:true},
|
||||
group:'Action Parameters',
|
||||
set:function(value) {
|
||||
}
|
||||
} */
|
||||
},
|
||||
outputs: {
|
||||
added: {
|
||||
type: 'signal',
|
||||
displayName: 'Added',
|
||||
group: 'Events'
|
||||
},
|
||||
tryRemove: {
|
||||
type: 'signal',
|
||||
displayName: 'Try Remove',
|
||||
group: 'Events'
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
displayName: 'Item Id',
|
||||
group: 'General',
|
||||
get() {
|
||||
return this.getItemId();
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
getItemId() {
|
||||
const model = this.nodeScope.componentOwner._forEachModel;
|
||||
return model && model.getId();
|
||||
},
|
||||
signalAdded: function () {
|
||||
this.sendSignalOnOutput('added');
|
||||
},
|
||||
tryRemove: function (callback) {
|
||||
if (this.getOutput('tryRemove').hasConnections()) {
|
||||
this._internal.removeCompletedCallback = callback;
|
||||
this.sendSignalOnOutput('tryRemove');
|
||||
} else {
|
||||
// Schedule for later in this frame so any collection nodes
|
||||
// being delete can complete data persistence before being
|
||||
// deleted
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
},
|
||||
itemActionTriggered(name) {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const itemId = this.getItemId();
|
||||
const parentForEach = this.nodeScope.componentOwner._forEachNode;
|
||||
parentForEach.signalItemAction(name, itemId, this._internal.actionParameters || {});
|
||||
});
|
||||
},
|
||||
setItemActionParameter(name) {
|
||||
if (!this._internal.actionParameters) this._internal.actionParameters = {};
|
||||
this._internal.actionParameters[name] = name;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('itemAction-'))
|
||||
return this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: this.itemActionTriggered.bind(this, name)
|
||||
})
|
||||
});
|
||||
|
||||
if (name.startsWith('itemActionParameter-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setItemActionParameter.bind(this, name)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ForEachActionsDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* graphModel.on("nodeAdded.For Each Actions", function (node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
var actions = node.parameters['itemActions'];
|
||||
if(actions) {
|
||||
actions.split(',').forEach((a) => {
|
||||
ports.push({
|
||||
name:'itemAction-' + a,
|
||||
displayName:a,
|
||||
plug:'input',
|
||||
type:'signal',
|
||||
group:'Actions',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var parameters = node.parameters['itemActionParameters'];
|
||||
if(parameters) {
|
||||
parameters.split(',').forEach((p) => {
|
||||
ports.push({
|
||||
name:'itemActionParameter-' + p,
|
||||
displayName:p,
|
||||
plug:'input',
|
||||
type:'*',
|
||||
group:'Parameters',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated',function(event) {
|
||||
if(event.name === 'itemActions' || event.name === 'itemActionParameters') _updatePorts();
|
||||
})
|
||||
|
||||
})*/
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model'),
|
||||
Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
const defaultMapCode =
|
||||
'map({\n' +
|
||||
'\t// Here you add mappings between the input object and the mapped output object.\n' +
|
||||
"\t//myOutputProp: 'inputProp',\n" +
|
||||
"\t//anotherProperty: function(object) { return object.get('someProperty') + ' ' + object.get('otherProp') }\n" +
|
||||
'})\n';
|
||||
|
||||
var MapCollectionNode = {
|
||||
name: 'Map Collection',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/array-map',
|
||||
displayNodeName: 'Array Map',
|
||||
shortDesc: 'Map array fields',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var _this = this;
|
||||
|
||||
this._internal.collectionChangedCallback = function () {
|
||||
_this.scheduleMap();
|
||||
};
|
||||
|
||||
// this._internal.mappedCollection = Collection.get();
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this.setCollection(value);
|
||||
this.scheduleMap();
|
||||
}
|
||||
},
|
||||
mapScript: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
displayName: 'Script',
|
||||
default: defaultMapCode,
|
||||
set: function (value) {
|
||||
this._internal.mapCode = value;
|
||||
try {
|
||||
this._internal.mapFunc = new Function('map', 'object', this._internal.mapCode);
|
||||
} catch (e) {
|
||||
this._internal.mapFunc = undefined;
|
||||
console.log('Error while parsing map script: ' + e);
|
||||
}
|
||||
this.scheduleMap();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.mappedCollection;
|
||||
}
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.mappedCollection ? this._internal.mappedCollection.size() : 0;
|
||||
}
|
||||
},
|
||||
modified: {
|
||||
group: 'Events',
|
||||
type: 'signal',
|
||||
displayName: 'Changed'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
setCollection: function (collection) {
|
||||
this.bindCollection(collection);
|
||||
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();
|
||||
},
|
||||
scheduleMap: function () {
|
||||
if (this.collectionChangedScheduled) return;
|
||||
this.collectionChangedScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.collectionChangedScheduled = false;
|
||||
if (this._internal.collection === undefined) return;
|
||||
|
||||
var mappedModels = this._internal.collection.map((model) => {
|
||||
var m = Model.create();
|
||||
this._internal.mapFunc(function (mappings) {
|
||||
for (var key in mappings) {
|
||||
if (typeof mappings[key] === 'function') {
|
||||
m.set(key, mappings[key](model));
|
||||
} else if (typeof mappings[key] === 'string') {
|
||||
m.set(key, model.get(mappings[key]));
|
||||
}
|
||||
}
|
||||
}, model);
|
||||
return m;
|
||||
});
|
||||
|
||||
this._internal.mappedCollection = Collection.create(mappedModels);
|
||||
|
||||
this.sendSignalOnOutput('modified');
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: MapCollectionNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
'use strict';
|
||||
|
||||
const { Services } = require('@noodl/runtime');
|
||||
|
||||
var PersistHelper = function (args) {
|
||||
for (var i in args) this[i] = args[i];
|
||||
|
||||
this.hasScheduled = {};
|
||||
|
||||
requestInitialFetchOnConnected();
|
||||
};
|
||||
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
// Handling of initial fetch for sync models and collections
|
||||
// Each model/collection requiring initial fetch is only requested once
|
||||
var hasScheduledInitialFetchForId = {};
|
||||
var initialFetchHandlerForType = {};
|
||||
|
||||
PersistHelper.setInitialFetchHandlerForType = function (type, handler) {
|
||||
initialFetchHandlerForType[type] = handler;
|
||||
};
|
||||
|
||||
function initialFetchMessageHandler(message) {
|
||||
var id = message.topic.substring('ndl/persist/getaccepted/'.length);
|
||||
if (hasScheduledInitialFetchForId[id] === message.payload.clientToken) {
|
||||
var type = id.split('/')[0];
|
||||
var _id = id.substring(type.length + 1);
|
||||
initialFetchHandlerForType[type](_id, message.payload.data);
|
||||
}
|
||||
}
|
||||
|
||||
function requestInitialFetchForId(id) {
|
||||
if (hasScheduledInitialFetchForId[id]) return;
|
||||
|
||||
Services.pubsub.subscribe('ndl/persist/getaccepted/' + id, initialFetchMessageHandler);
|
||||
|
||||
hasScheduledInitialFetchForId[id] = guid();
|
||||
Services.pubsub.publish('ndl/persist/get/' + id, {
|
||||
clientToken: hasScheduledInitialFetchForId[id]
|
||||
});
|
||||
}
|
||||
|
||||
var connectionHandlerInstalled = false;
|
||||
function requestInitialFetchOnConnected() {
|
||||
if (!connectionHandlerInstalled) {
|
||||
connectionHandlerInstalled = true;
|
||||
|
||||
Services.pubsub.on('connected', function () {
|
||||
// Must request all sync models
|
||||
for (var id in hasScheduledInitialFetchForId) {
|
||||
Services.pubsub.publish('ndl/persist/get/' + id, {
|
||||
clientToken: hasScheduledInitialFetchForId[id]
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handling of delta notifications from persist servier
|
||||
// each delta notification is handled only once by the first
|
||||
// handler that is registered
|
||||
var deltaHandlers = {};
|
||||
var deltaHandlerForType = {};
|
||||
|
||||
PersistHelper.setDeltaHandlerForType = function (type, handler) {
|
||||
deltaHandlerForType[type] = handler;
|
||||
};
|
||||
|
||||
function deltaMessageHandler(message) {
|
||||
var id = message.topic.substring('ndl/persist/delta/'.length);
|
||||
if (deltaHandlers[id]) {
|
||||
var type = id.split('/')[0];
|
||||
var _id = id.substring(type.length + 1);
|
||||
deltaHandlerForType[type](_id, message.payload.delta);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToDeltaForId(id) {
|
||||
if (!deltaHandlers[id]) {
|
||||
// First subscrive, register handler and notify pubsub that we want to subscribe to the topic
|
||||
deltaHandlers[id] = { cnt: 1 };
|
||||
Services.pubsub.subscribe('ndl/persist/delta/' + id, deltaMessageHandler);
|
||||
} else {
|
||||
// If this is the second time someone subscribes for this id, just update the count
|
||||
deltaHandlers[id].cnt++;
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribeToDeltaForId(id) {
|
||||
if (deltaHandlers[id]) {
|
||||
deltaHandlers[id].cnt--; // Reduce count
|
||||
if (deltaHandlers[id].cnt === 0) {
|
||||
// No more interested in delta for this id, unsubscribe from persist service
|
||||
Services.pubsub.unsubscribe('ndl/persist/delta/' + id, deltaMessageHandler);
|
||||
delete deltaHandlers[id];
|
||||
delete hasScheduledInitialFetchForId[id]; // Next time someone subscribes for deltas, make a new initial fetch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PersistHelper.prototype.setId = function (id) {
|
||||
this.id = id;
|
||||
|
||||
var _this = this;
|
||||
this.schedule('subscribeToPersistService', function () {
|
||||
_this.subscribeToPersistService();
|
||||
});
|
||||
};
|
||||
|
||||
PersistHelper.prototype.isWorking = function () {
|
||||
return !!this.persistWorking;
|
||||
};
|
||||
|
||||
PersistHelper.prototype.setPersistType = function (type) {
|
||||
this.persistType = type;
|
||||
|
||||
var _this = this;
|
||||
this.schedule('subscribeToPersistService', function () {
|
||||
_this.subscribeToPersistService();
|
||||
});
|
||||
};
|
||||
|
||||
PersistHelper.prototype.schedule = function (type, callback) {
|
||||
var _this = this;
|
||||
|
||||
if (this.hasScheduled[type]) return;
|
||||
this.hasScheduled[type] = true;
|
||||
|
||||
this.scheduleFunction(function () {
|
||||
callback();
|
||||
|
||||
_this.hasScheduled[type] = false;
|
||||
});
|
||||
};
|
||||
|
||||
PersistHelper.prototype.unsubscribe = function () {
|
||||
if (this.lastSubscribedId) {
|
||||
// Unsubscribe to current model id
|
||||
Services.pubsub.unsubscribe('ndl/persist/updateaccepted/' + this.lastSubscribedId, this.messageHandler);
|
||||
Services.pubsub.unsubscribe('ndl/persist/updaterejected/' + this.lastSubscribedId, this.messageHandler);
|
||||
Services.pubsub.unsubscribe('ndl/persist/getaccepted/' + this.lastSubscribedId, this.messageHandler);
|
||||
Services.pubsub.unsubscribe('ndl/persist/getrejected/' + this.lastSubscribedId, this.messageHandler);
|
||||
unsubscribeToDeltaForId(this.lastSubscribedId);
|
||||
}
|
||||
};
|
||||
|
||||
PersistHelper.prototype.handleMessage = function (message) {
|
||||
// Handle update accepted
|
||||
if (message.topic.indexOf('ndl/persist/updateaccepted/') === 0) {
|
||||
if (message.payload.clientToken === this.persistUpdateClientToken) {
|
||||
this.persistWorking = false;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
this.onPersistSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle update rejected
|
||||
else if (message.topic.indexOf('ndl/persist/updaterejected/') === 0) {
|
||||
if (message.payload.clientToken === this.persistUpdateClientToken) {
|
||||
this.persistWorking = false;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
this.onPersistFailure();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle get accepted
|
||||
else if (message.topic.indexOf('ndl/persist/getaccepted/') === 0) {
|
||||
if (message.payload.clientToken === this.persistGetClientToken) {
|
||||
this.persistWorking = false;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
this.onFetchSuccess(message.payload.data);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle get rejected
|
||||
else if (message.topic.indexOf('ndl/persist/getrejected/') === 0) {
|
||||
if (message.payload.clientToken === this.persistGetClientToken) {
|
||||
this.persistWorking = false;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
this.onFetchFailure();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PersistHelper.prototype.subscribeToPersistService = function () {
|
||||
var _this = this;
|
||||
var id = this.id !== '' ? this.id : undefined;
|
||||
|
||||
if (!(this.persistType === 'global' || this.persistType === 'sync')) return; // Only subscribe in global and sync mode
|
||||
if (this.lastSubscribedId === id) return; // We are already subscribed to this id
|
||||
|
||||
this.unsubscribe();
|
||||
this.lastSubscribedId = id;
|
||||
if (id === undefined) return;
|
||||
|
||||
// Subscribe to persistance topics
|
||||
if (this.persistType === 'global' || this.persistType === 'sync') {
|
||||
this.messageHandler = this.handleMessage.bind(this);
|
||||
|
||||
// Subscribe to persist accepted for this model
|
||||
Services.pubsub.subscribe('ndl/persist/updateaccepted/' + id, this.messageHandler);
|
||||
|
||||
// Subscrive to presist rejected for this model
|
||||
Services.pubsub.subscribe('ndl/persist/updaterejected/' + id, this.messageHandler);
|
||||
|
||||
// Subscrive to get accepted for this model
|
||||
Services.pubsub.subscribe('ndl/persist/getaccepted/' + id, this.messageHandler);
|
||||
|
||||
// Subscrive to get rejected for this model
|
||||
Services.pubsub.subscribe('ndl/persist/getrejected/' + id, this.messageHandler);
|
||||
|
||||
// Subscrive to delta topics
|
||||
if (this.persistType === 'sync') {
|
||||
subscribeToDeltaForId(id);
|
||||
|
||||
// If we are in sync mode and we have not previously scheduled a fetch for this id
|
||||
// fetch it
|
||||
requestInitialFetchForId(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PersistHelper.prototype.schedulePersist = function () {
|
||||
var _this = this;
|
||||
this.schedule('schedulePersist', function () {
|
||||
_this.persistData(_this.onPersistDataNeeded());
|
||||
});
|
||||
};
|
||||
|
||||
PersistHelper.prototype.persistData = function (data) {
|
||||
if (!data) return;
|
||||
|
||||
var persistType = this.persistType;
|
||||
var id = this.id;
|
||||
|
||||
if (persistType === 'local') {
|
||||
// Store model on local storage
|
||||
try {
|
||||
localStorage['noodl-persist-' + id] = JSON.stringify(data);
|
||||
this.onPersistSuccess();
|
||||
} catch (e) {
|
||||
this.onPersistFailure();
|
||||
}
|
||||
} else if (persistType === 'global' || persistType === 'sync') {
|
||||
// Send update request to persist service
|
||||
this.persistUpdateClientToken = guid();
|
||||
Services.pubsub.publish('ndl/persist/update/' + id, {
|
||||
data: data,
|
||||
clientToken: this.persistUpdateClientToken
|
||||
});
|
||||
|
||||
this.persistWorking = true;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
}
|
||||
};
|
||||
|
||||
PersistHelper.prototype.scheduleFetch = function () {
|
||||
var _this = this;
|
||||
this.schedule('scheduleFetch', function () {
|
||||
_this.fetchData();
|
||||
});
|
||||
};
|
||||
|
||||
PersistHelper.prototype.fetchData = function () {
|
||||
var persistType = this.persistType;
|
||||
var id = this.id;
|
||||
|
||||
if (persistType === 'local') {
|
||||
// Store model on local storage
|
||||
try {
|
||||
var json = localStorage['noodl-persist-' + id];
|
||||
var data = JSON.parse(json ? json : '{}');
|
||||
if (id.indexOf('collection/') === 0) {
|
||||
// This is a collection, fetch all models
|
||||
var items = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var modelJson = localStorage['noodl-persist-model/' + data[i]];
|
||||
var modelData = JSON.parse(modelJson ? modelJson : '{}');
|
||||
modelData.id = data[i];
|
||||
items.push(modelData);
|
||||
}
|
||||
this.onFetchSuccess(items);
|
||||
} else this.onFetchSuccess(data);
|
||||
} catch (e) {
|
||||
this.onFetchFailure();
|
||||
}
|
||||
} else if (persistType === 'global' || persistType === 'sync') {
|
||||
// Send get request to persist service
|
||||
this.persistGetClientToken = guid();
|
||||
Services.pubsub.publish('ndl/persist/get/' + id, {
|
||||
clientToken: this.persistGetClientToken
|
||||
});
|
||||
|
||||
this.persistWorking = true;
|
||||
this.onWorkingChanged(this.persistWorking);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PersistHelper;
|
||||
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
const Model = require('@noodl/runtime/src/model');
|
||||
const Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
var SetVariableNodeDefinition = {
|
||||
name: 'Set Variable',
|
||||
docs: 'https://docs.noodl.net/nodes/data/variable/set-variable',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'name',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.variablesModel = Model.get('--ndl--global-variables');
|
||||
},
|
||||
outputs: {
|
||||
done: {
|
||||
type: 'signal',
|
||||
displayName: 'Done',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
name: {
|
||||
type: {
|
||||
name: 'string',
|
||||
identifierOf: 'VariableName',
|
||||
identifierDisplayName: 'Variable names'
|
||||
},
|
||||
displayName: 'Name',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.name = value;
|
||||
}
|
||||
},
|
||||
setWith: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Empty string', value: 'emptyString' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Object', value: 'object' },
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'Any', value: '*' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Set as',
|
||||
default: '*',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.setWith = value;
|
||||
}
|
||||
},
|
||||
do: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleStore();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue: function (value) {
|
||||
this._internal.value = value;
|
||||
},
|
||||
scheduleStore: function () {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
this.hasScheduledStore = false;
|
||||
|
||||
var value = internal.setWith === 'emptyString' ? '' : internal.value;
|
||||
|
||||
if (internal.setWith === 'object' && typeof value === 'string') value = Model.get(value); // Can set arrays with "id" or array
|
||||
if (internal.setWith === 'array' && typeof value === 'string') value = Collection.get(value); // Can set arrays with "id" or array
|
||||
if (internal.setWith === 'boolean') value = !!value;
|
||||
|
||||
//use forceChange to always trigger Variable nodes to send the value on their output, even if it's the same value twice
|
||||
internal.variablesModel.set(internal.name, value, {
|
||||
forceChange: true
|
||||
});
|
||||
this.sendSignalOnOutput('done');
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'value')
|
||||
this.registerInput(name, {
|
||||
set: this.setValue.bind(this)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: SetVariableNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Set Variable', function (node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
if (node.parameters.setWith === 'emptyString') {
|
||||
// No ports needed
|
||||
} else {
|
||||
ports.push({
|
||||
type: node.parameters.setWith !== undefined ? node.parameters.setWith : '*',
|
||||
plug: 'input',
|
||||
group: 'General',
|
||||
name: 'value',
|
||||
displayName: 'Value'
|
||||
});
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
_updatePorts();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
const Collection = require('@noodl/runtime/src/collection');
|
||||
|
||||
function CSVToArray(strData, strDelimiter) {
|
||||
// Check to see if the delimiter is defined. If not,
|
||||
// then default to comma.
|
||||
strDelimiter = strDelimiter || ',';
|
||||
|
||||
// Create a regular expression to parse the CSV values.
|
||||
var objPattern = new RegExp(
|
||||
// Delimiters.
|
||||
'(\\' +
|
||||
strDelimiter +
|
||||
'|\\r?\\n|\\r|^)' +
|
||||
// Quoted fields.
|
||||
'(?:"([^"]*(?:""[^"]*)*)"|' +
|
||||
// Standard fields.
|
||||
'([^"\\' +
|
||||
strDelimiter +
|
||||
'\\r\\n]*))',
|
||||
'gi'
|
||||
);
|
||||
|
||||
// Create an array to hold our data. Give the array
|
||||
// a default empty first row.
|
||||
var arrData = [[]];
|
||||
|
||||
// Create an array to hold our individual pattern
|
||||
// matching groups.
|
||||
var arrMatches = null;
|
||||
|
||||
var prevLastIndex;
|
||||
|
||||
// Keep looping over the regular expression matches
|
||||
// until we can no longer find a match.
|
||||
while ((arrMatches = objPattern.exec(strData)) && prevLastIndex !== objPattern.lastIndex) {
|
||||
prevLastIndex = objPattern.lastIndex;
|
||||
|
||||
// Get the delimiter that was found.
|
||||
var strMatchedDelimiter = arrMatches[1];
|
||||
|
||||
// Check to see if the given delimiter has a length
|
||||
// (is not the start of string) and if it matches
|
||||
// field delimiter. If id does not, then we know
|
||||
// that this delimiter is a row delimiter.
|
||||
if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) {
|
||||
// Since we have reached a new row of data,
|
||||
// add an empty row to our data array.
|
||||
arrData.push([]);
|
||||
}
|
||||
|
||||
var strMatchedValue;
|
||||
|
||||
// Now that we have our delimiter out of the way,
|
||||
// let's check to see which kind of value we
|
||||
// captured (quoted or unquoted).
|
||||
if (arrMatches[2]) {
|
||||
// We found a quoted value. When we capture
|
||||
// this value, unescape any double quotes.
|
||||
strMatchedValue = arrMatches[2].replace(new RegExp('""', 'g'), '"');
|
||||
} else {
|
||||
// We found a non-quoted value.
|
||||
strMatchedValue = arrMatches[3];
|
||||
}
|
||||
|
||||
// Now that we have our value string, let's add
|
||||
// it to the data array.
|
||||
arrData[arrData.length - 1].push(strMatchedValue);
|
||||
}
|
||||
|
||||
// Return the parsed data.
|
||||
return arrData;
|
||||
}
|
||||
|
||||
var CSVNode = {
|
||||
name: 'Static Data',
|
||||
docs: 'https://docs.noodl.net/nodes/data/array/static-array',
|
||||
displayNodeName: 'Static Array',
|
||||
shortDesc: 'Store static data to populate a Collection with items.',
|
||||
category: 'Data',
|
||||
color: 'data',
|
||||
nodeDoubleClickAction: [
|
||||
{
|
||||
focusPort: 'JSON'
|
||||
},
|
||||
{
|
||||
focusPort: 'CSV'
|
||||
}
|
||||
],
|
||||
getInspectInfo() {
|
||||
if (this._internal.collection) {
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: this._internal.collection.items
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
dynamicports: [
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'type = csv OR type NOT SET',
|
||||
inputs: ['csv']
|
||||
},
|
||||
{
|
||||
name: 'conditionalports/extended',
|
||||
condition: 'type = json',
|
||||
inputs: ['json']
|
||||
}
|
||||
],
|
||||
inputs: {
|
||||
type: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'CSV', value: 'csv' },
|
||||
{ label: 'JSON', value: 'json' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Type',
|
||||
group: 'General',
|
||||
default: 'csv',
|
||||
set: function (value) {
|
||||
this._internal.type = value;
|
||||
}
|
||||
},
|
||||
csv: {
|
||||
type: { name: 'string', codeeditor: 'text', allowEditOnly: true },
|
||||
displayName: 'CSV',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.csv = value;
|
||||
this.scheduleParseData();
|
||||
}
|
||||
},
|
||||
json: {
|
||||
type: { name: 'string', codeeditor: 'json', allowEditOnly: true },
|
||||
displayName: 'JSON',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.json = value;
|
||||
this.scheduleParseData();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
group: 'General',
|
||||
getter: function () {
|
||||
return this._internal.collection;
|
||||
}
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
displayName: 'Count',
|
||||
group: 'General',
|
||||
get() {
|
||||
return this._internal.collection ? this._internal.collection.size() : 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleParseData: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.hasScheduledParseData) {
|
||||
internal.hasScheduledParseData = true;
|
||||
this.scheduleAfterInputsHaveUpdated(this.parseData.bind(this));
|
||||
}
|
||||
},
|
||||
parseData: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.hasScheduledParseData = false;
|
||||
|
||||
internal.collection = Collection.get();
|
||||
|
||||
if (internal.type === undefined || internal.type === 'csv') {
|
||||
// Data is string, parse it as CSV
|
||||
var data = CSVToArray(internal.csv);
|
||||
var json = [];
|
||||
var fields = data[0];
|
||||
for (var i = 1; i < data.length; i++) {
|
||||
var row = data[i];
|
||||
var obj = {};
|
||||
for (var j = 0; j < fields.length; j++) {
|
||||
obj[fields[j]] = row[j];
|
||||
}
|
||||
json.push(obj);
|
||||
}
|
||||
|
||||
internal.collection.set(json);
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
} else if (internal.type === 'json') {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'json-parse-warning');
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(internal.json);
|
||||
internal.collection.set(json);
|
||||
this.flagOutputDirty('items');
|
||||
this.flagOutputDirty('count');
|
||||
} catch (e) {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'json-parse-warning',
|
||||
{
|
||||
showGlobally: true,
|
||||
message: e.message
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CSVNode
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
var Model = require('@noodl/runtime/src/model');
|
||||
|
||||
var VariableNodeDefinition = {
|
||||
name: 'Variable2',
|
||||
displayNodeName: 'Variable',
|
||||
docs: 'https://docs.noodl.net/nodes/data/variable/variable-node',
|
||||
category: 'Data',
|
||||
usePortAsLabel: 'name',
|
||||
color: 'data',
|
||||
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;
|
||||
}
|
||||
},
|
||||
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');
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
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);
|
||||
});
|
||||
},
|
||||
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,213 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
|
||||
const EventReceiver = {
|
||||
name: 'Event Receiver',
|
||||
docs: 'https://docs.noodl.net/nodes/events/receive-event',
|
||||
displayNodeName: 'Receive Event',
|
||||
category: 'Events',
|
||||
usePortAsLabel: 'channelName',
|
||||
color: 'component',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.outputValues = {};
|
||||
internal.outputNames = [];
|
||||
internal.eventReceived = false;
|
||||
internal._isEnabled = true;
|
||||
internal.channelName = '';
|
||||
},
|
||||
inputs: {
|
||||
enabled: {
|
||||
displayName: 'Enabled',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
set: function (value) {
|
||||
this._internal._isEnabled = value ? true : false;
|
||||
}
|
||||
},
|
||||
consume: {
|
||||
displayName: 'Consume',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Never', value: 'never' },
|
||||
{ label: 'Always', value: 'always' }
|
||||
]
|
||||
},
|
||||
default: 'never',
|
||||
set: function (value) {
|
||||
this._internal.consume = value;
|
||||
}
|
||||
},
|
||||
channelName: {
|
||||
type: { name: 'string', identifierOf: 'EventChannelName' },
|
||||
displayName: 'Channel',
|
||||
set: function (value) {
|
||||
if (this._internal.onEventReceivedCallback) {
|
||||
//remove old listener
|
||||
this.context.eventSenderEmitter.removeListener(
|
||||
this._internal.channelName,
|
||||
this._internal.onEventReceivedCallback
|
||||
);
|
||||
this._internal.onEventReceivedCallback = null;
|
||||
}
|
||||
|
||||
this._internal.channelName = value;
|
||||
this.registerListenersForChannel(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
eventReceived: {
|
||||
displayName: 'Received',
|
||||
type: 'signal'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this._internal.outputNames.push(name);
|
||||
this.registerOutput(name, {
|
||||
getter: function () {
|
||||
return self._internal.outputValues[name];
|
||||
}
|
||||
});
|
||||
},
|
||||
handleEvent: function (eventData) {
|
||||
if (this._internal._isEnabled === false) {
|
||||
return;
|
||||
}
|
||||
this.sendSignalOnOutput('eventReceived');
|
||||
|
||||
for (var name in eventData) {
|
||||
if (this.hasOutput(name)) {
|
||||
this._internal.outputValues[name] = eventData[name];
|
||||
this.flagOutputDirty(name);
|
||||
}
|
||||
}
|
||||
|
||||
return this._internal.consume === 'always';
|
||||
},
|
||||
onEventReceived: function (eventData) {
|
||||
this.handleEvent(eventData);
|
||||
},
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
if (this._internal.onEventReceivedCallback) {
|
||||
var eventEmitter = this.context.eventSenderEmitter;
|
||||
eventEmitter.removeListener(this._internal.channelName, this._internal.onEventReceivedCallback);
|
||||
}
|
||||
},
|
||||
registerListenersForChannel: function (channelName) {
|
||||
var eventEmitter = this.context.eventSenderEmitter;
|
||||
this._internal.onEventReceivedCallback = this.onEventReceived.bind(this);
|
||||
eventEmitter.on(channelName, this._internal.onEventReceivedCallback);
|
||||
|
||||
var self = this;
|
||||
this.context.eventEmitter.once('applicationDataReloaded', function () {
|
||||
if (self._internal.onEventReceivedCallback) {
|
||||
eventEmitter.removeListener(channelName, self._internal.onEventReceivedCallback);
|
||||
}
|
||||
});
|
||||
},
|
||||
getChannelName: function () {
|
||||
return this._internal.channelName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: EventReceiver
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: EventReceiver,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onEventReceiver(node) {
|
||||
var channelName = node.parameters.channelName;
|
||||
|
||||
function _collectPayloadPorts() {
|
||||
var eventSenders = graphModel.getNodesWithType('Event Sender');
|
||||
var matching = eventSenders.filter((node) => node.parameters.channelName === channelName);
|
||||
|
||||
var portKeys = {};
|
||||
matching.forEach((node) => {
|
||||
const ports = node.parameters.payload ? node.parameters.payload.split(',') : [];
|
||||
for (let key of ports) {
|
||||
portKeys[key] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var ports = [];
|
||||
for (var key in portKeys) {
|
||||
ports.push({
|
||||
name: key,
|
||||
type: '*',
|
||||
plug: 'output',
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'output'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_collectPayloadPorts();
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'channelName') {
|
||||
channelName = event.value;
|
||||
_collectPayloadPorts();
|
||||
}
|
||||
});
|
||||
|
||||
// Track all event senders and update ports when they change
|
||||
function _trackEventSender(node) {
|
||||
//_collectPayloadPorts();
|
||||
|
||||
node.on('inputPortAdded', function (event) {
|
||||
_collectPayloadPorts();
|
||||
});
|
||||
|
||||
node.on('inputPortRemoved', function (event) {
|
||||
_collectPayloadPorts();
|
||||
});
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'channelName') {
|
||||
_collectPayloadPorts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.getNodesWithType('Event Sender').forEach(_trackEventSender);
|
||||
|
||||
graphModel.on('nodeAdded.Event Sender', _trackEventSender);
|
||||
|
||||
graphModel.on('nodeRemoved.Event Sender', (node) => {
|
||||
_collectPayloadPorts();
|
||||
});
|
||||
}
|
||||
|
||||
//wait with dynamic ports until the entire graph is loaded
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
//all future added nodes though delta updates
|
||||
graphModel.on('nodeAdded.Event Receiver', (node) => onEventReceiver(node));
|
||||
|
||||
//existing nodes from the initial export
|
||||
graphModel.getNodesWithType('Event Receiver').forEach((node) => onEventReceiver(node));
|
||||
});
|
||||
}
|
||||
};
|
||||
136
packages/noodl-viewer-react/src/nodes/std-library/eventsender.js
Normal file
136
packages/noodl-viewer-react/src/nodes/std-library/eventsender.js
Normal file
@@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
|
||||
const EventSender = {
|
||||
name: 'Event Sender',
|
||||
docs: 'https://docs.noodl.net/nodes/events/send-event',
|
||||
displayNodeName: 'Send Event',
|
||||
category: 'Events',
|
||||
usePortAsLabel: 'channelName',
|
||||
color: 'component',
|
||||
exportDynamicPorts: true,
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.channelName = '';
|
||||
this._internal.propagation = 'global';
|
||||
},
|
||||
inputs: {
|
||||
sendEvent: {
|
||||
displayName: 'Send',
|
||||
valueChangedToTrue: function () {
|
||||
var self = this;
|
||||
|
||||
//wait for all other inputs to update before sending
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (self._internal.propagation === 'global') {
|
||||
self.context.sendGlobalEventFromEventSender(self._internal.channelName, self._internal.inputValues);
|
||||
} else {
|
||||
self.nodeScope.sendEventFromThisScope(
|
||||
self._internal.channelName,
|
||||
self._internal.inputValues,
|
||||
self._internal.propagation
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
channelName: {
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
identifierOf: 'EventChannelName',
|
||||
identifierDisplayName: 'Event Channels'
|
||||
},
|
||||
default: '',
|
||||
group: 'Settings',
|
||||
displayName: 'Channel Name',
|
||||
set: function (value) {
|
||||
this._internal.channelName = value;
|
||||
this._internal.inputValues._channelName = value;
|
||||
}
|
||||
},
|
||||
propagation: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ value: 'global', label: 'Global' },
|
||||
{ value: 'parent', label: 'Parent' },
|
||||
{ value: 'children', label: 'Children' },
|
||||
{ value: 'siblings', label: 'Siblings' }
|
||||
]
|
||||
},
|
||||
default: 'global',
|
||||
group: 'Settings',
|
||||
displayName: 'Send to',
|
||||
set: function (value) {
|
||||
this._internal.propagation = value;
|
||||
}
|
||||
},
|
||||
payload: {
|
||||
type: {
|
||||
name: 'stringlist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Payload'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
registerInputIfNeeded: {
|
||||
value: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
self._internal.inputValues[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
var ports = [];
|
||||
|
||||
// Add payload inputs
|
||||
var payload = parameters.payload;
|
||||
if (payload) {
|
||||
payload = payload.split(',');
|
||||
for (const p of payload) {
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*',
|
||||
allowConnectionsOnly: true
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Payload',
|
||||
name: p,
|
||||
displayName: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: EventSender,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.Event Sender', function (node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
const ExternalLinkNode = {
|
||||
name: 'net.noodl.externallink',
|
||||
displayNodeName: 'External Link',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/external-link',
|
||||
category: 'Navigation',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'link'
|
||||
},
|
||||
inputs: {
|
||||
link: {
|
||||
type: 'string',
|
||||
displayName: 'Link'
|
||||
},
|
||||
openInNewTab: {
|
||||
type: 'boolean',
|
||||
displayName: 'Open In New Tab',
|
||||
default: true
|
||||
},
|
||||
do: {
|
||||
type: 'signal',
|
||||
displayName: 'Do',
|
||||
valueChangedToTrue() {
|
||||
const openInNewTab = this.getInputValue('openInNewTab');
|
||||
const params = openInNewTab ? 'noopener,noreferrer' : '';
|
||||
const target = openInNewTab === true || openInNewTab === undefined ? '_blank' : '_self';
|
||||
|
||||
window.open(this.getInputValue('link'), target, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
node: ExternalLinkNode
|
||||
};
|
||||
720
packages/noodl-viewer-react/src/nodes/std-library/javascript.js
Normal file
720
packages/noodl-viewer-react/src/nodes/std-library/javascript.js
Normal file
@@ -0,0 +1,720 @@
|
||||
'use strict';
|
||||
|
||||
const Node = require('@noodl/runtime').Node,
|
||||
JavascriptNodeParser = require('@noodl/runtime/src/javascriptnodeparser');
|
||||
|
||||
const guid = require('../../guid');
|
||||
|
||||
/*const defaultCode = "define({\n"+
|
||||
"\t// The input ports of the Javascript node, name of input and type\n"+
|
||||
"\tinputs:{\n"+
|
||||
"\t // ExampleInput:'number',\n"+
|
||||
"\t // Available types are 'number', 'string', 'boolean', 'color' and 'signal',\n"+
|
||||
"\t mySignal:'signal',\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// The output ports of the Javascript node, name of output and type\n"+
|
||||
"\toutputs:{\n"+
|
||||
"\t // ExampleOutput:'string',\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// All signal inputs need their own function with the corresponding name that\n"+
|
||||
"\t// will be run when a signal is received on the input.\n"+
|
||||
"\tmySignal:function(inputs,outputs) {\n"+
|
||||
"\t\t// ...\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// This function will be called when any of the inputs have changed\n"+
|
||||
"\tchange:function(inputs,outputs) {\n"+
|
||||
"\t\t// ...\n"+
|
||||
"\t}\n"+
|
||||
"})\n";*/
|
||||
|
||||
/*
|
||||
const defaultCode = "script({\n"+
|
||||
"\t// The input ports of the Javascript node, name of input and type\n"+
|
||||
"\tinputs:{\n"+
|
||||
"\t // ExampleInput:'number',\n"+
|
||||
"\t // Available types are 'number', 'string', 'boolean', 'color'\n"+
|
||||
"\t //myNumber:'number',\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// The output ports of the Javascript node, name of output and type\n"+
|
||||
"\toutputs:{\n"+
|
||||
"\t // ExampleOutput:'string',\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// Declare signal handle functions here, each function will be \n"+
|
||||
"\t// exposed as a signal input to this node.\n"+
|
||||
"\tsignals:{\n"+
|
||||
"\t\t// mySignal:function() { }\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// These functions will be called when the correspinding input\n"+
|
||||
"\t// is changed and the new value is provided\n"+
|
||||
"\tchanged:{\n"+
|
||||
"\t\t// myNumber:function(value) { }\n"+
|
||||
"\t},\n"+
|
||||
"\t\n"+
|
||||
"\t// Here you can declare any function that will then be available\n"+
|
||||
"\t// in this. So you can acces the function below with this.aFunction()\n"+
|
||||
"\tmethods:{\n"+
|
||||
"\t\t// aFunction:function(value) { }\n"+
|
||||
"\t}\n"+
|
||||
"})\n";
|
||||
*/
|
||||
|
||||
const defaultCode = '';
|
||||
|
||||
var Javascript = {
|
||||
name: 'Javascript2',
|
||||
docs: 'https://docs.noodl.net/nodes/javascript/script',
|
||||
displayNodeName: 'Script',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Code'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
exportDynamicPorts: true,
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.inputValues = {};
|
||||
internal.outputValues = {};
|
||||
internal.outputProperties = {};
|
||||
internal.runScheduled = false;
|
||||
internal.setupScheduled = false;
|
||||
internal.runNextFrameScheduled = false;
|
||||
internal.isWaitingForExternalFileToLoad = false;
|
||||
internal.useExternalFile = false;
|
||||
internal.runFunction = undefined;
|
||||
internal.destroyFunction = undefined;
|
||||
internal.setupFunction = undefined;
|
||||
internal.hasParsedCode = false;
|
||||
internal.changedInputs = {};
|
||||
internal.signalScheduled = {};
|
||||
internal.killed = false;
|
||||
internal.inputQueue = [];
|
||||
|
||||
var self = this;
|
||||
internal.userFunctionScope = {
|
||||
createComponent(componentName) {
|
||||
if (componentName && componentName.length > 0 && componentName[0] !== '/') {
|
||||
componentName = '/' + componentName;
|
||||
}
|
||||
|
||||
return self.nodeScope.createNode(componentName, guid());
|
||||
},
|
||||
deleteComponent(component) {
|
||||
self.nodeScope.deleteNode(component);
|
||||
},
|
||||
flagOutputDirty: function (name) {
|
||||
if (!name) {
|
||||
throw new Error('Output port name must be specified');
|
||||
}
|
||||
self.flagOutputDirty(name);
|
||||
},
|
||||
runNextFrame: function () {
|
||||
if (internal.runNextFrameScheduled) {
|
||||
return;
|
||||
}
|
||||
internal.runNextFrameScheduled = true;
|
||||
self.context.scheduleNextFrame(function () {
|
||||
internal.runNextFrameScheduled = false;
|
||||
|
||||
if (!internal.killed) {
|
||||
scheduleRun.call(self);
|
||||
}
|
||||
});
|
||||
},
|
||||
sendSignalOnOutput: function (name) {
|
||||
self.sendSignalOnOutput(name);
|
||||
}
|
||||
};
|
||||
|
||||
internal.onFrameStart = onFrameStart.bind(this);
|
||||
},
|
||||
dynamicports: [
|
||||
{
|
||||
condition: 'useExternalFile = no OR useExternalFile NOT SET',
|
||||
inputs: ['code']
|
||||
},
|
||||
{
|
||||
condition: 'useExternalFile = yes',
|
||||
inputs: ['externalFile']
|
||||
}
|
||||
],
|
||||
inputs: {
|
||||
scriptInputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Inputs',
|
||||
set: function (value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Outputs',
|
||||
set: function (value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
useExternalFile: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{
|
||||
value: 'yes',
|
||||
label: 'Yes'
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
label: 'No'
|
||||
}
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'no',
|
||||
displayName: 'Use External File',
|
||||
group: 'Code',
|
||||
set: function (value) {
|
||||
this._internal.isWaitingForExternalFileToLoad = value === 'yes';
|
||||
this._internal.useExternalFile = value === 'yes';
|
||||
}
|
||||
},
|
||||
code: {
|
||||
displayName: 'Code',
|
||||
group: 'Code',
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
default: defaultCode,
|
||||
set: function (value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (this._internal.useExternalFile === false) {
|
||||
this._callDestroyFunction();
|
||||
var parser = JavascriptNodeParser.createFromCode(value, {
|
||||
node: this
|
||||
});
|
||||
self._onCodeParsed(parser);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
externalFile: {
|
||||
displayName: 'File Path',
|
||||
group: 'Code',
|
||||
type: {
|
||||
name: 'source',
|
||||
allowEditOnly: true
|
||||
},
|
||||
set: function (url) {
|
||||
if (this._internal.useExternalFile === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
JavascriptNodeParser.createFromURL(
|
||||
url,
|
||||
function (parser) {
|
||||
self._internal.isWaitingForExternalFileToLoad = false;
|
||||
self._onCodeParsed(parser);
|
||||
},
|
||||
{
|
||||
node: this
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
_onNodeDeleted: function () {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
this._internal.killed = true;
|
||||
this._callDestroyFunction();
|
||||
},
|
||||
update: function () {
|
||||
if (this._internal.isWaitingForExternalFileToLoad === true) {
|
||||
this._dirty = false;
|
||||
} else {
|
||||
Node.prototype.update.call(this);
|
||||
}
|
||||
},
|
||||
_onCodeParsed: function (parser) {
|
||||
const editorConnection = this.context.editorConnection;
|
||||
|
||||
if (editorConnection) {
|
||||
for (const w of ['js-destroy-waring', 'js-run-waring', 'js-setup-waring']) {
|
||||
editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, w);
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
//register all color inputs with type 'color' to enable color resolving
|
||||
Object.keys(this.model.inputPorts).forEach((name) => {
|
||||
const type = this.model.inputPorts[name].type;
|
||||
if (type === 'color' || type.name === 'color') {
|
||||
this._internal.inputValues[name] = undefined;
|
||||
|
||||
if (!this.hasInput(name)) {
|
||||
this.registerInput(name, {
|
||||
type: 'color',
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
} else {
|
||||
//input was registered before js was done parsing
|
||||
//patch it instead of creating a new one
|
||||
this.getInput(name).type = 'color';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this.model.outputPorts).forEach((name) => {
|
||||
this.registerOutputIfNeeded(name);
|
||||
});
|
||||
|
||||
this._internal.setupFunction = parser.setup;
|
||||
this._internal.runFunction = parser.change; // Run function is actually called change
|
||||
this._internal.destroyFunction = parser.destroy;
|
||||
|
||||
this._internal.definedObject = parser.definedObject;
|
||||
|
||||
if (this._internal.setupFunction) {
|
||||
scheduleSetup.call(this);
|
||||
}
|
||||
|
||||
if (this._internal.runFunction) {
|
||||
scheduleRun.call(this);
|
||||
}
|
||||
|
||||
this._internal.hasParsedCode = true;
|
||||
|
||||
//set all the inputs that arrived before the code was parsed
|
||||
if (this._internal.inputQueue) {
|
||||
for (const { name, value } of this._internal.inputQueue) {
|
||||
this.setInputValue(name, value);
|
||||
}
|
||||
|
||||
//delete the queue, not needed anymore
|
||||
this._internal.inputQueue = undefined;
|
||||
}
|
||||
|
||||
// Node API
|
||||
parser.apis.Node.Inputs = this._internal.inputValues;
|
||||
parser.apis.Node.Outputs = this._internal.outputProperties;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.inputValues[name] = undefined;
|
||||
|
||||
this.registerInput(name, {
|
||||
set: userInputSetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
const isSignal = _typename(this.model.outputPorts[name].type) === 'signal';
|
||||
|
||||
Object.defineProperty(this._internal.outputProperties, name, {
|
||||
set: function (value) {
|
||||
if (isSignal) return; // Cannot set signal functions
|
||||
|
||||
self._internal.outputValues[name] = value;
|
||||
self.flagOutputDirty(name);
|
||||
},
|
||||
get: function () {
|
||||
if (isSignal)
|
||||
return () => {
|
||||
if (self.hasOutput(name)) self.sendSignalOnOutput(name);
|
||||
};
|
||||
return self._internal.outputValues[name];
|
||||
}
|
||||
});
|
||||
|
||||
this.registerOutput(name, {
|
||||
getter: userOutputGetter.bind(this, name)
|
||||
});
|
||||
},
|
||||
_callRunFunction: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.runFunction || internal.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
internal.runFunction.call(
|
||||
internal.userFunctionScope,
|
||||
internal.inputValues,
|
||||
internal.outputProperties,
|
||||
internal.changedInputs
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('Error in JS node run code.', Object.getPrototypeOf(e).constructor.name + ': ' + e.message);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'js-run-waring', {
|
||||
showGlobally: true,
|
||||
message: '<strong>run</strong>: ' + e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_callSignalFunction: function (name) {
|
||||
var internal = this._internal;
|
||||
if (!internal.definedObject || internal.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal.definedObject[name] || typeof internal.definedObject[name] !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
internal.definedObject[name].call(internal.userFunctionScope, internal.inputValues, internal.outputProperties);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Error in JS node signal function code.',
|
||||
Object.getPrototypeOf(e).constructor.name + ': ' + e.message
|
||||
);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'js-run-waring', {
|
||||
showGlobally: true,
|
||||
message: '<strong>run</strong>: ' + e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_callDestroyFunction: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
if (!internal.destroyFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
internal.destroyFunction.call(internal.userFunctionScope, internal.inputValues, internal.outputProperties);
|
||||
} catch (e) {
|
||||
console.log('Error in JS node destroy code.', Object.getPrototypeOf(e).constructor.name + ': ' + e.message);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'js-destroy-waring', {
|
||||
showGlobally: true,
|
||||
message: '<strong>setup</strong>: ' + e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
_callSetupFunction: function () {
|
||||
var internal = this._internal;
|
||||
if (!internal.setupFunction || internal.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
internal.setupFunction.call(internal.userFunctionScope, internal.inputValues, internal.outputProperties);
|
||||
} catch (e) {
|
||||
console.log('Error in JS node setup code.', Object.getPrototypeOf(e).constructor.name + ': ' + e.message);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(this.nodeScope.componentOwner.name, this.id, 'js-setup-waring', {
|
||||
showGlobally: true,
|
||||
message: '<strong>setup</strong>: ' + e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function scheduleSetup() {
|
||||
/* jshint validthis:true */
|
||||
if (this._internal.setupScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.setupScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (!this._internal.killed) {
|
||||
this._callSetupFunction();
|
||||
this._internal.setupScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleRun() {
|
||||
/* jshint validthis:true */
|
||||
if (this._internal.runScheduled || this._internal.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.runScheduled = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (!this._internal.killed) {
|
||||
this._callRunFunction();
|
||||
this._internal.changedInputs = {};
|
||||
this._internal.runScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleSignal(name) {
|
||||
/* jshint validthis:true */
|
||||
if (this._internal.signalScheduled[name] || this._internal.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.signalScheduled[name] = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (!this._internal.killed) {
|
||||
this._callSignalFunction(name);
|
||||
this._internal.signalScheduled[name] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onFrameStart() {
|
||||
/* jshint validthis:true */
|
||||
this._internal.runNextFrame = false;
|
||||
scheduleRun.call(this);
|
||||
}
|
||||
|
||||
function _typename(type) {
|
||||
if (typeof type === 'string') return type;
|
||||
else return type.name;
|
||||
}
|
||||
|
||||
function userInputSetter(name, value) {
|
||||
/* jshint validthis:true */
|
||||
|
||||
if (this._internal.hasParsedCode === true) {
|
||||
if (this.model.inputPorts[name] !== undefined && _typename(this.model.inputPorts[name].type) === 'signal') {
|
||||
// If this is a signal, call the signal function
|
||||
if (this._internal.definedObject && typeof this._internal.definedObject[name] === 'function') {
|
||||
// This is a signal input, schedule a call to the signal function
|
||||
if (value) scheduleSignal.call(this, name);
|
||||
}
|
||||
} else {
|
||||
this._internal.inputValues[name] = value;
|
||||
this._internal.changedInputs[name] = true;
|
||||
scheduleRun.call(this);
|
||||
}
|
||||
} else {
|
||||
//inputs are arriving before the code is parsed
|
||||
//queue them up and set them later to make sure signals are
|
||||
//properly recognized
|
||||
this._internal.inputQueue.push({
|
||||
name,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function userOutputGetter(name) {
|
||||
/* jshint validthis:true */
|
||||
return this._internal.outputValues[name];
|
||||
}
|
||||
|
||||
function _parseAndSourceJavascript(nodeModel, context, fn) {
|
||||
var editorConnection = context.editorConnection;
|
||||
|
||||
if (!nodeModel.parameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
function clearWarnings() {
|
||||
for (const w of ['js-parse-waring', 'js-destroy-waring', 'js-run-waring', 'js-setup-waring']) {
|
||||
editorConnection.clearWarning(nodeModel.component.name, nodeModel.id, w);
|
||||
}
|
||||
}
|
||||
|
||||
function onCodeParsed(parser) {
|
||||
if (parser.error) {
|
||||
editorConnection.sendWarning(nodeModel.component.name, nodeModel.id, 'js-parse-waring', {
|
||||
showGlobally: true,
|
||||
message: parser.error
|
||||
});
|
||||
} else {
|
||||
clearWarnings();
|
||||
}
|
||||
|
||||
fn(parser.getPorts());
|
||||
}
|
||||
|
||||
if (nodeModel.parameters.externalFile && nodeModel.parameters.useExternalFile === 'yes') {
|
||||
var url = nodeModel.parameters.externalFile;
|
||||
JavascriptNodeParser.createFromURL(url, onCodeParsed);
|
||||
} else if (nodeModel.parameters.code) {
|
||||
var parser = JavascriptNodeParser.createFromCode(nodeModel.parameters.code);
|
||||
onCodeParsed(parser);
|
||||
} else {
|
||||
//no code, just send empty port list
|
||||
clearWarnings();
|
||||
fn([]);
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
node: Javascript,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _inputTypeEnums = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'Array'
|
||||
}
|
||||
];
|
||||
|
||||
const _outputTypeEnums = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'Array'
|
||||
},
|
||||
{
|
||||
value: 'signal',
|
||||
label: 'Signal'
|
||||
}
|
||||
];
|
||||
|
||||
// Outputs
|
||||
if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _outputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inputs
|
||||
if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _inputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseAndSourceJavascript(node, context, function (_ports) {
|
||||
// Merge in ports from script
|
||||
_ports.forEach((p) => {
|
||||
if (ports.find((_p) => _p.name === p.name && _p.plug === p.plug)) return; // Port already exists
|
||||
|
||||
ports.push(p);
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
});
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.Javascript2', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('Javascript2')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
const NumberRemapperNode = {
|
||||
name: 'Number Remapper',
|
||||
docs: 'https://docs.noodl.net/nodes/math/number-remapper',
|
||||
category: 'Math',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal._currentInputValue = 0;
|
||||
internal._remappedValue = 0;
|
||||
internal._minInputValue = 0;
|
||||
internal._maxInputValue = 0;
|
||||
internal._minOutputValue = 0;
|
||||
internal._maxOutputValue = 1;
|
||||
internal._clampOutput = true;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal._remappedValue;
|
||||
},
|
||||
inputs: {
|
||||
inputValue: {
|
||||
group: 'Value to Remap',
|
||||
type: {
|
||||
name: 'number',
|
||||
allowConnectionOnly: true
|
||||
},
|
||||
default: 0,
|
||||
displayName: 'Input Value',
|
||||
set: function (value) {
|
||||
this._internal._currentInputValue = value;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
},
|
||||
minInputValue: {
|
||||
group: 'Input Parameters',
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
default: 0,
|
||||
displayName: 'Input Minimum',
|
||||
set: function (value) {
|
||||
this._internal._minInputValue = value;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
},
|
||||
maxInputValue: {
|
||||
group: 'Input Parameters',
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
default: 0,
|
||||
displayName: 'Input Maximum',
|
||||
set: function (value) {
|
||||
this._internal._maxInputValue = value;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
},
|
||||
minOutputValue: {
|
||||
group: 'Output Parameters',
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
default: 0,
|
||||
displayName: 'Output Minimum',
|
||||
set: function (value) {
|
||||
this._internal._minOutputValue = value;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
},
|
||||
maxOutputValue: {
|
||||
group: 'Output Parameters',
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
default: 1,
|
||||
displayName: 'Output Maximum',
|
||||
set: function (value) {
|
||||
this._internal._maxOutputValue = value;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
},
|
||||
clamp: {
|
||||
group: 'Output Parameters',
|
||||
type: {
|
||||
name: 'boolean',
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: true,
|
||||
displayName: 'Clamp Output',
|
||||
set: function (value) {
|
||||
this._internal._clampOutput = value ? true : false;
|
||||
this._calculateNewOutputValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
remappedValue: {
|
||||
type: 'number',
|
||||
displayName: 'Remapped Value',
|
||||
group: 'Outputs',
|
||||
getter: function () {
|
||||
return this._internal._remappedValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
_calculateNewOutputValue: {
|
||||
value: function () {
|
||||
var normalizedValue,
|
||||
_internal = this._internal;
|
||||
|
||||
if (_internal._maxInputValue === _internal._minInputValue) {
|
||||
normalizedValue = 0;
|
||||
} else {
|
||||
normalizedValue =
|
||||
(_internal._currentInputValue - _internal._minInputValue) /
|
||||
(_internal._maxInputValue - _internal._minInputValue);
|
||||
}
|
||||
|
||||
if (_internal._clampOutput) {
|
||||
normalizedValue = Math.max(0, Math.min(1, normalizedValue));
|
||||
}
|
||||
_internal._remappedValue =
|
||||
_internal._minOutputValue + normalizedValue * (_internal._maxOutputValue - _internal._minOutputValue);
|
||||
this.flagOutputDirty('remappedValue');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: NumberRemapperNode
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
const OpenFilePicker = {
|
||||
name: 'Open File Picker',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/open-file-picker',
|
||||
category: 'Utilities',
|
||||
getInspectInfo() {
|
||||
if (this._internal.file) {
|
||||
return this._internal.file.path;
|
||||
}
|
||||
},
|
||||
initialize() {
|
||||
//for some reason the input element has to be created here, it doesn't
|
||||
//work predictably in Safari when created in the open signal function.
|
||||
//Creating it here and reusing it seems to work correctly in all browsers.
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
||||
this._internal.inputElement = input;
|
||||
},
|
||||
inputs: {
|
||||
open: {
|
||||
type: 'signal',
|
||||
displayName: 'Open',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
const input = this._internal.inputElement;
|
||||
|
||||
const onChange = (e) => {
|
||||
this._internal.file = e.target.files[0];
|
||||
|
||||
this.flagOutputDirty('file');
|
||||
this.flagOutputDirty('path');
|
||||
this.flagOutputDirty('name');
|
||||
this.flagOutputDirty('sizeInBytes');
|
||||
this.flagOutputDirty('type');
|
||||
|
||||
this.sendSignalOnOutput('success');
|
||||
|
||||
input.onchange = null;
|
||||
input.value = ''; //reset value so the same file can be picked again
|
||||
};
|
||||
|
||||
input.accept = this._internal.acceptedFileTypes;
|
||||
|
||||
input.onchange = onChange;
|
||||
input.click();
|
||||
}
|
||||
},
|
||||
acceptedFileTypes: {
|
||||
group: 'General',
|
||||
type: 'string',
|
||||
displayName: 'Accepted file types',
|
||||
set(value) {
|
||||
this._internal.acceptedFileTypes = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
file: {
|
||||
type: '*',
|
||||
displayName: 'File',
|
||||
group: 'General',
|
||||
get() {
|
||||
return this._internal.file;
|
||||
}
|
||||
},
|
||||
path: {
|
||||
displayName: 'Path',
|
||||
group: 'Metadata',
|
||||
type: 'string',
|
||||
get() {
|
||||
return this._internal.file && this._internal.file.path;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
displayName: 'Name',
|
||||
group: 'Metadata',
|
||||
type: 'string',
|
||||
get() {
|
||||
return this._internal.file && this._internal.file.name;
|
||||
}
|
||||
},
|
||||
sizeInBytes: {
|
||||
displayName: 'Size in bytes',
|
||||
group: 'Metadata',
|
||||
type: 'number',
|
||||
get() {
|
||||
return this._internal.file && this._internal.file.size;
|
||||
}
|
||||
},
|
||||
type: {
|
||||
displayName: 'Type',
|
||||
group: 'Metadata',
|
||||
type: 'string',
|
||||
get() {
|
||||
return this._internal.file && this._internal.file.type;
|
||||
}
|
||||
},
|
||||
success: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'Success'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: OpenFilePicker
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const ScreenResolution = {
|
||||
name: 'Screen Resolution',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/screen-resolution',
|
||||
category: 'Utilities',
|
||||
initialize() {
|
||||
// Add SSR Support
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this._viewportSizeChanged();
|
||||
});
|
||||
this._viewportSizeChanged();
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.width + ' x ' + this._internal.height;
|
||||
},
|
||||
outputs: {
|
||||
width: {
|
||||
type: 'number',
|
||||
displayName: 'Width',
|
||||
get() {
|
||||
return this._internal.width;
|
||||
}
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
displayName: 'Height',
|
||||
get() {
|
||||
return this._internal.height;
|
||||
}
|
||||
},
|
||||
aspectRatio: {
|
||||
type: 'number',
|
||||
displayName: 'Aspect Ratio',
|
||||
get() {
|
||||
return this._internal.width / this._internal.height;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_viewportSizeChanged() {
|
||||
this._internal.width = window.innerWidth;
|
||||
this._internal.height = window.innerHeight;
|
||||
this.flagAllOutputsDirty();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ScreenResolution
|
||||
};
|
||||
764
packages/noodl-viewer-react/src/nodes/std-library/states.js
Normal file
764
packages/noodl-viewer-react/src/nodes/std-library/states.js
Normal file
@@ -0,0 +1,764 @@
|
||||
'use strict';
|
||||
|
||||
const { EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const EaseCurves = require('../../easecurves');
|
||||
const BezierEasing = require('bezier-easing');
|
||||
|
||||
const defaultDuration = 300;
|
||||
const previousStates = {},
|
||||
previousValues = {};
|
||||
|
||||
function setRGBA(result, hex) {
|
||||
if (hex === 'transparent' || !hex) {
|
||||
result[3] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const numComponents = (hex.length - 1) / 2;
|
||||
|
||||
for (let i = 0; i < numComponents; ++i) {
|
||||
const index = 1 + i * 2;
|
||||
result[i] = parseInt(hex.substring(index, index + 2), 16);
|
||||
}
|
||||
}
|
||||
|
||||
function componentToHex(c) {
|
||||
var hex = c.toString(16);
|
||||
return hex.length == 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
function rgbaToHex(rgba) {
|
||||
return '#' + componentToHex(rgba[0]) + componentToHex(rgba[1]) + componentToHex(rgba[2]) + componentToHex(rgba[3]);
|
||||
}
|
||||
|
||||
const StatesNode = {
|
||||
name: 'States',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/logic/states',
|
||||
shortDesc: 'Define states with values and this node can interpolate between these values when the state is changed.',
|
||||
category: 'Animation',
|
||||
initialize: function () {
|
||||
var _this = this,
|
||||
_internal = this._internal;
|
||||
|
||||
_internal.useTransitions = true;
|
||||
_internal.currentValues = {};
|
||||
_internal.stateParameters = {};
|
||||
_internal.stateParameterTypes = {};
|
||||
_internal.startValues = {};
|
||||
_internal.bezierEaseCurves = {};
|
||||
_internal.transitionFuncs = {};
|
||||
|
||||
_internal.valuesAreInitialised = false;
|
||||
_internal.animation = this.context.timerScheduler.createTimer({
|
||||
duration: defaultDuration,
|
||||
ease: EaseCurves.easeOut,
|
||||
onStart: function () {
|
||||
var values = _internal.values;
|
||||
var startValues = _internal.startValues;
|
||||
var stateValues = _internal.stateParameters;
|
||||
var valueTypes = _internal.stateParameterTypes;
|
||||
var prefix = 'value-' + _internal.state + '-';
|
||||
|
||||
this.targetValues = {};
|
||||
this.startValues = {};
|
||||
this.valueTypes = {};
|
||||
|
||||
for (var v in this.transitionCurves) {
|
||||
// var v = values[i];
|
||||
|
||||
if (valueTypes['type-' + v] === 'number' || valueTypes['type-' + v] === undefined) {
|
||||
this.valueTypes[v] = 'number';
|
||||
this.startValues[v] = startValues[v];
|
||||
this.targetValues[v] = stateValues[prefix + v] || 0;
|
||||
} else if (valueTypes['type-' + v] === 'color') {
|
||||
this.valueTypes[v] = 'color';
|
||||
|
||||
this.startValues[v] = [0, 0, 0, 255];
|
||||
setRGBA(this.startValues[v], _this.context.styles.resolveColor(startValues[v] || '#000000'));
|
||||
|
||||
this.targetValues[v] = [0, 0, 0, 255];
|
||||
setRGBA(this.targetValues[v], _this.context.styles.resolveColor(stateValues[prefix + v] || '#000000'));
|
||||
}
|
||||
}
|
||||
},
|
||||
onRunning: function (t) {
|
||||
var ms = t * this.duration;
|
||||
var currentValues = _internal.currentValues;
|
||||
|
||||
var rgba2 = [0, 0, 0, 255];
|
||||
for (var v in this.transitionCurves) {
|
||||
var c = this.transitionCurves[v];
|
||||
// var v = values[i];
|
||||
|
||||
if (ms < c.delay) currentValues[v] = this.startValues[v];
|
||||
else if (ms >= c.delay + c.dur)
|
||||
currentValues[v] = this.valueTypes[v] === 'color' ? rgbaToHex(this.targetValues[v]) : this.targetValues[v];
|
||||
else {
|
||||
var _t = _internal.transitionFuncs[v].get((ms - c.delay) / c.dur);
|
||||
if (this.valueTypes[v] === 'number') {
|
||||
//convert values to Numers, since they might be strings, which can cause NaN
|
||||
currentValues[v] = EaseCurves.linear(Number(this.startValues[v]), Number(this.targetValues[v]), _t);
|
||||
} else if (this.valueTypes[v] === 'color') {
|
||||
let rgba0 = this.startValues[v];
|
||||
let rgba1 = this.targetValues[v];
|
||||
rgba2[0] = Math.floor(EaseCurves.linear(rgba0[0], rgba1[0], _t));
|
||||
rgba2[1] = Math.floor(EaseCurves.linear(rgba0[1], rgba1[1], _t));
|
||||
rgba2[2] = Math.floor(EaseCurves.linear(rgba0[2], rgba1[2], _t));
|
||||
rgba2[3] = Math.floor(EaseCurves.linear(rgba0[3], rgba1[3], _t));
|
||||
|
||||
currentValues[v] = rgbaToHex(rgba2);
|
||||
}
|
||||
}
|
||||
_this.flagOutputDirty(v);
|
||||
}
|
||||
},
|
||||
onFinish: function () {
|
||||
var port = 'reached-' + _internal.state;
|
||||
if (_this.hasOutput(port)) _this.sendSignalOnOutput(port);
|
||||
}
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
return `Current state: ${this._internal.state}`;
|
||||
},
|
||||
inputs: {
|
||||
states: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'States',
|
||||
group: 'States',
|
||||
set: function (value) {
|
||||
this._internal.states = value ? value.split(',') : [];
|
||||
|
||||
// Set the state to first value if no state change is scheduled during
|
||||
// input updates
|
||||
if (this._internal.states.length > 0) {
|
||||
var _this = this;
|
||||
if (!_this._internal.state) this.scheduleGoToState(_this._internal.startState || _this._internal.states[0]);
|
||||
/* this.scheduleAfterInputsHaveUpdated(function () {
|
||||
if (!_this._internal.state) _this.goToState(_this._internal.startState ||_this._internal.states[0]);
|
||||
});*/
|
||||
}
|
||||
}
|
||||
},
|
||||
values: {
|
||||
type: { name: 'stringlist', allowEditOnly: true },
|
||||
displayName: 'Values',
|
||||
group: 'Values',
|
||||
set: function (value) {
|
||||
var internal = this._internal;
|
||||
internal.values = value.split(',');
|
||||
|
||||
// Register output values at this point
|
||||
for (var i in internal.values) {
|
||||
this.registerOutputIfNeeded(internal.values[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
toggle: {
|
||||
group: 'Go to state',
|
||||
displayName: 'Toggle',
|
||||
valueChangedToTrue: function () {
|
||||
var internal = this._internal;
|
||||
var _this = this;
|
||||
|
||||
if (!internal.states) return;
|
||||
|
||||
// Figure out which state to toggle to
|
||||
var idx = internal.states.indexOf(internal.state);
|
||||
var nextIdx = (idx + 1) % internal.states.length;
|
||||
|
||||
// Go to state when all updates have updated
|
||||
//this._internal.scheduledToGoToState = internal.states[nextIdx];
|
||||
this.scheduleGoToState(internal.states[nextIdx]);
|
||||
/* this.scheduleAfterInputsHaveUpdated(function () {
|
||||
_this.goToState(internal.states[nextIdx]);
|
||||
});*/
|
||||
}
|
||||
},
|
||||
useTransitions: {
|
||||
type: 'boolean',
|
||||
displayName: 'Use Transitions',
|
||||
group: 'General',
|
||||
default: true,
|
||||
set: function (value) {
|
||||
var internal = this._internal;
|
||||
internal.useTransitions = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
currentState: {
|
||||
type: 'string',
|
||||
displayName: 'State',
|
||||
group: 'Current State',
|
||||
getter: function () {
|
||||
return this._internal.state;
|
||||
}
|
||||
},
|
||||
stateChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'State Changed',
|
||||
group: 'Current State'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
registerOutputIfNeeded: function (name) {
|
||||
var internal = this._internal;
|
||||
|
||||
if (this.hasOutput(name)) return;
|
||||
|
||||
this.registerOutput(name, {
|
||||
getter: function () {
|
||||
return internal.currentValues[name];
|
||||
}
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
var _this = this;
|
||||
var internal = this._internal;
|
||||
|
||||
if (this.hasInput(name)) return;
|
||||
|
||||
if (name.indexOf('to-') === 0) {
|
||||
// This is a go to state signal input
|
||||
var state = name.substring(3);
|
||||
this.registerInput(name, {
|
||||
set: EdgeTriggeredInput.createSetter({
|
||||
valueChangedToTrue: function () {
|
||||
//this._internal.scheduledToGoToState = state;
|
||||
this.scheduleGoToState(state);
|
||||
//this.scheduleAfterInputsHaveUpdated(function () { _this.goToState(state) });
|
||||
}
|
||||
})
|
||||
});
|
||||
} else if (name === 'startState') {
|
||||
// Note: this is kept for backwards compatability, but this port is no longer part of the dynamic ports def
|
||||
// Other state parameters are stored
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
//this._internal.scheduledToGoToState = value;
|
||||
this._internal.startState = value;
|
||||
this.scheduleGoToState(value);
|
||||
// this.scheduleAfterInputsHaveUpdated(function () { _this.goToState(value); });
|
||||
}
|
||||
});
|
||||
} else if (name === 'currentState') {
|
||||
this.registerInput(name, {
|
||||
set: this.scheduleGoToState.bind(this)
|
||||
});
|
||||
} else if (name.indexOf('type-') === 0) {
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.stateParameterTypes[name] = value;
|
||||
}
|
||||
});
|
||||
} else if (name.indexOf('value-') === 0) {
|
||||
// Other state parameters are stored
|
||||
var parts = name.split('-');
|
||||
var state = parts[1];
|
||||
var valueName = parts[2];
|
||||
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.stateParameters[name] = value;
|
||||
if (internal.state === state) {
|
||||
// If we are at the state, update the current value immediately
|
||||
internal.currentValues[valueName] = value;
|
||||
_this.flagOutputDirty(valueName);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (name.search(/duration-/g) === 0) {
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.stateParameters[name] = value;
|
||||
}
|
||||
});
|
||||
} else if (name.search(/transition/g) === 0) {
|
||||
var state = name.substring(11);
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.stateParameters[name] = value;
|
||||
/* if (value === 'cubicBezier') {
|
||||
this.updateCubicBezierFunction(state); //create a default bezier easing curve
|
||||
}*/
|
||||
}
|
||||
});
|
||||
}
|
||||
/* else if (name.search(/cubicBezierP[1-2][X-Y]-/g) === 0) {
|
||||
var state = name.substring(15);
|
||||
this.registerInput(name, {
|
||||
set: function (value) {
|
||||
internal.stateParameters[name] = value;
|
||||
this.updateCubicBezierFunction(state); //update easing curve
|
||||
}
|
||||
|
||||
});
|
||||
}*/
|
||||
},
|
||||
/* updateCubicBezierFunction: function (state) {
|
||||
var points = [];
|
||||
var internal = this._internal;
|
||||
['1X', '1Y', '2X', '2Y'].forEach(function (p, i) {
|
||||
var value = internal.stateParameters['cubicBezierP' + p + '-' + state] || 0;
|
||||
if (i % 2 === 0) {
|
||||
//X values must equal [0,1]
|
||||
value = Math.min(1, Math.max(0, value));
|
||||
}
|
||||
points.push(value);
|
||||
});
|
||||
var cubicBezierEase = BezierEasing(points);
|
||||
internal.bezierEaseCurves[state] = function (start, end, t) {
|
||||
return EaseCurves.linear(start, end, cubicBezierEase.get(t));
|
||||
};
|
||||
},*/
|
||||
setCurrentState: function (value) {
|
||||
this.scheduleGoToState(value);
|
||||
},
|
||||
jumpToState: function (state) {
|
||||
var internal = this._internal;
|
||||
if (!internal.states) return;
|
||||
if (!state) state = internal.states[0];
|
||||
if (internal.state === state) return;
|
||||
|
||||
internal.animation.stop();
|
||||
|
||||
var prefix = 'value-' + state + '-';
|
||||
for (var i in internal.values) {
|
||||
var v = internal.values[i];
|
||||
|
||||
internal.currentValues[v] = internal.stateParameters[prefix + v] || 0;
|
||||
|
||||
this.flagOutputDirty(v);
|
||||
}
|
||||
|
||||
internal.state = state;
|
||||
//console.log('currentState dirty:' + state);
|
||||
this.flagOutputDirty('currentState');
|
||||
|
||||
if (internal.valuesAreInitialised) {
|
||||
// Do not send state changed on first initial state set
|
||||
//console.log('stateChanged signal', state);
|
||||
this.sendSignalOnOutput('stateChanged');
|
||||
}
|
||||
internal.valuesAreInitialised = true;
|
||||
|
||||
this.updateAtStatePorts();
|
||||
},
|
||||
scheduleGoToState: function (state) {
|
||||
var _this = this;
|
||||
|
||||
this._internal.goToState = state;
|
||||
|
||||
//console.log('set go to state: ' + state)
|
||||
if (this.hasScheduledGoToState) return;
|
||||
this.hasScheduledGoToState = true;
|
||||
this.scheduleAfterInputsHaveUpdated(function () {
|
||||
//console.log('changing state: ' + _this._internal.goToState)
|
||||
_this.hasScheduledGoToState = false;
|
||||
_this.goToState(_this._internal.goToState);
|
||||
});
|
||||
},
|
||||
goToState: function (state) {
|
||||
var internal = this._internal;
|
||||
if (!internal.states) return;
|
||||
if (!state) state = internal.states[0];
|
||||
if (internal.state === state) return;
|
||||
|
||||
//this._internal.scheduledToGoToState = undefined;
|
||||
if (!internal.valuesAreInitialised) {
|
||||
// First time go to state is called, jump to the state
|
||||
this.jumpToState(state);
|
||||
} else {
|
||||
// Copy current values as start values
|
||||
var delay = 0;
|
||||
var dur = 0;
|
||||
var transitionCurves = {};
|
||||
for (var i in internal.values) {
|
||||
var v = internal.values[i];
|
||||
|
||||
internal.startValues[v] = internal.currentValues[v];
|
||||
|
||||
const parameterType = internal.stateParameterTypes['type-' + v];
|
||||
if (parameterType === 'boolean') {
|
||||
// These types don't transition, just set them
|
||||
var _b = internal.stateParameters['value-' + state + '-' + v];
|
||||
internal.currentValues[v] = _b === undefined ? false : !!_b;
|
||||
this.flagOutputDirty(v);
|
||||
} else if (parameterType === 'string' || parameterType === 'textStyle') {
|
||||
// These types don't transition, just set them
|
||||
internal.currentValues[v] = internal.stateParameters['value-' + state + '-' + v];
|
||||
this.flagOutputDirty(v);
|
||||
} else {
|
||||
// Figure out transition curve
|
||||
var transitionCurve = internal.stateParameters['transition-' + state + '-' + v];
|
||||
if (!transitionCurve)
|
||||
transitionCurve = internal.stateParameters['transitiondef-' + state] || {
|
||||
curve: [0.0, 0.0, 0.58, 1.0],
|
||||
dur: 300,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
if ((transitionCurve.dur === 0 && transitionCurve.delay === 0) || !internal.useTransitions) {
|
||||
// Simply set the target value
|
||||
internal.currentValues[v] = internal.stateParameters['value-' + state + '-' + v];
|
||||
this.flagOutputDirty(v);
|
||||
} else {
|
||||
// Calculate total duration and delay
|
||||
internal.transitionFuncs[v] = BezierEasing(transitionCurve.curve);
|
||||
transitionCurves[v] = transitionCurve;
|
||||
delay = Math.min(delay, transitionCurve.delay);
|
||||
dur = Math.max(dur, transitionCurve.dur + transitionCurve.delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup and start animation
|
||||
//var easeCurveName = internal.stateParameters['transition-' + state] || 'easeOut';
|
||||
//internal.animation.ease = easeCurveName === 'cubicBezier' ? internal.bezierEaseCurves[state] : EaseCurves[easeCurveName];
|
||||
//var durationKey = 'duration-' + state;
|
||||
//internal.animation.duration = internal.stateParameters.hasOwnProperty(durationKey) ? internal.stateParameters[durationKey] : defaultDuration;
|
||||
if (dur > 0 || delay > 0) {
|
||||
internal.animation.transitionCurves = transitionCurves;
|
||||
internal.animation.duration = dur;
|
||||
internal.animation.delay = delay;
|
||||
internal.animation.start();
|
||||
}
|
||||
|
||||
internal.state = state;
|
||||
//console.log('currentState dirty:' + state);
|
||||
this.flagOutputDirty('currentState');
|
||||
|
||||
//console.log('stateChanged signal', state);
|
||||
this.sendSignalOnOutput('stateChanged');
|
||||
this.updateAtStatePorts();
|
||||
|
||||
if (dur == 0 && delay == 0) {
|
||||
// Send reached signal if no transition
|
||||
var port = 'reached-' + internal.state;
|
||||
if (this.hasOutput(port)) this.sendSignalOnOutput(port);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateAtStatePorts: function () {
|
||||
var internal = this._internal;
|
||||
var states = internal.states;
|
||||
for (var i in states) {
|
||||
var s = states[i];
|
||||
var port = 'at-' + s;
|
||||
|
||||
internal.currentValues[port] = internal.state === s;
|
||||
if (this.hasOutput(port)) this.flagOutputDirty(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
var states = parameters.states;
|
||||
var values = parameters.values;
|
||||
|
||||
var ports = [];
|
||||
|
||||
// Add value outputs
|
||||
values = values ? values.split(',') : undefined;
|
||||
for (var i in values) {
|
||||
var p = values[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: parameters['type-' + p] || 'number',
|
||||
allowConnectionsOnly: true
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Values',
|
||||
name: p
|
||||
});
|
||||
|
||||
// Type selector
|
||||
ports.push({
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Color', value: 'color' },
|
||||
{ label: 'Text Style', value: 'textStyle' }
|
||||
],
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'number',
|
||||
plug: 'input',
|
||||
group: 'Types',
|
||||
displayName: p,
|
||||
name: 'type-' + p
|
||||
});
|
||||
}
|
||||
|
||||
// Add state value inputs
|
||||
states = states ? states.split(',') : undefined;
|
||||
states &&
|
||||
states.forEach(function (state) {
|
||||
values &&
|
||||
values.forEach(function (value) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: parameters['type-' + value] || 'number',
|
||||
group: state + ' Values',
|
||||
name: 'value-' + state + '-' + value,
|
||||
displayName: value,
|
||||
editorName: state + '|' + value
|
||||
});
|
||||
});
|
||||
|
||||
// State transition
|
||||
if (values && parameters['useTransitions'] !== false) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: 'curve',
|
||||
displayName: 'Default',
|
||||
default: { curve: [0.0, 0.0, 0.58, 1.0], dur: 300, delay: 0 },
|
||||
group: state + ' Transitions',
|
||||
name: 'transitiondef-' + state
|
||||
});
|
||||
|
||||
values.forEach(function (value) {
|
||||
if (
|
||||
parameters['type-' + value] === undefined ||
|
||||
parameters['type-' + value] === 'number' ||
|
||||
parameters['type-' + value] === 'color'
|
||||
) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: { name: 'curve' },
|
||||
default: parameters['transitiondef-' + state] || {
|
||||
curve: [0.0, 0.0, 0.58, 1.0],
|
||||
dur: 300,
|
||||
delay: 0
|
||||
},
|
||||
group: state + ' Transitions',
|
||||
name: 'transition-' + state + '-' + value,
|
||||
displayName: value,
|
||||
editorName: 'Transition ' + state + '|' + value
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ports.push({
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ 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' }
|
||||
]
|
||||
},
|
||||
default: 'easeOut',
|
||||
displayName: "Easing Curve",
|
||||
group: state + ' Transition',
|
||||
name: 'transition-' + state,
|
||||
});
|
||||
|
||||
//add cubic bezier inputs if transition is set to cubic
|
||||
if (parameters['transition-' + state] === 'cubicBezier') {
|
||||
ports = ports.concat([
|
||||
{
|
||||
name: 'cubicBezierP1X-' + state,
|
||||
editorName: state + '|' + 'P1 X',
|
||||
displayName: 'P1 X',
|
||||
group: state + ' Transition',
|
||||
plug: 'input',
|
||||
type: 'number',
|
||||
default: 0
|
||||
},
|
||||
{
|
||||
name: 'cubicBezierP1Y-' + state,
|
||||
editorName: state + '|' + 'P1 Y',
|
||||
displayName: 'P1 Y',
|
||||
group: state + ' Transition',
|
||||
plug: 'input',
|
||||
type: 'number',
|
||||
default: 0
|
||||
},
|
||||
{
|
||||
name: 'cubicBezierP2X-' + state,
|
||||
editorName: state + '|' + 'P2 X',
|
||||
displayName: 'P2 X',
|
||||
group: state + ' Transition',
|
||||
plug: 'input',
|
||||
type: 'number',
|
||||
default: 0
|
||||
},
|
||||
{
|
||||
name: 'cubicBezierP2Y-' + state,
|
||||
editorName: state + '|' + 'P2 Y',
|
||||
displayName: 'P2 Y',
|
||||
group: state + ' Transition',
|
||||
plug: 'input',
|
||||
type: 'number',
|
||||
default: 0
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: 'number',
|
||||
default: defaultDuration,
|
||||
displayName: "Duration",
|
||||
group: state + ' Transition',
|
||||
name: 'duration-' + state,
|
||||
});*/
|
||||
|
||||
// Go to state port
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: { name: 'signal', allowConnectionsOnly: true },
|
||||
displayName: 'To ' + state,
|
||||
name: 'to-' + state,
|
||||
group: 'Go to state'
|
||||
});
|
||||
|
||||
// At state output
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
type: 'boolean',
|
||||
displayName: 'At ' + state,
|
||||
name: 'at-' + state,
|
||||
group: 'Current state'
|
||||
});
|
||||
|
||||
// Has reached state output
|
||||
ports.push({
|
||||
plug: 'output',
|
||||
type: 'signal',
|
||||
displayName: 'Has Reached ' + state,
|
||||
name: 'reached-' + state,
|
||||
group: 'Current state'
|
||||
});
|
||||
});
|
||||
|
||||
// Add current state port
|
||||
if (states) {
|
||||
ports.push({
|
||||
plug: 'input',
|
||||
type: { name: 'enum', enums: states },
|
||||
group: 'States',
|
||||
displayName: 'State',
|
||||
name: 'currentState',
|
||||
default: parameters['startState'] || states[0] // This is kept for backwards compatability, the startState port does no longer exist
|
||||
});
|
||||
}
|
||||
|
||||
// Detect state and value rename
|
||||
var stateRenamed = detectRename(previousStates[nodeId], states);
|
||||
previousStates[nodeId] = states;
|
||||
|
||||
var valueRenamed = detectRename(previousValues[nodeId], values);
|
||||
previousValues[nodeId] = values;
|
||||
|
||||
let renamed;
|
||||
if (stateRenamed) {
|
||||
renamed = {
|
||||
plug: 'input',
|
||||
before: stateRenamed.before,
|
||||
after: stateRenamed.after,
|
||||
patterns: [
|
||||
'transition-{{*}}',
|
||||
/* 'duration-{{*}}',
|
||||
'cubicBezierP1X-{{*}}',
|
||||
'cubicBezierP2X-{{*}}',
|
||||
'cubicBezierP1Y-{{*}}',
|
||||
'cubicBezierP2Y-{{*}}',*/
|
||||
'to-{{*}}',
|
||||
'at-{{*}}',
|
||||
'reached-{{*}}'
|
||||
]
|
||||
};
|
||||
|
||||
// A state has been renamed
|
||||
values &&
|
||||
values.forEach(function (value) {
|
||||
renamed.patterns.push('value-{{*}}-' + value);
|
||||
});
|
||||
} else if (valueRenamed) {
|
||||
renamed = [
|
||||
{
|
||||
plug: 'output',
|
||||
before: valueRenamed.before,
|
||||
after: valueRenamed.after,
|
||||
patterns: ['{{*}}']
|
||||
},
|
||||
{
|
||||
plug: 'input',
|
||||
before: valueRenamed.before,
|
||||
after: valueRenamed.after,
|
||||
patterns: ['type-{{*}}']
|
||||
},
|
||||
{
|
||||
plug: 'input',
|
||||
before: valueRenamed.before,
|
||||
after: valueRenamed.after,
|
||||
patterns: states
|
||||
? states.map(function (s) {
|
||||
return 'value-' + s + '-' + '{{*}}';
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, { renamed: renamed });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: StatesNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.States', function (node) {
|
||||
if (node.parameters.states) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
}
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (
|
||||
event.name === 'useTransitions' ||
|
||||
event.name === 'states' ||
|
||||
event.name === 'values' ||
|
||||
event.name.startsWith('transition') ||
|
||||
event.name.startsWith('type-')
|
||||
) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
92
packages/noodl-viewer-react/src/nodes/std-library/switch.js
Normal file
92
packages/noodl-viewer-react/src/nodes/std-library/switch.js
Normal file
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const Switch = {
|
||||
name: 'Switch',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/switch',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.state = false;
|
||||
this._internal.initialized = false;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this._internal.state;
|
||||
},
|
||||
inputs: {
|
||||
on: {
|
||||
displayName: 'On',
|
||||
group: 'Change State',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.state === true) {
|
||||
return;
|
||||
}
|
||||
this._internal.state = true;
|
||||
this.flagOutputDirty('state');
|
||||
this.emitSignals();
|
||||
}
|
||||
},
|
||||
off: {
|
||||
displayName: 'Off',
|
||||
group: 'Change State',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal.state === false) {
|
||||
return;
|
||||
}
|
||||
this._internal.state = false;
|
||||
this.flagOutputDirty('state');
|
||||
this.emitSignals();
|
||||
}
|
||||
},
|
||||
flip: {
|
||||
displayName: 'Flip',
|
||||
group: 'Change State',
|
||||
valueChangedToTrue: function () {
|
||||
this._internal.state = !this._internal.state;
|
||||
this.flagOutputDirty('state');
|
||||
this.emitSignals();
|
||||
}
|
||||
},
|
||||
onFromStart: {
|
||||
type: 'boolean',
|
||||
displayName: 'State',
|
||||
group: 'General',
|
||||
default: false,
|
||||
set: function (value) {
|
||||
this._internal.state = !!value;
|
||||
this.flagOutputDirty('state');
|
||||
this.emitSignals();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
state: {
|
||||
type: 'boolean',
|
||||
displayName: 'Current State',
|
||||
getter: function () {
|
||||
return this._internal.state;
|
||||
}
|
||||
},
|
||||
switchedToOn: {
|
||||
displayName: 'Switched To On',
|
||||
type: 'signal',
|
||||
group: 'Signals'
|
||||
},
|
||||
switchedToOff: {
|
||||
displayName: 'Switched To Off',
|
||||
type: 'signal',
|
||||
group: 'Signals'
|
||||
}
|
||||
},
|
||||
prototypeExtensions: {
|
||||
emitSignals: function () {
|
||||
if (this._internal.state === true) {
|
||||
this.sendSignalOnOutput('switchedToOn');
|
||||
} else {
|
||||
this.sendSignalOnOutput('switchedToOff');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: Switch
|
||||
};
|
||||
85
packages/noodl-viewer-react/src/nodes/std-library/timer.js
Normal file
85
packages/noodl-viewer-react/src/nodes/std-library/timer.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const Timer = {
|
||||
name: 'Timer',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/delay',
|
||||
displayName: 'Delay',
|
||||
category: 'Utilities',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'duration'
|
||||
},
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
this._internal._animation = this.context.timerScheduler.createTimer({
|
||||
duration: 0,
|
||||
onStart: function () {
|
||||
self.sendSignalOnOutput('timerStarted');
|
||||
},
|
||||
onFinish: function () {
|
||||
self.sendSignalOnOutput('timerFinished');
|
||||
}
|
||||
});
|
||||
|
||||
this.addDeleteListener(() => {
|
||||
this._internal._animation.stop();
|
||||
});
|
||||
},
|
||||
getInspectInfo() {
|
||||
if (this._internal._animation.isRunning()) {
|
||||
return Math.floor(this._internal._animation.durationLeft() / 10) / 100 + ' seconds';
|
||||
}
|
||||
return 'Not running';
|
||||
},
|
||||
inputs: {
|
||||
start: {
|
||||
displayName: 'Start',
|
||||
valueChangedToTrue: function () {
|
||||
if (this._internal._animation._isRunning === false) {
|
||||
this._internal._animation.start();
|
||||
}
|
||||
}
|
||||
},
|
||||
restart: {
|
||||
displayName: 'Restart',
|
||||
valueChangedToTrue: function () {
|
||||
this._internal._animation.start();
|
||||
}
|
||||
},
|
||||
duration: {
|
||||
type: 'number',
|
||||
displayName: 'Duration',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal._animation.duration = value;
|
||||
}
|
||||
},
|
||||
startDelay: {
|
||||
type: 'number',
|
||||
displayName: 'Start Delay',
|
||||
default: 0,
|
||||
set: function (value) {
|
||||
this._internal._animation.delay = value;
|
||||
}
|
||||
},
|
||||
stop: {
|
||||
displayName: 'Stop',
|
||||
valueChangedToTrue: function () {
|
||||
this._internal._animation.stop();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
timerStarted: {
|
||||
type: 'signal',
|
||||
displayName: 'Started'
|
||||
},
|
||||
timerFinished: {
|
||||
type: 'signal',
|
||||
displayName: 'Finished'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: Timer
|
||||
};
|
||||
138
packages/noodl-viewer-react/src/nodes/std-library/uploadfile.js
Normal file
138
packages/noodl-viewer-react/src/nodes/std-library/uploadfile.js
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
const CloudFile = require('@noodl/runtime/src/api/cloudfile');
|
||||
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
|
||||
|
||||
const UploadFile = {
|
||||
name: 'Upload File',
|
||||
docs: 'https://docs.noodl.net/nodes/data/cloud-data/upload-file',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
getInspectInfo() {
|
||||
return this._internal.response;
|
||||
},
|
||||
inputs: {
|
||||
file: {
|
||||
group: 'General',
|
||||
displayName: 'File',
|
||||
type: '*',
|
||||
set(file) {
|
||||
this._internal.file = file;
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
type: 'signal',
|
||||
displayName: 'Upload',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const file = this._internal.file;
|
||||
|
||||
if (!file) {
|
||||
this.setError('No file specified');
|
||||
return;
|
||||
}
|
||||
|
||||
CloudStore.instance.uploadFile({
|
||||
file,
|
||||
onUploadProgress: (p) => {
|
||||
this._internal.progressTotal = p.total;
|
||||
this._internal.progressLoaded = p.loaded;
|
||||
|
||||
this.flagOutputDirty('progressTotalBytes');
|
||||
this.flagOutputDirty('progressLoadedBytes');
|
||||
this.flagOutputDirty('progressLoadedPercent');
|
||||
this.sendSignalOnOutput('progressChanged');
|
||||
},
|
||||
success: (response) => {
|
||||
this._internal.cloudFile = new CloudFile(response);
|
||||
this.flagOutputDirty('cloudFile');
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => this.setError(e)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
cloudFile: {
|
||||
group: 'General',
|
||||
displayName: 'Cloud File',
|
||||
type: 'cloudfile',
|
||||
get() {
|
||||
return this._internal.cloudFile;
|
||||
}
|
||||
},
|
||||
success: {
|
||||
group: 'Events',
|
||||
displayName: 'Success',
|
||||
type: 'signal'
|
||||
},
|
||||
failure: {
|
||||
group: 'Events',
|
||||
displayName: 'Failure',
|
||||
type: 'signal'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
get() {
|
||||
return this._internal.error;
|
||||
}
|
||||
},
|
||||
errorStatus: {
|
||||
type: 'number',
|
||||
displayName: 'Error Status Code',
|
||||
group: 'Error',
|
||||
get() {
|
||||
return this._internal.errorStatus;
|
||||
}
|
||||
},
|
||||
progressChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Progress Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
progressTotalBytes: {
|
||||
type: 'number',
|
||||
displayName: 'Total Bytes',
|
||||
group: 'Progress',
|
||||
get() {
|
||||
return this._internal.progressTotal;
|
||||
}
|
||||
},
|
||||
progressLoadedBytes: {
|
||||
type: 'number',
|
||||
displayName: 'Uploaded Bytes',
|
||||
group: 'Progress',
|
||||
get() {
|
||||
return this._internal.progressLoaded;
|
||||
}
|
||||
},
|
||||
progressLoadedPercent: {
|
||||
type: 'number',
|
||||
displayName: 'Uploaded Percent',
|
||||
group: 'Progress',
|
||||
get() {
|
||||
if (!this._internal.progressTotal) return 0;
|
||||
return (this._internal.progressLoaded / this._internal.progressTotal) * 100;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setError(err) {
|
||||
this._internal.error = err.hasOwnProperty('error') ? err.error : err;
|
||||
//use the error code. If there is none, use the http status
|
||||
this._internal.errorStatus = err.code || err.status || 0;
|
||||
this.flagOutputDirty('error');
|
||||
this.flagOutputDirty('errorStatus');
|
||||
this.sendSignalOnOutput('failure');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: UploadFile
|
||||
};
|
||||
188
packages/noodl-viewer-react/src/nodes/std-library/user/login.js
Normal file
188
packages/noodl-viewer-react/src/nodes/std-library/user/login.js
Normal file
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var LoginNodeDefinition = {
|
||||
name: 'net.noodl.user.LogIn',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/log-in',
|
||||
displayNodeName: 'Log In',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
login: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleLogIn();
|
||||
}
|
||||
},
|
||||
username: {
|
||||
displayName: 'Username',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
},
|
||||
password: {
|
||||
displayName: 'Password',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.password = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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, 'user-login-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-login-warning');
|
||||
}
|
||||
},
|
||||
scheduleLogIn: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.logInScheduled === true) return;
|
||||
this.logInScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.logInScheduled = false;
|
||||
|
||||
UserService.instance.logIn({
|
||||
username: this._internal.username,
|
||||
password: this._internal.password,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*function updatePorts(nodeId, parameters, editorConnection, dbCollections) {
|
||||
var ports = [];
|
||||
|
||||
ports.push({
|
||||
name: 'collectionName',
|
||||
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.collectionName && dbCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = dbCollections.find((c) => c.name === parameters.collectionName);
|
||||
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') {
|
||||
|
||||
}
|
||||
else { // Other schema type ports
|
||||
const _typeMap = {
|
||||
"String":"string",
|
||||
"Boolean":"boolean",
|
||||
"Number":"number",
|
||||
"Date":"date"
|
||||
}
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type]?_typeMap[p.type]:'*',
|
||||
},
|
||||
plug: 'output',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key,
|
||||
})
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: key+ ' Changed',
|
||||
name: 'changed-' + key,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}*/
|
||||
|
||||
module.exports = {
|
||||
node: LoginNodeDefinition,
|
||||
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) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
})
|
||||
}
|
||||
|
||||
graphModel.on("editorImportComplete", ()=> {
|
||||
graphModel.on("nodeAdded.DbModel2", function (node) {
|
||||
_managePortsForNode(node)
|
||||
})
|
||||
|
||||
for(const node of graphModel.getNodesWithType('DbModel2')) {
|
||||
_managePortsForNode(node)
|
||||
}
|
||||
})*/
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var LogOutNodeDefinition = {
|
||||
name: 'net.noodl.user.LogOut',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/log-out',
|
||||
displayNodeName: 'Log Out',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
login: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleLogOut();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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, 'user-login-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-login-warning');
|
||||
}
|
||||
},
|
||||
scheduleLogOut: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.logOutScheduled === true) return;
|
||||
this.logOutScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.logOutScheduled = false;
|
||||
|
||||
UserService.instance.logOut({
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: LogOutNodeDefinition,
|
||||
setup: function (context, graphModel) {}
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var RequestPasswordResetNodeDefinition = {
|
||||
name: 'net.noodl.user.RequestPasswordReset',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/request-password-reset',
|
||||
displayNodeName: 'Request Password Reset',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
deprecated: true, // Use cloud functions
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
send: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRequestPasswordReset();
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
displayName: 'Email',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.email = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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,
|
||||
'user-send-email-verification-warning',
|
||||
{
|
||||
message: err,
|
||||
showGlobally: true
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'user-send-email-verification-warning'
|
||||
);
|
||||
}
|
||||
},
|
||||
scheduleRequestPasswordReset: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.sendScheduled === true) return;
|
||||
this.sendScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.sendScheduled = false;
|
||||
|
||||
UserService.instance.requestPasswordReset({
|
||||
email: this._internal.email,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: RequestPasswordResetNodeDefinition,
|
||||
setup: function (context, graphModel) {}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var ResetPasswordNodeDefinition = {
|
||||
name: 'net.noodl.user.ResetPassword',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/reset-password',
|
||||
displayNodeName: 'Reset Password',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
deprecated: true, // Use cloud functions
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
reset: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleResetPassword();
|
||||
}
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
displayName: 'Token',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.token = value;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
displayName: 'Username',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
},
|
||||
newPassword: {
|
||||
type: 'string',
|
||||
displayName: 'New Password',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.newPassword = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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,
|
||||
'user-verify-email-warning',
|
||||
{
|
||||
message: err,
|
||||
showGlobally: true
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'user-verify-email-warning'
|
||||
);
|
||||
}
|
||||
},
|
||||
scheduleResetPassword: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.resetPasswordScheduled === true) return;
|
||||
this.resetPasswordScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.resetPasswordScheduled = false;
|
||||
|
||||
UserService.instance.resetPassword({
|
||||
token: this._internal.token,
|
||||
username: this._internal.username,
|
||||
newPassword: this._internal.newPassword,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ResetPasswordNodeDefinition,
|
||||
setup: function (context, graphModel) {}
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var SendEmailVerificationNodeDefinition = {
|
||||
name: 'net.noodl.user.SendEmailVerification',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/send-email-verification',
|
||||
displayNodeName: 'Send Email Verification',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
deprecated: true, // Use cloud functions
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
send: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleSendEmailVerification();
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
displayName: 'Email',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.email = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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,
|
||||
'user-send-email-verification-warning',
|
||||
{
|
||||
message: err,
|
||||
showGlobally: true
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'user-send-email-verification-warning'
|
||||
);
|
||||
}
|
||||
},
|
||||
scheduleSendEmailVerification: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.sendScheduled === true) return;
|
||||
this.sendScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.sendScheduled = false;
|
||||
|
||||
UserService.instance.sendEmailVerification({
|
||||
email: this._internal.email,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: SendEmailVerificationNodeDefinition,
|
||||
setup: function (context, graphModel) {}
|
||||
};
|
||||
199
packages/noodl-viewer-react/src/nodes/std-library/user/signup.js
Normal file
199
packages/noodl-viewer-react/src/nodes/std-library/user/signup.js
Normal file
@@ -0,0 +1,199 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var SignUpNodeDefinition = {
|
||||
name: 'net.noodl.user.SignUp',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/sign-up',
|
||||
displayNodeName: 'Sign Up',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
|
||||
internal.userProperties = {};
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
signup: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleSignUp();
|
||||
}
|
||||
},
|
||||
username: {
|
||||
displayName: 'Username',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
},
|
||||
password: {
|
||||
displayName: 'Password',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.password = value;
|
||||
}
|
||||
},
|
||||
email: {
|
||||
displayName: 'Email',
|
||||
type: 'string',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.email = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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, 'user-login-warning', {
|
||||
message: err,
|
||||
showGlobally: true
|
||||
});
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(this.nodeScope.componentOwner.name, this.id, 'user-login-warning');
|
||||
}
|
||||
},
|
||||
scheduleSignUp: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.signUpScheduled === true) return;
|
||||
this.signUpScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.signUpScheduled = false;
|
||||
|
||||
UserService.instance.signUp({
|
||||
username: this._internal.username,
|
||||
password: this._internal.password,
|
||||
email: this._internal.email,
|
||||
properties: internal.userProperties,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
setUserProperty: function (name, value) {
|
||||
this._internal.userProperties[name] = value;
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('prop-'))
|
||||
return this.registerInput(name, {
|
||||
set: this.setUserProperty.bind(this, name.substring('prop-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection, systemCollections) {
|
||||
var ports = [];
|
||||
|
||||
if (systemCollections) {
|
||||
// Fetch ports from collection keys
|
||||
var c = systemCollections.find((c) => c.name === '_User');
|
||||
if (c && c.schema && c.schema.properties) {
|
||||
var props = c.schema.properties;
|
||||
const _ignoreKeys = ['authData', 'password', 'username', 'createdAt', 'updatedAt', 'emailVerified', 'email'];
|
||||
for (var key in props) {
|
||||
if (_ignoreKeys.indexOf(key) !== -1) continue;
|
||||
|
||||
var p = props[key];
|
||||
if (ports.find((_p) => _p.name === key)) continue;
|
||||
|
||||
if (p.type === 'Relation') {
|
||||
} else {
|
||||
// Other schema type ports
|
||||
const _typeMap = {
|
||||
String: 'string',
|
||||
Boolean: 'boolean',
|
||||
Number: 'number',
|
||||
Date: 'date'
|
||||
};
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: _typeMap[p.type] ? _typeMap[p.type] : '*'
|
||||
},
|
||||
plug: 'input',
|
||||
group: 'Properties',
|
||||
name: 'prop-' + key,
|
||||
displayName: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: SignUpNodeDefinition,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, graphModel.getMetaData('systemCollections'));
|
||||
});
|
||||
|
||||
graphModel.on('metadataChanged.systemCollections', function (data) {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection, data);
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.net.noodl.user.SignUp', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('net.noodl.user.SignUp')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,327 @@
|
||||
const NoodlRuntime = require('@noodl/runtime');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const guid = require('../../../guid');
|
||||
const CloudStore = require('@noodl/runtime/src/api/cloudstore');
|
||||
|
||||
class UserService {
|
||||
constructor() {
|
||||
this._initCloudServices();
|
||||
|
||||
this.events = new EventEmitter();
|
||||
this.events.setMaxListeners(100000);
|
||||
|
||||
// Check for current user session, and validate if it exists
|
||||
const currentUser = this.getUserFromLocalStorage();
|
||||
|
||||
if (currentUser) {
|
||||
this.current = this.getUserModel();
|
||||
this.fetchCurrentUser({
|
||||
success: () => {},
|
||||
error: () => {
|
||||
// The session is nolonger valid
|
||||
delete localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
delete this.current;
|
||||
this.events.emit('sessionLost');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getUserFromLocalStorage() {
|
||||
const currentUser = localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
if (currentUser) {
|
||||
try {
|
||||
return JSON.parse(currentUser);
|
||||
} catch (e) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_initCloudServices() {
|
||||
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
||||
|
||||
if (cloudServices) {
|
||||
this.appId = cloudServices.appId;
|
||||
this.endpoint = cloudServices.endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
on() {
|
||||
this.events.on.apply(this.events, arguments);
|
||||
}
|
||||
|
||||
off() {
|
||||
this.events.off.apply(this.events, arguments);
|
||||
}
|
||||
|
||||
_makeRequest(path, options) {
|
||||
if (!this.endpoint) {
|
||||
if (options.error) {
|
||||
options.error({ error: 'No active cloud service', status: 0 });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(xhr.response);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr.status === 200 || xhr.status === 201) {
|
||||
options.success(json || xhr.response);
|
||||
} else options.error(json || { error: xhr.responseText, status: xhr.status });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(options.method || 'GET', this.endpoint + path, true);
|
||||
|
||||
xhr.setRequestHeader('X-Parse-Application-Id', this.appId);
|
||||
|
||||
// Installation Id
|
||||
var _iid = localStorage['Parse/' + this.appId + '/installationId'];
|
||||
if (_iid === undefined) {
|
||||
_iid = localStorage['Parse/' + this.appId + '/installationId'] = guid();
|
||||
}
|
||||
xhr.setRequestHeader('X-Parse-Installation-Id', _iid);
|
||||
|
||||
// Check for current users
|
||||
if (options.sessionToken) xhr.setRequestHeader('X-Parse-Session-Token', options.sessionToken);
|
||||
else {
|
||||
var currentUser = this.getUserFromLocalStorage();
|
||||
if (currentUser !== undefined) {
|
||||
xhr.setRequestHeader('X-Parse-Session-Token', currentUser.sessionToken);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(options.content));
|
||||
}
|
||||
|
||||
logIn(options) {
|
||||
this._makeRequest('/login', {
|
||||
method: 'POST',
|
||||
content: {
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
_method: 'GET'
|
||||
},
|
||||
success: (response) => {
|
||||
// Store current user
|
||||
localStorage['Parse/' + this.appId + '/currentUser'] = JSON.stringify(response);
|
||||
this.current = this.getUserModel(); // Make sure the user model is updated
|
||||
options.success(response);
|
||||
this.events.emit('loggedIn');
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logOut(options) {
|
||||
this._makeRequest('/logout', {
|
||||
method: 'POST',
|
||||
content: {},
|
||||
success: (response) => {
|
||||
// Store current user
|
||||
delete localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
delete this.current;
|
||||
options.success();
|
||||
this.events.emit('loggedOut');
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
signUp(options) {
|
||||
//make a shallow copy to feed through CloudStore._serializeObject, which will modify the object
|
||||
const additionalUserProps = options.properties
|
||||
? CloudStore._serializeObject({ ...options.properties }, '_User')
|
||||
: {};
|
||||
|
||||
this._makeRequest('/users', {
|
||||
method: 'POST',
|
||||
content: Object.assign({}, additionalUserProps, {
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
email: options.email
|
||||
}),
|
||||
success: (response) => {
|
||||
// Store current user
|
||||
const _cu = Object.assign(response, { username: options.username }, options.properties);
|
||||
localStorage['Parse/' + this.appId + '/currentUser'] = JSON.stringify(_cu);
|
||||
this.current = this.getUserModel(); // Make sure the user model is updated
|
||||
options.success(response);
|
||||
this.events.emit('loggedIn');
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setUserProperties(options) {
|
||||
const _cu = this.getCurrentUser();
|
||||
if (_cu !== undefined) {
|
||||
//make a shallow copy to feed through CloudStore._serializeObject, which will modify the object
|
||||
const propsToSave = CloudStore._serializeObject({ ...options.properties }, '_User');
|
||||
|
||||
const _content = Object.assign({}, { email: options.email, username: options.username }, propsToSave);
|
||||
|
||||
delete _content.emailVerified; // Remove props you cannot set
|
||||
delete _content.createdAt;
|
||||
delete _content.updatedAt;
|
||||
//delete _content.username;
|
||||
|
||||
this._makeRequest('/users/' + _cu.objectId, {
|
||||
method: 'PUT',
|
||||
content: _content,
|
||||
success: (response) => {
|
||||
// Store current user
|
||||
Object.assign(_cu, _content);
|
||||
localStorage['Parse/' + this.appId + '/currentUser'] = JSON.stringify(_cu);
|
||||
this.current = this.getUserModel(); // Make sure the user model is updated
|
||||
options.success(response);
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchCurrentUser(options) {
|
||||
this._makeRequest('/users/me', {
|
||||
method: 'GET',
|
||||
sessionToken: options.sessionToken,
|
||||
success: (response) => {
|
||||
// Store current user
|
||||
localStorage['Parse/' + this.appId + '/currentUser'] = JSON.stringify(response);
|
||||
this.current = this.getUserModel(); // Make sure the user model is updated
|
||||
this.events.emit('sessionGained');
|
||||
options.success(response);
|
||||
},
|
||||
error: (e) => {
|
||||
if (e.code === 209) {
|
||||
delete localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
this.events.emit('sessionLost');
|
||||
}
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
verifyEmail(options) {
|
||||
this._makeRequest(
|
||||
'/apps/' + this.appId + '/verify_email?username=' + options.username + '&token=' + options.token,
|
||||
{
|
||||
method: 'GET',
|
||||
success: (response) => {
|
||||
if (response.indexOf('Successfully verified your email') !== -1) {
|
||||
options.success();
|
||||
} else if (response.indexOf('Invalid Verification Link')) {
|
||||
options.error('Invalid verification token');
|
||||
} else {
|
||||
options.error('Failed to verify email');
|
||||
}
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sendEmailVerification(options) {
|
||||
this._makeRequest('/verificationEmailRequest', {
|
||||
method: 'POST',
|
||||
content: { email: options.email },
|
||||
success: (response) => {
|
||||
options.success();
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetPassword(options) {
|
||||
this._makeRequest('/apps/' + this.appId + '/request_password_reset', {
|
||||
method: 'POST',
|
||||
content: {
|
||||
username: options.username,
|
||||
token: options.token,
|
||||
new_password: options.newPassword
|
||||
},
|
||||
success: (response) => {
|
||||
if (
|
||||
response.indexOf('Password successfully reset') !== -1 ||
|
||||
response.indexOf('Successfully updated your password') !== -1
|
||||
) {
|
||||
options.success();
|
||||
} else if (response.indexOf('Invalid Link')) {
|
||||
options.error('Invalid verification token');
|
||||
} else {
|
||||
options.error('Failed to verify email');
|
||||
}
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestPasswordReset(options) {
|
||||
this._makeRequest('/requestPasswordReset', {
|
||||
method: 'POST',
|
||||
content: { email: options.email },
|
||||
success: (response) => {
|
||||
options.success();
|
||||
},
|
||||
error: (e) => {
|
||||
options.error(e.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
var _cu = localStorage['Parse/' + this.appId + '/currentUser'];
|
||||
if (_cu !== undefined) return JSON.parse(_cu);
|
||||
}
|
||||
|
||||
getUserModel() {
|
||||
const _cu = this.getCurrentUser();
|
||||
if (_cu !== undefined) {
|
||||
delete _cu.sessionToken;
|
||||
delete _cu.ACL;
|
||||
delete _cu.className;
|
||||
delete _cu.__type;
|
||||
return CloudStore._fromJSON(_cu, '_User');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UserService.forScope = (modelScope) => {
|
||||
// On the viewer, always return main scope
|
||||
return UserService.instance;
|
||||
};
|
||||
|
||||
var _instance;
|
||||
Object.defineProperty(UserService, 'instance', {
|
||||
get: function () {
|
||||
if (_instance === undefined) _instance = new UserService();
|
||||
return _instance;
|
||||
}
|
||||
});
|
||||
|
||||
NoodlRuntime.Services.UserService = UserService;
|
||||
|
||||
module.exports = UserService;
|
||||
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const { Node, EdgeTriggeredInput } = require('@noodl/runtime');
|
||||
const UserService = require('./userservice');
|
||||
|
||||
var VerifyEmailNodeDefinition = {
|
||||
name: 'net.noodl.user.VerifyEmail',
|
||||
docs: 'https://docs.noodl.net/nodes/data/user/verify-email',
|
||||
displayNodeName: 'Verify Email',
|
||||
category: 'Cloud Services',
|
||||
color: 'data',
|
||||
deprecated: true, // Use cloud functions
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
},
|
||||
getInspectInfo() {},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'signal',
|
||||
displayName: 'Success',
|
||||
group: 'Events'
|
||||
},
|
||||
failure: {
|
||||
type: 'signal',
|
||||
displayName: 'Failure',
|
||||
group: 'Events'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
group: 'Error',
|
||||
getter: function () {
|
||||
return this._internal.error;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
verify: {
|
||||
displayName: 'Do',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleVerifyEmail();
|
||||
}
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
displayName: 'Token',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.token = value;
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
displayName: 'Username',
|
||||
group: 'General',
|
||||
set: function (value) {
|
||||
this._internal.username = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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,
|
||||
'user-verify-email-warning',
|
||||
{
|
||||
message: err,
|
||||
showGlobally: true
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
clearWarnings() {
|
||||
if (this.context.editorConnection) {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'user-verify-email-warning'
|
||||
);
|
||||
}
|
||||
},
|
||||
scheduleVerifyEmail: function () {
|
||||
const internal = this._internal;
|
||||
|
||||
if (this.logOutScheduled === true) return;
|
||||
this.logOutScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.logOutScheduled = false;
|
||||
|
||||
UserService.instance.verifyEmail({
|
||||
token: this._internal.token,
|
||||
username: this._internal.username,
|
||||
success: () => {
|
||||
this.sendSignalOnOutput('success');
|
||||
},
|
||||
error: (e) => {
|
||||
this.setError(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: VerifyEmailNodeDefinition,
|
||||
setup: function (context, graphModel) {}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
const ValueChangedNode = {
|
||||
name: 'Value Changed',
|
||||
docs: 'https://docs.noodl.net/nodes/logic/value-changed',
|
||||
category: 'Logic',
|
||||
initialize: function () {
|
||||
this._internal.lastValue = undefined;
|
||||
this._internal.changeCount = 0;
|
||||
},
|
||||
getInspectInfo() {
|
||||
if (this._internal.changeCount) {
|
||||
return 'Triggered ' + this._internal.changeCount + (this._internal.changeCount === 1 ? ' time' : ' times');
|
||||
}
|
||||
return 'Not triggered';
|
||||
},
|
||||
inputs: {
|
||||
value: {
|
||||
type: '*',
|
||||
displayName: 'Input',
|
||||
set: function (value) {
|
||||
if (this._internal.lastValue === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.changeCount++;
|
||||
this.sendSignalOnOutput('valueChanged');
|
||||
this._internal.lastValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
valueChanged: {
|
||||
type: 'signal',
|
||||
displayName: 'Value Changed'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: ValueChangedNode
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const VariableBase = require('@noodl/runtime/src/nodes/std-library/variables/variablebase');
|
||||
|
||||
module.exports = {
|
||||
node: VariableBase.createDefinition({
|
||||
name: 'Color',
|
||||
docs: 'https://docs.noodl.net/nodes/data/color',
|
||||
startValue: '#f1f2f4',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'value'
|
||||
},
|
||||
type: {
|
||||
name: 'color'
|
||||
},
|
||||
cast: function (value) {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
};
|
||||
125
packages/noodl-viewer-react/src/nodes/visual/circle.js
Normal file
125
packages/noodl-viewer-react/src/nodes/visual/circle.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Circle } from '../../components/visual/Circle';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const CircleNode = {
|
||||
name: 'Circle',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/circle',
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Fill',
|
||||
'Stroke',
|
||||
'Dimensions',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'Mounted',
|
||||
'Margin and padding',
|
||||
'Pointer Events',
|
||||
'Hover Events'
|
||||
]
|
||||
},
|
||||
getReactComponent() {
|
||||
return Circle;
|
||||
},
|
||||
noodlNodeAsProp: true,
|
||||
allowChildren: false,
|
||||
defaultCss: {
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
display: 'flex'
|
||||
},
|
||||
inputProps: {
|
||||
size: {
|
||||
displayName: 'Size',
|
||||
default: '100',
|
||||
group: 'Dimension',
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
index: 10,
|
||||
allowVisualStates: true
|
||||
},
|
||||
fillEnabled: {
|
||||
group: 'Fill',
|
||||
displayName: 'Fill',
|
||||
default: true,
|
||||
type: 'boolean',
|
||||
index: 20,
|
||||
allowVisualStates: true
|
||||
},
|
||||
fillColor: {
|
||||
group: 'Fill',
|
||||
displayName: 'Fill Color',
|
||||
default: 'red',
|
||||
type: 'color',
|
||||
index: 21,
|
||||
allowVisualStates: true
|
||||
},
|
||||
strokeEnabled: {
|
||||
index: 23,
|
||||
group: 'Stroke',
|
||||
default: false,
|
||||
displayName: 'Stroke',
|
||||
type: 'boolean',
|
||||
allowVisualStates: true
|
||||
},
|
||||
strokeWidth: {
|
||||
index: 24,
|
||||
group: 'Stroke',
|
||||
displayName: 'Stroke Width',
|
||||
default: 10,
|
||||
type: {
|
||||
name: 'number'
|
||||
},
|
||||
allowVisualStates: true
|
||||
},
|
||||
strokeColor: {
|
||||
index: 25,
|
||||
group: 'Stroke',
|
||||
displayName: 'Stroke Color',
|
||||
type: 'color',
|
||||
default: 'black',
|
||||
allowVisualStates: true
|
||||
},
|
||||
strokeLineCap: {
|
||||
index: 26,
|
||||
group: 'Stroke',
|
||||
displayName: 'Line Cap',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Butt', value: 'butt' },
|
||||
{ label: 'Round', value: 'round' }
|
||||
]
|
||||
},
|
||||
default: 'butt',
|
||||
allowVisualStates: true
|
||||
},
|
||||
startAngle: {
|
||||
displayName: 'Start Angle',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
group: 'Style',
|
||||
index: 198,
|
||||
allowVisualStates: true
|
||||
},
|
||||
endAngle: {
|
||||
displayName: 'End Angle',
|
||||
type: 'number',
|
||||
default: 360,
|
||||
group: 'Style',
|
||||
index: 199,
|
||||
allowVisualStates: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addTransformInputs(CircleNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(CircleNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(CircleNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(CircleNode);
|
||||
NodeSharedPortDefinitions.addPointerEventOutputs(CircleNode);
|
||||
|
||||
export default createNodeFromReactComponent(CircleNode);
|
||||
136
packages/noodl-viewer-react/src/nodes/visual/columns.js
Normal file
136
packages/noodl-viewer-react/src/nodes/visual/columns.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Columns } from '../../components/visual/Columns';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const ColumnsNode = {
|
||||
name: 'net.noodl.visual.columns',
|
||||
displayName: 'Columns',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/columns',
|
||||
allowChildren: true,
|
||||
noodlNodeAsProp: true,
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Label',
|
||||
'Label Text Style',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
|
||||
initialize() {
|
||||
this.props.layoutString = '1 2 1';
|
||||
this.props.minWidth = 0;
|
||||
this.props.marginX = 16;
|
||||
this.props.marginY = 16;
|
||||
this.props.direction = 'row';
|
||||
this.props.justifyContent = 'flex-start';
|
||||
},
|
||||
|
||||
getReactComponent() {
|
||||
return Columns;
|
||||
},
|
||||
|
||||
inputs: {
|
||||
layoutString: {
|
||||
group: 'Layout Settings',
|
||||
displayName: 'Layout String',
|
||||
type: 'string',
|
||||
default: '1 2 1',
|
||||
set(value) {
|
||||
this.props.layoutString = value;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'layout-type-warning',
|
||||
{
|
||||
message: 'Layout String needs to be a string.'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.context.editorConnection.clearWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'layout-type-warning'
|
||||
);
|
||||
}
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
inputProps: {
|
||||
marginX: {
|
||||
group: 'Layout Settings',
|
||||
displayName: 'Horizontal Gap',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 16
|
||||
},
|
||||
marginY: {
|
||||
group: 'Layout Settings',
|
||||
displayName: 'Vertical Gap',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 16
|
||||
},
|
||||
minWidth: {
|
||||
group: 'Constraints',
|
||||
displayName: 'Min Column Width',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 0
|
||||
},
|
||||
direction: {
|
||||
group: 'Layout Settings',
|
||||
displayName: 'Layout Direction',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{
|
||||
label: 'Horizontal',
|
||||
value: 'row'
|
||||
},
|
||||
{
|
||||
label: 'Vertical',
|
||||
value: 'column'
|
||||
}
|
||||
]
|
||||
},
|
||||
default: 'row'
|
||||
},
|
||||
justifyContent: {
|
||||
group: 'Justify Content',
|
||||
displayName: 'Justify Content',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Start', value: 'flex-start' },
|
||||
{ label: 'End', value: 'flex-end' },
|
||||
{ label: 'Center', value: 'center' }
|
||||
],
|
||||
alignComp: 'align-items'
|
||||
},
|
||||
default: 'flex-start'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default createNodeFromReactComponent(ColumnsNode);
|
||||
@@ -0,0 +1,86 @@
|
||||
const _refCount = new Map(); //node id to ref count
|
||||
|
||||
const CSSDefinition = {
|
||||
name: 'CSS Definition',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/css-definition',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Style'
|
||||
},
|
||||
initialize: function () {
|
||||
var internal = this._internal;
|
||||
internal.style = '';
|
||||
|
||||
const styleId = this.getStyleRefId();
|
||||
|
||||
this.addDeleteListener(() => {
|
||||
_refCount.set(styleId, _refCount.get(styleId) - 1);
|
||||
if (_refCount.get(styleId) === 0) {
|
||||
this.removeStyleDeclaration();
|
||||
_refCount.delete(styleId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!_refCount.has(styleId)) {
|
||||
_refCount.set(styleId, 0);
|
||||
}
|
||||
|
||||
_refCount.set(styleId, _refCount.get(styleId) + 1);
|
||||
},
|
||||
inputs: {
|
||||
style: {
|
||||
index: 4005,
|
||||
type: { name: 'string', allowEditOnly: true, codeeditor: 'css' },
|
||||
displayName: 'Style',
|
||||
group: 'Content',
|
||||
default: '',
|
||||
|
||||
set: function (value) {
|
||||
this.updateStyle(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
getStyleRefId: function () {
|
||||
return 'style_' + this.id;
|
||||
},
|
||||
removeStyleDeclaration: function () {
|
||||
// Add SSR Support
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
var styleRefId = this.getStyleRefId();
|
||||
var styleObj = document.getElementById(styleRefId);
|
||||
if (styleObj !== null) {
|
||||
styleObj.parentNode.removeChild(styleObj);
|
||||
}
|
||||
},
|
||||
updateStyle: function (style) {
|
||||
// Add SSR Support
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
var internal = this._internal;
|
||||
var styleRefId = this.getStyleRefId();
|
||||
internal.style = style;
|
||||
|
||||
if (style !== null) {
|
||||
var styleObj = document.getElementById(styleRefId);
|
||||
if (styleObj === null) {
|
||||
styleObj = document.createElement('style');
|
||||
styleObj.id = styleRefId;
|
||||
styleObj.type = 'text/css';
|
||||
document.head.appendChild(styleObj);
|
||||
}
|
||||
|
||||
styleObj.innerHTML = '\n' + style + '\n';
|
||||
} else {
|
||||
this.removeStyleDeclaration();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
node: CSSDefinition
|
||||
};
|
||||
169
packages/noodl-viewer-react/src/nodes/visual/drag.js
Normal file
169
packages/noodl-viewer-react/src/nodes/visual/drag.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Drag } from '../../components/visual/Drag';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const DragNode = {
|
||||
name: 'Drag',
|
||||
docs: 'https://docs.noodl.net/nodes/utilities/drag',
|
||||
allowChildren: true,
|
||||
noodlNodeAsProp: true,
|
||||
getReactComponent() {
|
||||
return Drag;
|
||||
},
|
||||
initialize() {
|
||||
this._internal.snapPositionX = 0;
|
||||
this._internal.snapPositionY = 0;
|
||||
this._internal.snapDurationX = 300;
|
||||
this._internal.snapDurationY = 300;
|
||||
},
|
||||
inputs: {
|
||||
'snapToPositionX.do': {
|
||||
group: 'Snap To Position X',
|
||||
displayName: 'Do',
|
||||
editorName: 'Do|Snap To Position X',
|
||||
type: 'signal',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const { snapPositionX, snapDurationX } = this._internal;
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.snapToPositionX(snapPositionX, snapDurationX);
|
||||
});
|
||||
}
|
||||
},
|
||||
'snapToPositionX.value': {
|
||||
default: 0,
|
||||
group: 'Snap To Position X',
|
||||
displayName: 'Value',
|
||||
editorName: 'Value|Snap To Position X',
|
||||
type: 'number',
|
||||
set(value) {
|
||||
this._internal.snapPositionX = value;
|
||||
}
|
||||
},
|
||||
'snapToPositionX.duration': {
|
||||
default: 300,
|
||||
group: 'Snap To Position X',
|
||||
displayName: 'Duration',
|
||||
editorName: 'Duration|Snap To Position X',
|
||||
type: 'number',
|
||||
set(value) {
|
||||
this._internal.snapDurationX = value;
|
||||
}
|
||||
},
|
||||
'snapToPositionY.do': {
|
||||
group: 'Snap To Position Y',
|
||||
displayName: 'Do',
|
||||
editorName: 'Do|Snap To Position Y',
|
||||
type: 'signal',
|
||||
valueChangedToTrue() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const { snapPositionY, snapDurationY } = this._internal;
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.snapToPositionY(snapPositionY, snapDurationY);
|
||||
});
|
||||
}
|
||||
},
|
||||
'snapToPositionY.value': {
|
||||
default: 0,
|
||||
group: 'Snap To Position Y',
|
||||
displayName: 'Value',
|
||||
editorName: 'Value|Snap To Position Y',
|
||||
type: 'number',
|
||||
set(value) {
|
||||
this._internal.snapPositionY = value;
|
||||
}
|
||||
},
|
||||
'snapToPositionY.duration': {
|
||||
default: 300,
|
||||
group: 'Snap To Position Y',
|
||||
displayName: 'Duration',
|
||||
editorName: 'Duration|Snap To Position Y',
|
||||
type: 'number',
|
||||
set(value) {
|
||||
this._internal.snapDurationY = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
enabled: {
|
||||
group: 'Drag',
|
||||
displayName: 'Enabled',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
axis: {
|
||||
group: 'Drag',
|
||||
displayName: 'Axis',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'X', value: 'x' },
|
||||
{ label: 'Y', value: 'y' },
|
||||
{ label: 'Both', value: 'both' }
|
||||
]
|
||||
},
|
||||
default: 'x'
|
||||
},
|
||||
useParentBounds: {
|
||||
group: 'Drag',
|
||||
displayName: 'Constrain to parent',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
inputPositionX: {
|
||||
displayName: 'Start Drag X',
|
||||
type: {
|
||||
name: 'number'
|
||||
}
|
||||
},
|
||||
inputPositionY: {
|
||||
displayName: 'Start Drag Y',
|
||||
type: {
|
||||
name: 'number'
|
||||
}
|
||||
},
|
||||
scale: {
|
||||
displayName: 'Scale',
|
||||
default: 1.0,
|
||||
type: {
|
||||
name: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
onStart: {
|
||||
group: 'Signals',
|
||||
type: 'signal',
|
||||
displayName: 'Drag Started'
|
||||
},
|
||||
onStop: {
|
||||
group: 'Signals',
|
||||
type: 'signal',
|
||||
displayName: 'Drag Ended'
|
||||
},
|
||||
onDrag: {
|
||||
group: 'Signals',
|
||||
type: 'signal',
|
||||
displayName: 'Drag Moved'
|
||||
},
|
||||
positionX: {
|
||||
group: 'Values',
|
||||
displayName: 'Drag X',
|
||||
type: 'number'
|
||||
},
|
||||
positionY: {
|
||||
group: 'Values',
|
||||
displayName: 'Drag Y',
|
||||
type: 'number'
|
||||
},
|
||||
deltaX: {
|
||||
group: 'Values',
|
||||
displayName: 'Delta X',
|
||||
type: 'number'
|
||||
},
|
||||
deltaY: {
|
||||
group: 'Values',
|
||||
displayName: 'Delta Y',
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default createNodeFromReactComponent(DragNode);
|
||||
443
packages/noodl-viewer-react/src/nodes/visual/group.js
Normal file
443
packages/noodl-viewer-react/src/nodes/visual/group.js
Normal file
@@ -0,0 +1,443 @@
|
||||
import { Group } from '../../components/visual/Group';
|
||||
import { flexDirectionValues } from '../../constants/flex';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import { createTooltip } from '../../tooltips';
|
||||
|
||||
const GroupNode = {
|
||||
name: 'Group',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/group',
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Style', 'Events', 'Mounted', 'Hover Events', 'Pointer Events', 'Focus', 'Scroll']
|
||||
},
|
||||
initialize() {
|
||||
this._internal = {
|
||||
scrollElementDuration: 500,
|
||||
scrollIndexDuration: 500,
|
||||
scrollIndex: 0
|
||||
};
|
||||
this.props.layout = 'column';
|
||||
},
|
||||
getReactComponent() {
|
||||
return Group;
|
||||
},
|
||||
noodlNodeAsProp: true,
|
||||
visualStates: [
|
||||
{ name: 'neutral', label: 'Neutral' },
|
||||
{ name: 'hover', label: 'Hover' }
|
||||
],
|
||||
defaultCss: {
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
inputs: {
|
||||
flexDirection: {
|
||||
//don't rename for backwards compat
|
||||
index: 12,
|
||||
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();
|
||||
}
|
||||
},
|
||||
'scrollToIndex.do': {
|
||||
displayName: 'Scroll To Index - Do',
|
||||
group: 'Scroll To Index',
|
||||
type: 'signal',
|
||||
index: 505,
|
||||
valueChangedToTrue() {
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
if (!this.innerReactComponentRef) return;
|
||||
const childIndex = this._internal.scrollIndex;
|
||||
const duration = this._internal.scrollIndexDuration;
|
||||
this.innerReactComponentRef.scrollToIndex(childIndex, duration);
|
||||
});
|
||||
}
|
||||
},
|
||||
'scrollToElement.do': {
|
||||
displayName: 'Scroll To Element - Do',
|
||||
group: 'Scroll To Element',
|
||||
type: 'signal',
|
||||
index: 500,
|
||||
valueChangedToTrue() {
|
||||
if (!this.innerReactComponentRef) return;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
const element = this._internal.scrollElement;
|
||||
const duration = this._internal.scrollElementDuration;
|
||||
this.innerReactComponentRef.scrollToElement(element, duration);
|
||||
});
|
||||
}
|
||||
},
|
||||
'scrollToElement.element': {
|
||||
displayName: 'Scroll To Element - Element',
|
||||
group: 'Scroll To Element',
|
||||
type: 'reference',
|
||||
index: 501,
|
||||
set(value) {
|
||||
this._internal.scrollElement = value;
|
||||
}
|
||||
},
|
||||
'scrollToElement.duration': {
|
||||
displayName: 'Scroll To Element - Duration',
|
||||
group: 'Scroll To Element',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
index: 502,
|
||||
set(value) {
|
||||
this._internal.scrollElementDuration = value;
|
||||
}
|
||||
},
|
||||
'scrollToIndex.index': {
|
||||
displayName: 'Scroll To Index - Index',
|
||||
group: 'Scroll To Index',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
index: 506,
|
||||
set(value) {
|
||||
this._internal.scrollIndex = value;
|
||||
}
|
||||
},
|
||||
'scrollToIndex.duration': {
|
||||
displayName: 'Scroll To Index - Duration',
|
||||
group: 'Scroll To Index',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
index: 507,
|
||||
set(value) {
|
||||
this._internal.scrollIndexDuration = value;
|
||||
}
|
||||
},
|
||||
focus: {
|
||||
displayName: 'Focus',
|
||||
type: 'signal',
|
||||
group: 'Focus',
|
||||
valueChangedToTrue() {
|
||||
this.context.setNodeFocused(this, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
clip: {
|
||||
index: 19,
|
||||
displayName: 'Clip Content',
|
||||
type: 'boolean',
|
||||
group: 'Layout',
|
||||
default: false
|
||||
},
|
||||
scrollEnabled: {
|
||||
index: 54,
|
||||
group: 'Scroll',
|
||||
displayName: 'Enable Scroll',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
scrollSnapEnabled: {
|
||||
index: 55,
|
||||
displayName: 'Snap',
|
||||
group: 'Scroll',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
scrollSnapToEveryItem: {
|
||||
index: 56,
|
||||
displayName: 'Snap To Every Item',
|
||||
group: 'Scroll',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
showScrollbar: {
|
||||
index: 57,
|
||||
displayName: 'Show Scrollbar',
|
||||
group: 'Scroll',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
scrollBounceEnabled: {
|
||||
index: 58,
|
||||
displayName: 'Bounce at boundaries',
|
||||
group: 'Scroll',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
nativeScroll: {
|
||||
index: 60,
|
||||
group: 'Scroll',
|
||||
displayName: 'Native platform scroll',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
as: {
|
||||
index: 100000,
|
||||
group: 'Advanced HTML',
|
||||
displayName: 'Tag',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: '<div>', value: 'div' },
|
||||
{ label: '<section>', value: 'section' },
|
||||
{ label: '<article>', value: 'article' },
|
||||
{ label: '<aside>', value: 'aside' },
|
||||
{ label: '<nav>', value: 'nav' },
|
||||
{ label: '<header>', value: 'header' },
|
||||
{ label: '<footer>', value: 'footer' },
|
||||
{ label: '<main>', value: 'main' },
|
||||
{ label: '<span>', value: 'span' }
|
||||
]
|
||||
},
|
||||
default: 'div'
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
alignItems: {
|
||||
index: 13,
|
||||
group: 'Align and justify content',
|
||||
displayName: 'Align Items',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Start', value: 'flex-start' },
|
||||
{ label: 'End', value: 'flex-end' },
|
||||
{ label: 'Center', value: 'center' }
|
||||
],
|
||||
alignComp: 'align-items'
|
||||
},
|
||||
default: 'flex-start'
|
||||
},
|
||||
justifyContent: {
|
||||
index: 14,
|
||||
group: 'Align and justify content',
|
||||
displayName: 'Justify Content',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Start', value: 'flex-start' },
|
||||
{ label: 'End', value: 'flex-end' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Space Between', value: 'space-between' },
|
||||
{ label: 'Space Around', value: 'space-around' },
|
||||
{ label: 'Space Evenly', value: 'space-evenly' }
|
||||
],
|
||||
alignComp: 'justify-content'
|
||||
},
|
||||
default: 'flex-start',
|
||||
applyDefault: false
|
||||
},
|
||||
flexWrap: {
|
||||
index: 15,
|
||||
displayName: 'Multi Line Wrap',
|
||||
group: 'Layout',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Off', value: 'nowrap' },
|
||||
{ label: 'On', value: 'wrap' },
|
||||
{ label: 'On Reverse', value: 'wrap-reverse' }
|
||||
]
|
||||
},
|
||||
default: 'nowrap',
|
||||
onChange(value) {
|
||||
this.props.flexWrap = value;
|
||||
this.forceUpdate(); //scroll direction needs to be recomputed
|
||||
},
|
||||
applyDefault: false
|
||||
},
|
||||
alignContent: {
|
||||
index: 16,
|
||||
group: 'Layout',
|
||||
displayName: 'Align Content',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Start', value: 'flex-start' },
|
||||
{ label: 'End', value: 'flex-end' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Space Between', value: 'space-between' },
|
||||
{ label: 'Space Around', value: 'space-around' },
|
||||
{ label: 'Space Evenly', value: 'space-evenly' }
|
||||
],
|
||||
alignComp: 'align-content'
|
||||
}
|
||||
// default: 'flex-start'
|
||||
},
|
||||
rowGap: {
|
||||
index: 17,
|
||||
displayName: 'Vertical Gap',
|
||||
group: 'Layout',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', '%', 'em'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 0,
|
||||
applyDefault: false
|
||||
},
|
||||
columnGap: {
|
||||
index: 18,
|
||||
displayName: 'Horizontal Gap',
|
||||
group: 'Layout',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['px', '%', 'em'],
|
||||
defaultUnit: 'px'
|
||||
},
|
||||
default: 0,
|
||||
applyDefault: false
|
||||
},
|
||||
backgroundColor: {
|
||||
index: 201,
|
||||
displayName: 'Background Color',
|
||||
group: 'Style',
|
||||
type: 'color',
|
||||
default: 'transparent',
|
||||
applyDefault: false,
|
||||
allowVisualStates: true
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
onScrollPositionChanged: {
|
||||
displayName: 'Scroll Position',
|
||||
type: 'number',
|
||||
group: 'Scroll'
|
||||
},
|
||||
onScrollStart: {
|
||||
displayName: 'Scroll Start',
|
||||
type: 'signal',
|
||||
group: 'Scroll'
|
||||
},
|
||||
onScrollEnd: {
|
||||
displayName: 'Scroll End',
|
||||
type: 'signal',
|
||||
group: 'Scroll'
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
focused: {
|
||||
displayName: 'Focused',
|
||||
type: 'signal',
|
||||
group: 'Focus'
|
||||
},
|
||||
focusLost: {
|
||||
displayName: 'Focus Lost',
|
||||
type: 'signal',
|
||||
group: 'Focus'
|
||||
}
|
||||
},
|
||||
dynamicports: [
|
||||
{
|
||||
condition: 'flexDirection != none',
|
||||
inputs: ['scrollEnabled']
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection != none AND scrollEnabled = true',
|
||||
inputs: ['nativeScroll']
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection != none AND scrollEnabled = true AND nativeScroll = false',
|
||||
inputs: [
|
||||
'scrollBounceEnabled',
|
||||
'scrollSnapEnabled',
|
||||
'showScrollbar',
|
||||
'scrollToElement.do',
|
||||
'scrollToElement.element',
|
||||
'scrollToElement.duration',
|
||||
'scrollToIndex.do',
|
||||
'scrollToIndex.index',
|
||||
'scrollToIndex.duration'
|
||||
]
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection != none AND scrollEnabled = true AND scrollSnapEnabled = true',
|
||||
inputs: ['scrollSnapToEveryItem']
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection != none',
|
||||
inputs: ['flexWrap']
|
||||
},
|
||||
{
|
||||
condition: 'flexWrap = wrap OR flexWrap = wrap-reverse',
|
||||
inputs: ['alignContent']
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection = row OR flexWrap = wrap OR flexWrap = wrap-reverse',
|
||||
inputs: ['columnGap']
|
||||
},
|
||||
{
|
||||
condition: 'flexDirection = column OR flexWrap = wrap OR flexWrap = wrap-reverse',
|
||||
inputs: ['rowGap']
|
||||
}
|
||||
],
|
||||
methods: {
|
||||
_focus() {
|
||||
this.sendSignalOnOutput('focused');
|
||||
},
|
||||
_blur() {
|
||||
this.sendSignalOnOutput('focusLost');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(GroupNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addPointerEventOutputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(GroupNode);
|
||||
NodeSharedPortDefinitions.addShadowInputs(GroupNode);
|
||||
|
||||
function defineTooltips(node) {
|
||||
node.inputProps.clip.tooltip = createTooltip({
|
||||
title: 'Clip content',
|
||||
body: 'Controls if elements that are too big to fit will be clipped',
|
||||
images: [
|
||||
{ src: 'clip-enabled.svg', label: 'Enabled' },
|
||||
{ src: 'clip-disabled.svg', label: 'Disabled' }
|
||||
]
|
||||
});
|
||||
|
||||
node.inputCss.flexWrap.tooltip = createTooltip({
|
||||
title: 'Multiline wrap',
|
||||
body: "Elements will wrap to the next line when there's not enough space",
|
||||
images: [
|
||||
{ src: 'multiline-h.svg', body: 'Using a horizontal layout' },
|
||||
{ src: 'multiline-v.svg', body: 'Using a vertical layout' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (!Noodl.runDeployed) {
|
||||
defineTooltips(GroupNode);
|
||||
}
|
||||
|
||||
export default createNodeFromReactComponent(GroupNode);
|
||||
45
packages/noodl-viewer-react/src/nodes/visual/icon.js
Normal file
45
packages/noodl-viewer-react/src/nodes/visual/icon.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Icon } from '../../components/visual/Icon';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const IconNode = {
|
||||
name: 'net.noodl.visual.icon',
|
||||
displayName: 'Icon',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/icon',
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'States',
|
||||
'Mounted',
|
||||
'Hover Events',
|
||||
'Pointer Events',
|
||||
'Focus Events'
|
||||
]
|
||||
},
|
||||
getReactComponent() {
|
||||
return Icon;
|
||||
}
|
||||
};
|
||||
NodeSharedPortDefinitions.addAlignInputs(IconNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(IconNode);
|
||||
NodeSharedPortDefinitions.addPaddingInputs(IconNode, {
|
||||
defaults: {
|
||||
paddingTop: 5,
|
||||
paddingRight: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 5
|
||||
}
|
||||
});
|
||||
NodeSharedPortDefinitions.addMarginInputs(IconNode);
|
||||
NodeSharedPortDefinitions.addIconInputs(IconNode, {
|
||||
hideEnableIconInput: true,
|
||||
defaults: { useIcon: true }
|
||||
});
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(IconNode);
|
||||
|
||||
export default createNodeFromReactComponent(IconNode);
|
||||
133
packages/noodl-viewer-react/src/nodes/visual/image.js
Normal file
133
packages/noodl-viewer-react/src/nodes/visual/image.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { getAbsoluteUrl } from '@noodl/runtime/src/utils';
|
||||
|
||||
import { Image } from '../../components/visual/Image';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const ImageNode = {
|
||||
name: 'Image',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/image',
|
||||
noodlNodeAsProp: true,
|
||||
visualStates: [
|
||||
{ name: 'neutral', label: 'Neutral' },
|
||||
{ name: 'hover', label: 'Hover' }
|
||||
],
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Image',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'Mounted',
|
||||
'Pointer Events',
|
||||
'Hover Events',
|
||||
'Dimensions',
|
||||
'Margin and padding'
|
||||
]
|
||||
},
|
||||
initialize() {
|
||||
this.props.default = '';
|
||||
},
|
||||
getReactComponent() {
|
||||
return Image;
|
||||
},
|
||||
getInspectInfo() {
|
||||
if (this.props.dom.srcSet) {
|
||||
return this.props.dom.srcSet;
|
||||
} else if (this.props.dom.src) {
|
||||
const src = this.props.dom.src.toString();
|
||||
return [
|
||||
{ type: 'text', value: src },
|
||||
{ type: 'image', value: src }
|
||||
];
|
||||
}
|
||||
},
|
||||
allowChildren: false,
|
||||
defaultCss: {
|
||||
display: 'block',
|
||||
flexShrink: 0
|
||||
},
|
||||
inputCss: {
|
||||
objectFit: {
|
||||
displayName: 'Image Fit',
|
||||
group: 'Dimensions',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Fill', value: 'fill' },
|
||||
{ label: 'Contain', value: 'contain' },
|
||||
{ label: 'Cover', value: 'cover' },
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: 'Scale Down', value: 'scale-down' }
|
||||
]
|
||||
},
|
||||
default: 'contain',
|
||||
allowVisualStates: true
|
||||
}
|
||||
},
|
||||
dynamicports: [
|
||||
{
|
||||
condition: 'sizeMode = explicit',
|
||||
inputs: ['objectFit']
|
||||
}
|
||||
],
|
||||
inputs: {
|
||||
src: {
|
||||
displayName: 'Source',
|
||||
group: 'Image',
|
||||
propPath: 'dom',
|
||||
type: {
|
||||
name: 'image'
|
||||
},
|
||||
index: 30,
|
||||
allowVisualStates: true,
|
||||
set(url) {
|
||||
this.props.dom.src = getAbsoluteUrl(url);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
srcSet: {
|
||||
displayName: 'Source Set',
|
||||
group: 'Image',
|
||||
propPath: 'dom',
|
||||
type: {
|
||||
name: 'string'
|
||||
},
|
||||
index: 31,
|
||||
allowVisualStates: true
|
||||
},
|
||||
alt: {
|
||||
displayName: 'Alternate text',
|
||||
tooltip: "The alt text is used by screen readers, or if the image can't be downloaded or displayed",
|
||||
type: 'string',
|
||||
propPath: 'dom',
|
||||
index: 1000,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
onLoad: {
|
||||
displayName: 'On Load',
|
||||
propPath: 'dom',
|
||||
type: 'signal',
|
||||
group: 'Events'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(ImageNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Image'
|
||||
});
|
||||
NodeSharedPortDefinitions.addTransformInputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addPointerEventOutputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(ImageNode);
|
||||
NodeSharedPortDefinitions.addShadowInputs(ImageNode);
|
||||
|
||||
export default createNodeFromReactComponent(ImageNode);
|
||||
169
packages/noodl-viewer-react/src/nodes/visual/text.js
Normal file
169
packages/noodl-viewer-react/src/nodes/visual/text.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Text } from '../../components/visual/Text';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
import { createTooltip } from '../../tooltips';
|
||||
|
||||
const TextNode = {
|
||||
name: 'Text',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/text',
|
||||
visualStates: [
|
||||
{ name: 'neutral', label: 'Neutral' },
|
||||
{ name: 'hover', label: 'Hover' }
|
||||
],
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
usePortAsLabel: 'text',
|
||||
portLabelTruncationMode: 'length',
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Text', 'Text Style', 'Style', 'Events', 'Mounted', 'Hover Events', 'Pointer Events']
|
||||
},
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'text'
|
||||
},
|
||||
getReactComponent() {
|
||||
return Text;
|
||||
},
|
||||
getInspectInfo() {
|
||||
return this.props.text;
|
||||
},
|
||||
defaultCss: {
|
||||
position: 'relative',
|
||||
display: 'flex'
|
||||
},
|
||||
inputProps: {
|
||||
text: {
|
||||
index: 19,
|
||||
group: 'Text',
|
||||
displayName: 'Text',
|
||||
default: 'Text',
|
||||
type: {
|
||||
name: 'string',
|
||||
multiline: true
|
||||
}
|
||||
},
|
||||
as: {
|
||||
index: 100000,
|
||||
group: 'Advanced HTML',
|
||||
displayName: 'Tag',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: '<div>', value: 'div' },
|
||||
{ label: '<h1>', value: 'h1' },
|
||||
{ label: '<h2>', value: 'h2' },
|
||||
{ label: '<h3>', value: 'h3' },
|
||||
{ label: '<h4>', value: 'h4' },
|
||||
{ label: '<h5>', value: 'h5' },
|
||||
{ label: '<h6>', value: 'h6' },
|
||||
{ label: '<p>', value: 'p' },
|
||||
{ label: '<span>', value: 'span' }
|
||||
// { label: '<a>', value: 'a' },
|
||||
]
|
||||
},
|
||||
default: 'div'
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
wordBreak: {
|
||||
index: 27,
|
||||
group: 'Text',
|
||||
displayName: 'Word Break',
|
||||
applyDefault: false,
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{ label: 'Normal', value: 'normal' },
|
||||
{ label: 'Break All', value: 'break-all' }
|
||||
]
|
||||
},
|
||||
default: 'normal'
|
||||
}
|
||||
},
|
||||
inputs: {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(TextNode, {
|
||||
defaultSizeMode: 'contentHeight',
|
||||
contentLabel: 'Text'
|
||||
});
|
||||
NodeSharedPortDefinitions.addTextStyleInputs(TextNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(TextNode);
|
||||
NodeSharedPortDefinitions.addTransformInputs(TextNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(TextNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(TextNode);
|
||||
NodeSharedPortDefinitions.addPointerEventOutputs(TextNode);
|
||||
|
||||
function defineTooltips(node) {
|
||||
node.inputCss.wordBreak.tooltip = createTooltip({
|
||||
title: 'Word break',
|
||||
body: [
|
||||
'Control where line breaks are allowed',
|
||||
'- Normal: Break on spaces and other whitespace characters',
|
||||
'- Break All: Allow line breaks between any two characters, including inside words'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (!Noodl.runDeployed) {
|
||||
defineTooltips(TextNode);
|
||||
}
|
||||
|
||||
export default createNodeFromReactComponent(TextNode);
|
||||
237
packages/noodl-viewer-react/src/nodes/visual/video.js
Normal file
237
packages/noodl-viewer-react/src/nodes/visual/video.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { getAbsoluteUrl } from '@noodl/runtime/src/utils';
|
||||
|
||||
import { Video } from '../../components/visual/Video';
|
||||
import NodeSharedPortDefinitions from '../../node-shared-port-definitions';
|
||||
import { createNodeFromReactComponent } from '../../react-component-node';
|
||||
|
||||
const VideoNode = {
|
||||
name: 'Video',
|
||||
docs: 'https://docs.noodl.net/nodes/basic-elements/video',
|
||||
connectionPanel: {
|
||||
groupPriority: [
|
||||
'General',
|
||||
'Video',
|
||||
'Video Actions',
|
||||
'Style',
|
||||
'Actions',
|
||||
'Events',
|
||||
'Mounted',
|
||||
'Playback',
|
||||
'Pointer Events',
|
||||
'Hover Events',
|
||||
'Dimensions',
|
||||
'Margin and padding'
|
||||
]
|
||||
},
|
||||
getReactComponent() {
|
||||
return Video;
|
||||
},
|
||||
allowChildren: false,
|
||||
noodlNodeAsProp: true,
|
||||
defaultCss: {
|
||||
display: 'block'
|
||||
},
|
||||
inputs: {
|
||||
srcObject: {
|
||||
displayName: 'Source Object',
|
||||
group: 'Video',
|
||||
type: 'mediastream',
|
||||
default: null,
|
||||
set(value) {
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.setSourceObject(value);
|
||||
}
|
||||
},
|
||||
play: {
|
||||
type: 'signal',
|
||||
group: 'Video Actions',
|
||||
displayName: 'Play',
|
||||
tooltip: {
|
||||
standard: 'Play the video'
|
||||
},
|
||||
valueChangedToTrue() {
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.play();
|
||||
}
|
||||
},
|
||||
restart: {
|
||||
type: 'signal',
|
||||
group: 'Video Actions',
|
||||
displayName: 'Restart',
|
||||
tooltip: {
|
||||
standard: 'Restart the video from the beginning'
|
||||
},
|
||||
valueChangedToTrue() {
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.restart();
|
||||
}
|
||||
},
|
||||
pause: {
|
||||
type: 'boolean',
|
||||
group: 'Video Actions',
|
||||
displayName: 'Pause',
|
||||
valueChangedToTrue() {
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.pause();
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
type: 'boolean',
|
||||
group: 'Video Actions',
|
||||
displayName: 'Reset',
|
||||
valueChangedToTrue() {
|
||||
this.innerReactComponentRef && this.innerReactComponentRef.reset();
|
||||
}
|
||||
},
|
||||
src: {
|
||||
displayName: 'Source',
|
||||
group: 'Video',
|
||||
type: 'string',
|
||||
set(src) {
|
||||
this.props.dom.src = getAbsoluteUrl(src);
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
poster: {
|
||||
displayName: 'Poster',
|
||||
group: 'Video',
|
||||
type: 'image',
|
||||
set(src) {
|
||||
this.props.dom.poster = getAbsoluteUrl(src);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
inputProps: {
|
||||
autoplay: {
|
||||
displayName: 'Autoplay',
|
||||
propPath: 'dom',
|
||||
group: 'Video',
|
||||
type: 'boolean'
|
||||
},
|
||||
controls: {
|
||||
displayName: 'Controls',
|
||||
propPath: 'dom',
|
||||
group: 'Video',
|
||||
type: 'boolean'
|
||||
},
|
||||
volume: {
|
||||
displayName: 'Volume',
|
||||
propPath: 'dom',
|
||||
group: 'Video',
|
||||
type: 'number',
|
||||
default: 1
|
||||
},
|
||||
muted: {
|
||||
displayName: 'Muted',
|
||||
propPath: 'dom',
|
||||
group: 'Video',
|
||||
type: 'boolean'
|
||||
},
|
||||
loop: {
|
||||
displayName: 'Loop',
|
||||
propPath: 'dom',
|
||||
group: 'Video',
|
||||
type: 'boolean'
|
||||
},
|
||||
objectPositionX: {
|
||||
displayName: 'Video Position X',
|
||||
group: 'Video Layout',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['%', 'px'],
|
||||
defaultUnit: '%'
|
||||
},
|
||||
default: 50
|
||||
},
|
||||
objectPositionY: {
|
||||
displayName: 'Video Position Y',
|
||||
group: 'Video Layout',
|
||||
type: {
|
||||
name: 'number',
|
||||
units: ['%', 'px'],
|
||||
defaultUnit: '%'
|
||||
},
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
inputCss: {
|
||||
objectFit: {
|
||||
displayName: 'Object Fit',
|
||||
group: 'Video Layout',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: [
|
||||
{
|
||||
label: 'Contain',
|
||||
value: 'contain'
|
||||
},
|
||||
{
|
||||
label: 'Cover',
|
||||
value: 'cover'
|
||||
},
|
||||
{
|
||||
label: 'Fill',
|
||||
value: 'fill'
|
||||
},
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none'
|
||||
}
|
||||
]
|
||||
},
|
||||
default: 'contain'
|
||||
}
|
||||
},
|
||||
outputProps: {
|
||||
onCanPlay: {
|
||||
type: 'signal',
|
||||
group: 'Events',
|
||||
displayName: 'On Can Play'
|
||||
},
|
||||
onTimeUpdate: {
|
||||
group: 'Playback',
|
||||
displayName: 'Playback Position',
|
||||
type: 'number',
|
||||
propPath: 'dom',
|
||||
getValue(event) {
|
||||
return event.target.currentTime;
|
||||
}
|
||||
},
|
||||
onPlay: {
|
||||
group: 'Events',
|
||||
displayName: 'On Play',
|
||||
type: 'signal',
|
||||
propPath: 'dom'
|
||||
},
|
||||
onPause: {
|
||||
group: 'Events',
|
||||
displayName: 'On Pause',
|
||||
type: 'signal',
|
||||
propPath: 'dom'
|
||||
},
|
||||
onVideoElementCreated: {
|
||||
type: 'domelement',
|
||||
displayName: 'DOM Element'
|
||||
},
|
||||
videoWidth: {
|
||||
group: 'Playback',
|
||||
type: 'number',
|
||||
displayName: 'Video Width'
|
||||
},
|
||||
videoHeight: {
|
||||
group: 'Playback',
|
||||
type: 'number',
|
||||
displayName: 'Video Height'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NodeSharedPortDefinitions.addDimensions(VideoNode, {
|
||||
defaultSizeMode: 'contentSize',
|
||||
contentLabel: 'Video'
|
||||
});
|
||||
NodeSharedPortDefinitions.addTransformInputs(VideoNode);
|
||||
NodeSharedPortDefinitions.addMarginInputs(VideoNode);
|
||||
NodeSharedPortDefinitions.addSharedVisualInputs(VideoNode);
|
||||
NodeSharedPortDefinitions.addAlignInputs(VideoNode);
|
||||
NodeSharedPortDefinitions.addPointerEventOutputs(VideoNode);
|
||||
NodeSharedPortDefinitions.addBorderInputs(VideoNode);
|
||||
|
||||
export default createNodeFromReactComponent(VideoNode);
|
||||
Reference in New Issue
Block a user