+
{this.props.data.map((dataItem, itemId) => {
const scratchURL = dataItem.md5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
diff --git a/src/components/modal.jsx b/src/components/modal.jsx
deleted file mode 100644
index 65a7275e6d2..00000000000
--- a/src/components/modal.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-const React = require('react');
-const ReactModal = require('react-modal');
-const stylePropType = require('react-style-proptype');
-
-class ModalComponent extends React.Component {
- render () {
- return (
-
(this.modal = m)}
- style={this.props.modalStyle}
- onRequestClose={this.props.onRequestClose}
- >
-
- {'x'}
-
- {this.props.children}
-
- );
- }
-}
-
-const modalStyle = {
- overlay: {
- zIndex: 1000,
- backgroundColor: 'rgba(0, 0, 0, .75)'
- },
- content: {
- position: 'absolute',
- overflow: 'visible',
- borderRadius: '6px',
- padding: 0,
- top: '5%',
- bottom: '5%',
- left: '5%',
- right: '5%',
- background: '#fcfcfc'
- }
-};
-
-const closeButtonStyle = {
- color: 'rgb(255, 255, 255)',
- background: 'rgb(50, 50, 50)',
- borderRadius: '15px',
- width: '30px',
- height: '25px',
- textAlign: 'center',
- paddingTop: '5px',
- position: 'absolute',
- right: '3px',
- top: '3px',
- cursor: 'pointer'
-};
-
-ModalComponent.defaultProps = {
- modalStyle: modalStyle,
- closeButtonStyle: closeButtonStyle
-};
-
-ModalComponent.propTypes = {
- children: React.PropTypes.node,
- closeButtonStyle: stylePropType,
- modalStyle: React.PropTypes.shape({
- overlay: stylePropType, // eslint-disable-line react/no-unused-prop-types
- content: stylePropType // eslint-disable-line react/no-unused-prop-types
- }),
- onRequestClose: React.PropTypes.func,
- visible: React.PropTypes.bool
-};
-
-module.exports = ModalComponent;
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
new file mode 100644
index 00000000000..b9cb8527b1d
--- /dev/null
+++ b/src/components/modal/modal.css
@@ -0,0 +1,36 @@
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ background-color: rgba(0, 0, 0, .75);
+}
+.modal-content {
+ outline: none;
+ position: absolute;
+ overflow: visible;
+ -webkit-overflow-scrolling: 'touch';
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ padding: 0;
+ top: 5%;
+ right: 5%;
+ bottom: 5%;
+ left: 5%;
+ background: #fcfcfc;
+}
+.modal-close-button {
+ color: rgb(255, 255, 255);
+ background: rgb(50, 50, 50);
+ border-radius: 15px;
+ width: 30px;
+ height: 25px;
+ text-align: center;
+ padding-top: 5px;
+ position: absolute;
+ right: 3px;
+ top: 3px;
+ cursor: pointer
+}
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
new file mode 100644
index 00000000000..3c36ba50921
--- /dev/null
+++ b/src/components/modal/modal.jsx
@@ -0,0 +1,34 @@
+const React = require('react');
+const ReactModal = require('react-modal');
+
+const styles = require('./modal.css');
+
+class ModalComponent extends React.Component {
+ render () {
+ return (
+
(this.modal = m)}
+ onRequestClose={this.props.onRequestClose}
+ >
+
+ {'x'}
+
+ {this.props.children}
+
+ );
+ }
+}
+
+ModalComponent.propTypes = {
+ children: React.PropTypes.node,
+ onRequestClose: React.PropTypes.func,
+ visible: React.PropTypes.bool
+};
+
+module.exports = ModalComponent;
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
new file mode 100644
index 00000000000..29941a9f9c9
--- /dev/null
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -0,0 +1,10 @@
+.sprite-selector-item {
+ border: 1px solid;
+ border-color: transparent;
+ display: inline-block;
+ height: 72px;
+ width: 72px;
+}
+.sprite-selector-item.is-selected {
+ border-color: black;
+}
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
new file mode 100644
index 00000000000..a7de0d93c86
--- /dev/null
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -0,0 +1,33 @@
+const classNames = require('classnames');
+const React = require('react');
+
+const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx');
+const styles = require('./sprite-selector-item.css');
+
+const SpriteSelectorItem = props => (
+
+ {props.costumeURL ? (
+
+ ) : null}
+
{props.name}
+
+);
+
+SpriteSelectorItem.propTypes = {
+ costumeURL: React.PropTypes.string,
+ name: React.PropTypes.string,
+ onClick: React.PropTypes.func,
+ selected: React.PropTypes.bool
+};
+
+module.exports = SpriteSelectorItem;
diff --git a/src/components/sprite-selector.jsx b/src/components/sprite-selector.jsx
deleted file mode 100644
index 069829b7347..00000000000
--- a/src/components/sprite-selector.jsx
+++ /dev/null
@@ -1,59 +0,0 @@
-const React = require('react');
-
-const SpriteSelectorComponent = function (props) {
- const {
- onChange,
- sprites,
- value,
- openNewSprite,
- openNewCostume,
- openNewBackdrop,
- ...componentProps
- } = props;
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-SpriteSelectorComponent.propTypes = {
- onChange: React.PropTypes.func,
- openNewBackdrop: React.PropTypes.func,
- openNewCostume: React.PropTypes.func,
- openNewSprite: React.PropTypes.func,
- sprites: React.PropTypes.arrayOf(
- React.PropTypes.shape({
- id: React.PropTypes.string,
- name: React.PropTypes.string
- })
- ),
- value: React.PropTypes.arrayOf(React.PropTypes.string)
-};
-
-module.exports = SpriteSelectorComponent;
diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css
new file mode 100644
index 00000000000..1eadc56d506
--- /dev/null
+++ b/src/components/sprite-selector/sprite-selector.css
@@ -0,0 +1,3 @@
+.sprite-selector {
+ width: 400px;
+}
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
new file mode 100644
index 00000000000..770e7ca6ffd
--- /dev/null
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -0,0 +1,55 @@
+const React = require('react');
+
+const SpriteSelectorItem = require('../../containers/sprite-selector-item.jsx');
+
+const styles = require('./sprite-selector.css');
+
+const SpriteSelectorComponent = function (props) {
+ const {
+ onSelectSprite,
+ selectedId,
+ sprites,
+ ...componentProps
+ } = props;
+ return (
+
+ {Object.keys(sprites)
+ // Re-order by list order
+ .sort((id1, id2) => sprites[id1].order - sprites[id2].order)
+ .map(id => (
+
+ ))
+ }
+
+ );
+};
+
+SpriteSelectorComponent.propTypes = {
+ onSelectSprite: React.PropTypes.func,
+ selectedId: React.PropTypes.string,
+ sprites: React.PropTypes.shape({
+ id: React.PropTypes.shape({
+ costume: React.PropTypes.shape({
+ skin: React.PropTypes.string,
+ name: React.PropTypes.string,
+ bitmapResolution: React.PropTypes.number,
+ rotationCenterX: React.PropTypes.number,
+ rotationCenterY: React.PropTypes.number
+ }),
+ name: React.PropTypes.string,
+ order: React.PropTypes.number
+ })
+ })
+};
+
+module.exports = SpriteSelectorComponent;
diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css
new file mode 100644
index 00000000000..0981e0ebeea
--- /dev/null
+++ b/src/components/stage-selector/stage-selector.css
@@ -0,0 +1,11 @@
+.stage-selector {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 72px;
+ border: 1px solid;
+ border-color: transparent;
+}
+.stage-selector.is-selected {
+ border-color: black;
+}
diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx
new file mode 100644
index 00000000000..db6b93e97c1
--- /dev/null
+++ b/src/components/stage-selector/stage-selector.jsx
@@ -0,0 +1,36 @@
+const classNames = require('classnames');
+const React = require('react');
+
+const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx');
+const styles = require('./stage-selector.css');
+
+const StageSelector = props => (
+
+
Stage
+
Backgrounds
+
{props.backdropCount}
+
+ {props.url ? (
+
+ ) : null}
+
+);
+
+StageSelector.propTypes = {
+ backdropCount: React.PropTypes.number,
+ onClick: React.PropTypes.func,
+ selected: React.PropTypes.bool,
+ url: React.PropTypes.string
+};
+
+module.exports = StageSelector;
diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css
new file mode 100644
index 00000000000..60d9f9a1023
--- /dev/null
+++ b/src/components/stage/stage.css
@@ -0,0 +1,5 @@
+.stage {
+ position: absolute;
+ top: 40px;
+ right: 0;
+}
diff --git a/src/components/stage.jsx b/src/components/stage/stage.jsx
similarity index 83%
rename from src/components/stage.jsx
rename to src/components/stage/stage.jsx
index 10e31209f73..daf447999b6 100644
--- a/src/components/stage.jsx
+++ b/src/components/stage/stage.jsx
@@ -1,5 +1,7 @@
const React = require('react');
+const styles = require('./stage.css');
+
class StageComponent extends React.Component {
render () {
const {
@@ -10,12 +12,9 @@ class StageComponent extends React.Component {
} = this.props;
return (
+ );
+};
+const spriteShape = React.PropTypes.shape({
+ costume: React.PropTypes.shape({
+ skin: React.PropTypes.string,
+ name: React.PropTypes.string,
+ bitmapResolution: React.PropTypes.number,
+ rotationCenterX: React.PropTypes.number,
+ rotationCenterY: React.PropTypes.number
+ }),
+ id: React.PropTypes.string,
+ name: React.PropTypes.string,
+ order: React.PropTypes.number
+});
+
+TargetPane.propTypes = {
+ backdropLibraryVisible: React.PropTypes.bool,
+ costumeLibraryVisible: React.PropTypes.bool,
+ editingTarget: React.PropTypes.string,
+ mediaLibrary: React.PropTypes.instanceOf(MediaLibrary),
+ onNewBackdropClick: React.PropTypes.func,
+ onNewCostumeClick: React.PropTypes.func,
+ onNewSpriteClick: React.PropTypes.func,
+ onRequestCloseBackdropLibrary: React.PropTypes.func,
+ onRequestCloseCostumeLibrary: React.PropTypes.func,
+ onRequestCloseSpriteLibrary: React.PropTypes.func,
+ onSelectSprite: React.PropTypes.func,
+ spriteLibraryVisible: React.PropTypes.bool,
+ sprites: React.PropTypes.objectOf(spriteShape),
+ stage: spriteShape,
+ vm: React.PropTypes.instanceOf(VM)
+};
+
+module.exports = TargetPane;
diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx
index ace824bead4..dbd06b5acef 100644
--- a/src/containers/backdrop-library.jsx
+++ b/src/containers/backdrop-library.jsx
@@ -3,13 +3,16 @@ const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
-const LibaryComponent = require('../components/library.jsx');
+const LibaryComponent = require('../components/library/library.jsx');
class BackdropLibrary extends React.Component {
constructor (props) {
super(props);
- bindAll(this, ['setData', 'handleItemSelect']);
+ bindAll(this, [
+ 'setData',
+ 'handleItemSelect'
+ ]);
this.state = {backdropData: []};
}
componentWillReceiveProps (nextProps) {
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index f0ac707e9b2..5a579f6e459 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -4,7 +4,7 @@ const React = require('react');
const ScratchBlocks = require('scratch-blocks');
const VM = require('scratch-vm');
-const BlocksComponent = require('../components/blocks.jsx');
+const BlocksComponent = require('../components/blocks/blocks.jsx');
class Blocks extends React.Component {
constructor (props) {
diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx
index 0dd08eabe6c..edb1e8c3f15 100644
--- a/src/containers/costume-library.jsx
+++ b/src/containers/costume-library.jsx
@@ -3,13 +3,16 @@ const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
-const LibaryComponent = require('../components/library.jsx');
+const LibaryComponent = require('../components/library/library.jsx');
class CostumeLibrary extends React.Component {
constructor (props) {
super(props);
- bindAll(this, ['setData', 'handleItemSelected']);
+ bindAll(this, [
+ 'handleItemSelected',
+ 'setData'
+ ]);
this.state = {costumeData: []};
}
componentWillReceiveProps (nextProps) {
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index bb44a60ca84..c254817b4bd 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -1,33 +1,12 @@
-const bindAll = require('lodash.bindall');
-const defaultsDeep = require('lodash.defaultsdeep');
const React = require('react');
const VM = require('scratch-vm');
-const VMManager = require('../lib/vm-manager');
-const MediaLibrary = require('../lib/media-library');
-const shapeFromPropTypes = require('../lib/shape-from-prop-types');
+const vmListenerHOC = require('../lib/vm-listener-hoc.jsx');
-const Blocks = require('./blocks.jsx');
-const GUIComponent = require('../components/gui.jsx');
-const GreenFlag = require('./green-flag.jsx');
-const SpriteSelector = require('./sprite-selector.jsx');
-const Stage = require('./stage.jsx');
-const StopAll = require('./stop-all.jsx');
-
-const SpriteLibrary = require('./sprite-library.jsx');
-const CostumeLibrary = require('./costume-library.jsx');
-const BackdropLibrary = require('./backdrop-library.jsx');
+const GUIComponent = require('../components/gui/gui.jsx');
class GUI extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, ['closeModal']);
- this.vmManager = new VMManager(this.props.vm);
- this.mediaLibrary = new MediaLibrary();
- this.state = {currentModal: null};
- }
componentDidMount () {
- this.vmManager.attachKeyboardEvents();
this.props.vm.loadProject(this.props.projectData);
this.props.vm.start();
}
@@ -37,105 +16,29 @@ class GUI extends React.Component {
}
}
componentWillUnmount () {
- this.vmManager.detachKeyboardEvents();
this.props.vm.stopAll();
}
- openModal (modalName) {
- this.setState({currentModal: modalName});
- }
- closeModal () {
- this.setState({currentModal: null});
- }
render () {
- let {
- backdropLibraryProps,
- basePath,
- blocksProps,
- costumeLibraryProps,
- greenFlagProps,
+ const {
projectData, // eslint-disable-line no-unused-vars
- spriteLibraryProps,
- spriteSelectorProps,
- stageProps,
- stopAllProps,
vm,
- ...guiProps
+ ...componentProps
} = this.props;
- backdropLibraryProps = defaultsDeep({}, backdropLibraryProps, {
- mediaLibrary: this.mediaLibrary,
- onRequestClose: this.closeModal,
- visible: this.state.currentModal === 'backdrop-library'
- });
- blocksProps = defaultsDeep({}, blocksProps, {
- options: {
- media: `${basePath}static/blocks-media/`
- }
- });
- costumeLibraryProps = defaultsDeep({}, costumeLibraryProps, {
- mediaLibrary: this.mediaLibrary,
- onRequestClose: this.closeModal,
- visible: this.state.currentModal === 'costume-library'
- });
- spriteLibraryProps = defaultsDeep({}, spriteLibraryProps, {
- mediaLibrary: this.mediaLibrary,
- onRequestClose: this.closeModal,
- visible: this.state.currentModal === 'sprite-library'
- });
- spriteSelectorProps = defaultsDeep({}, spriteSelectorProps, {
- openNewBackdrop: () => this.openModal('backdrop-library'),
- openNewCostume: () => this.openModal('costume-library'),
- openNewSprite: () => this.openModal('sprite-library')
- });
- if (this.props.children) {
- return (
-
- {this.props.children}
-
- );
- }
- /* eslint-disable react/jsx-max-props-per-line, lines-around-comment */
return (
-
-
-
-
-
-
-
-
-
-
+
);
- /* eslint-enable react/jsx-max-props-per-line, lines-around-comment */
}
}
GUI.propTypes = {
- backdropLibraryProps: shapeFromPropTypes(BackdropLibrary.propTypes, {omit: ['vm']}),
- basePath: React.PropTypes.string,
- blocksProps: shapeFromPropTypes(Blocks.propTypes, {omit: ['vm']}),
- children: React.PropTypes.node,
- costumeLibraryProps: shapeFromPropTypes(CostumeLibrary.propTypes, {omit: ['vm']}),
- greenFlagProps: shapeFromPropTypes(GreenFlag.propTypes, {omit: ['vm']}),
+ ...GUIComponent.propTypes,
projectData: React.PropTypes.string,
- spriteLibraryProps: shapeFromPropTypes(SpriteLibrary.propTypes, {omit: ['vm']}),
- spriteSelectorProps: shapeFromPropTypes(SpriteSelector.propTypes, {omit: ['vm']}),
- stageProps: shapeFromPropTypes(Stage.propTypes, {omit: ['vm']}),
- stopAllProps: shapeFromPropTypes(StopAll.propTypes, {omit: ['vm']}),
vm: React.PropTypes.instanceOf(VM)
};
-GUI.defaultProps = {
- backdropLibraryProps: {},
- basePath: '/',
- blocksProps: {},
- costumeLibraryProps: {},
- greenFlagProps: {},
- spriteSelectorProps: {},
- spriteLibraryProps: {},
- stageProps: {},
- stopAllProps: {},
- vm: new VM()
-};
+GUI.defaultProps = GUIComponent.defaultProps;
-module.exports = GUI;
+module.exports = vmListenerHOC(GUI);
diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx
index 9f2cb0a67cd..462240088e5 100644
--- a/src/containers/sprite-library.jsx
+++ b/src/containers/sprite-library.jsx
@@ -3,13 +3,20 @@ const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
-const LibaryComponent = require('../components/library.jsx');
+const LibaryComponent = require('../components/library/library.jsx');
class SpriteLibrary extends React.Component {
constructor (props) {
super(props);
- bindAll(this, ['setData', 'handleItemSelect', 'setSpriteData']);
- this.state = {data: [], spriteData: {}};
+ bindAll(this, [
+ 'handleItemSelect',
+ 'setData',
+ 'setSpriteData'
+ ]);
+ this.state = {
+ data: [],
+ spriteData: {}
+ };
}
componentWillReceiveProps (nextProps) {
if (nextProps.visible && this.state.data.length === 0) {
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
new file mode 100644
index 00000000000..9ea66dcf3c8
--- /dev/null
+++ b/src/containers/sprite-selector-item.jsx
@@ -0,0 +1,40 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const SpriteSelectorItemComponent = require('../components/sprite-selector-item/sprite-selector-item.jsx');
+
+class SpriteSelectorItem extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleClick'
+ ]);
+ }
+ handleClick (e) {
+ e.preventDefault();
+ this.props.onClick(this.props.id);
+ }
+ render () {
+ const {
+ id, // eslint-disable-line no-unused-vars
+ onClick, // eslint-disable-line no-unused-vars
+ ...props
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+SpriteSelectorItem.propTypes = {
+ costumeURL: React.PropTypes.string,
+ id: React.PropTypes.string,
+ name: React.PropTypes.string,
+ onClick: React.PropTypes.func,
+ selected: React.PropTypes.bool
+};
+
+module.exports = SpriteSelectorItem;
diff --git a/src/containers/sprite-selector.jsx b/src/containers/sprite-selector.jsx
deleted file mode 100644
index 52beebd1c47..00000000000
--- a/src/containers/sprite-selector.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-const bindAll = require('lodash.bindall');
-const React = require('react');
-const VM = require('scratch-vm');
-
-const SpriteSelectorComponent = require('../components/sprite-selector.jsx');
-
-class SpriteSelector extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, ['handleChange', 'targetsUpdate']);
- this.state = {
- targets: {
- targetList: []
- }
- };
- }
- componentDidMount () {
- this.props.vm.on('targetsUpdate', this.targetsUpdate);
- }
- handleChange (event) {
- this.props.vm.setEditingTarget(event.target.value);
- }
- targetsUpdate (data) {
- this.setState({targets: data});
- }
- render () {
- const {
- vm, // eslint-disable-line no-unused-vars
- openNewSprite,
- openNewCostume,
- openNewBackdrop,
- ...props
- } = this.props;
- return (
-
(
- {
- id: target[0],
- name: target[1]
- }
- ))}
- value={this.state.targets.editingTarget && [this.state.targets.editingTarget]}
- onChange={this.handleChange}
- {...props}
- />
- );
- }
-}
-
-SpriteSelector.propTypes = {
- openNewBackdrop: React.PropTypes.func,
- openNewCostume: React.PropTypes.func,
- openNewSprite: React.PropTypes.func,
- vm: React.PropTypes.instanceOf(VM)
-};
-
-module.exports = SpriteSelector;
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
new file mode 100644
index 00000000000..6748ce2c09d
--- /dev/null
+++ b/src/containers/stage-selector.jsx
@@ -0,0 +1,38 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const StageSelectorComponent = require('../components/stage-selector/stage-selector.jsx');
+
+class StageSelector extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleClick'
+ ]);
+ }
+ handleClick (e) {
+ e.preventDefault();
+ this.props.onSelect(this.props.id);
+ }
+ render () {
+ const {
+ /* eslint-disable no-unused-vars */
+ id,
+ onSelect,
+ /* eslint-enable no-unused-vars */
+ ...componentProps
+ } = this.props;
+ return (
+
+ );
+ }
+}
+StageSelector.propTypes = {
+ ...StageSelectorComponent.propTypes,
+ id: React.PropTypes.string,
+ onSelect: React.PropTypes.func
+};
+module.exports = StageSelector;
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 53f2aa86995..55c3607248f 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -3,7 +3,7 @@ const React = require('react');
const Renderer = require('scratch-render');
const VM = require('scratch-vm');
-const StageComponent = require('../components/stage.jsx');
+const StageComponent = require('../components/stage/stage.jsx');
class Stage extends React.Component {
constructor (props) {
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
new file mode 100644
index 00000000000..119628930b7
--- /dev/null
+++ b/src/containers/target-pane.jsx
@@ -0,0 +1,81 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+
+const {connect} = require('react-redux');
+
+const {
+ openBackdropLibrary,
+ openCostumeLibrary,
+ openSpriteLibrary,
+ closeBackdropLibrary,
+ closeCostumeLibrary,
+ closeSpriteLibrary
+} = require('../reducers/modals');
+
+const TargetPaneComponent = require('../components/target-pane/target-pane.jsx');
+
+class TargetPane extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleSelectSprite'
+ ]);
+ }
+ handleSelectSprite (id) {
+ this.props.vm.setEditingTarget(id);
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+const {
+ onSelectSprite, // eslint-disable-line no-unused-vars
+ ...targetSelectorProps
+} = TargetPaneComponent.propTypes;
+
+TargetPane.propTypes = {
+ ...targetSelectorProps
+};
+
+const mapStateToProps = state => ({
+ editingTarget: state.targets.editingTarget,
+ sprites: state.targets.sprites,
+ stage: state.targets.stage,
+ spriteLibraryVisible: state.modals.spriteLibrary,
+ costumeLibraryVisible: state.modals.costumeLibrary,
+ backdropLibraryVisible: state.modals.backdropLibrary
+});
+const mapDispatchToProps = dispatch => ({
+ onNewBackdropClick: e => {
+ e.preventDefault();
+ dispatch(openBackdropLibrary());
+ },
+ onNewCostumeClick: e => {
+ e.preventDefault();
+ dispatch(openCostumeLibrary());
+ },
+ onNewSpriteClick: e => {
+ e.preventDefault();
+ dispatch(openSpriteLibrary());
+ },
+ onRequestCloseBackdropLibrary: () => {
+ dispatch(closeBackdropLibrary());
+ },
+ onRequestCloseCostumeLibrary: () => {
+ dispatch(closeCostumeLibrary());
+ },
+ onRequestCloseSpriteLibrary: () => {
+ dispatch(closeSpriteLibrary());
+ }
+});
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TargetPane);
diff --git a/src/index.jsx b/src/index.jsx
index 7f2d8c07319..393cb95deec 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,8 +1,12 @@
const React = require('react');
const ReactDOM = require('react-dom');
+const {Provider} = require('react-redux');
+const {createStore} = require('redux');
+
const GUI = require('./containers/gui.jsx');
const log = require('./lib/log');
const ProjectLoader = require('./lib/project-loader');
+const reducer = require('./reducers/gui');
class App extends React.Component {
constructor (props) {
@@ -22,7 +26,7 @@ class App extends React.Component {
window.removeEventListener('hashchange', this.updateProject);
}
fetchProjectId () {
- return location.hash.substring(1);
+ return window.location.hash.substring(1);
}
updateProject () {
const projectId = this.fetchProjectId();
@@ -56,5 +60,12 @@ App.propTypes = {
const appTarget = document.createElement('div');
document.body.appendChild(appTarget);
-
-ReactDOM.render(, appTarget);
+const store = createStore(
+ reducer,
+ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
+);
+ReactDOM.render((
+
+
+
+), appTarget);
diff --git a/src/lib/media-library.js b/src/lib/media-library.js
index 6e2d122495e..cadcfc761fa 100644
--- a/src/lib/media-library.js
+++ b/src/lib/media-library.js
@@ -1,7 +1,6 @@
const xhr = require('xhr');
-const LIBRARY_PREFIX = 'https://cdn.scratch.mit.edu/scratchr2/static/' +
- '__8d9c95eb5aa1272a311775ca32568417__/medialibraries/';
+const LIBRARY_PREFIX = 'https://cdn.scratch.mit.edu/scratchr2/static/__8d9c95eb5aa1272a311775ca32568417__/medialibraries/';
const LIBRARY_URL = {
sprite: `${LIBRARY_PREFIX}spriteLibrary.json`,
costume: `${LIBRARY_PREFIX}costumeLibrary.json`,
diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx
new file mode 100644
index 00000000000..87c76c56aef
--- /dev/null
+++ b/src/lib/vm-listener-hoc.jsx
@@ -0,0 +1,112 @@
+const bindAll = require('lodash.bindall');
+const React = require('react');
+const VM = require('scratch-vm');
+
+const {connect} = require('react-redux');
+
+const targets = require('../reducers/targets');
+
+/*
+ * Higher Order Component to manage events emitted by the VM
+ * @param {React.Component} WrappedComponent component to manage VM events for
+ * @returns {React.Component} connected component with vm events bound to redux
+ */
+const vmListenerHOC = function (WrappedComponent) {
+ class VMListener extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleKeyDown',
+ 'handleKeyUp'
+ ]);
+ // We have to start listening to the vm here rather than in
+ // componentDidMount because the HOC mounts the wrapped component,
+ // so the HOC componentDidMount triggers after the wrapped component
+ // mounts.
+ // If the wrapped component uses the vm in componentDidMount, then
+ // we need to start listening before mounting the wrapped component.
+ this.props.vm.on('targetsUpdate', this.props.onTargetsUpdate);
+ this.props.vm.on('SPRITE_INFO_REPORT', this.props.onSpriteInfoReport);
+ }
+ componentDidMount () {
+ if (this.props.attachKeyboardEvents) {
+ document.addEventListener('keydown', this.handleKeyDown);
+ document.addEventListener('keyup', this.handleKeyUp);
+ }
+ }
+ componentWillUnmount () {
+ if (this.props.attachKeyboardEvents) {
+ document.removeEventListener('keydown', this.handleKeyDown);
+ document.removeEventListener('keyup', this.handleKeyUp);
+ }
+ }
+ handleKeyDown (e) {
+ // Don't capture keys intended for Blockly inputs.
+ if (e.target !== document && e.target !== document.body) return;
+
+ this.props.vm.postIOData('keyboard', {
+ keyCode: e.keyCode,
+ isDown: true
+ });
+
+ // Don't stop browser keyboard shortcuts
+ if (e.metaKey || e.altKey || e.ctrlKey) return;
+
+ e.preventDefault();
+ }
+ handleKeyUp (e) {
+ // Always capture up events,
+ // even those that have switched to other targets.
+ this.props.vm.postIOData('keyboard', {
+ keyCode: e.keyCode,
+ isDown: false
+ });
+
+ // E.g., prevent scroll.
+ if (e.target !== document && e.target !== document.body) {
+ e.preventDefault();
+ }
+ }
+ render () {
+ const {
+ /* eslint-disable no-unused-vars */
+ attachKeyboardEvents,
+ onKeyDown,
+ onKeyUp,
+ onSpriteInfoReport,
+ onTargetsUpdate,
+ /* eslint-enable no-unused-vars */
+ ...props
+ } = this.props;
+ return ;
+ }
+ }
+ VMListener.propTypes = {
+ attachKeyboardEvents: React.PropTypes.bool,
+ onKeyDown: React.PropTypes.func,
+ onKeyUp: React.PropTypes.func,
+ onSpriteInfoReport: React.PropTypes.func,
+ onTargetsUpdate: React.PropTypes.func,
+ vm: React.PropTypes.instanceOf(VM).isRequired
+ };
+ VMListener.defaultProps = {
+ attachKeyboardEvents: true,
+ vm: new VM()
+ };
+ const mapStateToProps = () => ({});
+ const mapDispatchToProps = dispatch => ({
+ onTargetsUpdate: data => {
+ dispatch(targets.updateEditingTarget(data.editingTarget));
+ dispatch(targets.updateTargets(data.targetList));
+ },
+ onSpriteInfoReport: spriteInfo => {
+ dispatch(targets.updateTarget(spriteInfo));
+ }
+ });
+ return connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(VMListener);
+};
+
+module.exports = vmListenerHOC;
diff --git a/src/lib/vm-manager.js b/src/lib/vm-manager.js
deleted file mode 100644
index 9ac6f2b8197..00000000000
--- a/src/lib/vm-manager.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const bindAll = require('lodash.bindall');
-
-class VMManager {
- constructor (vm) {
- bindAll(this, [
- 'attachKeyboardEvents',
- 'detachKeyboardEvents',
- 'onKeyDown',
- 'onKeyUp'
- ]);
- this.vm = vm;
- }
- attachKeyboardEvents () {
- // Feed keyboard events as VM I/O events.
- document.addEventListener('keydown', this.onKeyDown);
- document.addEventListener('keyup', this.onKeyUp);
- }
- detachKeyboardEvents () {
- document.removeEventListener('keydown', this.onKeyDown);
- document.removeEventListener('keyup', this.onKeyUp);
- }
- onKeyDown (e) {
- // Don't capture keys intended for Blockly inputs.
- if (e.target !== document && e.target !== document.body) return;
-
- this.vm.postIOData('keyboard', {
- keyCode: e.keyCode,
- isDown: true
- });
-
- // Don't stop browser keyboard shortcuts
- if (e.metaKey || e.altKey || e.ctrlKey) return;
-
- e.preventDefault();
- }
- onKeyUp (e) {
- // Always capture up events,
- // even those that have switched to other targets.
- this.vm.postIOData('keyboard', {
- keyCode: e.keyCode,
- isDown: false
- });
-
- // E.g., prevent scroll.
- if (e.target !== document && e.target !== document.body) {
- e.preventDefault();
- }
- }
-}
-
-module.exports = VMManager;
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
new file mode 100644
index 00000000000..325c85b1138
--- /dev/null
+++ b/src/reducers/gui.js
@@ -0,0 +1,6 @@
+const {combineReducers} = require('redux');
+
+module.exports = combineReducers({
+ modals: require('./modals'),
+ targets: require('./targets')
+});
diff --git a/src/reducers/modals.js b/src/reducers/modals.js
new file mode 100644
index 00000000000..2acce9e7621
--- /dev/null
+++ b/src/reducers/modals.js
@@ -0,0 +1,59 @@
+const OPEN_MODAL = 'scratch-gui/modals/OPEN_MODAL';
+const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL';
+
+const MODAL_BACKDROP_LIBRARY = 'backdropLibrary';
+const MODAL_COSTUME_LIBRARY = 'costumeLibrary';
+const MODAL_SPRITE_LIBRARY = 'spriteLibrary';
+
+const initialState = {
+ [MODAL_BACKDROP_LIBRARY]: false,
+ [MODAL_COSTUME_LIBRARY]: false,
+ [MODAL_SPRITE_LIBRARY]: false
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case OPEN_MODAL:
+ return Object.assign({}, state, {
+ [action.modal]: true
+ });
+ case CLOSE_MODAL:
+ return Object.assign({}, state, {
+ [action.modal]: false
+ });
+ default:
+ return state;
+ }
+};
+reducer.openModal = function (modal) {
+ return {
+ type: OPEN_MODAL,
+ modal: modal
+ };
+};
+reducer.closeModal = function (modal) {
+ return {
+ type: CLOSE_MODAL,
+ modal: modal
+ };
+};
+reducer.openBackdropLibrary = function () {
+ return reducer.openModal(MODAL_BACKDROP_LIBRARY);
+};
+reducer.openCostumeLibrary = function () {
+ return reducer.openModal(MODAL_COSTUME_LIBRARY);
+};
+reducer.openSpriteLibrary = function () {
+ return reducer.openModal(MODAL_SPRITE_LIBRARY);
+};
+reducer.closeBackdropLibrary = function () {
+ return reducer.closeModal(MODAL_BACKDROP_LIBRARY);
+};
+reducer.closeCostumeLibrary = function () {
+ return reducer.closeModal(MODAL_COSTUME_LIBRARY);
+};
+reducer.closeSpriteLibrary = function () {
+ return reducer.closeModal(MODAL_SPRITE_LIBRARY);
+};
+module.exports = reducer;
diff --git a/src/reducers/targets.js b/src/reducers/targets.js
new file mode 100644
index 00000000000..61c01ebb36d
--- /dev/null
+++ b/src/reducers/targets.js
@@ -0,0 +1,73 @@
+const defaultsDeep = require('lodash.defaultsdeep');
+
+const UPDATE_EDITING_TARGET = 'scratch-gui/targets/UPDATE_EDITING_TARGET';
+const UPDATE_TARGET_LIST = 'scratch-gui/targets/UPDATE_TARGET_LIST';
+const UPDATE_TARGET = 'scratch/targets/UPDATE_TARGET';
+
+const initialState = {
+ sprites: {},
+ stage: {}
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case UPDATE_TARGET:
+ if (action.target.id === state.stage.id) {
+ return Object.assign({}, state, {
+ stage: Object.assign({}, state.stage, action.target)
+ });
+ }
+ return Object.assign({}, state, {
+ sprites: defaultsDeep(
+ {[action.target.id]: action.target},
+ state.sprites
+ )
+ });
+ case UPDATE_TARGET_LIST:
+ return Object.assign({}, state, {
+ sprites: action.targets
+ .filter(target => !target.isStage)
+ .reduce(
+ (targets, target, listId) => defaultsDeep(
+ {[target.id]: {order: listId}},
+ {[target.id]: state.sprites[target.id]},
+ targets
+ ),
+ {}
+ ),
+ stage: action.targets
+ .filter(target => target.isStage)
+ .reduce(
+ (stage, target) => {
+ if (target.id !== stage.id) return target;
+ return defaultsDeep(target, stage);
+ },
+ state.stage
+ )
+ });
+ case UPDATE_EDITING_TARGET:
+ return Object.assign({}, state, {editingTarget: action.target});
+ default:
+ return state;
+ }
+};
+reducer.updateTarget = function (target) {
+ return {
+ type: UPDATE_TARGET,
+ target: target
+ };
+};
+reducer.updateTargets = function (targetList) {
+ return {
+ type: UPDATE_TARGET_LIST,
+ targets: targetList
+ };
+};
+reducer.updateEditingTarget = function (editingTarget) {
+ return {
+ type: UPDATE_EDITING_TARGET,
+ target: editingTarget
+ };
+};
+module.exports = reducer;
diff --git a/webpack.config.js b/webpack.config.js
index c5dc1544f85..3f5e6880296 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,3 +1,5 @@
+var autoprefixer = require('autoprefixer');
+var combineLoaders = require('webpack-combine-loaders');
var path = require('path');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -26,6 +28,21 @@ module.exports = {
presets: ['es2015', 'react']
}
},
+ {
+ test: /\.css$/,
+ loader: combineLoaders([{
+ loader: 'style'
+ }, {
+ loader: 'css',
+ query: {
+ modules: true,
+ localIdentName: '[name]_[local]_[hash:base64:5]',
+ camelCase: true
+ }
+ }, {
+ loader: 'postcss'
+ }])
+ },
{
test: /\.svg$/,
loader: 'svg-url-loader?noquotes'
@@ -35,6 +52,7 @@ module.exports = {
loader: 'json-loader'
}]
},
+ postcss: [autoprefixer],
plugins: [
new webpack.DefinePlugin({
'process.env.BASE_PATH': '"' + (process.env.BASE_PATH || '/') + '"'