diff --git a/package.json b/package.json index 7765b8dd74165a1be9eafcd6b8bcabc1d63c7a36..54d7f7f2f954ce07c745b544613aba57b2d3ad2d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "react-dom": "^15" }, "devDependencies": { - "autoprefixer": "6.7.7", + "autoprefixer": "7.1.0", "babel-core": "^6.23.1", "babel-eslint": "^7.1.1", "babel-loader": "^7.0.0", @@ -32,11 +32,11 @@ "babel-preset-react": "^6.22.0", "classnames": "2.2.5", "copy-webpack-plugin": "4.0.1", - "css-loader": "0.28.0", + "css-loader": "0.28.1", "eslint": "^3.16.1", "eslint-config-scratch": "^3.0.0", - "eslint-plugin-react": "^6.10.0", - "gh-pages": "^0.12.0", + "eslint-plugin-react": "^7.0.1", + "gh-pages": "^1.0.0", "html-webpack-plugin": "2.28.0", "lodash.bindall": "4.4.0", "lodash.defaultsdeep": "4.6.0", @@ -45,26 +45,26 @@ "lodash.pick": "4.4.0", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "postcss-import": "9.1.0", - "postcss-loader": "1.3.3", - "postcss-simple-vars": "3.1.0", - "prop-types": "15.5.8", + "postcss-import": "^10.0.0", + "postcss-loader": "^2.0.5", + "postcss-simple-vars": "^4.0.0", + "prop-types": "^15.5.10", "react": "15.5.4", "react-dom": "15.5.4", "react-draggable": "2.2.6", "react-modal": "1.7.7", - "react-redux": "5.0.4", + "react-redux": "5.0.5", "react-style-proptype": "3.0.0", - "react-tabs": "0.8.3", + "react-tabs": "1.0.0", "redux": "3.6.0", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "^0.1.0-prerelease.0", "scratch-blocks": "^0.1.0-prerelease.0", "scratch-render": "^0.1.0-prerelease.0", - "scratch-storage": "^0.0.1-prerelease.0", + "scratch-storage": "^0.1.0", "scratch-vm": "^0.1.0-prerelease.0", - "style-loader": "0.16.1", + "style-loader": "^0.17.0", "svg-to-image": "1.1.3", "svg-url-loader": "2.0.2", "travis-after-all": "^1.4.4", diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index d0d076bab5b9be6ecc64a152cb8e3f6954021106..0ddc2bb51202771fc9861ac603174bb28716773f 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -27,6 +27,7 @@ const Selector = props => { <Box className={styles.listArea}> {items.map((item, index) => ( <SpriteSelectorItem + assetId={item.assetId} className={styles.listItem} costumeURL={item.url} id={index} diff --git a/src/components/button/button.css b/src/components/button/button.css new file mode 100644 index 0000000000000000000000000000000000000000..44243650f970d16aa787b8a44da3763e595802ce --- /dev/null +++ b/src/components/button/button.css @@ -0,0 +1,3 @@ +.button { + cursor: pointer; +} diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..dbc852395625e7e2b3fec55a7a71de91e5e1aa37 --- /dev/null +++ b/src/components/button/button.jsx @@ -0,0 +1,31 @@ +const classNames = require('classnames'); +const PropTypes = require('prop-types'); +const React = require('react'); + +const styles = require('./button.css'); + +const ButtonComponent = ({ + className, + onClick, + children, + ...props +}) => ( + <span + className={classNames( + styles.button, + className + )} + role="button" + onClick={onClick} + {...props} + > + {children} + </span> +); + +ButtonComponent.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func.isRequired +}; +module.exports = ButtonComponent; diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index e2fe4db4333437657c6c3009fdde3c8df478b158..ebaf67d845cc1893abe6f63fc6fbfebcf04b19dd 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -47,7 +47,7 @@ border-bottom: 0 !important; } -.tab-list .tab { +.tab { flex-grow: 1; height: 80%; margin-left: 1px; @@ -63,9 +63,9 @@ align-items: center; } - -.tab-list .tab[aria-selected="true"] { +.tab.is-selected { color: #40B9F5; + background-color: #FFFFFF; } .tabs { @@ -80,6 +80,10 @@ position: relative; flex-grow: 1; flex-shrink: 0; + display: none; +} + +.tab-panel.is-selected { display: flex; } diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 91ed77ec2ada63de1bcb53e85b0a6acd38989c89..47fb1b327a32a91fc25b4851a5e457b9364add5e 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -1,6 +1,10 @@ +const classNames = require('classnames'); const PropTypes = require('prop-types'); const React = require('react'); +const {Tab, Tabs, TabList, TabPanel} = require('react-tabs'); +const tabStyles = require('react-tabs/style/react-tabs.css'); const VM = require('scratch-vm'); + const Blocks = require('../../containers/blocks.jsx'); const CostumeTab = require('../../containers/costume-tab.jsx'); const GreenFlag = require('../../containers/green-flag.jsx'); @@ -8,17 +12,20 @@ const TargetPane = require('../../containers/target-pane.jsx'); const SoundTab = require('../../containers/sound-tab.jsx'); const Stage = require('../../containers/stage.jsx'); const StopAll = require('../../containers/stop-all.jsx'); -const MenuBar = require('../menu-bar/menu-bar.jsx'); -const {Tab, Tabs, TabList, TabPanel} = require('react-tabs'); const Box = require('../box/box.jsx'); +const MenuBar = require('../menu-bar/menu-bar.jsx'); + const styles = require('./gui.css'); + const GUIComponent = props => { const { basePath, children, vm, + onTabSelect, + tabIndex, ...componentProps } = props; if (children) { @@ -29,11 +36,13 @@ const GUIComponent = props => { ); } - // @todo hack to resize blockly manually in case resize happened while hidden - const handleTabSelect = tabIndex => { - if (tabIndex === 0) { - setTimeout(() => window.dispatchEvent(new Event('resize'))); - } + const tabClassNames = { + tabs: styles.tabs, + tab: classNames(tabStyles.reactTabsTab, styles.tab), + tabList: classNames(tabStyles.reactTabsTabList, styles.tabList), + tabPanel: classNames(tabStyles.reactTabsTabPanel, styles.tabPanel), + tabPanelSelected: classNames(tabStyles.reactTabsTabPanelSelected, styles.isSelected), + tabSelected: classNames(tabStyles.reactTabsTabSelected, styles.isSelected) }; return ( @@ -46,19 +55,22 @@ const GUIComponent = props => { <Box className={styles.flexWrapper}> <Box className={styles.editorWrapper}> <Tabs - className={styles.tabs} + className={tabClassNames.tabs} forceRenderTabPanel={true} // eslint-disable-line react/jsx-boolean-value - onSelect={handleTabSelect} + selectedTabClassName={tabClassNames.tabSelected} + selectedTabPanelClassName={tabClassNames.tabPanelSelected} + onSelect={onTabSelect} > - <TabList className={styles.tabList}> - <Tab className={styles.tab}>Scripts</Tab> - <Tab className={styles.tab}>Costumes</Tab> - <Tab className={styles.tab}>Sounds</Tab> + <TabList className={tabClassNames.tabList}> + <Tab className={tabClassNames.tab}>Scripts</Tab> + <Tab className={tabClassNames.tab}>Costumes</Tab> + <Tab className={tabClassNames.tab}>Sounds</Tab> </TabList> - <TabPanel className={styles.tabPanel}> + <TabPanel className={tabClassNames.tabPanel}> <Box className={styles.blocksWrapper}> <Blocks grow={1} + isVisible={tabIndex === 0} // Scripts tab options={{ media: `${basePath}static/blocks-media/` }} @@ -66,10 +78,10 @@ const GUIComponent = props => { /> </Box> </TabPanel> - <TabPanel className={styles.tabPanel}> + <TabPanel className={tabClassNames.tabPanel}> <CostumeTab vm={vm} /> </TabPanel> - <TabPanel className={styles.tabPanel}> + <TabPanel className={tabClassNames.tabPanel}> <SoundTab vm={vm} /> </TabPanel> </Tabs> @@ -102,6 +114,8 @@ const GUIComponent = props => { GUIComponent.propTypes = { basePath: PropTypes.string, children: PropTypes.node, + onTabSelect: PropTypes.func, + tabIndex: PropTypes.number, vm: PropTypes.instanceOf(VM).isRequired }; GUIComponent.defaultProps = { diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 288b760546027f8308c5c579cec7ff7630602ba2..3102d3d2d2a29d5f58db70f638b3d15fc5404491 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -26,11 +26,6 @@ border-color: #1dacf4; } -.library-item.is-selected { - border-width: 2px; - border-color: #1dacf4; -} - .library-item-image-container { height: 100px; } diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 31e722b0e8fd6cc9bb7d76cd24bd8fdf12cfe9e5..8e113c9be08af583a36272da486fe8afe31e0367 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -1,4 +1,3 @@ -const classNames = require('classnames'); const bindAll = require('lodash.bindall'); const PropTypes = require('prop-types'); const React = require('react'); @@ -9,20 +8,29 @@ const styles = require('./library-item.css'); class LibraryItem extends React.Component { constructor (props) { super(props); - bindAll(this, ['handleClick']); + bindAll(this, [ + 'handleClick', + 'handleMouseEnter', + 'handleMouseLeave' + ]); } handleClick (e) { this.props.onSelect(this.props.id); e.preventDefault(); } + handleMouseEnter () { + this.props.onMouseEnter(this.props.id); + } + handleMouseLeave () { + this.props.onMouseLeave(this.props.id); + } render () { return ( <Box - className={classNames({ - [styles.libraryItem]: true, - [styles.isSelected]: this.props.selected - })} + className={styles.libraryItem} onClick={this.handleClick} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} > <Box className={styles.libraryItemImageContainer}> <img @@ -40,8 +48,9 @@ LibraryItem.propTypes = { iconURL: PropTypes.string.isRequired, id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, - onSelect: PropTypes.func.isRequired, - selected: PropTypes.bool.isRequired + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired, + onSelect: PropTypes.func.isRequired }; module.exports = LibraryItem; diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 27cfe4116e404c87802b929b9063eacca6f84782..9b03b0b8dd03af8ef0e38bdf96087d122a917762 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -11,8 +11,10 @@ class LibraryComponent extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleSelect', - 'handleFilterChange' + 'handleFilterChange', + 'handleMouseEnter', + 'handleMouseLeave', + 'handleSelect' ]); this.state = { selectedItem: null, @@ -20,16 +22,14 @@ class LibraryComponent extends React.Component { }; } handleSelect (id) { - if (this.state.selectedItem === id) { - // Double select: select as the library's value. - this.props.onRequestClose(); - this.props.onItemSelected(this.getFilteredData()[id]); - } else { - if (this.props.onItemChosen) { - this.props.onItemChosen(this.getFilteredData()[id]); - } - } - this.setState({selectedItem: id}); + this.props.onRequestClose(); + this.props.onItemSelected(this.getFilteredData()[id]); + } + handleMouseEnter (id) { + if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]); + } + handleMouseLeave (id) { + if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]); } handleFilterChange (event) { this.setState({ @@ -61,7 +61,8 @@ class LibraryComponent extends React.Component { id={index} key={`item_${index}`} name={dataItem.name} - selected={this.state.selectedItem === index} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} /> ); @@ -84,7 +85,8 @@ LibraryComponent.propTypes = { }) /* eslint-enable react/no-unused-prop-types, lines-around-comment */ ), - onItemChosen: PropTypes.func, + onItemMouseEnter: PropTypes.func, + onItemMouseLeave: PropTypes.func, onItemSelected: PropTypes.func, onRequestClose: PropTypes.func, title: PropTypes.string.isRequired, diff --git a/src/components/load-button/load-button.css b/src/components/load-button/load-button.css new file mode 100644 index 0000000000000000000000000000000000000000..527718028aedbf6ec6636afe0df0625e2005c35c --- /dev/null +++ b/src/components/load-button/load-button.css @@ -0,0 +1,3 @@ +.file-input { + display: none; +} diff --git a/src/components/load-button/load-button.jsx b/src/components/load-button/load-button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..29b2962e5a6f2b6642c80ac9449f458f2ea70f85 --- /dev/null +++ b/src/components/load-button/load-button.jsx @@ -0,0 +1,36 @@ +const PropTypes = require('prop-types'); +const React = require('react'); + +const ButtonComponent = require('../button/button.jsx'); + +const styles = require('./load-button.css'); + +const LoadButtonComponent = ({ + inputRef, + onChange, + onClick, + title, + ...props +}) => ( + <span {...props}> + <ButtonComponent onClick={onClick}>{title}</ButtonComponent> + <input + className={styles.fileInput} + ref={inputRef} + type="file" + onChange={onChange} + /> + </span> +); + +LoadButtonComponent.propTypes = { + className: PropTypes.string, + inputRef: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + title: PropTypes.string +}; +LoadButtonComponent.defaultProps = { + title: 'Load' +}; +module.exports = LoadButtonComponent; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index b34a2e1900a937ecba22f8d9944f48563136223f..4397b5002ea47789ed47cd3283a6ba917c24373d 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -2,6 +2,9 @@ const classNames = require('classnames'); const React = require('react'); const Box = require('../box/box.jsx'); +const LoadButton = require('../../containers/load-button.jsx'); +const SaveButton = require('../../containers/save-button.jsx'); + const styles = require('./menu-bar.css'); const scratchLogo = require('./scratch-logo.svg'); @@ -18,7 +21,8 @@ const MenuBar = function MenuBar () { src={scratchLogo} /> </div> - <div className={styles.menuItem} >Animation Playtest Prototype</div> + <SaveButton className={styles.menuItem} /> + <LoadButton className={styles.menuItem} /> </Box> ); }; diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index df48343745100b55e7f2a1334681b0aec65c3e58..3410c75809e683e67f664d11b01049f003fd0855 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -53,17 +53,15 @@ const SpriteSelectorComponent = function (props) { {Object.keys(sprites) // Re-order by list order .sort((id1, id2) => sprites[id1].order - sprites[id2].order) - .map(id => ( + .map(id => sprites[id]) + .map(sprite => ( <SpriteSelectorItem + assetId={sprite.costume && sprite.costume.assetId} className={styles.sprite} - costumeURL={ - sprites[id].costume && - sprites[id].costume.url - } - id={id} - key={id} - name={sprites[id].name} - selected={id === selectedId} + id={sprite.id} + key={sprite.id} + name={sprite.name} + selected={sprite.id === selectedId} onClick={onSelectSprite} onDeleteButtonClick={onDeleteSprite} /> diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 5d8fc1915cd185125ad12104a80c341590406895..c779f274713413e13d62851dae45c54223caadaa 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -1,5 +1,3 @@ -const isEqual = require('lodash.isequal'); -const omit = require('lodash.omit'); const classNames = require('classnames'); const PropTypes = require('prop-types'); const React = require('react'); @@ -23,119 +21,106 @@ const addIcon = require('./icon--add.svg'); * @param {object} props Props for the component * @returns {React.Component} rendered component */ -class TargetPane extends React.Component { - shouldComponentUpdate (nextProps) { - return ( - // Do a normal shallow compare on all props except sprites - Object.keys(omit(nextProps, ['sprites'])) - .reduce((all, k) => all || nextProps[k] !== this.props[k], false) || - // Deep compare on sprites object - !isEqual(this.props.sprites, nextProps.sprites) - ); - } - render () { - const { - editingTarget, - backdropLibraryVisible, - costumeLibraryVisible, - soundLibraryVisible, - spriteLibraryVisible, - onChangeSpriteDirection, - onChangeSpriteName, - onChangeSpriteRotationStyle, - onChangeSpriteVisibility, - onChangeSpriteX, - onChangeSpriteY, - onDeleteSprite, - onNewSpriteClick, - onNewBackdropClick, - onRequestCloseBackdropLibrary, - onRequestCloseCostumeLibrary, - onRequestCloseSoundLibrary, - onRequestCloseSpriteLibrary, - onSelectSprite, - stage, - sprites, - vm, - ...componentProps - } = this.props; - return ( - <Box - className={styles.targetPane} - {...componentProps} - > +const TargetPane = ({ + editingTarget, + backdropLibraryVisible, + costumeLibraryVisible, + soundLibraryVisible, + spriteLibraryVisible, + onChangeSpriteDirection, + onChangeSpriteName, + onChangeSpriteRotationStyle, + onChangeSpriteVisibility, + onChangeSpriteX, + onChangeSpriteY, + onDeleteSprite, + onNewSpriteClick, + onNewBackdropClick, + onRequestCloseBackdropLibrary, + onRequestCloseCostumeLibrary, + onRequestCloseSoundLibrary, + onRequestCloseSpriteLibrary, + onSelectSprite, + stage, + sprites, + vm, + ...componentProps +}) => ( + <Box + className={styles.targetPane} + {...componentProps} + > - <SpriteSelectorComponent - selectedId={editingTarget} - sprites={sprites} - onChangeSpriteDirection={onChangeSpriteDirection} - onChangeSpriteName={onChangeSpriteName} - onChangeSpriteRotationStyle={onChangeSpriteRotationStyle} - onChangeSpriteVisibility={onChangeSpriteVisibility} - onChangeSpriteX={onChangeSpriteX} - onChangeSpriteY={onChangeSpriteY} - onDeleteSprite={onDeleteSprite} - onSelectSprite={onSelectSprite} - /> - <Box className={styles.stageSelectorWrapper}> - {stage.id && <StageSelector - backdropCount={stage.costumeCount} - id={stage.id} - selected={stage.id === editingTarget} - url={ - stage.costume && - stage.costume.url - } - onSelect={onSelectSprite} - />} - <Box> + <SpriteSelectorComponent + selectedId={editingTarget} + sprites={sprites} + onChangeSpriteDirection={onChangeSpriteDirection} + onChangeSpriteName={onChangeSpriteName} + onChangeSpriteRotationStyle={onChangeSpriteRotationStyle} + onChangeSpriteVisibility={onChangeSpriteVisibility} + onChangeSpriteX={onChangeSpriteX} + onChangeSpriteY={onChangeSpriteY} + onDeleteSprite={onDeleteSprite} + onSelectSprite={onSelectSprite} + /> + <Box className={styles.stageSelectorWrapper}> + {stage.id && <StageSelector + assetId={ + stage.costume && + stage.costume.assetId + } + backdropCount={stage.costumeCount} + id={stage.id} + selected={stage.id === editingTarget} + onSelect={onSelectSprite} + />} + <Box> - <button - className={classNames(styles.addButtonWrapper, styles.addButtonWrapperSprite)} - onClick={onNewSpriteClick} - > - <img - className={styles.addButton} - src={addIcon} - /> - </button> + <button + className={classNames(styles.addButtonWrapper, styles.addButtonWrapperSprite)} + onClick={onNewSpriteClick} + > + <img + className={styles.addButton} + src={addIcon} + /> + </button> - <button - className={classNames(styles.addButtonWrapper, styles.addButtonWrapperStage)} - onClick={onNewBackdropClick} - > - <img - className={styles.addButton} - src={addIcon} - /> - </button> + <button + className={classNames(styles.addButtonWrapper, styles.addButtonWrapperStage)} + onClick={onNewBackdropClick} + > + <img + className={styles.addButton} + src={addIcon} + /> + </button> - <SpriteLibrary - visible={spriteLibraryVisible} - vm={vm} - onRequestClose={onRequestCloseSpriteLibrary} - /> - <CostumeLibrary - visible={costumeLibraryVisible} - vm={vm} - onRequestClose={onRequestCloseCostumeLibrary} - /> - <SoundLibrary - visible={soundLibraryVisible} - vm={vm} - onRequestClose={onRequestCloseSoundLibrary} - /> - <BackdropLibrary - visible={backdropLibraryVisible} - vm={vm} - onRequestClose={onRequestCloseBackdropLibrary} - /> - </Box> - </Box> + <SpriteLibrary + visible={spriteLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseSpriteLibrary} + /> + <CostumeLibrary + visible={costumeLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseCostumeLibrary} + /> + <SoundLibrary + visible={soundLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseSoundLibrary} + /> + <BackdropLibrary + visible={backdropLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseBackdropLibrary} + /> </Box> - ); - } -} + </Box> + </Box> +); + const spriteShape = PropTypes.shape({ costume: PropTypes.shape({ url: PropTypes.string, diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx index b19921b4c77605d0f1f8700d1e70e7e814aea283..62e9cd473a21a9db548868af3bd1d273bcc18359 100644 --- a/src/containers/backdrop-library.jsx +++ b/src/containers/backdrop-library.jsx @@ -4,7 +4,7 @@ const React = require('react'); const VM = require('scratch-vm'); const backdropLibraryContent = require('../lib/libraries/backdrops.json'); -const LibaryComponent = require('../components/library/library.jsx'); +const LibraryComponent = require('../components/library/library.jsx'); class BackdropLibrary extends React.Component { @@ -26,7 +26,7 @@ class BackdropLibrary extends React.Component { } render () { return ( - <LibaryComponent + <LibraryComponent data={backdropLibraryContent} title="Backdrop Library" visible={this.props.visible} diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index ecd816204ee5a7d760e04a67d0f34599065eb1f6..487bed48078b808835bc7209ae5696913fa2d784 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -52,7 +52,21 @@ class Blocks extends React.Component { this.attachVM(); } shouldComponentUpdate (nextProps, nextState) { - return this.state.prompt !== nextState.prompt; + return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible; + } + componentDidUpdate (prevProps) { + if (this.props.isVisible === prevProps.isVisible) { + return; + } + + // @todo hack to resize blockly manually in case resize happened while hidden + if (this.props.isVisible) { // Scripts tab + window.dispatchEvent(new Event('resize')); + this.workspace.setVisible(true); + this.workspace.toolbox_.refreshSelection(); + } else { + this.workspace.setVisible(false); + } } componentWillUnmount () { this.detachVM(); @@ -60,10 +74,11 @@ class Blocks extends React.Component { } attachVM () { this.workspace.addChangeListener(this.props.vm.blockListener); - this.workspace + this.flyoutWorkspace = this.workspace .getFlyout() - .getWorkspace() - .addChangeListener(this.props.vm.flyoutBlockListener); + .getWorkspace(); + this.flyoutWorkspace.addChangeListener(this.props.vm.flyoutBlockListener); + this.flyoutWorkspace.addChangeListener(this.props.vm.monitorBlockListener); this.props.vm.addListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); this.props.vm.addListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); this.props.vm.addListener('BLOCK_GLOW_ON', this.onBlockGlowOn); @@ -146,6 +161,7 @@ class Blocks extends React.Component { const { options, // eslint-disable-line no-unused-vars vm, // eslint-disable-line no-unused-vars + isVisible, // eslint-disable-line no-unused-vars ...props } = this.props; return ( @@ -169,11 +185,12 @@ class Blocks extends React.Component { } Blocks.propTypes = { + isVisible: PropTypes.bool.isRequired, options: PropTypes.shape({ media: PropTypes.string, zoom: PropTypes.shape({ - controls: PropTypes.boolean, - wheel: PropTypes.boolean, + controls: PropTypes.bool, + wheel: PropTypes.bool, startScale: PropTypes.number }), colours: PropTypes.shape({ diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx index 1fea39a2dda106f7941084469c81fb0886f32130..d0a004ba674e37c4710c581ef1c72bd7658d798d 100644 --- a/src/containers/costume-library.jsx +++ b/src/containers/costume-library.jsx @@ -4,7 +4,7 @@ const React = require('react'); const VM = require('scratch-vm'); const costumeLibraryContent = require('../lib/libraries/costumes.json'); -const LibaryComponent = require('../components/library/library.jsx'); +const LibraryComponent = require('../components/library/library.jsx'); class CostumeLibrary extends React.Component { @@ -26,7 +26,7 @@ class CostumeLibrary extends React.Component { } render () { return ( - <LibaryComponent + <LibraryComponent data={costumeLibraryContent} title="Costume Library" visible={this.props.visible} diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 2c6c04a7e65e777d575a9e009555a5543ecbce10..af73857e75f5a2427da74c69e9f2ebc5b65769be 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -23,28 +23,25 @@ class CostumeTab extends React.Component { this.state = {selectedCostumeIndex: 0}; } + componentWillReceiveProps (nextProps) { + const { + editingTarget, + sprites, + stage + } = nextProps; + + const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; + if (target && target.costumes && this.state.selectedCostumeIndex > target.costumes.length - 1) { + this.setState({selectedCostumeIndex: target.costumes.length - 1}); + } + } + handleSelectCostume (costumeIndex) { this.setState({selectedCostumeIndex: costumeIndex}); } handleDeleteCostume (costumeIndex) { - // @todo the VM should handle all of this logic - const {editingTarget} = this.props.vm; - - if (costumeIndex === editingTarget.currentCostume) { - editingTarget.setCostume(costumeIndex - 1); - } - - editingTarget.sprite.costumes = editingTarget.sprite.costumes - .slice(0, costumeIndex) - .concat(editingTarget.sprite.costumes.slice(costumeIndex + 1)); - this.props.vm.runtime.spriteInfoReport(editingTarget); - // @todo not sure if this is getting redrawn correctly - this.props.vm.runtime.requestRedraw(); - - this.setState({ - selectedCostumeIndex: this.state.selectedCostumeIndex % editingTarget.sprite.costumes.length - }); + this.props.vm.deleteCostume(costumeIndex); } render () { diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index dec5c6c6d671587f28259b64f9f2841d8527af7f..fbc11d7da672f36029eb3a5c8373b17ac5e44fbe 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -1,12 +1,20 @@ const PropTypes = require('prop-types'); const React = require('react'); const VM = require('scratch-vm'); +const bindAll = require('lodash.bindall'); const vmListenerHOC = require('../lib/vm-listener-hoc.jsx'); const GUIComponent = require('../components/gui/gui.jsx'); class GUI extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleTabSelect' + ]); + this.state = {tabIndex: 0}; + } componentDidMount () { this.props.vm.loadProject(this.props.projectData); this.props.vm.setCompatibilityMode(true); @@ -20,6 +28,9 @@ class GUI extends React.Component { componentWillUnmount () { this.props.vm.stopAll(); } + handleTabSelect (tabIndex) { + this.setState({tabIndex}); + } render () { const { projectData, // eslint-disable-line no-unused-vars @@ -28,7 +39,9 @@ class GUI extends React.Component { } = this.props; return ( <GUIComponent + tabIndex={this.state.tabIndex} vm={vm} + onTabSelect={this.handleTabSelect} {...componentProps} /> ); diff --git a/src/containers/load-button.jsx b/src/containers/load-button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..dcbd4fe016094c04c940428758c6c858b682deb0 --- /dev/null +++ b/src/containers/load-button.jsx @@ -0,0 +1,55 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const {connect} = require('react-redux'); + +const LoadButtonComponent = require('../components/load-button/load-button.jsx'); + +class LoadButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setFileInput', + 'handleChange', + 'handleClick' + ]); + } + handleChange (e) { + const reader = new FileReader(); + reader.onload = () => this.props.loadProject(reader.result); + reader.readAsText(e.target.files[0]); + } + handleClick () { + this.fileInput.click(); + } + setFileInput (input) { + this.fileInput = input; + } + render () { + const { + loadProject, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <LoadButtonComponent + inputRef={this.setFileInput} + onChange={this.handleChange} + onClick={this.handleClick} + {...props} + /> + ); + } +} + +LoadButton.propTypes = { + loadProject: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + loadProject: state.vm.fromJSON.bind(state.vm) +}); + +module.exports = connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(LoadButton); diff --git a/src/containers/save-button.jsx b/src/containers/save-button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f5e7ab018681e8008171ac64e8964db3e77a59ec --- /dev/null +++ b/src/containers/save-button.jsx @@ -0,0 +1,62 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const {connect} = require('react-redux'); + +const ButtonComponent = require('../components/button/button.jsx'); + +class SaveButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick' + ]); + } + handleClick () { + const json = this.props.saveProjectSb3(); + + // Download project data into a file - create link element, + // simulate click on it, and then remove it. + const saveLink = document.createElement('a'); + document.body.appendChild(saveLink); + + const data = new Blob([json], {type: 'text'}); + const url = window.URL.createObjectURL(data); + saveLink.href = url; + + // File name: project-DATE-TIME + const date = new Date(); + const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`; + saveLink.download = `project-${timestamp}.json`; + saveLink.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(saveLink); + } + render () { + const { + saveProjectSb3, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <ButtonComponent + onClick={this.handleClick} + {...props} + > + Save + </ButtonComponent> + ); + } +} + +SaveButton.propTypes = { + saveProjectSb3: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + saveProjectSb3: state.vm.saveProjectSb3.bind(state.vm) +}); + +module.exports = connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(SaveButton); diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index d478d141a7f6d8c538a03830b7583d4d39ad990c..53aa6bde02f9d54e0d394691549bd04416c7bce5 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -3,7 +3,7 @@ const PropTypes = require('prop-types'); const React = require('react'); const VM = require('scratch-vm'); const AudioEngine = require('scratch-audio'); -const LibaryComponent = require('../components/library/library.jsx'); +const LibraryComponent = require('../components/library/library.jsx'); const soundIcon = require('../components/asset-panel/icon--sound.svg'); @@ -14,14 +14,19 @@ class SoundLibrary extends React.Component { super(props); bindAll(this, [ 'handleItemSelected', - 'handleItemChosen' + 'handleItemMouseEnter', + 'handleItemMouseLeave' ]); } componentDidMount () { this.audioEngine = new AudioEngine(); this.player = this.audioEngine.createPlayer(); } - handleItemChosen (soundItem) { + componentWillReceiveProps (newProps) { + // Stop playing sounds if the library closes without a mouseleave (e.g. by using the escape key) + if (this.player && !newProps.visible) this.player.stopAllSounds(); + } + handleItemMouseEnter (soundItem) { const md5ext = soundItem._md5; const idParts = md5ext.split('.'); const md5 = idParts[0]; @@ -39,6 +44,9 @@ class SoundLibrary extends React.Component { this.player.playSound(soundItem._md5); }); } + handleItemMouseLeave () { + this.player.stopAllSounds(); + } handleItemSelected (soundItem) { const vmSound = { format: soundItem.format, @@ -64,11 +72,12 @@ class SoundLibrary extends React.Component { }); return ( - <LibaryComponent + <LibraryComponent data={soundLibraryThumbnailData} title="Sound Library" visible={this.props.visible} - onItemChosen={this.handleItemChosen} + onItemMouseEnter={this.handleItemMouseEnter} + onItemMouseLeave={this.handleItemMouseLeave} onItemSelected={this.handleItemSelected} onRequestClose={this.props.onRequestClose} /> diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index 227919dff524a8fee93616b2f1ba427feceef92a..70dfbdef7f6430f52dbb0d826e38cbfd58778fd8 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -23,6 +23,20 @@ class SoundTab extends React.Component { this.state = {selectedSoundIndex: 0}; } + componentWillReceiveProps (nextProps) { + const { + editingTarget, + sprites, + stage + } = nextProps; + + const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; + + if (target && target.sounds && this.state.selectedSoundIndex > target.sounds.length - 1) { + this.setState({selectedSoundIndex: target.sounds.length - 1}); + } + } + handleSelectSound (soundIndex) { const sound = this.props.vm.editingTarget.sprite.sounds[soundIndex]; this.props.vm.editingTarget.audioPlayer.playSound(sound.md5); @@ -30,17 +44,7 @@ class SoundTab extends React.Component { } handleDeleteSound (soundIndex) { - // @todo the VM should handle all of this logic - const {editingTarget} = this.props.vm; - editingTarget.sprite.sounds = editingTarget.sprite.sounds - .slice(0, soundIndex) - .concat(editingTarget.sprite.sounds.slice(soundIndex + 1)); - this.props.vm.emitTargetsUpdate(); - this.props.vm.runtime.requestRedraw(); - - this.setState({ - selectedSoundIndex: this.state.selectedSoundIndex % editingTarget.sprite.sounds.length - }); + this.props.vm.deleteSound(soundIndex); } render () { diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx index c08748eb2337728cd862fe051b78b0da03dfe22e..0a1bcaa3ebcad309733aa10c5f98faf25c68f56a 100644 --- a/src/containers/sprite-library.jsx +++ b/src/containers/sprite-library.jsx @@ -5,7 +5,7 @@ const VM = require('scratch-vm'); const spriteLibraryContent = require('../lib/libraries/sprites.json'); -const LibaryComponent = require('../components/library/library.jsx'); +const LibraryComponent = require('../components/library/library.jsx'); class SpriteLibrary extends React.Component { constructor (props) { @@ -19,7 +19,7 @@ class SpriteLibrary extends React.Component { } render () { return ( - <LibaryComponent + <LibraryComponent data={spriteLibraryContent} title="Sprite Library" visible={this.props.visible} diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index a9182996e171321066dc8cf52233816b32b6aa4f..d6c738cb3002688b3d812367f30b8cd8e27a1406 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -2,6 +2,8 @@ const bindAll = require('lodash.bindall'); const PropTypes = require('prop-types'); const React = require('react'); +const {connect} = require('react-redux'); + const SpriteSelectorItemComponent = require('../components/sprite-selector-item/sprite-selector-item.jsx'); class SpriteSelectorItem extends React.Component { @@ -24,9 +26,12 @@ class SpriteSelectorItem extends React.Component { } render () { const { - id, // eslint-disable-line no-unused-vars - onClick, // eslint-disable-line no-unused-vars - onDeleteButtonClick, // eslint-disable-line no-unused-vars + /* eslint-disable no-unused-vars */ + assetId, + id, + onClick, + onDeleteButtonClick, + /* eslint-enable no-unused-vars */ ...props } = this.props; return ( @@ -40,6 +45,7 @@ class SpriteSelectorItem extends React.Component { } SpriteSelectorItem.propTypes = { + assetId: PropTypes.string, costumeURL: PropTypes.string, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), name: PropTypes.string, @@ -48,4 +54,10 @@ SpriteSelectorItem.propTypes = { selected: PropTypes.bool }; -module.exports = SpriteSelectorItem; +const mapStateToProps = (state, {assetId, costumeURL}) => ({ + costumeURL: costumeURL || (assetId && state.vm.runtime.storage.get(assetId).encodeDataURI()) +}); + +module.exports = connect( + mapStateToProps +)(SpriteSelectorItem); diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx index d37a4437c51d2c6c8b6b631005fc5a938e3f7256..9b4b83d0d7a3efcdd98697915af2da8c51ba6a62 100644 --- a/src/containers/stage-selector.jsx +++ b/src/containers/stage-selector.jsx @@ -2,6 +2,8 @@ const bindAll = require('lodash.bindall'); const PropTypes = require('prop-types'); const React = require('react'); +const {connect} = require('react-redux'); + const StageSelectorComponent = require('../components/stage-selector/stage-selector.jsx'); class StageSelector extends React.Component { @@ -18,6 +20,7 @@ class StageSelector extends React.Component { render () { const { /* eslint-disable no-unused-vars */ + assetId, id, onSelect, /* eslint-enable no-unused-vars */ @@ -36,4 +39,12 @@ StageSelector.propTypes = { id: PropTypes.string, onSelect: PropTypes.func }; -module.exports = StageSelector; + +const mapStateToProps = (state, {assetId}) => ({ + url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI() +}); + +module.exports = connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(StageSelector); diff --git a/src/lib/blocks.js b/src/lib/blocks.js index cfd64d98f555686606109b458eefdaefa4ff692e..c0cf9e3ef107c1bbcbd22986d04ad9c6adfcd13d 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -9,7 +9,9 @@ module.exports = function (vm) { { type: 'field_dropdown', name: name, - options: start.concat(menuOptionsFn()) + options: function () { + return start.concat(menuOptionsFn()); + } } ], inputsInline: true, diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 8c80ae5d9694711a50a3974aa2704883d57de76e..e49de705e5aa5174d91d37829a6b483ba5ea7351 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -3,11 +3,10 @@ const PropTypes = require('prop-types'); const React = require('react'); const VM = require('scratch-vm'); -const Storage = require('./storage'); - const {connect} = require('react-redux'); const targets = require('../reducers/targets'); +const monitors = require('../reducers/monitors'); /* * Higher Order Component to manage events emitted by the VM @@ -29,7 +28,8 @@ const vmListenerHOC = function (WrappedComponent) { // 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); + this.props.vm.on('MONITORS_UPDATE', this.props.onMonitorsUpdate); + } componentDidMount () { if (this.props.attachKeyboardEvents) { @@ -76,7 +76,7 @@ const vmListenerHOC = function (WrappedComponent) { attachKeyboardEvents, onKeyDown, onKeyUp, - onSpriteInfoReport, + onMonitorsUpdate, onTargetsUpdate, /* eslint-enable no-unused-vars */ ...props @@ -88,24 +88,23 @@ const vmListenerHOC = function (WrappedComponent) { attachKeyboardEvents: PropTypes.bool, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, - onSpriteInfoReport: PropTypes.func, + onMonitorsUpdate: PropTypes.func, onTargetsUpdate: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; - const defaultVM = new VM('vm-listener-hoc'); - defaultVM.attachStorage(new Storage()); VMListener.defaultProps = { - attachKeyboardEvents: true, - vm: defaultVM + attachKeyboardEvents: true }; - const mapStateToProps = () => ({}); + const mapStateToProps = state => ({ + vm: state.vm + }); const mapDispatchToProps = dispatch => ({ onTargetsUpdate: data => { dispatch(targets.updateEditingTarget(data.editingTarget)); dispatch(targets.updateTargets(data.targetList)); }, - onSpriteInfoReport: spriteInfo => { - dispatch(targets.updateTarget(spriteInfo)); + onMonitorsUpdate: monitorList => { + dispatch(monitors.updateMonitors(monitorList)); } }); return connect( diff --git a/src/reducers/gui.js b/src/reducers/gui.js index bde1a2f3f1c6a02d9a8e1a7131754ef91ee3f33d..9b1cf60671fd255e31ba620dbdff2dd1d6f280ef 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -2,6 +2,7 @@ const {combineReducers} = require('redux'); module.exports = combineReducers({ modals: require('./modals'), + monitors: require('./monitors'), targets: require('./targets'), - monitors: require('./monitors') + vm: require('./vm') }); diff --git a/src/reducers/targets.js b/src/reducers/targets.js index c2f6219acea9de791eedbbe496bc0926448fe48f..0ed67cf2de94ceadf32e143037321458c5347a68 100644 --- a/src/reducers/targets.js +++ b/src/reducers/targets.js @@ -1,8 +1,5 @@ -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: {}, @@ -12,39 +9,19 @@ const initialState = { 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}}, - {[target.id]: state.sprites[target.id]}, - targets + (targets, target, listId) => Object.assign( + targets, + {[target.id]: {order: listId, ...target}} ), {} ), stage: action.targets - .filter(target => target.isStage) - .reduce( - (stage, target) => { - if (target.id !== stage.id) return target; - return defaultsDeep(target, stage); - }, - state.stage - ) + .filter(target => target.isStage)[0] || {} }); case UPDATE_EDITING_TARGET: return Object.assign({}, state, {editingTarget: action.target}); @@ -52,15 +29,6 @@ const reducer = function (state, action) { return state; } }; -reducer.updateTarget = function (target) { - return { - type: UPDATE_TARGET, - target: target, - meta: { - throttle: 30 - } - }; -}; reducer.updateTargets = function (targetList) { return { type: UPDATE_TARGET_LIST, diff --git a/src/reducers/vm.js b/src/reducers/vm.js new file mode 100644 index 0000000000000000000000000000000000000000..1fd0eb4a2f199b7329783d825445adcf550b6176 --- /dev/null +++ b/src/reducers/vm.js @@ -0,0 +1,24 @@ +const VM = require('scratch-vm'); +const Storage = require('../lib/storage'); + +const SET_VM = 'scratch-gui/vm/SET_VM'; +const defaultVM = new VM(); +defaultVM.attachStorage(new Storage()); +const initialState = defaultVM; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_VM: + return action.vm; + default: + return state; + } +}; +reducer.setVM = function (vm) { + return { + type: SET_VM, + vm: vm + }; +}; +module.exports = reducer;