Merge branch 'develop' into feature/scroll-by-id

This commit is contained in:
Eric Rosenbaum authored and GitHub committed 2018-05-14 16:51:47 -04:00
commit bdfd35e89d
17 files changed
+483 -68

No files matched your search

+1 -1
View File
@@ -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"]
}
+2
View File
@@ -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
+4 -3
View File
@@ -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",
+5
View File
@@ -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
}
}
};
@@ -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}
@@ -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;
+34 -32
View File
@@ -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
};
+34 -1
View File
@@ -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;
}
+9 -23
View File
@@ -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 = {
+3 -2
View File
@@ -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
+183
View File
@@ -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);
+8 -4
View File
@@ -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)
);
+2
View File
@@ -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,
+59
View File
@@ -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);
+3
View File
@@ -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});
+2 -2
View File
@@ -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',
+24
View File
@@ -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
};