diff --git a/.babelrc b/.babelrc index 66742d7038c163433869430388c7ff25601b0aaf..8f910cdf7a6c0ef4465e1f823877bed5a3f58996 100644 --- a/.babelrc +++ b/.babelrc @@ -6,5 +6,5 @@ ["react-intl", { "messagesDir": "./translations/messages/" }]], - "presets": [["env", {"targets": {browsers: ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], "react"] + "presets": [["env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], "react"] } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0b7c6752bbb44a476fc0a4c4468d99761ef67bb5..f502d934ada49b0e0b094b03b7b33c37244cd2d3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,8 @@ _Explain why these changes should be made_ _Please show how you have added tests to cover your changes_ ### Browser Coverage +Check the OS/browser combinations tested (At least 2) + Mac * [ ] Chrome * [ ] Firefox diff --git a/package.json b/package.json index 3d39030d0ef54406dffbf233afbe4b60a3231d1d..2a5e742a78ea9d08a2edee82fc5fef431ae86224 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "react-tabs": "2.2.1", "react-test-renderer": "16.2.0", "react-tooltip": "3.4.0", + "react-virtualized": "9.18.5", "redux": "3.7.2", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", @@ -91,10 +92,10 @@ "scratch-audio": "0.1.0-prerelease.1523977528", "scratch-blocks": "0.1.0-prerelease.1526329848", "scratch-l10n": "2.0.20180108132626", - "scratch-paint": "0.2.0-prerelease.20180504180528", - "scratch-render": "0.1.0-prerelease.20180508205430", + "scratch-paint": "0.2.0-prerelease.20180514184134", + "scratch-render": "0.1.0-prerelease.20180514172756", "scratch-storage": "0.4.1", - "scratch-svg-renderer": "0.1.0-prerelease.20180423193917", + "scratch-svg-renderer": "0.1.0-prerelease.20180514170126", "scratch-vm": "0.1.0-prerelease.1526320682", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 8dab3c69cf9190d315e6c0851ed3760cbf583c8b..14b082cdfe7746734f72a2bb5cc2cefa6a0096d2 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -13,5 +13,10 @@ module.exports = { 'import/no-amd': 'error', 'import/no-nodejs-modules': 'error', 'react/jsx-no-literals': 'error' + }, + settings: { + react: { + version: '16.2' // Prevent 16.3 lifecycle method errors + } } }; diff --git a/src/components/monitor-list/monitor-list.jsx b/src/components/monitor-list/monitor-list.jsx index 50263220dd5bbbc164cee2c95bdfbbdf75a5c935..07b6a817be90020956891f340b73a344f5b21afd 100644 --- a/src/components/monitor-list/monitor-list.jsx +++ b/src/components/monitor-list/monitor-list.jsx @@ -24,6 +24,7 @@ const MonitorList = props => ( opcode={monitorData.opcode} params={monitorData.params} spriteName={monitorData.spriteName} + targetId={monitorData.targetId} value={monitorData.value} width={monitorData.width} x={monitorData.x} diff --git a/src/components/monitor/list-monitor-scroller.jsx b/src/components/monitor/list-monitor-scroller.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fd6da9babbef90a2d491e51dca96968b67461c62 --- /dev/null +++ b/src/components/monitor/list-monitor-scroller.jsx @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import bindAll from 'lodash.bindall'; + +import styles from './monitor.css'; +import {List} from 'react-virtualized'; + +class ListMonitorScroller extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'rowRenderer', + 'noRowsRenderer', + 'handleEventFactory' + ]); + } + handleEventFactory (index) { + return () => this.props.onActivate(index); + } + noRowsRenderer () { + return ( + <div className={styles.listEmpty}> + {'(empty)' /* TODO waiting for design before translation */} + </div> + ); + } + rowRenderer ({index, key, style}) { + return ( + <div + className={styles.listRow} + key={key} + style={style} + > + <div className={styles.listIndex}>{index + 1 /* one indexed */}</div> + <div + className={styles.listValue} + dataIndex={index} + style={{background: this.props.categoryColor}} + onClick={this.handleEventFactory(index)} + > + {this.props.activeIndex === index ? ( + <div className={styles.inputWrapper}> + <input + autoFocus + autoComplete={false} + className={classNames(styles.listInput, 'no-drag')} + spellCheck={false} + type="text" + value={this.props.activeValue} + onBlur={this.props.onDeactivate} + onChange={this.props.onInput} + onFocus={this.props.onFocus} + onKeyDown={this.props.onKeyPress} // key down to get ahead of blur + /> + <div + className={styles.removeButton} + onMouseDown={this.props.onRemove} // mousedown to get ahead of blur + > + {'✖︎'} + </div> + </div> + + ) : ( + <div className={styles.valueInner}>{this.props.values[index]}</div> + )} + </div> + </div> + ); + } + render () { + const {height, values, width, activeIndex, activeValue} = this.props; + // Keep the active index in view if defined, else must be undefined for List component + const scrollToIndex = activeIndex === null ? undefined : activeIndex; /* eslint-disable-line no-undefined */ + return ( + <List + activeIndex={activeIndex} + activeValue={activeValue} + height={(height) - 44 /* Header/footer size, approx */} + noRowsRenderer={this.noRowsRenderer} + rowCount={values.length} + rowHeight={24 /* Row size is same for all rows */} + rowRenderer={this.rowRenderer} + scrollToIndex={scrollToIndex} /* eslint-disable-line no-undefined */ + values={values} + width={width} + /> + ); + } +} + +ListMonitorScroller.propTypes = { + activeIndex: PropTypes.number, + activeValue: PropTypes.string, + categoryColor: PropTypes.string, + height: PropTypes.number, + onActivate: PropTypes.func, + onDeactivate: PropTypes.func, + onFocus: PropTypes.func, + onInput: PropTypes.func, + onKeyPress: PropTypes.func, + onRemove: PropTypes.func, + values: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ])), + width: PropTypes.number +}; +export default ListMonitorScroller; diff --git a/src/components/monitor/list-monitor.jsx b/src/components/monitor/list-monitor.jsx index c50cc5703cf806a294eb7aea39fc57c6f9a3fef7..a29afa283a395da93256321e30df72cd5e379dd9 100644 --- a/src/components/monitor/list-monitor.jsx +++ b/src/components/monitor/list-monitor.jsx @@ -1,67 +1,69 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import styles from './monitor.css'; +import ListMonitorScroller from './list-monitor-scroller.jsx'; -const ListMonitor = ({categoryColor, label, width, height, value}) => ( +const ListMonitor = ({label, width, height, value, onResizeMouseDown, onAdd, ...rowProps}) => ( <div className={styles.listMonitor} style={{ - width: `${width || 80}px`, - height: `${height || 200}px` + width: `${width}px`, + height: `${height}px` }} > <div className={styles.listHeader}> {label} </div> <div className={styles.listBody}> - {!value || value.length === 0 ? ( - <div className={styles.listEmpty}> - {'(empty)' /* @todo not translating, awaiting design */} - </div> - ) : value.map((v, i) => ( - <div - className={styles.listRow} - key={`label-${i}`} - > - <div className={styles.listIndex}>{i + 1 /* one indexed */}</div> - <div - className={styles.listValue} - style={{background: categoryColor}} - > - <div className={styles.valueInner}>{v}</div> - </div> - </div> - ))} + <ListMonitorScroller + height={height} + values={value} + width={width} + {...rowProps} + /> </div> <div className={styles.listFooter}> - <div className={styles.footerButton}> - {/* @todo add button here */} + <div + className={styles.addButton} + onClick={onAdd} + > + {'+' /* TODO waiting on asset */} </div> <div className={styles.footerLength}> - <span className={styles.lengthNumber}> - {value.length} - </span> + {`length ${value.length}`} </div> - <div className={styles.resizeHandle}> - {/* @todo resize handle */} + <div + className={classNames(styles.resizeHandle, 'no-drag')} + onMouseDown={onResizeMouseDown} + > + {'=' /* TODO waiting on asset */} </div> </div> </div> ); ListMonitor.propTypes = { + activeIndex: PropTypes.number, categoryColor: PropTypes.string.isRequired, height: PropTypes.number, label: PropTypes.string.isRequired, - value: PropTypes.arrayOf(PropTypes.oneOfType([ + onActivate: PropTypes.func, + onAdd: PropTypes.func, + onResizeMouseDown: PropTypes.func, + value: PropTypes.oneOfType([ PropTypes.string, - PropTypes.number - ])), + PropTypes.number, + PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ])) + ]), width: PropTypes.number }; ListMonitor.defaultProps = { - width: 80, + width: 110, height: 200 }; diff --git a/src/components/monitor/monitor.css b/src/components/monitor/monitor.css index 783cec7068671c87e8011fb21140929b64afacd1..348f11e0cdee8f91d279438f12c6a2a3548573bc 100644 --- a/src/components/monitor/monitor.css +++ b/src/components/monitor/monitor.css @@ -61,7 +61,6 @@ } .list-monitor { - min-width: 80px; display: flex; flex-direction: column; } @@ -83,6 +82,7 @@ display: flex; flex-direction: column; overflow-y: scroll; + overflow-x: hidden; height: calc(100% - 44px); } @@ -109,6 +109,7 @@ border-radius: calc($space / 2); border: 1px solid $ui-black-transparent; flex-grow: 1; + height: 22px; } .list-footer { @@ -137,3 +138,35 @@ padding: 3px 5px; min-height: 22px; } + +.list-input { + padding: 3px 5px; + border: 0; + background: rgba(0, 0, 0, 0.1); + color: $ui-white; + outline: none; + font-size: 0.75rem; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +.remove-button { + position: absolute; + top: 4px; + right: 3px; + cursor: pointer; + color: $ui-white; +} + +.add-button { + cursor: pointer; + margin-right: 3px; +} + +.resize-handle { + cursor: nwse-resize; + margin-left: 3px; +} + +.footer-length { + text-align: center; +} diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index 95387a1ec78370be823f1bd05ca0d2b670147c39..a45fe52142a26090245023d4e558b720fd3d95cb 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -7,8 +7,8 @@ import {ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; import Box from '../box/box.jsx'; import DefaultMonitor from './default-monitor.jsx'; import LargeMonitor from './large-monitor.jsx'; -import SliderMonitor from './slider-monitor.jsx'; -import ListMonitor from './list-monitor.jsx'; +import SliderMonitor from '../../containers/slider-monitor.jsx'; +import ListMonitor from '../../containers/list-monitor.jsx'; import styles from './monitor.css'; @@ -29,7 +29,10 @@ const modes = { }; const MonitorComponent = props => ( - <ContextMenuTrigger id={`monitor-${props.label}`}> + <ContextMenuTrigger + holdToDisplay={props.mode === 'slider' ? -1 : 1000} + id={`monitor-${props.label}`} + > <Draggable bounds=".monitor-overlay" // Class for monitor container cancel=".no-drag" // Class used for slider input to prevent drag @@ -41,14 +44,9 @@ const MonitorComponent = props => ( componentRef={props.componentRef} onDoubleClick={props.mode === 'list' ? null : props.onNextMode} > - {(modes[props.mode] || modes.default)({ // Use default until other modes arrive + {React.createElement(modes[props.mode], { categoryColor: categories[props.category], - label: props.label, - value: props.value, - width: props.width, - height: props.height, - min: props.min, - max: props.max + ...props })} </Box> </Draggable> @@ -90,25 +88,13 @@ const monitorModes = Object.keys(modes); MonitorComponent.propTypes = { category: PropTypes.oneOf(Object.keys(categories)), componentRef: PropTypes.func.isRequired, - height: PropTypes.number, label: PropTypes.string.isRequired, - max: PropTypes.number, - min: PropTypes.number, mode: PropTypes.oneOf(monitorModes), onDragEnd: PropTypes.func.isRequired, onNextMode: PropTypes.func.isRequired, onSetModeToDefault: PropTypes.func.isRequired, onSetModeToLarge: PropTypes.func.isRequired, - onSetModeToSlider: PropTypes.func, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.arrayOf(PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ])) - ]), - width: PropTypes.number + onSetModeToSlider: PropTypes.func }; MonitorComponent.defaultProps = { diff --git a/src/components/monitor/slider-monitor.jsx b/src/components/monitor/slider-monitor.jsx index a6c410898edc6325df5d3cfafd060a9935eee34c..f8187cfee6a9be6fe0811ddfb8751c00bf0eaa64 100644 --- a/src/components/monitor/slider-monitor.jsx +++ b/src/components/monitor/slider-monitor.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './monitor.css'; -const SliderMonitor = ({categoryColor, label, min, max, value}) => ( +const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate}) => ( <div className={styles.defaultMonitor}> <div className={styles.row}> <div className={styles.label}> @@ -22,6 +22,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value}) => ( min={min} type="range" value={value} + onChange={onSliderUpdate} // @todo onChange callback /> </div> @@ -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 0000000000000000000000000000000000000000..4b7cc80dbc299c0830b135a11e4318aad645688f --- /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 ( + <ListMonitorComponent + {...props} + activeIndex={this.state.activeIndex} + activeValue={this.state.activeValue} + height={this.state.height} + width={this.state.width} + onActivate={this.handleActivate} + onAdd={this.handleAdd} + onDeactivate={this.handleDeactivate} + onFocus={this.handleFocus} + onInput={this.handleInput} + onKeyPress={this.handleKeyPress} + onRemove={this.handleRemove} + onResizeMouseDown={this.handleResizeMouseDown} + /> + ); + } +} + +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 85a3b20e3ec840808e27b99f7c81b6a338022cf4..b3dcd5b7c7cfb2943f27caab43a0ff80191037ce 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 cdb87f20eacea93fcafa446ee379ea4ba61f1ca8..b2480a41299ed592a11a0fa115eea292ba81c6fb 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 0000000000000000000000000000000000000000..8df6f92a9879c7aaa9ee364424ac0dd955b3e688 --- /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 ( + <SliderMonitorComponent + {...props} + value={this.state.value} + onSliderUpdate={this.handleSliderUpdate} + /> + ); + } +} + +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 57614135f9b15eb2c799193f6c515623892bb34a..ee3154a2207aa8769815a78950044a6ec2cbf768 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 4f4b3f113cd36ccd78d7375bd99e6711f2ea2777..f1cacd3659abfc82fd8a10ca9bbc73fbcd69db59 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 0000000000000000000000000000000000000000..12bcf4704fb343b0fdc826f5cd2dd0058c00cdbd --- /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 +};