Spaces:
Runtime error
Runtime error
| import bindAll from 'lodash.bindall'; | |
| import React from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import {injectIntl, intlShape, defineMessages} from 'react-intl'; | |
| import monitorAdapter from '../lib/monitor-adapter.js'; | |
| import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx'; | |
| import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout'; | |
| import {getVariable, setVariableValue} from '../lib/variable-utils'; | |
| import importCSV from '../lib/import-csv'; | |
| import downloadBlob from '../lib/download-blob'; | |
| import SliderPrompt from './slider-prompt.jsx'; | |
| import {connect} from 'react-redux'; | |
| import {Map} from 'immutable'; | |
| import VM from 'scratch-vm'; | |
| const availableModes = opcode => ( | |
| monitorModes.filter(mode => { | |
| if (opcode === 'data_variable') { | |
| return mode !== 'list'; | |
| } else if (opcode === 'data_listcontents') { | |
| return mode === 'list'; | |
| } else if (opcode === 'canvas_canvasGetter') { | |
| return mode === 'image'; | |
| } | |
| return mode !== 'slider' && mode !== 'list'; | |
| }) | |
| ); | |
| const messages = defineMessages({ | |
| columnPrompt: { | |
| defaultMessage: 'Which column should be used (1-{numberOfColumns})?', | |
| description: 'Prompt for which column should be used', | |
| id: 'gui.monitors.importListColumnPrompt' | |
| } | |
| }); | |
| class Monitor extends React.Component { | |
| constructor (props) { | |
| super(props); | |
| bindAll(this, [ | |
| 'handleDragEnd', | |
| 'handleHide', | |
| 'handleNextMode', | |
| 'handleSetModeToDefault', | |
| 'handleSetModeToLarge', | |
| 'handleSetModeToSlider', | |
| 'handleSliderPromptClose', | |
| 'handleSliderPromptOk', | |
| 'handleSliderPromptOpen', | |
| 'handleImport', | |
| 'handleExport', | |
| 'setElement' | |
| ]); | |
| this.state = { | |
| sliderPrompt: false | |
| }; | |
| } | |
| componentDidMount () { | |
| let rect; | |
| const isNum = num => typeof num === 'number' && !isNaN(num); | |
| // Load the VM provided position if not loaded already | |
| // If a monitor has numbers for the x and y positions, load the saved position. | |
| // Otherwise, auto-position the monitor. | |
| if (isNum(this.props.x) && isNum(this.props.y) && | |
| !this.props.monitorLayout.savedMonitorPositions[this.props.id]) { | |
| rect = { | |
| upperStart: {x: this.props.x, y: this.props.y}, | |
| lowerEnd: {x: this.props.x + this.element.offsetWidth, y: this.props.y + this.element.offsetHeight} | |
| }; | |
| this.props.addMonitorRect(this.props.id, rect, true /* savePosition */); | |
| } else { // Newly created user monitor | |
| rect = getInitialPosition( | |
| this.props.monitorLayout, this.props.id, this.element.offsetWidth, this.element.offsetHeight); | |
| this.props.addMonitorRect(this.props.id, rect); | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| x: rect.upperStart.x, | |
| y: rect.upperStart.y | |
| })); | |
| } | |
| this.element.style.top = `${rect.upperStart.y}px`; | |
| this.element.style.left = `${rect.upperStart.x}px`; | |
| } | |
| shouldComponentUpdate (nextProps, nextState) { | |
| if (nextState !== this.state) return true; | |
| if (nextProps !== this.props) return true; | |
| if (nextProps.monitorLayout !== this.props.monitorLayout) return true; | |
| if (nextProps.value !== this.props.value) return true; | |
| if (!nextProps.value?._monitorUpToDate) return true; | |
| return false; | |
| } | |
| componentDidUpdate () { | |
| // tw: if monitor is not draggable (ie. not in editor), do not calculate size of monitor for performance | |
| if (!this.props.draggable) { | |
| return; | |
| } | |
| this.props.resizeMonitorRect(this.props.id, this.element.offsetWidth, this.element.offsetHeight); | |
| } | |
| componentWillUnmount () { | |
| this.props.removeMonitorRect(this.props.id); | |
| } | |
| handleDragEnd (e, {x, y}) { | |
| const newX = parseInt(this.element.style.left, 10) + x; | |
| const newY = parseInt(this.element.style.top, 10) + y; | |
| this.props.onDragEnd( | |
| this.props.id, | |
| newX, | |
| newY | |
| ); | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| x: newX, | |
| y: newY | |
| })); | |
| } | |
| handleHide () { | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| visible: false | |
| })); | |
| } | |
| handleNextMode () { | |
| const modes = availableModes(this.props.opcode); | |
| const modeIndex = modes.indexOf(this.props.mode); | |
| const newMode = modes[(modeIndex + 1) % modes.length]; | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| mode: newMode | |
| })); | |
| } | |
| handleSetModeToDefault () { | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| mode: 'default' | |
| })); | |
| } | |
| handleSetModeToLarge () { | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| mode: 'large' | |
| })); | |
| } | |
| handleSetModeToSlider () { | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| mode: 'slider' | |
| })); | |
| } | |
| handleSliderPromptClose () { | |
| this.setState({sliderPrompt: false}); | |
| } | |
| handleSliderPromptOpen () { | |
| this.setState({sliderPrompt: true}); | |
| } | |
| handleSliderPromptOk (min, max, isDiscrete) { | |
| const realMin = Math.min(min, max); | |
| const realMax = Math.max(min, max); | |
| this.props.vm.runtime.requestUpdateMonitor(Map({ | |
| id: this.props.id, | |
| sliderMin: realMin, | |
| sliderMax: realMax, | |
| isDiscrete: isDiscrete | |
| })); | |
| this.handleSliderPromptClose(); | |
| } | |
| setElement (monitorElt) { | |
| this.element = monitorElt; | |
| } | |
| handleImport () { | |
| importCSV().then(async ({rows, text}) => { | |
| const numberOfColumns = rows[0].length; | |
| let columnNumber = 1; | |
| if (numberOfColumns > 1) { | |
| const msg = this.props.intl.formatMessage(messages.columnPrompt, {numberOfColumns}); | |
| // prompt() returns Promise in desktop app | |
| columnNumber = parseInt(await prompt(msg), 10); // eslint-disable-line no-alert | |
| } | |
| let newListValue; | |
| if (isNaN(columnNumber) || numberOfColumns === 1) { | |
| newListValue = text.replace(/\r/g, '').split('\n'); | |
| } else { | |
| newListValue = rows.map(row => row[columnNumber - 1]) | |
| .filter(item => typeof item === 'string'); // CSV importer can leave undefineds | |
| } | |
| const {vm, targetId, id: variableId} = this.props; | |
| setVariableValue(vm, targetId, variableId, newListValue); | |
| }); | |
| } | |
| handleExport () { | |
| const {vm, targetId, id: variableId} = this.props; | |
| const variable = getVariable(vm, targetId, variableId); | |
| const text = variable.value.join('\r\n'); | |
| const blob = new Blob([text], {type: 'text/plain;charset=utf-8'}); | |
| downloadBlob(`${variable.name}.txt`, blob); | |
| } | |
| render () { | |
| const monitorProps = monitorAdapter(this.props); | |
| const showSliderOption = availableModes(this.props.opcode).indexOf('slider') !== -1; | |
| const isList = this.props.mode === 'list'; | |
| const isImage = this.props.mode === 'image'; | |
| return ( | |
| <React.Fragment> | |
| {this.state.sliderPrompt && <SliderPrompt | |
| isDiscrete={this.props.isDiscrete} | |
| maxValue={parseFloat(this.props.max)} | |
| minValue={parseFloat(this.props.min)} | |
| onCancel={this.handleSliderPromptClose} | |
| onOk={this.handleSliderPromptOk} | |
| />} | |
| <MonitorComponent | |
| componentRef={this.setElement} | |
| {...monitorProps} | |
| opcode={this.props.opcode} | |
| draggable={this.props.draggable} | |
| height={this.props.height} | |
| isDiscrete={this.props.isDiscrete} | |
| max={this.props.max} | |
| min={this.props.min} | |
| mode={this.props.mode} | |
| targetId={this.props.targetId} | |
| width={this.props.width} | |
| onDragEnd={this.handleDragEnd} | |
| onExport={isList || isImage ? this.handleExport : null} | |
| onImport={isList || isImage ? this.handleImport : null} | |
| onHide={this.handleHide} | |
| onNextMode={this.handleNextMode} | |
| onSetModeToDefault={isList || isImage ? null : this.handleSetModeToDefault} | |
| onSetModeToLarge={isList || isImage ? null : this.handleSetModeToLarge} | |
| onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null} | |
| onSliderPromptOpen={this.handleSliderPromptOpen} | |
| /> | |
| </React.Fragment> | |
| ); | |
| } | |
| } | |
| Monitor.propTypes = { | |
| addMonitorRect: PropTypes.func.isRequired, | |
| draggable: PropTypes.bool, | |
| height: PropTypes.number, | |
| id: PropTypes.string.isRequired, | |
| intl: intlShape, | |
| isDiscrete: PropTypes.bool, | |
| max: PropTypes.number, | |
| min: PropTypes.number, | |
| mode: PropTypes.oneOf(['default', 'slider', 'large', 'list']), | |
| monitorLayout: PropTypes.shape({ | |
| monitors: PropTypes.object, // eslint-disable-line react/forbid-prop-types | |
| savedMonitorPositions: PropTypes.object // eslint-disable-line react/forbid-prop-types | |
| }).isRequired, | |
| onDragEnd: PropTypes.func.isRequired, | |
| opcode: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types | |
| params: PropTypes.object, // eslint-disable-line react/no-unused-prop-types, react/forbid-prop-types | |
| 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, | |
| PropTypes.arrayOf(PropTypes.oneOfType([ | |
| PropTypes.string, | |
| PropTypes.number | |
| ])) | |
| ]), // eslint-disable-line react/no-unused-prop-types | |
| vm: PropTypes.instanceOf(VM), | |
| width: PropTypes.number, | |
| x: PropTypes.number, | |
| y: PropTypes.number | |
| }; | |
| const mapStateToProps = state => ({ | |
| monitorLayout: state.scratchGui.monitorLayout, | |
| vm: state.scratchGui.vm | |
| }); | |
| const mapDispatchToProps = dispatch => ({ | |
| addMonitorRect: (id, rect, savePosition) => | |
| dispatch(addMonitorRect(id, rect.upperStart, rect.lowerEnd, savePosition)), | |
| resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)), | |
| removeMonitorRect: id => dispatch(removeMonitorRect(id)) | |
| }); | |
| export default injectIntl(connect( | |
| mapStateToProps, | |
| mapDispatchToProps | |
| )(Monitor)); | |