diff --git a/package.json b/package.json index d5b05b928bf..a094b12769a 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,18 @@ "react-dom": "15.x.x" }, "devDependencies": { + "autoprefixer": "6.5.3", "babel-core": "6.14.0", "babel-eslint": "7.0.0", "babel-loader": "6.2.5", "babel-plugin-transform-object-rest-spread": "6.16.0", "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", + "classnames": "2.2.5", "copy-webpack-plugin": "3.0.1", + "css-loader": "0.26.1", "eslint": "3.8.1", - "eslint-config-scratch": "^2.0.0", + "eslint-config-scratch": "^3.0.0", "eslint-plugin-react": "6.4.1", "gh-pages": "0.11.0", "html-webpack-plugin": "2.22.0", @@ -48,17 +51,22 @@ "lodash.defaultsdeep": "4.4.0", "minilog": "3.0.1", "opt-cli": "1.5.1", + "postcss-loader": "1.2.0", "react": "15.3.2", "react-dom": "15.3.2", "react-modal": "1.5.2", + "react-redux": "4.4.6", "react-style-proptype": "1.2.0", + "redux": "3.6.0", "scratch-blocks": "latest", "scratch-render": "latest", "scratch-vm": "latest", + "style-loader": "0.13.1", "svg-to-image": "1.1.3", "svg-url-loader": "1.1.0", "travis-after-all": "1.4.4", "webpack": "1.13.2", + "webpack-combine-loaders": "2.0.3", "webpack-dev-server": "1.15.2", "xhr": "2.2.2" } diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 77bf09f3fe1..0abd74d675f 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -1,9 +1,9 @@ module.exports = { + root: true, + extends: ['scratch', 'scratch/es6', 'scratch/react'], env: { - node: false, browser: true }, - extends: ['scratch/es6', 'scratch/react'], globals: { process: true } diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css new file mode 100644 index 00000000000..3d006624961 --- /dev/null +++ b/src/components/blocks/blocks.css @@ -0,0 +1,7 @@ +.blocks { + position: absolute; + top: 40px; + right: 500px; + bottom: 0; + left: 0; +} diff --git a/src/components/blocks.jsx b/src/components/blocks/blocks.jsx similarity index 62% rename from src/components/blocks.jsx rename to src/components/blocks/blocks.jsx index 8e1b9a19a91..fa00102abe5 100644 --- a/src/components/blocks.jsx +++ b/src/components/blocks/blocks.jsx @@ -1,5 +1,7 @@ const React = require('react'); +const styles = require('./blocks.css'); + class BlocksComponent extends React.Component { render () { const { @@ -8,15 +10,8 @@ class BlocksComponent extends React.Component { } = this.props; return (
); diff --git a/src/components/costume-canvas.jsx b/src/components/costume-canvas/costume-canvas.jsx similarity index 100% rename from src/components/costume-canvas.jsx rename to src/components/costume-canvas/costume-canvas.jsx diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css new file mode 100644 index 00000000000..b8000e48277 --- /dev/null +++ b/src/components/green-flag/green-flag.css @@ -0,0 +1,11 @@ +.green-flag { + position: absolute; + top: 8px; + right: calc(480px - 16px); + width: 16px; + height: 16px; +} + +.active { + filter: saturate(200%) brightness(150%); +} diff --git a/src/components/green-flag/green-flag.jsx b/src/components/green-flag/green-flag.jsx index 8e0c12d593c..f1b24b15164 100644 --- a/src/components/green-flag/green-flag.jsx +++ b/src/components/green-flag/green-flag.jsx @@ -1,5 +1,8 @@ +const classNames = require('classnames'); const React = require('react'); + const greenFlagIcon = require('./green-flag.svg'); +const styles = require('./green-flag.css'); const GreenFlagComponent = function (props) { const { @@ -10,16 +13,11 @@ const GreenFlagComponent = function (props) { } = props; return ( - {children} -
- ); -}; - -GUIComponent.propTypes = { - children: React.PropTypes.node -}; - -module.exports = GUIComponent; diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css new file mode 100644 index 00000000000..78b6f009ac5 --- /dev/null +++ b/src/components/gui/gui.css @@ -0,0 +1,7 @@ +.gui { + position: absolute; + top: 0; + right: 4px; + bottom: 0; + left: 4px; +} diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx new file mode 100644 index 00000000000..31e5e6ae2d0 --- /dev/null +++ b/src/components/gui/gui.jsx @@ -0,0 +1,90 @@ +const defaultsDeep = require('lodash.defaultsdeep'); +const React = require('react'); +const VM = require('scratch-vm'); + +const MediaLibrary = require('../../lib/media-library'); +const shapeFromPropTypes = require('../../lib/shape-from-prop-types'); + +const Blocks = require('../../containers/blocks.jsx'); +const GreenFlag = require('../../containers/green-flag.jsx'); +const TargetPane = require('../../containers/target-pane.jsx'); +const Stage = require('../../containers/stage.jsx'); +const StopAll = require('../../containers/stop-all.jsx'); + +const styles = require('./gui.css'); + +const GUIComponent = props => { + let { + basePath, + blocksProps, + children, + greenFlagProps, + mediaLibrary, + targetPaneProps, + stageProps, + stopAllProps, + vm + } = props; + blocksProps = defaultsDeep({}, blocksProps, { + options: { + media: `${basePath}static/blocks-media/` + } + }); + if (children) { + return ( +
+ {children} +
+ ); + } + return ( +
+ + + + + +
+ ); +}; + +GUIComponent.propTypes = { + basePath: React.PropTypes.string, + blocksProps: shapeFromPropTypes(Blocks.propTypes, {omit: ['vm']}), + children: React.PropTypes.node, + greenFlagProps: shapeFromPropTypes(GreenFlag.propTypes, {omit: ['vm']}), + mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + stageProps: shapeFromPropTypes(Stage.propTypes, {omit: ['vm']}), + stopAllProps: shapeFromPropTypes(StopAll.propTypes, {omit: ['vm']}), + targetPaneProps: shapeFromPropTypes(TargetPane.propTypes, {omit: ['vm']}), + vm: React.PropTypes.instanceOf(VM) +}; + +GUIComponent.defaultProps = { + basePath: '/', + blocksProps: {}, + greenFlagProps: {}, + mediaLibrary: new MediaLibrary(), + targetPaneProps: {}, + stageProps: {}, + stopAllProps: {}, + vm: new VM() +}; + +module.exports = GUIComponent; diff --git a/src/components/library-item.jsx b/src/components/library-item.jsx deleted file mode 100644 index 307bd97866f..00000000000 --- a/src/components/library-item.jsx +++ /dev/null @@ -1,62 +0,0 @@ -const bindAll = require('lodash.bindall'); -const React = require('react'); -const stylePropType = require('react-style-proptype'); - -const CostumeCanvas = require('./costume-canvas.jsx'); - -class LibraryItem extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['handleClick']); - } - handleClick (e) { - this.props.onSelect(this.props.id); - e.preventDefault(); - } - render () { - const style = (this.props.selected) ? - this.props.selectedGridTileStyle : this.props.gridTileStyle; - return ( -
- -

{this.props.name}

-
- ); - } -} - -LibraryItem.defaultProps = { - gridTileStyle: { - float: 'left', - width: '140px', - marginLeft: '5px', - marginRight: '5px', - textAlign: 'center', - cursor: 'pointer' - }, - selectedGridTileStyle: { - float: 'left', - width: '140px', - marginLeft: '5px', - marginRight: '5px', - textAlign: 'center', - cursor: 'pointer', - background: '#aaa', - borderRadius: '6px' - } -}; - -LibraryItem.propTypes = { - gridTileStyle: stylePropType, - iconURL: React.PropTypes.string, - id: React.PropTypes.number, - name: React.PropTypes.string, - onSelect: React.PropTypes.func, - selected: React.PropTypes.bool, - selectedGridTileStyle: stylePropType -}; - -module.exports = LibraryItem; diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css new file mode 100644 index 00000000000..ab334283577 --- /dev/null +++ b/src/components/library-item/library-item.css @@ -0,0 +1,12 @@ +.library-item { + float: left; + width: 140px; + margin-left: 5px; + margin-right: 5px; + text-align: center; + cursor: pointer; +} +.library-item.is-selected { + background: #aaa; + border-radius: 6px; +} diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx new file mode 100644 index 00000000000..641f989da0e --- /dev/null +++ b/src/components/library-item/library-item.jsx @@ -0,0 +1,41 @@ +const classNames = require('classnames'); +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx'); +const styles = require('./library-item.css'); + +class LibraryItem extends React.Component { + constructor (props) { + super(props); + bindAll(this, ['handleClick']); + } + handleClick (e) { + this.props.onSelect(this.props.id); + e.preventDefault(); + } + render () { + return ( +
+ +

{this.props.name}

+
+ ); + } +} + +LibraryItem.propTypes = { + iconURL: React.PropTypes.string, + id: React.PropTypes.number, + name: React.PropTypes.string, + onSelect: React.PropTypes.func, + selected: React.PropTypes.bool +}; + +module.exports = LibraryItem; diff --git a/src/components/library/library.css b/src/components/library/library.css new file mode 100644 index 00000000000..02101f22397 --- /dev/null +++ b/src/components/library/library.css @@ -0,0 +1,8 @@ +.library-scroll-grid { + overflow: scroll; + position: absolute; + top: 70px; + bottom: 20px; + left: 30px; + right: 30px; +} diff --git a/src/components/library.jsx b/src/components/library/library.jsx similarity index 86% rename from src/components/library.jsx rename to src/components/library/library.jsx index 66faaa32306..043ece72915 100644 --- a/src/components/library.jsx +++ b/src/components/library/library.jsx @@ -1,8 +1,10 @@ const bindAll = require('lodash.bindall'); const React = require('react'); -const LibraryItem = require('./library-item.jsx'); -const ModalComponent = require('./modal.jsx'); +const LibraryItem = require('../library-item/library-item.jsx'); +const ModalComponent = require('../modal/modal.jsx'); + +const styles = require('./library.css'); class LibraryComponent extends React.Component { constructor (props) { @@ -19,21 +21,13 @@ class LibraryComponent extends React.Component { this.setState({selectedItem: id}); } render () { - const scrollGridStyle = { - overflow: 'scroll', - position: 'absolute', - top: '70px', - bottom: '20px', - left: '30px', - right: '30px' - }; return (

{this.props.title}

-
+
{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 ( + + +

+ + {editingTarget === stage.id ? ( + + ) : ( + + )} + + + +

+
+ ); +}; +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 || '/') + '"'