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 || 80, 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.min(this.initialWidth + dx, 480), 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 = { value: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), vm: PropTypes.instanceOf(VM), targetId: PropTypes.string, id: PropTypes.string, height: PropTypes.number, width: PropTypes.number, x: PropTypes.number, y: PropTypes.number }; const mapStateToProps = state => ({vm: state.vm}); export default connect(mapStateToProps)(ListMonitor);