diff --git a/package.json b/package.json index 7d221cc7cc3b81884a7ab67e7e34256a3b8d8f8d..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,30 +32,31 @@ "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", "lodash.defaultsdeep": "4.6.0", "lodash.isequal": "4.5.0", "lodash.omit": "4.5.0", "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", @@ -64,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/close-button/close-button.css b/src/components/close-button/close-button.css index ce9cc99ac96ba61f8b30bcdcd815de1dbfe63ded..66b79d8a6fd84948e29d846277eb2ed3577155c0 100644 --- a/src/components/close-button/close-button.css +++ b/src/components/close-button/close-button.css @@ -6,40 +6,46 @@ align-items: center; justify-content: center; - color: gray; - background-color: $blue; - + overflow: hidden; /* Mask the icon animation */ + background-color: rgba(0, 0, 0, 0.1); border-radius: 50%; - border-color: #dbdbdb; - border-style: solid; - + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - cursor: pointer; user-select: none; - transition: all 0.15s ease-out; /* @todo: standardize with var */ + cursor: pointer; + transition: all 0.15s ease-out; +} + +.close-button.large:hover { + transform: scale(1.1, 1.1); + box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.15); } .small { width: 1rem; height: 1rem; - border-width: 1px; + color: #FFF; + background-color: $blue; } .large { - width: 2.75rem; - height: 2.75rem; - border-width: 2px; + width: 1.75rem; + height: 1.75rem; } -.close-button:hover { - transform: scale(1.1, 1.1); -} - -/* Same icon as Sprite Selector Add button, but rotated. - @todo: reuse? -*/ .close-icon { + position: relative; + margin: 0.25rem; + user-select: none; transform-origin: 50%; transform: rotate(45deg); +} + +.small .close-icon { width: 40%; } + +.large .close-icon { + width: 0.75rem; + height: 0.75rem; +} diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx index f7c7fc141f28a93bd2833c67aaea1651e6e6d652..c2d78c79620b207c36e934ad16edf8106482cafe 100644 --- a/src/components/close-button/close-button.jsx +++ b/src/components/close-button/close-button.jsx @@ -11,8 +11,8 @@ const CloseButton = props => ( styles.closeButton, props.className, { - [styles.large]: props.size === CloseButton.SIZE_LARGE, - [styles.small]: props.size === CloseButton.SIZE_SMALL + [styles.small]: props.size === CloseButton.SIZE_SMALL, + [styles.large]: props.size === CloseButton.SIZE_LARGE } )} onClick={props.onClick} @@ -24,13 +24,13 @@ const CloseButton = props => ( </div> ); -CloseButton.SIZE_LARGE = 'large'; CloseButton.SIZE_SMALL = 'small'; +CloseButton.SIZE_LARGE = 'large'; CloseButton.propTypes = { className: PropTypes.string, onClick: PropTypes.func.isRequired, - size: PropTypes.oneOf([CloseButton.SIZE_LARGE, CloseButton.SIZE_SMALL]) + size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE]) }; CloseButton.defaultProps = { diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css new file mode 100644 index 0000000000000000000000000000000000000000..fa8e072e10bc8cea39150526b1d0184f46b9fa75 --- /dev/null +++ b/src/components/filter/filter.css @@ -0,0 +1,103 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; + +.filter { + display: flex; + flex-direction: row; + align-items: center; + /* + Should we use width 100% instead, for cases when component's + parent is not a flexbox container? + */ + flex-grow: 1; + + padding: 0.3rem 0.5rem; + background: rgba(0, 0, 0, 0.10); + border: 1px solid rgba(255, 255, 255, 0.10); + border-radius: 10rem; + user-select: none; +} + +.filter:hover { + background: rgba(0, 0, 0, 0.15); +} + +.filter-icon { + height: 0.9rem; + width: 0.9rem; + margin: 0 0.5rem 0 0.75rem; +} + +/* + Hidden state +*/ +.x-icon-wrapper { + opacity: 0; + + display: flex; + justify-content: center; + align-items: center; + + overflow: hidden; /* Mask the icon animation */ + height: 1.25rem; + width: 1.25rem; + margin: 0 0.25rem 0 0.5rem; /* @todo: move to parent to make component*/ + + border-radius: 50%; + pointer-events: none; + cursor: default; + transition: opacity 0.05s linear; +} + +/* + Shown state +*/ +.filter.is-active .x-icon-wrapper { + pointer-events: auto; + cursor: pointer; + opacity: 1; + transition: opacity 0.05s linear; +} + +.filter.is-active .x-icon-wrapper:hover { + transform: scale(1.2, 1.2); +} + +/* + Hidden state +*/ +.x-icon { + position: relative; + margin: 0.25rem; + user-select: none; + transform: translateX(0.5rem); + transition: transform 0.085s cubic-bezier(0.78, 1, 1, 1); +} + +/* + Shown state +*/ +.filter.is-active .x-icon-wrapper .x-icon { + transform: translateX(0); +} + +.filter-input { + flex-grow: 1; + line-height: 1.25rem; + background-color: transparent; + -webkit-appearance: none; + outline: none; + border: 0; + color: white; + font-size: 0.75rem; + letter-spacing: 0.15px; + cursor: text; +} + +.filter-input::placeholder { + opacity: 0.75; + padding: 0 0 0 0.25rem; + color: white; + font-size: 0.75rem; + letter-spacing: 0.15px; +} diff --git a/src/components/filter/filter.jsx b/src/components/filter/filter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6e140eb80b3c4bd6bd0f8bad350f447cb7865b36 --- /dev/null +++ b/src/components/filter/filter.jsx @@ -0,0 +1,57 @@ +const classNames = require('classnames'); +const PropTypes = require('prop-types'); +const React = require('react'); + +const filterIcon = require('./icon--filter.svg'); +const xIcon = require('./icon--x.svg'); +const styles = require('./filter.css'); + +const FilterComponent = props => { + const { + onChange, + onClear, + placeholderText, + filterQuery + } = props; + return ( + <div + className={classNames({ + [styles.filter]: true, + [styles.isActive]: filterQuery.length > 0 + })} + > + <img + className={styles.filterIcon} + src={filterIcon} + /> + <input + autoFocus + className={styles.filterInput} + placeholder={placeholderText} + type="text" + value={filterQuery} + onChange={onChange} + /> + <div + className={styles.xIconWrapper} + onClick={onClear} + > + <img + className={styles.xIcon} + src={xIcon} + /> + </div> + </div> + ); +}; + +FilterComponent.propTypes = { + filterQuery: PropTypes.string, + onChange: PropTypes.func, + onClear: PropTypes.func, + placeholderText: PropTypes.string +}; +FilterComponent.defaultProps = { + placeholderText: 'what are you looking for?' +}; +module.exports = FilterComponent; diff --git a/src/components/filter/icon--filter.svg b/src/components/filter/icon--filter.svg new file mode 100644 index 0000000000000000000000000000000000000000..2a0c75fcb9ffc6aa604b66b0ce6320994e2b086a Binary files /dev/null and b/src/components/filter/icon--filter.svg differ diff --git a/src/components/filter/icon--x.svg b/src/components/filter/icon--x.svg new file mode 100644 index 0000000000000000000000000000000000000000..e78a2a8e24a8fe526004cd1ec4e1a2622dec76d2 Binary files /dev/null and b/src/components/filter/icon--x.svg differ 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..3102d3d2d2a29d5f58db70f638b3d15fc5404491 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -6,8 +6,9 @@ align-items: center; justify-content: center; flex-basis: 160px; + max-width: 160px; height: 160px; - margin: calc($space / 2); + margin: $space; padding: 1rem 1rem 0 1rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #575e75; @@ -23,14 +24,6 @@ .library-item:hover { border-width: 2px; border-color: #1dacf4; - transition: 0.1s ease-out; - 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 { @@ -47,12 +40,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.css b/src/components/library/library.css index b8147a1736f330d8c5c8cd3ec6ef9e63984bfc6f..73f615191d159177433d271193fb8b7b73f9f47c 100644 --- a/src/components/library/library.css +++ b/src/components/library/library.css @@ -1,24 +1,14 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; + .library-scroll-grid { display: flex; + justify-content: center; + align-content: flex-start; + background: $ui-pane-gray; flex-grow: 1; - justify-content: space-between; flex-wrap: wrap; - overflow-y: scroll; - height: calc(100% - 6rem); /* @todo: currently estimate, fix precision */ - - /* - Gives sprites a bit of room so they don't get cut off when they grow on hover - @todo: sync as a var, with the transform defined on .library-item:hover - */ - padding: 0.15rem; -} - -.modal-header { - width: 100%; - margin-bottom: 2rem; + overflow-y: auto; + height: calc(100% - $library-header-height); padding: 0.5rem; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1.5rem; - font-weight: normal; - color: #8e8f95; } diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 16327c10c80cb075658e28484465998bf1ce7c4c..ad33743be1e025b8587db9c031df00f6b5378a5b 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -10,42 +10,62 @@ const styles = require('./library.css'); class LibraryComponent extends React.Component { constructor (props) { super(props); - bindAll(this, ['handleSelect']); - this.state = {selectedItem: null}; + bindAll(this, [ + 'handleFilterChange', + 'handleFilterClear', + 'handleMouseEnter', + 'handleMouseLeave', + 'handleSelect' + ]); + this.state = { + selectedItem: null, + filterQuery: '' + }; } 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.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({filterQuery: event.target.value}); + } + handleFilterClear () { + this.setState({filterQuery: ''}); + } + getFilteredData () { + return this.props.data.filter(dataItem => + dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1); } render () { if (!this.props.visible) return null; return ( <ModalComponent contentLabel={this.props.title} + filterQuery={this.state.filterQuery} visible={this.props.visible} + onFilterChange={this.handleFilterChange} + onFilterClear={this.handleFilterClear} onRequestClose={this.props.onRequestClose} > - <h1 className={styles.modalHeader}>{this.props.title}</h1> <div className={styles.libraryScrollGrid}> - {this.props.data.map((dataItem, itemId) => { + {this.getFilteredData().map((dataItem, index) => { const scratchURL = dataItem.md5 ? `https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` : dataItem.rawURL; return ( <LibraryItem iconURL={scratchURL} - id={itemId} - key={`item_${itemId}`} + id={index} + key={`item_${index}`} name={dataItem.name} - selected={this.state.selectedItem === itemId} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} /> ); @@ -59,6 +79,7 @@ class LibraryComponent extends React.Component { LibraryComponent.propTypes = { data: PropTypes.arrayOf( /* eslint-disable react/no-unused-prop-types, lines-around-comment */ + // An item in the library PropTypes.shape({ // @todo remove md5/rawURL prop from library, refactor to use storage md5: PropTypes.string, @@ -67,7 +88,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/modal/modal.css b/src/components/modal/modal.css index 25a134f5e689d5d80dae6ca88fe6a7a693dccce3..a72a4f06dbc18c67a672a166b7fddb57fbe66a0f 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -11,32 +11,69 @@ background-color: rgba(0, 0, 0, .75); } -.modal-content { +/* @todo: extract to type: full ? */ +.full-modal-content { position: absolute; outline: none; - overflow-y: scroll; + overflow-y: auto; -webkit-overflow-scrolling: 'touch'; - border: 1px solid #ccc; - padding: 0; - top: 5%; - right: 5%; - bottom: 5%; - left: 5%; - border-radius: $space; user-select: none; + height: 100%; + width: 100%; + display: flex; } -.modal-children { - overflow: hidden; - height: 100%; - z-index: 0; - padding: 2rem; - background: $ui-pane-gray; +/* + Modal header has 3 items: + |filter title x| + + Use the same width for both side item containers, + so that title remains centered +*/ +$sides: 20rem; + +.header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + height: $library-header-height; + + box-sizing: border-box; + width: 100%; + background-color: $blue; + + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: normal; } -.close-button { - position: absolute; - top: 1rem; - right: 1rem; - z-index: 2; +.header-item { + display: flex; + align-items: center; + padding: 1rem; + text-decoration: none; + color: white; + user-select: none; } + +.header-item-filter { + display: flex; + flex-basis: $sides; + justify-content: flex-start; +} + +.header-item-title { + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + user-select: none; + letter-spacing: 0.4px; + cursor: default; +} + +.header-item-close { + flex-basis: $sides; + justify-content: flex-end; +} + diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index 3a3fc25db56a00488657e121ea6a41c076c3bfc6..2a743bd74447dae5cbf7465a906134551dfeafd0 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -1,9 +1,11 @@ +const classNames = require('classnames'); const PropTypes = require('prop-types'); const React = require('react'); const ReactModal = require('react-modal'); const Box = require('../box/box.jsx'); const CloseButton = require('../close-button/close-button.jsx'); +const Filter = require('../filter/filter.jsx'); const styles = require('./modal.css'); @@ -11,21 +13,44 @@ class ModalComponent extends React.Component { render () { return ( <ReactModal - className={styles.modalContent} + className={styles.fullModalContent} contentLabel={this.props.contentLabel} isOpen={this.props.visible} overlayClassName={styles.modalOverlay} ref={m => (this.modal = m)} - onRequestClose={this.props.onRequestClose} > - <CloseButton - className={styles.closeButton} - onClick={this.props.onRequestClose} - /> <Box - className={styles.modalChildren} direction="column" + grow={1} > + <div className={styles.header}> + <div className={classNames(styles.headerItem, styles.headerItemFilter)}> + <Filter + filterQuery={this.props.filterQuery} + onChange={this.props.onFilterChange} + onClear={this.props.onFilterClear} + /> + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemTitle + )} + > + {this.props.contentLabel} + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemClose + )} + > + <CloseButton + size={CloseButton.SIZE_LARGE} + onClick={this.props.onRequestClose} + /> + </div> + </div> {this.props.children} </Box> </ReactModal> @@ -36,7 +61,10 @@ class ModalComponent extends React.Component { ModalComponent.propTypes = { children: PropTypes.node, contentLabel: PropTypes.string.isRequired, - onRequestClose: PropTypes.func.isRequired, + filterQuery: PropTypes.string, + onFilterChange: PropTypes.func, + onFilterClear: PropTypes.func, + onRequestClose: PropTypes.func, visible: PropTypes.bool.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/blocks.jsx b/src/containers/blocks.jsx index 487bed48078b808835bc7209ae5696913fa2d784..a2146d3f8eca07cb5ce5c4fa30cb7eb0b4e23045 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -1,4 +1,5 @@ const bindAll = require('lodash.bindall'); +const debounce = require('lodash.debounce'); const defaultsDeep = require('lodash.defaultsdeep'); const PropTypes = require('prop-types'); const React = require('react'); @@ -30,6 +31,7 @@ class Blocks extends React.Component { 'onScriptGlowOff', 'onBlockGlowOn', 'onBlockGlowOff', + 'onTargetsUpdate', 'onVisualReport', 'onWorkspaceUpdate', 'onWorkspaceMetricsChange', @@ -40,6 +42,7 @@ class Blocks extends React.Component { workspaceMetrics: {}, prompt: null }; + this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); } componentDidMount () { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); @@ -85,6 +88,7 @@ class Blocks extends React.Component { this.props.vm.addListener('BLOCK_GLOW_OFF', this.onBlockGlowOff); this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport); this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate); + this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -93,8 +97,25 @@ class Blocks extends React.Component { this.props.vm.removeListener('BLOCK_GLOW_OFF', this.onBlockGlowOff); this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport); this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate); + this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate); + } + updateToolboxBlockValue (id, value) { + this.workspace + .getFlyout() + .getWorkspace() + .getBlockById(id) + .inputList[0] + .fieldRow[0] + .setValue(value); + } + onTargetsUpdate () { + if (this.props.vm.editingTarget) { + ['glide', 'move', 'set'].forEach(prefix => { + this.updateToolboxBlockValue(`${prefix}x`, this.props.vm.editingTarget.x.toFixed(0)); + this.updateToolboxBlockValue(`${prefix}y`, this.props.vm.editingTarget.y.toFixed(0)); + }); + } } - onWorkspaceMetricsChange () { const target = this.props.vm.editingTarget; if (target && target.id) { 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/css/units.css b/src/css/units.css index ab4393ac1afefb3d770289b67b7c1b8983e6c2b9..3f9aaba17d8319e322b48c817438ed4e8b92458e 100644 --- a/src/css/units.css +++ b/src/css/units.css @@ -3,5 +3,7 @@ $space: 0.5rem; $sprites-per-row: 5; $menu-bar-height: 3rem; -$sprite-info-height: 5.25rem; /* TODO: SpriteInfo isn't explicitly set to this height yet */ +$sprite-info-height: 5.25rem; /* @todo: SpriteInfo isn't explicitly set to this height yet */ $stage-menu-height: 3rem; + +$library-header-height: 4.375rem; \ No newline at end of file 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,