diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0d2fcadcdfa..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "parser": "babel-eslint", - "rules": { - "curly": [2, "multi-line"], - "eol-last": [2], - "indent": [2, 4], - "linebreak-style": [2, "unix"], - "max-len": [2, 120, 4], - "no-trailing-spaces": [2, { "skipBlankLines": true }], - "no-unused-vars": [2, {"args": "after-used", "varsIgnorePattern": "^_"}], - "quotes": [2, "single"], - "semi": [2, "always"], - "space-before-function-paren": [2, "always"], - "strict": [2, "never"] - }, - "env": { - "browser": true, - "es6": true, - "node": true - }, - "plugins": [ - "react" - ], - "extends": ["eslint:recommended", "plugin:react/recommended"] -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..e667bb4c3f6 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + node: true + } +}; diff --git a/package.json b/package.json index 633b2a27cbe..9658b79dcac 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "start": "webpack-dev-server --port $npm_package_config_port --content-base=./build", "test": "npm run lint && npm run build" }, + "eslintConfig": { + "extends": ["scratch"] + }, "author": "Massachusetts Institute of Technology", "license": "BSD-3-Clause", "homepage": "https://github.com/LLK/scratch-gui#readme", @@ -31,14 +34,15 @@ }, "devDependencies": { "babel-core": "6.14.0", - "babel-eslint": "6.1.2", + "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", "copy-webpack-plugin": "3.0.1", "eslint": "3.5.0", - "eslint-plugin-react": "6.2.1", + "eslint-config-scratch": "^1.0.0", + "eslint-plugin-react": "6.4.1", "gh-pages": "0.11.0", "html-webpack-plugin": "2.22.0", "husky": "0.11.9", @@ -50,6 +54,7 @@ "react": "15.3.2", "react-dom": "15.3.2", "react-modal": "1.5.2", + "react-style-proptype": "1.2.0", "scratch-blocks": "latest", "scratch-render": "latest", "scratch-vm": "latest", diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 00000000000..95e29e0152c --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + node: false, + browser: true + }, + extends: ['scratch/es6', 'scratch/react'] +}; diff --git a/src/components/blocks.js b/src/components/blocks.jsx similarity index 100% rename from src/components/blocks.js rename to src/components/blocks.jsx index 357ae71b1f1..df835c0298e 100644 --- a/src/components/blocks.js +++ b/src/components/blocks.jsx @@ -8,8 +8,8 @@ class BlocksComponent extends React.Component { } = this.props; return (
img.height) { - scale = this.refs.costumeCanvas.width / img.width; + scale = this.canvas.width / img.width; } else { - scale = this.refs.costumeCanvas.height / img.height; + scale = this.canvas.height / img.height; } + // Rotate by the Scratch-value direction. const angle = (-90 + this.props.direction) * Math.PI / 180; + // Rotation origin point will be center of the canvas. - const contextTranslateX = this.refs.costumeCanvas.width / 2; - const contextTranslateY = this.refs.costumeCanvas.height / 2; + const contextTranslateX = this.canvas.width / 2; + const contextTranslateY = this.canvas.height / 2; + // First, clear the canvas. context.clearRect(0, 0, - this.refs.costumeCanvas.width, this.refs.costumeCanvas.height); + this.canvas.width, this.canvas.height); + // Translate the context to the center of the canvas, // then rotate canvas drawing by `angle`. context.translate(contextTranslateX, contextTranslateY); context.rotate(angle); context.drawImage(img, 0, 0, img.width, img.height, - -(scale * img.width / 2), -(scale * img.height / 2), + -(scale * img.width / 2), -(scale * img.height / 2), scale * img.width, scale * img.height); + // Reset the canvas rotation and translation to 0, (0, 0). context.rotate(-angle); context.translate(-contextTranslateX, -contextTranslateY); @@ -72,8 +80,8 @@ class CostumeCanvas extends React.Component { url: url }, (err, response, body) => { if (!err) { - svgToImage(body, (err, img) => { - if (!err) { + svgToImage(body, (svgErr, img) => { + if (!svgErr) { this.img = img; this.draw(); } @@ -84,7 +92,7 @@ class CostumeCanvas extends React.Component { } else { // Raster graphics: create Image and draw it. - let img = new Image(); + const img = new Image(); img.src = url; img.onload = () => { this.img = img; @@ -93,11 +101,13 @@ class CostumeCanvas extends React.Component { } } render () { - return ; + return ( + + ); } } @@ -108,10 +118,10 @@ CostumeCanvas.defaultProps = { }; CostumeCanvas.propTypes = { - url: React.PropTypes.string, - width: React.PropTypes.number, + direction: React.PropTypes.number, height: React.PropTypes.number, - direction: React.PropTypes.number + url: React.PropTypes.string, + width: React.PropTypes.number }; module.exports = CostumeCanvas; diff --git a/src/components/green-flag.js b/src/components/green-flag.jsx similarity index 100% rename from src/components/green-flag.js rename to src/components/green-flag.jsx diff --git a/src/components/gui.js b/src/components/gui.jsx similarity index 100% rename from src/components/gui.js rename to src/components/gui.jsx diff --git a/src/components/library-item.js b/src/components/library-item.jsx similarity index 62% rename from src/components/library-item.js rename to src/components/library-item.jsx index 39f65596a34..307bd97866f 100644 --- a/src/components/library-item.js +++ b/src/components/library-item.jsx @@ -1,13 +1,26 @@ +const bindAll = require('lodash.bindall'); const React = require('react'); +const stylePropType = require('react-style-proptype'); -const CostumeCanvas = require('./costume-canvas'); +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 () { - let style = (this.props.selected) ? + const style = (this.props.selected) ? this.props.selectedGridTileStyle : this.props.gridTileStyle; return ( -
this.props.onSelect(this.props.id)}> +

{this.props.name}

@@ -37,13 +50,13 @@ LibraryItem.defaultProps = { }; LibraryItem.propTypes = { - name: React.PropTypes.string, + gridTileStyle: stylePropType, iconURL: React.PropTypes.string, - gridTileStyle: React.PropTypes.object, - selectedGridTileStyle: React.PropTypes.object, - selected: React.PropTypes.bool, + id: React.PropTypes.number, + name: React.PropTypes.string, onSelect: React.PropTypes.func, - id: React.PropTypes.number + selected: React.PropTypes.bool, + selectedGridTileStyle: stylePropType }; module.exports = LibraryItem; diff --git a/src/components/library.js b/src/components/library.js deleted file mode 100644 index f23f4b8267d..00000000000 --- a/src/components/library.js +++ /dev/null @@ -1,69 +0,0 @@ -const bindAll = require('lodash.bindall'); -const React = require('react'); - -const LibraryItem = require('./library-item'); -const ModalComponent = require('./modal'); - -class LibraryComponent extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['onSelect']); - this.state = {selectedItem: null}; - } - onSelect (id) { - if (this.state.selectedItem == id) { - // Double select: select as the library's value. - this.props.onRequestClose(); - this.props.onItemSelected(this.props.data[id]); - } - this.setState({selectedItem: id}); - } - render () { - let itemId = 0; - let gridItems = this.props.data.map((dataItem) => { - let id = itemId; - itemId++; - const scratchURL = (dataItem.md5) ? 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + - dataItem.md5 + '/get/' : dataItem.rawURL; - return ; - }); - - const scrollGridStyle = { - overflow: 'scroll', - position: 'absolute', - top: '70px', - bottom: '20px', - left: '30px', - right: '30px' - }; - - return ( - -

{this.props.title}

-
- {gridItems} -
-
- ); - } -} - -LibraryComponent.propTypes = { - title: React.PropTypes.string, - data: React.PropTypes.array, - visible: React.PropTypes.bool, - onRequestClose: React.PropTypes.func, - onItemSelected: React.PropTypes.func -}; - -module.exports = LibraryComponent; diff --git a/src/components/library.jsx b/src/components/library.jsx new file mode 100644 index 00000000000..cc50b43eb02 --- /dev/null +++ b/src/components/library.jsx @@ -0,0 +1,74 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const LibraryItem = require('./library-item.jsx'); +const ModalComponent = require('./modal.jsx'); + +class LibraryComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, ['handleSelect']); + this.state = {selectedItem: null}; + } + handleSelect (id) { + if (this.state.selectedItem === id) { + // Double select: select as the library's value. + this.props.onRequestClose(); + this.props.onItemSelected(this.props.data[id]); + } + 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/` : + dataItem.rawURL; + return ( + + ); + })} +
+
+ ); + } +} + +LibraryComponent.propTypes = { + data: React.PropTypes.arrayOf( + /* eslint-disable react/no-unused-prop-types, lines-around-comment */ + React.PropTypes.shape({ + md5: React.PropTypes.string, + name: React.PropTypes.string, + rawURL: React.PropTypes.string, + }) + /* eslint-enable react/no-unused-prop-types, lines-around-comment */ + ), + onItemSelected: React.PropTypes.func, + onRequestClose: React.PropTypes.func, + title: React.PropTypes.string, + visible: React.PropTypes.bool +}; + +module.exports = LibraryComponent; diff --git a/src/components/modal.js b/src/components/modal.jsx similarity index 88% rename from src/components/modal.js rename to src/components/modal.jsx index a2c25714964..9a6e58cc686 100644 --- a/src/components/modal.js +++ b/src/components/modal.jsx @@ -1,20 +1,21 @@ 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 + {'x'}
{this.props.children}
@@ -61,8 +62,8 @@ ModalComponent.defaultProps = { ModalComponent.propTypes = { children: React.PropTypes.node, - modalStyle: React.PropTypes.object, - closeButtonStyle: React.PropTypes.object, + closeButtonStyle: stylePropType, + modalStyle: stylePropType, onRequestClose: React.PropTypes.func, visible: React.PropTypes.bool }; diff --git a/src/components/sprite-selector.js b/src/components/sprite-selector.jsx similarity index 85% rename from src/components/sprite-selector.js rename to src/components/sprite-selector.jsx index da00622bf8f..fd2807c2218 100644 --- a/src/components/sprite-selector.js +++ b/src/components/sprite-selector.jsx @@ -26,7 +26,10 @@ class SpriteSelectorComponent extends React.Component { onChange={onChange} > {sprites.map(sprite => ( - ))} @@ -43,16 +46,16 @@ class SpriteSelectorComponent extends React.Component { 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), - openNewSprite: React.PropTypes.func, - openNewCostume: React.PropTypes.func, - openNewBackdrop: React.PropTypes.func + value: React.PropTypes.arrayOf(React.PropTypes.string) }; module.exports = SpriteSelectorComponent; diff --git a/src/components/stage.js b/src/components/stage.jsx similarity index 88% rename from src/components/stage.js rename to src/components/stage.jsx index 89cfa4d1562..9f4f3a4584e 100644 --- a/src/components/stage.js +++ b/src/components/stage.jsx @@ -27,12 +27,12 @@ class StageComponent extends React.Component { StageComponent.propTypes = { canvasRef: React.PropTypes.func, - width: React.PropTypes.number, - height: React.PropTypes.number + height: React.PropTypes.number, + width: React.PropTypes.number }; StageComponent.defaultProps = { - canvasRef: function () {}, + canvasRef: () => {}, width: 480, height: 360 }; diff --git a/src/components/stop-all.js b/src/components/stop-all.jsx similarity index 100% rename from src/components/stop-all.js rename to src/components/stop-all.jsx diff --git a/src/containers/backdrop-library.js b/src/containers/backdrop-library.jsx similarity index 60% rename from src/containers/backdrop-library.js rename to src/containers/backdrop-library.jsx index bcac13b5237..ace824bead4 100644 --- a/src/containers/backdrop-library.js +++ b/src/containers/backdrop-library.jsx @@ -3,13 +3,13 @@ const React = require('react'); const VM = require('scratch-vm'); const MediaLibrary = require('../lib/media-library'); -const LibaryComponent = require('../components/library'); +const LibaryComponent = require('../components/library.jsx'); class BackdropLibrary extends React.Component { constructor (props) { super(props); - bindAll(this, ['setData', 'selectItem']); + bindAll(this, ['setData', 'handleItemSelect']); this.state = {backdropData: []}; } componentWillReceiveProps (nextProps) { @@ -20,9 +20,9 @@ class BackdropLibrary extends React.Component { setData (data) { this.setState({backdropData: data}); } - selectItem (item) { - var vmBackdrop = { - skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + item.md5 + '/get/', + handleItemSelect (item) { + const vmBackdrop = { + skin: `https://cdn.assets.scratch.mit.edu/internalapi/asset/${item.md5}/get/`, name: item.name, rotationCenterX: item.info[0], rotationCenterY: item.info[1] @@ -33,21 +33,23 @@ class BackdropLibrary extends React.Component { this.props.vm.addBackdrop(vmBackdrop); } render () { - return ; + return ( + + ); } } BackdropLibrary.propTypes = { - vm: React.PropTypes.instanceOf(VM).isRequired, mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + onRequestClose: React.PropTypes.func, visible: React.PropTypes.bool, - onRequestClose: React.PropTypes.func + vm: React.PropTypes.instanceOf(VM).isRequired }; module.exports = BackdropLibrary; diff --git a/src/containers/blocks.js b/src/containers/blocks.jsx similarity index 91% rename from src/containers/blocks.js rename to src/containers/blocks.jsx index c9f07ce1386..a5977a9f76c 100644 --- a/src/containers/blocks.js +++ 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'); +const BlocksComponent = require('../components/blocks.jsx'); class Blocks extends React.Component { constructor (props) { @@ -21,7 +21,7 @@ class Blocks extends React.Component { ]); } componentDidMount () { - let workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); + const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig); this.attachVM(); } @@ -31,9 +31,10 @@ class Blocks extends React.Component { } attachVM () { this.workspace.addChangeListener(this.props.vm.blockListener); - this.workspace.getFlyout().getWorkspace().addChangeListener( - this.props.vm.flyoutBlockListener - ); + this.workspace + .getFlyout() + .getWorkspace() + .addChangeListener(this.props.vm.flyoutBlockListener); this.props.vm.on('STACK_GLOW_ON', this.onStackGlowOn); this.props.vm.on('STACK_GLOW_OFF', this.onStackGlowOff); this.props.vm.on('BLOCK_GLOW_ON', this.onBlockGlowOn); @@ -79,7 +80,7 @@ class Blocks extends React.Component { } = this.props; return ( this.blocks = c} + componentRef={c => (this.blocks = c)} {...props} /> ); diff --git a/src/containers/costume-library.js b/src/containers/costume-library.jsx similarity index 60% rename from src/containers/costume-library.js rename to src/containers/costume-library.jsx index 091a736ffbb..07a09011f87 100644 --- a/src/containers/costume-library.js +++ b/src/containers/costume-library.jsx @@ -3,13 +3,13 @@ const React = require('react'); const VM = require('scratch-vm'); const MediaLibrary = require('../lib/media-library'); -const LibaryComponent = require('../components/library'); +const LibaryComponent = require('../components/library.jsx'); class CostumeLibrary extends React.Component { constructor (props) { super(props); - bindAll(this, ['setData', 'selectItem']); + bindAll(this, ['setData', 'handleItemSelected']); this.state = {costumeData: []}; } componentWillReceiveProps (nextProps) { @@ -20,9 +20,9 @@ class CostumeLibrary extends React.Component { setData (data) { this.setState({costumeData: data}); } - selectItem (item) { - var vmCostume = { - skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + item.md5 + '/get/', + handleItemSelected (item) { + const vmCostume = { + skin: `https://cdn.assets.scratch.mit.edu/internalapi/asset/$(item.md5)/get/`, name: item.name, rotationCenterX: item.info[0], rotationCenterY: item.info[1] @@ -33,21 +33,23 @@ class CostumeLibrary extends React.Component { this.props.vm.addCostume(vmCostume); } render () { - return ; + return ( + + ); } } CostumeLibrary.propTypes = { - vm: React.PropTypes.instanceOf(VM).isRequired, mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + onRequestClose: React.PropTypes.func, visible: React.PropTypes.bool, - onRequestClose: React.PropTypes.func + vm: React.PropTypes.instanceOf(VM).isRequired }; module.exports = CostumeLibrary; diff --git a/src/containers/green-flag.js b/src/containers/green-flag.jsx similarity index 68% rename from src/containers/green-flag.js rename to src/containers/green-flag.jsx index 233313b7950..0d8cd2f8609 100644 --- a/src/containers/green-flag.js +++ b/src/containers/green-flag.jsx @@ -1,14 +1,16 @@ const bindAll = require('lodash.bindall'); const React = require('react'); -const GreenFlagComponent = require('../components/green-flag'); +const VM = require('scratch-vm'); + +const GreenFlagComponent = require('../components/green-flag.jsx'); class GreenFlag extends React.Component { constructor (props) { super(props); - bindAll(this, ['onClick']); + bindAll(this, ['handleClick']); } - onClick (e) { + handleClick (e) { e.preventDefault(); this.props.vm.greenFlag(); } @@ -19,7 +21,7 @@ class GreenFlag extends React.Component { } = this.props; return ( ); @@ -27,7 +29,7 @@ class GreenFlag extends React.Component { } GreenFlag.propTypes = { - vm: React.PropTypes.object + vm: React.PropTypes.instanceOf(VM) }; module.exports = GreenFlag; diff --git a/src/containers/gui.js b/src/containers/gui.jsx similarity index 58% rename from src/containers/gui.js rename to src/containers/gui.jsx index 0ab742034fa..ffde713512d 100644 --- a/src/containers/gui.js +++ b/src/containers/gui.jsx @@ -5,17 +5,18 @@ 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 Blocks = require('./blocks'); -const GUIComponent = require('../components/gui'); -const GreenFlag = require('./green-flag'); -const SpriteSelector = require('./sprite-selector'); -const Stage = require('./stage'); -const StopAll = require('./stop-all'); +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'); -const CostumeLibrary = require('./costume-library'); -const BackdropLibrary = require('./backdrop-library'); +const SpriteLibrary = require('./sprite-library.jsx'); +const CostumeLibrary = require('./costume-library.jsx'); +const BackdropLibrary = require('./backdrop-library.jsx'); class GUI extends React.Component { constructor (props) { @@ -30,15 +31,15 @@ class GUI extends React.Component { this.props.vm.loadProject(this.props.projectData); this.props.vm.start(); } - componentWillUnmount () { - this.vmManager.detachKeyboardEvents(); - this.props.vm.stopAll(); - } componentWillReceiveProps (nextProps) { if (this.props.projectData !== nextProps.projectData) { this.props.vm.loadProject(nextProps.projectData); } } + componentWillUnmount () { + this.vmManager.detachKeyboardEvents(); + this.props.vm.stopAll(); + } openModal (modalName) { this.setState({currentModal: modalName}); } @@ -60,30 +61,30 @@ class GUI extends React.Component { vm, ...guiProps } = 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/' + media: `${basePath}static/blocks-media/` } }); - spriteSelectorProps = defaultsDeep({}, spriteSelectorProps, { - openNewBackdrop: () => this.openModal('backdrop-library'), - openNewCostume: () => this.openModal('costume-library'), - openNewSprite: () => this.openModal('sprite-library') - }); - spriteLibraryProps = defaultsDeep({}, spriteLibraryProps, { - mediaLibrary: this.mediaLibrary, - onRequestClose: this.closeModal, - visible: this.state.currentModal == 'sprite-library' - }); costumeLibraryProps = defaultsDeep({}, costumeLibraryProps, { mediaLibrary: this.mediaLibrary, onRequestClose: this.closeModal, - visible: this.state.currentModal == 'costume-library' + visible: this.state.currentModal === 'costume-library' }); - backdropLibraryProps = defaultsDeep({}, backdropLibraryProps, { + spriteLibraryProps = defaultsDeep({}, spriteLibraryProps, { mediaLibrary: this.mediaLibrary, onRequestClose: this.closeModal, - visible: this.state.currentModal == 'backdrop-library' + 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 ( @@ -91,36 +92,37 @@ class GUI extends React.Component { {this.props.children} ); - } else { - return ( - - - - - - - - - - - ); } + /* 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: React.PropTypes.object, + backdropLibraryProps: shapeFromPropTypes(BackdropLibrary.propTypes, {omit: ['vm']}), basePath: React.PropTypes.string, - blocksProps: React.PropTypes.object, - costumeLibraryProps: React.PropTypes.object, + blocksProps: shapeFromPropTypes(Blocks.propTypes, {omit: ['vm']}), children: React.PropTypes.node, - greenFlagProps: React.PropTypes.object, + costumeLibraryProps: shapeFromPropTypes(CostumeLibrary.propTypes, {omit: ['vm']}), + greenFlagProps: shapeFromPropTypes(GreenFlag.propTypes, {omit: ['vm']}), projectData: React.PropTypes.string, - spriteLibraryProps: React.PropTypes.object, - spriteSelectorProps: React.PropTypes.object, - stageProps: React.PropTypes.object, - stopAllProps: React.PropTypes.object, - vm: React.PropTypes.object, + 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 = { diff --git a/src/containers/sprite-library.js b/src/containers/sprite-library.js deleted file mode 100644 index 602fb177609..00000000000 --- a/src/containers/sprite-library.js +++ /dev/null @@ -1,61 +0,0 @@ -const bindAll = require('lodash.bindall'); -const React = require('react'); -const VM = require('scratch-vm'); -const MediaLibrary = require('../lib/media-library'); - -const LibaryComponent = require('../components/library'); - -class SpriteLibrary extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['setData', 'selectItem', 'setSpriteData']); - this.state = {data: [], spriteData: {}}; - } - componentWillReceiveProps (nextProps) { - if (nextProps.visible && this.state.data.length === 0) { - this.props.mediaLibrary.getMediaLibrary('sprite', this.setData); - } - } - setData (data) { - this.setState({data: data}); - for (let sprite of data) { - this.props.mediaLibrary.getSprite(sprite.md5, this.setSpriteData); - } - } - setSpriteData (md5, data) { - let spriteData = this.state.spriteData; - spriteData[md5] = data; - this.setState({spriteData: spriteData}); - } - selectItem (item) { - var spriteData = JSON.stringify(this.state.spriteData[item.json]); - this.props.vm.addSprite2(spriteData); - } - render () { - let libraryData = Object.keys(this.state.spriteData).map((libraryKey) => { - let libraryItem = this.state.spriteData[libraryKey]; - return { - name: libraryItem.objName, - md5: libraryItem.costumes[0].baseLayerMD5, - json: libraryKey - }; - }); - return ; - } -} - -SpriteLibrary.propTypes = { - vm: React.PropTypes.instanceOf(VM).isRequired, - mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), - visible: React.PropTypes.bool, - onRequestClose: React.PropTypes.func -}; - -module.exports = SpriteLibrary; diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx new file mode 100644 index 00000000000..9f2cb0a67cd --- /dev/null +++ b/src/containers/sprite-library.jsx @@ -0,0 +1,62 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); +const VM = require('scratch-vm'); +const MediaLibrary = require('../lib/media-library'); + +const LibaryComponent = require('../components/library.jsx'); + +class SpriteLibrary extends React.Component { + constructor (props) { + super(props); + bindAll(this, ['setData', 'handleItemSelect', 'setSpriteData']); + this.state = {data: [], spriteData: {}}; + } + componentWillReceiveProps (nextProps) { + if (nextProps.visible && this.state.data.length === 0) { + this.props.mediaLibrary.getMediaLibrary('sprite', this.setData); + } + } + setData (data) { + this.setState({data: data}); + for (const sprite of data) { + this.props.mediaLibrary.getSprite(sprite.md5, this.setSpriteData); + } + } + setSpriteData (md5, data) { + this.setState({ + spriteData: Object.assign({}, this.state.spriteData, {[md5]: data}) + }); + } + handleItemSelect (item) { + const spriteData = JSON.stringify(this.state.spriteData[item.json]); + this.props.vm.addSprite2(spriteData); + } + render () { + return ( + { + const libraryItem = this.state.spriteData[libraryKey]; + return { + name: libraryItem.objName, + md5: libraryItem.costumes[0].baseLayerMD5, + json: libraryKey + }; + })} + mediaLibrary={this.props.mediaLibrary} + title="Sprite Library" + visible={this.props.visible} + onItemSelected={this.handleItemSelect} + onRequestClose={this.props.onRequestClose} + /> + ); + } +} + +SpriteLibrary.propTypes = { + mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + onRequestClose: React.PropTypes.func, + visible: React.PropTypes.bool, + vm: React.PropTypes.instanceOf(VM).isRequired +}; + +module.exports = SpriteLibrary; diff --git a/src/containers/sprite-selector.js b/src/containers/sprite-selector.jsx similarity index 85% rename from src/containers/sprite-selector.js rename to src/containers/sprite-selector.jsx index c30ac713784..52beebd1c47 100644 --- a/src/containers/sprite-selector.js +++ b/src/containers/sprite-selector.jsx @@ -1,12 +1,13 @@ const bindAll = require('lodash.bindall'); const React = require('react'); +const VM = require('scratch-vm'); -const SpriteSelectorComponent = require('../components/sprite-selector'); +const SpriteSelectorComponent = require('../components/sprite-selector.jsx'); class SpriteSelector extends React.Component { constructor (props) { super(props); - bindAll(this, ['onChange', 'targetsUpdate']); + bindAll(this, ['handleChange', 'targetsUpdate']); this.state = { targets: { targetList: [] @@ -16,7 +17,7 @@ class SpriteSelector extends React.Component { componentDidMount () { this.props.vm.on('targetsUpdate', this.targetsUpdate); } - onChange (event) { + handleChange (event) { this.props.vm.setEditingTarget(event.target.value); } targetsUpdate (data) { @@ -32,17 +33,17 @@ class SpriteSelector extends React.Component { } = this.props; return ( ( { id: target[0], name: target[1] } ))} + value={this.state.targets.editingTarget && [this.state.targets.editingTarget]} + onChange={this.handleChange} {...props} /> ); @@ -50,10 +51,10 @@ class SpriteSelector extends React.Component { } SpriteSelector.propTypes = { - vm: React.PropTypes.object.isRequired, - openNewSprite: React.PropTypes.func, + openNewBackdrop: React.PropTypes.func, openNewCostume: React.PropTypes.func, - openNewBackdrop: React.PropTypes.func + openNewSprite: React.PropTypes.func, + vm: React.PropTypes.instanceOf(VM) }; module.exports = SpriteSelector; diff --git a/src/containers/stage.js b/src/containers/stage.jsx similarity index 88% rename from src/containers/stage.js rename to src/containers/stage.jsx index 3a195ccf77a..b65ea90cb68 100644 --- a/src/containers/stage.js +++ 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'); +const StageComponent = require('../components/stage.jsx'); class Stage extends React.Component { constructor (props) { @@ -40,8 +40,8 @@ class Stage extends React.Component { canvas.removeEventListener('mousedown', this.onMouseDown); } onMouseMove (e) { - let rect = this.canvas.getBoundingClientRect(); - let coordinates = { + const rect = this.canvas.getBoundingClientRect(); + const coordinates = { x: e.clientX - rect.left, y: e.clientY - rect.top, canvasWidth: rect.width, @@ -50,8 +50,8 @@ class Stage extends React.Component { this.props.vm.postIOData('mouse', coordinates); } onMouseUp (e) { - let rect = this.canvas.getBoundingClientRect(); - let data = { + const rect = this.canvas.getBoundingClientRect(); + const data = { isDown: false, x: e.clientX - rect.left, y: e.clientY - rect.top, @@ -62,8 +62,8 @@ class Stage extends React.Component { e.preventDefault(); } onMouseDown (e) { - let rect = this.canvas.getBoundingClientRect(); - let data = { + const rect = this.canvas.getBoundingClientRect(); + const data = { isDown: true, x: e.clientX - rect.left, y: e.clientY - rect.top, @@ -90,7 +90,7 @@ class Stage extends React.Component { } = this.props; return ( this.canvas = canvas} + canvasRef={canvas => (this.canvas = canvas)} {...props} /> ); diff --git a/src/containers/stop-all.js b/src/containers/stop-all.jsx similarity index 68% rename from src/containers/stop-all.js rename to src/containers/stop-all.jsx index 9b4f0f95d41..2f504b7eab5 100644 --- a/src/containers/stop-all.js +++ b/src/containers/stop-all.jsx @@ -1,14 +1,15 @@ const bindAll = require('lodash.bindall'); const React = require('react'); +const VM = require('scratch-vm'); -const StopAllComponent = require('../components/stop-all'); +const StopAllComponent = require('../components/stop-all.jsx'); class StopAll extends React.Component { constructor (props) { super(props); - bindAll(this, ['onClick']); + bindAll(this, ['handleClick']); } - onClick (e) { + handleClick (e) { e.preventDefault(); this.props.vm.stopAll(); } @@ -19,7 +20,7 @@ class StopAll extends React.Component { } = this.props; return ( ); @@ -27,7 +28,7 @@ class StopAll extends React.Component { } StopAll.propTypes = { - vm: React.PropTypes.object + vm: React.PropTypes.instanceOf(VM) }; module.exports = StopAll; diff --git a/src/index.js b/src/index.jsx similarity index 94% rename from src/index.js rename to src/index.jsx index 573f3a98388..7f2d8c07319 100644 --- a/src/index.js +++ b/src/index.jsx @@ -1,6 +1,6 @@ const React = require('react'); const ReactDOM = require('react-dom'); -const GUI = require('./containers/gui'); +const GUI = require('./containers/gui.jsx'); const log = require('./lib/log'); const ProjectLoader = require('./lib/project-loader'); @@ -25,7 +25,7 @@ class App extends React.Component { return location.hash.substring(1); } updateProject () { - let projectId = this.fetchProjectId(); + const projectId = this.fetchProjectId(); if (projectId !== this.state.projectId) { if (projectId.length < 1) { return this.setState({ diff --git a/src/lib/log.js b/src/lib/log.js index 9a364044f21..ab38732b175 100644 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -1,4 +1,4 @@ -var minilog = require('minilog'); +const minilog = require('minilog'); minilog.enable(); -module.exports = minilog('www'); +module.exports = minilog('gui'); diff --git a/src/lib/media-library.js b/src/lib/media-library.js index 6f33b25c919..6e2d122495e 100644 --- a/src/lib/media-library.js +++ b/src/lib/media-library.js @@ -3,9 +3,9 @@ const xhr = require('xhr'); 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', - backdrop: LIBRARY_PREFIX + 'backdropLibrary.json' + sprite: `${LIBRARY_PREFIX}spriteLibrary.json`, + costume: `${LIBRARY_PREFIX}costumeLibrary.json`, + backdrop: `${LIBRARY_PREFIX}backdropLibrary.json` }; const SPRITE_OBJECT_PREFIX = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/'; const SPRITE_OBJECT_SUFFIX = '/get/'; @@ -44,7 +44,7 @@ class MediaLibrary { url: LIBRARY_URL[libraryType] }, (err, response, body) => { if (!err) { - let data = JSON.parse(body); + const data = JSON.parse(body); this._libraryData[libraryType] = data; callback(this._libraryData[libraryType]); } @@ -68,7 +68,7 @@ class MediaLibrary { url: SPRITE_OBJECT_PREFIX + url + SPRITE_OBJECT_SUFFIX }, (err, response, body) => { if (!err) { - let data = JSON.parse(body); + const data = JSON.parse(body); this._spriteData[url] = data; callback(url, data); } diff --git a/src/lib/shape-from-prop-types.js b/src/lib/shape-from-prop-types.js new file mode 100644 index 00000000000..2d2415e1c84 --- /dev/null +++ b/src/lib/shape-from-prop-types.js @@ -0,0 +1,13 @@ +const React = require('react'); + +module.exports = function shapeFromPropTypes (propTypes, opts) { + opts = Object.assign({}, opts, {omit: []}); + const shape = Object + .keys(propTypes) + .filter(key => opts.omit.indexOf(key) !== -1) + .reduce((newPropTypes, key) => { + newPropTypes[key] = propTypes[key]; + return newPropTypes; + }, {}); + return React.PropTypes.shape(shape); +}; diff --git a/src/lib/vm-manager.js b/src/lib/vm-manager.js index db521c99f50..8da19206226 100644 --- a/src/lib/vm-manager.js +++ b/src/lib/vm-manager.js @@ -21,7 +21,7 @@ class VMManager { } onKeyDown (e) { // Don't capture keys intended for Blockly inputs. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { return; } this.vm.postIOData('keyboard', { @@ -37,8 +37,9 @@ class VMManager { keyCode: e.keyCode, isDown: false }); + // E.g., prevent scroll. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { e.preventDefault(); } } diff --git a/webpack.config.js b/webpack.config.js index 324f7c57b39..67a39f865db 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,10 +2,11 @@ var path = require('path'); var CopyWebpackPlugin = require('copy-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var webpack = require('webpack'); + module.exports = { entry: { vendor: ['react', 'react-dom'], - gui: './src/index.js' + gui: './src/index.jsx' }, output: { path: path.resolve(__dirname, 'build'), @@ -17,7 +18,7 @@ module.exports = { ReactDOM: 'react-dom' }, loaders: [{ - test: /\.js$/, + test: /\.jsx?$/, loader: 'babel-loader', include: path.resolve(__dirname, 'src'), query: { @@ -45,7 +46,7 @@ module.exports = { from: 'node_modules/scratch-blocks/media', to: 'static/blocks-media' }]) - ].concat(process.env.NODE_ENV == 'production' ? [ + ].concat(process.env.NODE_ENV === 'production' ? [ new webpack.optimize.UglifyJsPlugin({ include: /\.min\.js$/, minimize: true,