@@ -22,6 +22,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value}) => (
min={min}
type="range"
value={value}
+ onChange={onSliderUpdate}
// @todo onChange callback
/>
@@ -34,7 +35,7 @@ SliderMonitor.propTypes = {
label: PropTypes.string.isRequired,
max: PropTypes.number,
min: PropTypes.number,
- // @todo callback for change events
+ onSliderUpdate: PropTypes.func.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
diff --git a/src/containers/list-monitor.jsx b/src/containers/list-monitor.jsx
new file mode 100644
index 000000000..4b7cc80db
--- /dev/null
+++ b/src/containers/list-monitor.jsx
@@ -0,0 +1,183 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import VM from 'scratch-vm';
+import {connect} from 'react-redux';
+import {getEventXY} from '../lib/touch-utils';
+import {getVariableValue, setVariableValue} from '../lib/variable-utils';
+import ListMonitorComponent from '../components/monitor/list-monitor.jsx';
+
+class ListMonitor extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleActivate',
+ 'handleDeactivate',
+ 'handleInput',
+ 'handleRemove',
+ 'handleKeyPress',
+ 'handleFocus',
+ 'handleAdd',
+ 'handleResizeMouseDown'
+ ]);
+
+ this.state = {
+ activeIndex: null,
+ activeValue: null,
+ // TODO These will need to be sent back to the VM for saving
+ width: props.width || 100,
+ height: props.height || 200
+ };
+ }
+
+ handleActivate (index) {
+ this.setState({
+ activeIndex: index,
+ activeValue: this.props.value[index]
+ });
+ }
+
+ handleDeactivate () {
+ // Submit any in-progress value edits on blur
+ if (this.state.activeIndex !== null) {
+ const {vm, targetId, id: variableId} = this.props;
+ const newListValue = getVariableValue(vm, targetId, variableId);
+ newListValue[this.state.activeIndex] = this.state.activeValue;
+ setVariableValue(vm, targetId, variableId, newListValue);
+ this.setState({activeIndex: null, activeValue: null});
+ }
+ }
+
+ handleFocus (e) {
+ // Select all the text in the input when it is focused.
+ e.target.select();
+ }
+
+ handleKeyPress (e) {
+ // Special case for tab, arrow keys and enter.
+ // Tab / shift+tab navigate down / up the list.
+ // Arrow down / arrow up navigate down / up the list.
+ // Enter / shift+enter insert new blank item below / above.
+ const previouslyActiveIndex = this.state.activeIndex;
+ const {vm, targetId, id: variableId} = this.props;
+
+ let navigateDirection = 0;
+ if (e.key === 'Tab') navigateDirection = e.shiftKey ? -1 : 1;
+ else if (e.key === 'ArrowUp') navigateDirection = -1;
+ else if (e.key === 'ArrowDown') navigateDirection = 1;
+ if (navigateDirection) {
+ this.handleDeactivate(); // Submit in-progress edits
+ const newIndex = (previouslyActiveIndex + navigateDirection) % this.props.value.length;
+ this.setState({
+ activeIndex: newIndex,
+ activeValue: this.props.value[newIndex]
+ });
+ e.preventDefault(); // Stop default tab behavior, handled by this state change
+ } else if (e.key === 'Enter') {
+ this.handleDeactivate(); // Submit in-progress edits
+ const newListItemValue = ''; // Enter adds a blank item
+ const newValueOffset = e.shiftKey ? 0 : 1; // Shift-enter inserts above
+ const listValue = getVariableValue(vm, targetId, variableId);
+ const newListValue = listValue.slice(0, previouslyActiveIndex + newValueOffset)
+ .concat([newListItemValue])
+ .concat(listValue.slice(previouslyActiveIndex + newValueOffset));
+ setVariableValue(vm, targetId, variableId, newListValue);
+ const newIndex = (previouslyActiveIndex + newValueOffset) % newListValue.length;
+ this.setState({
+ activeIndex: newIndex,
+ activeValue: newListItemValue
+ });
+ }
+ }
+
+ handleInput (e) {
+ this.setState({activeValue: e.target.value});
+ }
+
+ handleRemove (e) {
+ e.preventDefault(); // Default would blur input, prevent that.
+ e.stopPropagation(); // Bubbling would activate, which will be handled here
+ const {vm, targetId, id: variableId} = this.props;
+ const listValue = getVariableValue(vm, targetId, variableId);
+ const newListValue = listValue.slice(0, this.state.activeIndex)
+ .concat(listValue.slice(this.state.activeIndex + 1));
+ setVariableValue(vm, targetId, variableId, newListValue);
+ this.handleActivate(Math.min(newListValue.length - 1, this.state.activeIndex));
+ }
+
+ handleAdd () {
+ // Add button appends a blank value and switches to it
+ const {vm, targetId, id: variableId} = this.props;
+ const newListValue = getVariableValue(vm, targetId, variableId).concat(['']);
+ setVariableValue(vm, targetId, variableId, newListValue);
+ this.setState({activeIndex: newListValue.length - 1, activeValue: ''});
+ }
+
+ handleResizeMouseDown (e) {
+ this.initialPosition = getEventXY(e);
+ this.initialWidth = this.state.width;
+ this.initialHeight = this.state.height;
+
+ const onMouseMove = ev => {
+ const newPosition = getEventXY(ev);
+ const dx = newPosition.x - this.initialPosition.x;
+ const dy = newPosition.y - this.initialPosition.y;
+ this.setState({
+ width: Math.max(Math.min(this.initialWidth + dx, 480), 100),
+ height: Math.max(Math.min(this.initialHeight + dy, 360), 60)
+ });
+ };
+
+ const onMouseUp = ev => {
+ onMouseMove(ev); // Make sure width/height are up-to-date
+ // TODO send these new sizes to the VM for saving
+ window.removeEventListener('mousemove', onMouseMove);
+ window.removeEventListener('mouseup', onMouseUp);
+ };
+
+ window.addEventListener('mousemove', onMouseMove);
+ window.addEventListener('mouseup', onMouseUp);
+
+ }
+ render () {
+ const {
+ vm, // eslint-disable-line no-unused-vars
+ ...props
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+ListMonitor.propTypes = {
+ height: PropTypes.number,
+ id: PropTypes.string,
+ targetId: PropTypes.string,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+ ]),
+ vm: PropTypes.instanceOf(VM),
+ width: PropTypes.number,
+ x: PropTypes.number,
+ y: PropTypes.number
+};
+
+const mapStateToProps = state => ({vm: state.vm});
+
+export default connect(mapStateToProps)(ListMonitor);
diff --git a/src/containers/monitor-list.jsx b/src/containers/monitor-list.jsx
index 85a3b20e3..b3dcd5b7c 100644
--- a/src/containers/monitor-list.jsx
+++ b/src/containers/monitor-list.jsx
@@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {moveMonitorRect} from '../reducers/monitor-layout';
+import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
+
import MonitorListComponent from '../components/monitor-list/monitor-list.jsx';
class MonitorList extends React.Component {
@@ -37,7 +39,9 @@ const mapDispatchToProps = dispatch => ({
moveMonitorRect: (id, x, y) => dispatch(moveMonitorRect(id, x, y))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(MonitorList);
+export default errorBoundaryHOC('Monitors')(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(MonitorList)
+);
diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx
index cdb87f20e..b2480a412 100644
--- a/src/containers/monitor.jsx
+++ b/src/containers/monitor.jsx
@@ -107,6 +107,7 @@ class Monitor extends React.Component {
max={this.props.max}
min={this.props.min}
mode={this.state.mode}
+ targetId={this.props.targetId}
width={this.props.width}
onDragEnd={this.handleDragEnd}
onNextMode={this.handleNextMode}
@@ -135,6 +136,7 @@ Monitor.propTypes = {
removeMonitorRect: PropTypes.func.isRequired,
resizeMonitorRect: PropTypes.func.isRequired,
spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
+ targetId: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
diff --git a/src/containers/slider-monitor.jsx b/src/containers/slider-monitor.jsx
new file mode 100644
index 000000000..8df6f92a9
--- /dev/null
+++ b/src/containers/slider-monitor.jsx
@@ -0,0 +1,59 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import VM from 'scratch-vm';
+import {setVariableValue} from '../lib/variable-utils';
+import {connect} from 'react-redux';
+
+import SliderMonitorComponent from '../components/monitor/slider-monitor.jsx';
+
+class SliderMonitor extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleSliderUpdate'
+ ]);
+
+ this.state = {
+ value: Number(props.value)
+ };
+ }
+ componentWillReceiveProps (nextProps) {
+ if (this.state.value !== nextProps.value) {
+ this.setState({value: nextProps.value});
+ }
+ }
+ handleSliderUpdate (e) {
+ this.setState({value: Number(e.target.value)});
+ const {vm, targetId, id: variableId} = this.props;
+ setVariableValue(vm, targetId, variableId, Number(e.target.value));
+ }
+ render () {
+ const {
+ vm, // eslint-disable-line no-unused-vars
+ value, // eslint-disable-line no-unused-vars
+ ...props
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+SliderMonitor.propTypes = {
+ id: PropTypes.string,
+ targetId: PropTypes.string,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string
+ ]),
+ vm: PropTypes.instanceOf(VM)
+};
+
+const mapStateToProps = state => ({vm: state.vm});
+
+export default connect(mapStateToProps)(SliderMonitor);
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 57614135f..ee3154a22 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -7,6 +7,7 @@ import {connect} from 'react-redux';
import {getEventXY} from '../lib/touch-utils';
import VideoProvider from '../lib/video/video-provider';
+import {SVGRenderer as V2SVGAdapter} from 'scratch-svg-renderer';
import StageComponent from '../components/stage/stage.jsx';
@@ -57,6 +58,7 @@ class Stage extends React.Component {
this.updateRect();
this.renderer = new Renderer(this.canvas);
this.props.vm.attachRenderer(this.renderer);
+ this.props.vm.attachV2SVGAdapter(new V2SVGAdapter());
this.props.vm.runtime.addListener('QUESTION', this.questionListener);
this.props.vm.setVideoProvider(new VideoProvider());
}
@@ -81,6 +83,7 @@ class Stage extends React.Component {
this.detachMouseEvents(this.canvas);
this.detachRectEvents();
this.stopColorPickingLoop();
+ this.props.vm.runtime.removeListener('QUESTION', this.questionListener);
}
questionListener (question) {
this.setState({question: question});
diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js
index 4f4b3f113..f1cacd365 100644
--- a/src/lib/default-project/index.js
+++ b/src/lib/default-project/index.js
@@ -19,12 +19,12 @@ export default [{
id: '83a9787d4cb6f3b7632b4ddfebf74367',
assetType: 'Sound',
dataFormat: 'WAV',
- data: new Uint16Array(popWav)
+ data: new Uint8Array(popWav)
}, {
id: '83c36d806dc92327b9e7049a565c6bff',
assetType: 'Sound',
dataFormat: 'WAV',
- data: new Uint16Array(meowWav)
+ data: new Uint8Array(meowWav)
}, {
id: '739b5e2a2435f6e1ec2993791b423146',
assetType: 'ImageBitmap',
diff --git a/src/lib/variable-utils.js b/src/lib/variable-utils.js
new file mode 100644
index 000000000..12bcf4704
--- /dev/null
+++ b/src/lib/variable-utils.js
@@ -0,0 +1,24 @@
+// Utility functions for updating variables in the VM
+// TODO (VM#1145) these should be moved to top-level VM API
+const getVariable = (vm, targetId, variableId) => {
+ const target = targetId ?
+ vm.runtime.getTargetById(targetId) :
+ vm.runtime.getTargetForStage();
+ return target.variables[variableId];
+};
+
+const getVariableValue = (vm, targetId, variableId) => {
+ const variable = getVariable(vm, targetId, variableId);
+ // If array, return a new copy for mutating, ensuring that updates stay immutable.
+ if (variable.value instanceof Array) return variable.value.slice();
+ return variable.value;
+};
+
+const setVariableValue = (vm, targetId, variableId, value) => {
+ getVariable(vm, targetId, variableId).value = value;
+};
+
+export {
+ getVariableValue,
+ setVariableValue
+};