diff --git a/package.json b/package.json index bd02cf06847e94372ac0e544049bd5df4a01d59b..eeabbe19feb400c151450533a50b8e8ddaa34392 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.debounce": "4.0.8", @@ -46,17 +46,17 @@ "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", @@ -65,7 +65,7 @@ "scratch-render": "^0.1.0-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/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 9bf21c40de33d13d0edb32d36522bbcac7a728c0..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,12 +12,13 @@ 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, @@ -31,6 +36,15 @@ const GUIComponent = props => { ); } + 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 ( <Box className={styles.pageWrapper} @@ -41,16 +55,18 @@ 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 + 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} @@ -62,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> diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index e25c478d2af2147ee88ebccb472fa3d00fff69dc..c702492f8939412c2222f49bf0d99daa5076a33f 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -27,12 +27,6 @@ transform: scale(1.02, 1.02); } -.library-item.is-selected { - border-width: 2px; - border-color: #1dacf4; - transition: 0.25s ease-out; -} - .library-item-image-container { height: 100px; } @@ -47,12 +41,12 @@ margin: 0.25rem 0; text-align: center; - /* + /* For truncating overflowing text gracefully Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - min-width: 0; + min-width: 0; } 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 16327c10c80cb075658e28484465998bf1ce7c4c..71b29ff612ff1552eae7df150f2eaa9782a9120d 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -10,20 +10,21 @@ const styles = require('./library.css'); class LibraryComponent extends React.Component { constructor (props) { super(props); - bindAll(this, ['handleSelect']); - this.state = {selectedItem: null}; + bindAll(this, [ + 'handleSelect', + 'handleMouseEnter', + 'handleMouseLeave' + ]); } 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]); - } else { - if (this.props.onItemChosen) { - this.props.onItemChosen(this.props.data[id]); - } - } - this.setState({selectedItem: id}); + this.props.onRequestClose(); + this.props.onItemSelected(this.props.data[id]); + } + handleMouseEnter (id) { + if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.props.data[id]); + } + handleMouseLeave (id) { + if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.props.data[id]); } render () { if (!this.props.visible) return null; @@ -45,7 +46,8 @@ class LibraryComponent extends React.Component { id={itemId} key={`item_${itemId}`} name={dataItem.name} - selected={this.state.selectedItem === itemId} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} /> ); @@ -67,7 +69,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/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/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 24f54bc7832152757c7b0cfb9fcccc2050729d2c..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.requestTargetsUpdate(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/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/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,