diff --git a/README.md b/README.md index c036a170930285319aff1fe3f5b348883df9ec65..4b5b91ea8c4d429d11f1e87b0f49af2d88ccf874 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,21 @@ npm start ``` Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component +## Developing alongside other Scratch repositories + +If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish +to have the other repositories use your local scratch-gui build instead of fetching the current production +version of the scratch-gui that is found by default using `npm install`. + +To do this: + +1. Make sure you have run `npm install` from this repository's top level +2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui +3. From this repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build` +4. From this repository's top level, establish a link to this repository by running `npm link` +5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui` +6. Build or run the repositories that depend on scratch-gui + ## Testing NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64. diff --git a/package.json b/package.json index f1ddf3c3ef546c10342d1c6a3d6b6d4367a2e5a0..c43fb30c03283d3ec1519601f6d88823dae46c46 100644 --- a/package.json +++ b/package.json @@ -96,13 +96,13 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1534513944", - "scratch-l10n": "3.0.20180817135616", - "scratch-paint": "0.2.0-prerelease.20180816205442", - "scratch-render": "0.1.0-prerelease.20180811013828", - "scratch-storage": "0.5.1", + "scratch-blocks": "0.1.0-prerelease.1535116879", + "scratch-l10n": "3.0.20180824134256", + "scratch-paint": "0.2.0-prerelease.20180823231354", + "scratch-render": "0.1.0-prerelease.20180824141819", + "scratch-storage": "1.0.0", "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", - "scratch-vm": "0.2.0-prerelease.20180816213529", + "scratch-vm": "0.2.0-prerelease.20180824135031", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.22.1", diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css index 9a999057b57a29f12b8e288365605a814b5864f1..bfd578909fdf2a8efd826bba4899fd7e8240a4bd 100644 --- a/src/components/asset-panel/asset-panel.css +++ b/src/components/asset-panel/asset-panel.css @@ -5,16 +5,32 @@ display: flex; flex-grow: 1; border: 1px solid $ui-black-transparent; - border-top-right-radius: $space; background: white; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 0.85rem; } +[dir="ltr"] .wrapper { + border-top-right-radius: $space; + border-bottom-right-radius: $space; +} + +[dir="rtl"] .wrapper { + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .detail-area { display: flex; flex-grow: 1; flex-shrink: 1; - border-left: 1px solid $ui-black-transparent; overflow-y: auto; } + +[dir="ltr"] .detail-area { + border-left: 1px solid $ui-black-transparent; +} + +[dir="rtl"] .detail-area { + border-right: 1px solid $ui-black-transparent; +} diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index 9143844d0973b35b457092b66132ed6135256d07..33585ca90de66225f89228aa5aa9a35f6798adca 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -9,7 +9,6 @@ .backpack-header { margin-top: 0.5rem; border: 1px solid $ui-black-transparent; - border-top-right-radius: $space; background: $ui-white; padding: 0.25rem; text-align: center; @@ -20,6 +19,14 @@ user-select: none; } +[dir="ltr"] .backpack-header { + border-top-right-radius: $space; +} + +[dir="rtl"] .backpack-header { + border-top-left-radius: $space; +} + .backpack-list { position: relative; display: flex; diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index e02486ce0fe376d091090b4355d2a0febc3c8331..3d87405893c9268041744d1d5d2aecfa4bd4afc6 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -12,6 +12,13 @@ border-bottom-right-radius: $space; } +[dir="rtl"] .blocks :global(.injectionDiv) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .blocks :global(.blocklyMainBackground) { stroke: none; } @@ -31,6 +38,11 @@ -ms-overflow-style: none; } +[dir="rtl"] .blocks :global(.blocklyToolboxDiv) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + .blocks :global(.blocklyToolboxDiv::-webkit-scrollbar) { display: none; } @@ -40,6 +52,12 @@ box-sizing: content-box; } +[dir="rtl"] .blocks :global(.blocklyFlyout) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + + .blocks :global(.blocklyBlockDragSurface) { /* Fix an issue where the drag surface was preventing hover events for sharing blocks. diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 46cf41d2b79baf011980c08bef45b07eac1e9611..757653e9ac32d18507cf2f8d2d433a2ecf8562e8 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -130,6 +130,10 @@ [dir="rtl"] .tab img { margin-left: 0.125rem; +} + +/* only mirror blocks tab icon */ +[dir="rtl"] .tab:nth-of-type(1) img { transform: scaleX(-1); } diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx index 1028d5ddc20f0490a19bfecbf82526869a5a2d27..60b130212281496375a7066f9c87aa4543104336 100644 --- a/src/components/import-modal/import-modal.jsx +++ b/src/components/import-modal/import-modal.jsx @@ -156,6 +156,7 @@ ImportModal.propTypes = { hasValidationError: PropTypes.bool.isRequired, inputValue: PropTypes.string.isRequired, intl: intlShape.isRequired, + isRtl: PropTypes.bool, onCancel: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onGoBack: PropTypes.func.isRequired, diff --git a/src/components/language-selector/language-selector.css b/src/components/language-selector/language-selector.css index d8ae588ae13fd05fc9d6b07eb6316de7427a4613..25b9951608e138250d158906fcd56833a3790348 100644 --- a/src/components/language-selector/language-selector.css +++ b/src/components/language-selector/language-selector.css @@ -5,19 +5,27 @@ height: 1.5rem; } -.disabled { - opacity: .5; -} - +/* Position the language select over the language icon, and make it transparent */ .language-select { - margin: .5rem; - height: 1.85rem; - border: 1px solid $motion-primary; + position: absolute; + width: $language-selector-width; + height: $menu-bar-height; + opacity: 0; user-select: none; - outline: none; - background: rgba(255, 255, 255, 0.5); - color: $motion-tertiary; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: .875rem; +} + +[dir="ltr"] .language-select { + right: 0; +} + +[dir="rtl"] .language-select { + left: 0; +} + +.language-select option { + opacity: 1; } .language-select:focus { diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx index a6707bbced246129ee8d464694b6a117ec833555..fd4caef5f93590ddcd5878920ca4c084d292545c 100644 --- a/src/components/language-selector/language-selector.jsx +++ b/src/components/language-selector/language-selector.jsx @@ -1,58 +1,38 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Box from '../box/box.jsx'; import locales from 'scratch-l10n'; import styles from './language-selector.css'; // supported languages to exclude from the menu, but allow as a URL option const ignore = ['he']; -class LanguageSelector extends React.Component { - render () { - const { - componentRef, - currentLocale, - onChange, - ...componentProps - } = this.props; - return ( - <Box - {...componentProps} - > - <div - className={styles.group} - ref={componentRef} - > - <select - className={styles.languageSelect} - value={currentLocale} - onChange={onChange} +const LanguageSelector = ({currentLocale, label, onChange}) => ( + <select + aria-label={label} + className={styles.languageSelect} + value={currentLocale} + onChange={onChange} + > + { + Object.keys(locales) + .filter(l => !ignore.includes(l)) + .map(locale => ( + <option + key={locale} + value={locale} > - { - Object.keys(locales) - .filter(l => !ignore.includes(l)) - .map(locale => ( - <option - key={locale} - value={locale} - > - {locales[locale].name} - </option> - )) - } - </select> - </div> - </Box> - ); - } -} + {locales[locale].name} + </option> + )) + } + </select> +); LanguageSelector.propTypes = { - componentRef: PropTypes.func, currentLocale: PropTypes.string, - onChange: PropTypes.func, - open: PropTypes.bool + label: PropTypes.string, + onChange: PropTypes.func }; export default LanguageSelector; diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 3258b4a7bd16162f239d73990a26df77894f2ea0..fd7217c6744937c184d5e2115a4dcdc284b0a7a9 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -99,7 +99,6 @@ .coming-soon-text { position: absolute; - transform: translate(calc(2 * $space), calc(2 * $space)); background-color: $data-primary; border-radius: 1rem; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25); @@ -108,3 +107,11 @@ font-weight: bold; color: $ui-white; } + +[dir="ltr"] .coming-soon-text { + transform: translate(calc(2 * $space), calc(2 * $space)); +} + +[dir="rtl"] .coming-soon-text { + transform: translate(calc(-2 * $space), calc(2 * $space)); +} diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index c1ae3f9fa2144059547841170b555535081062f8..de2854c5006508276a78718b87d337301e162094 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -48,8 +48,13 @@ height: 1.5rem; } +.language-caret { + margin-bottom: .625rem; +} + .language-menu { display: inline-flex; + width: $language-selector-width; } .menu { diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 11562411086b5cb47d97300f424d6494a1fd7000..5eb4ad3312d5a27d333b128ff70739c1eaae6c6b 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -137,7 +137,8 @@ class MenuBar extends React.Component { super(props); bindAll(this, [ 'handleLanguageMouseUp', - 'handleRestoreOption' + 'handleRestoreOption', + 'restoreOptionMessage' ]); } handleLanguageMouseUp (e) { @@ -151,6 +152,35 @@ class MenuBar extends React.Component { this.props.onRequestCloseEdit(); }; } + restoreOptionMessage (deletedItem) { + switch (deletedItem) { + case 'Sprite': + return (<FormattedMessage + defaultMessage="Restore Sprite" + description="Menu bar item for restoring the last deleted sprite." + id="gui.menuBar.restoreSprite" + />); + case 'Sound': + return (<FormattedMessage + defaultMessage="Restore Sound" + description="Menu bar item for restoring the last deleted sound." + id="gui.menuBar.restoreSound" + />); + case 'Costume': + return (<FormattedMessage + defaultMessage="Restore Costume" + description="Menu bar item for restoring the last deleted costume." + id="gui.menuBar.restoreCostume" + />); + default: { + return (<FormattedMessage + defaultMessage="Restore" + description="Menu bar item for restoring the last deleted item in its disabled state." /* eslint-disable-line max-len */ + id="gui.menuBar.restore" + />); + } + } + } render () { return ( <Box className={styles.menuBar}> @@ -165,39 +195,19 @@ class MenuBar extends React.Component { /> </div> <div - className={classNames(styles.menuBarItem, styles.hoverable, { - [styles.active]: this.props.languageMenuOpen - })} - onMouseUp={this.handleLanguageMouseUp} + className={classNames(styles.menuBarItem, styles.hoverable, styles.languageMenu)} > - {/* @TODO: remove coming soon tooltip wrapper https://github.com/LLK/scratch-gui/issues/2664 */} - <MenuBarItemTooltip - enable - id="menubar-selector" - place="right" - > - <div - aria-label={this.props.intl.formatMessage(ariaMessages.language)} - className={classNames(styles.languageMenu)} - > - <img - className={styles.languageIcon} - src={languageIcon} - /> - <img - className={styles.dropdownCaret} - src={dropdownCaret} - /> - </div> - <MenuBarMenu - open={this.props.languageMenuOpen} - place={this.props.isRtl ? 'left' : 'right'} - onRequestClose={this.props.onRequestCloseLanguage} - > - <LanguageSelector /> - </MenuBarMenu> - - </MenuBarItemTooltip> + <div> + <img + className={styles.languageIcon} + src={languageIcon} + /> + <img + className={styles.languageCaret} + src={dropdownCaret} + /> + </div> + <LanguageSelector label={this.props.intl.formatMessage(ariaMessages.language)} /> </div> <div className={classNames(styles.menuBarItem, styles.hoverable, { @@ -306,18 +316,7 @@ class MenuBar extends React.Component { className={classNames({[styles.disabled]: !restorable})} onClick={this.handleRestoreOption(handleRestore)} > - {deletedItem === 'Sprite' ? - <FormattedMessage - defaultMessage="Restore Sprite" - description="Menu bar item for restoring the last deleted sprite." - id="gui.menuBar.restoreSprite" - /> : - <FormattedMessage - defaultMessage="Restore" - description="Menu bar item for restoring the last deleted item in its disabled state." /* eslint-disable-line max-len */ - id="gui.menuBar.restore" - /> - } + {this.restoreOptionMessage(deletedItem)} </MenuItem> )}</DeletionRestorer> <MenuSection> diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index ae523e188013ad9c79433ed908f4623665551a50..911271f1997f4d2e37b5a12f8a5b18eda0fea582 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -88,8 +88,15 @@ .more-options-icon { width: .75rem; height: .75rem; - margin-left: .5rem; vertical-align: middle; padding-bottom: .2rem; opacity: .5; } + +[dir="ltr"] .more-options-icon { + margin-left: .5rem; +} + +[dir="rtl"] .more-options-icon { + margin-right: .5rem; +} diff --git a/src/components/stage-header/stage-header.css b/src/components/stage-header/stage-header.css index dd2dc2c65ca642338358d681f572a1b04796fd14..3e6f59c75ac245d74441f709acfdbb1689017ca2 100644 --- a/src/components/stage-header/stage-header.css +++ b/src/components/stage-header/stage-header.css @@ -59,6 +59,10 @@ height: 100%; } +[dir="rtl"] .stage-button-icon { + transform: scaleX(-1); +} + [dir="ltr"] .stage-button-first { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css index 404283d19fd8c77bafc1e9f0b307628168d1cfc4..07b07cb161a62ebd9f61f1a158115a80076a3471 100644 --- a/src/components/target-pane/target-pane.css +++ b/src/components/target-pane/target-pane.css @@ -11,5 +11,12 @@ display: flex; flex-basis: 72px; flex-shrink: 0; +} + +[dir="ltr"] .stage-selector-wrapper { margin-left: calc($space / 2); } + +[dir="rtl"] .stage-selector-wrapper { + margin-right: calc($space / 2); +} diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx index 7cc52870fea7dad1e58b168d406019a4a4549162..2869a047a6ba5722762d86f32875419cd14cfb0b 100644 --- a/src/containers/controls.jsx +++ b/src/containers/controls.jsx @@ -64,5 +64,7 @@ const mapStateToProps = state => ({ projectRunning: state.scratchGui.vmStatus.running, turbo: state.scratchGui.vmStatus.turbo }); +// no-op function to prevent dispatch prop being passed to component +const mapDispatchToProps = () => ({}); -export default connect(mapStateToProps)(Controls); +export default connect(mapStateToProps, mapDispatchToProps)(Controls); diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 454c5ffcef7a203ac7a927bb4b6873cb4b0041f1..4c63a8cceb7bec2c86632886b6e195ff1c4535ae 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -24,6 +24,8 @@ import { SOUNDS_TAB_INDEX } from '../reducers/editor-tab'; +import {setRestore} from '../reducers/restore-deletion'; + import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg'; import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg'; import fileUploadIcon from '../components/action-menu/icon--file-upload.svg'; @@ -135,7 +137,11 @@ class CostumeTab extends React.Component { this.setState({selectedCostumeIndex: costumeIndex}); } handleDeleteCostume (costumeIndex) { - this.props.vm.deleteCostume(costumeIndex); + const restoreCostumeFun = this.props.vm.deleteCostume(costumeIndex); + this.props.dispatchUpdateRestore({ + restoreFun: restoreCostumeFun, + deletedItem: 'Costume' + }); } handleDuplicateCostume (costumeIndex) { this.props.vm.duplicateCostume(costumeIndex); @@ -232,6 +238,7 @@ class CostumeTab extends React.Component { } render () { const { + dispatchUpdateRestore, // eslint-disable-line no-unused-vars intl, onNewCostumeFromCameraClick, onNewLibraryBackdropClick, @@ -325,6 +332,7 @@ class CostumeTab extends React.Component { CostumeTab.propTypes = { cameraModalVisible: PropTypes.bool, + dispatchUpdateRestore: PropTypes.func, editingTarget: PropTypes.string, intl: intlShape, onActivateSoundsTab: PropTypes.func.isRequired, @@ -372,6 +380,9 @@ const mapDispatchToProps = dispatch => ({ }, onRequestCloseCameraModal: () => { dispatch(closeCameraCapture()); + }, + dispatchUpdateRestore: restoreState => { + dispatch(setRestore(restoreState)); } }); diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx index fbb15f3b998d1deb80cb309146c1dfa48d88a593..6485a41b84b7cf227ec4e54e496f2afbe0d88c2f 100644 --- a/src/containers/error-boundary.jsx +++ b/src/containers/error-boundary.jsx @@ -78,6 +78,6 @@ const mapStateToProps = state => ({ }); // no-op function to prevent dispatch prop being passed to component -const mapDispatchToProps = () => {}; +const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(ErrorBoundary); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index cdcb87c8985b31498c8e5eee61c54067225901fe..33d8fe58b247c28a3760529ecdbaefcd31190599 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -94,6 +94,7 @@ class GUI extends React.Component { } GUI.propTypes = { + assetHost: PropTypes.string, children: PropTypes.node, fetchingProject: PropTypes.bool, importInfoVisible: PropTypes.bool, @@ -101,6 +102,7 @@ GUI.propTypes = { onSeeCommunity: PropTypes.func, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + projectHost: PropTypes.string, vm: PropTypes.instanceOf(VM) }; diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index 390f6ef598b7ca63f52868529ba9720a0f652d24..dc14578bd6112b8081a98025d6089aa7da5631c9 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -56,6 +56,7 @@ PaintEditorWrapper.propTypes = { name: PropTypes.string, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, + rtl: PropTypes.bool, selectedCostumeIndex: PropTypes.number.isRequired, vm: PropTypes.instanceOf(VM) }; @@ -74,6 +75,7 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { imageFormat: costume && costume.dataFormat, imageId: targetId && `${targetId}${costume.skinId}`, image: state.scratchGui.vm.getCostume(index), + rtl: state.locales.isRtl, vm: state.scratchGui.vm }; }; diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index 2245f3d2f6acda3304d8de506cfde811cd0ece16..a30b8f97e82f93fe1407cc55271f81273feef3f1 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -34,6 +34,8 @@ import { COSTUMES_TAB_INDEX } from '../reducers/editor-tab'; +import {setRestore} from '../reducers/restore-deletion'; + class SoundTab extends React.Component { constructor (props) { super(props); @@ -76,10 +78,11 @@ class SoundTab extends React.Component { } handleDeleteSound (soundIndex) { - this.props.vm.deleteSound(soundIndex); + const restoreFun = this.props.vm.deleteSound(soundIndex); if (soundIndex >= this.state.selectedSoundIndex) { this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)}); } + this.props.dispatchUpdateRestore({restoreFun, deletedItem: 'Sound'}); } handleDuplicateSound (soundIndex) { @@ -153,6 +156,7 @@ class SoundTab extends React.Component { render () { const { + dispatchUpdateRestore, // eslint-disable-line no-unused-vars intl, vm, onNewSoundFromLibraryClick, @@ -252,6 +256,7 @@ class SoundTab extends React.Component { } SoundTab.propTypes = { + dispatchUpdateRestore: PropTypes.func, editingTarget: PropTypes.string, intl: intlShape, onActivateCostumesTab: PropTypes.func.isRequired, @@ -294,6 +299,9 @@ const mapDispatchToProps = dispatch => ({ }, onRequestCloseSoundLibrary: () => { dispatch(closeSoundLibrary()); + }, + dispatchUpdateRestore: restoreState => { + dispatch(setRestore(restoreState)); } }); diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index a0197ad6dd9ecfc09e61c5a783878395eef7e509..01e617c8e77385244507977b037c7f31a918a26d 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -2,28 +2,24 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; -import {defineMessages, injectIntl, intlShape} from 'react-intl'; import {setHoveredSprite} from '../reducers/hovered-target'; import {updateAssetDrag} from '../reducers/asset-drag'; import {getEventXY} from '../lib/touch-utils'; +import VM from 'scratch-vm'; +import {SVGRenderer} from 'scratch-svg-renderer'; import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx'; const dragThreshold = 3; // Same as the block drag threshold - -const messages = defineMessages({ - deleteSpriteConfirmation: { - defaultMessage: 'Are you sure you want to delete this?', - description: 'Confirmation for deleting sprites', - id: 'gui.spriteSelectorItem.deleteSpriteConfirmation' - } -}); +// Contains 'font-family', but doesn't only contain 'font-family="none"' +const HAS_FONT_REGEXP = 'font-family(?!="none")'; class SpriteSelectorItem extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'getCostumeUrl', 'handleClick', 'handleDelete', 'handleDuplicate', @@ -34,6 +30,34 @@ class SpriteSelectorItem extends React.Component { 'handleMouseMove', 'handleMouseUp' ]); + this.svgRenderer = new SVGRenderer(); + // Asset ID of the SVG currently in SVGRenderer + this.svgRendererAssetId = null; + } + getCostumeUrl () { + if (this.props.costumeURL) return this.props.costumeURL; + if (!this.props.assetId) return null; + + const storage = this.props.vm.runtime.storage; + const asset = storage.get(this.props.assetId); + // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag. + // Avoid parsing the SVG when possible, since it's expensive. + if (asset.assetType === storage.AssetType.ImageVector) { + // If the asset ID has not changed, no need to re-parse + if (this.svgRendererAssetId === this.props.assetId) { + return this.cachedUrl; + } + + const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText(); + if (svgString.match(HAS_FONT_REGEXP)) { + this.svgRendererAssetId = this.props.assetId; + this.svgRenderer.loadString(svgString); + const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */); + this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; + return this.cachedUrl; + } + } + return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); } handleMouseUp () { this.initialOffset = null; @@ -58,7 +82,7 @@ class SpriteSelectorItem extends React.Component { const dy = currentOffset.y - this.initialOffset.y; if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) { this.props.onDrag({ - img: this.props.costumeURL, + img: this.getCostumeUrl(), currentOffset: currentOffset, dragging: true, dragType: this.props.dragType, @@ -84,10 +108,7 @@ class SpriteSelectorItem extends React.Component { } handleDelete (e) { e.stopPropagation(); // To prevent from bubbling back to handleClick - // eslint-disable-next-line no-alert - if (window.confirm(this.props.intl.formatMessage(messages.deleteSpriteConfirmation))) { - this.props.onDeleteButtonClick(this.props.id); - } + this.props.onDeleteButtonClick(this.props.id); } handleDuplicate (e) { e.stopPropagation(); // To prevent from bubbling back to handleClick @@ -115,11 +136,14 @@ class SpriteSelectorItem extends React.Component { onExportButtonClick, dragPayload, receivedBlocks, + costumeURL, + vm, /* eslint-enable no-unused-vars */ ...props } = this.props; return ( <SpriteSelectorItemComponent + costumeURL={this.getCostumeUrl()} onClick={this.handleClick} onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null} onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null} @@ -144,7 +168,6 @@ SpriteSelectorItem.propTypes = { dragType: PropTypes.string, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), index: PropTypes.number, - intl: intlShape.isRequired, name: PropTypes.string, onClick: PropTypes.func, onDeleteButtonClick: PropTypes.func, @@ -152,14 +175,15 @@ SpriteSelectorItem.propTypes = { onDuplicateButtonClick: PropTypes.func, onExportButtonClick: PropTypes.func, receivedBlocks: PropTypes.bool.isRequired, - selected: PropTypes.bool + selected: PropTypes.bool, + vm: PropTypes.instanceOf(VM).isRequired }; -const mapStateToProps = (state, {assetId, costumeURL, id}) => ({ - costumeURL: costumeURL || (assetId && state.scratchGui.vm.runtime.storage.get(assetId).encodeDataURI()), +const mapStateToProps = (state, {id}) => ({ dragging: state.scratchGui.assetDrag.dragging, receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks && - state.scratchGui.hoveredTarget.sprite === id + state.scratchGui.hoveredTarget.sprite === id, + vm: state.scratchGui.vm }); const mapDispatchToProps = dispatch => ({ dispatchSetHoveredSprite: spriteId => { @@ -168,8 +192,12 @@ const mapDispatchToProps = dispatch => ({ onDrag: data => dispatch(updateAssetDrag(data)) }); - -export default connect( +const ConnectedComponent = connect( mapStateToProps, mapDispatchToProps -)(injectIntl(SpriteSelectorItem)); +)(SpriteSelectorItem); + +export { + ConnectedComponent as default, + HAS_FONT_REGEXP // Exposed for testing +}; diff --git a/src/css/units.css b/src/css/units.css index 59d9a2e7cdd244f7bb3beaf8482adce58a6a3380..db51ed00934d08ee8d01204bab3f65353afd5905 100644 --- a/src/css/units.css +++ b/src/css/units.css @@ -6,6 +6,7 @@ $space: 0.5rem; $sprites-per-row: 5; $menu-bar-height: 3rem; +$language-selector-width: 3rem; $sprite-info-height: 6rem; $stage-menu-height: 2.75rem; diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx index 0bd3b36ee9a2dd9285139ca554a8ca564e9cf358..edd4fa512b6e2e414762aa6fccb9fdab5cf7dfdf 100644 --- a/src/lib/libraries/extensions/index.jsx +++ b/src/lib/libraries/extensions/index.jsx @@ -158,7 +158,7 @@ export default [ /> ), featured: true, - disabled: true, + disabled: false, launchDeviceConnectionFlow: true, useAutoScan: true, deviceImage: wedoDeviceImage, diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index b2256b82a4ace77d8d2bec3a426bb02852cd3464..67e70f90019e90b92c262fdef5067c12c9c927ff 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -89,6 +89,10 @@ const vmListenerHOC = function (WrappedComponent) { onKeyUp, onMonitorsUpdate, onTargetsUpdate, + onProjectRunStart, + onProjectRunStop, + onTurboModeOff, + onTurboModeOn, /* eslint-enable no-unused-vars */ ...props } = this.props; diff --git a/test/integration/localization.test.js b/test/integration/localization.test.js index abd8ee4676db49ad620e6aae480aff6cefbea363..cf02aa86b1bc2c9707950fbb7317506302c836c5 100644 --- a/test/integration/localization.test.js +++ b/test/integration/localization.test.js @@ -26,7 +26,6 @@ describe('Localization', () => { await loadUri(uri); await clickXpath('//button[@title="Try It"]'); await clickXpath('//*[@aria-label="language selector"]'); - await clickText('English'); await clickText('Deutsch'); await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 7c306a5eee2c9c05c2d16d1c9ed8a1a489f5370c..ca1a3d275d8271e63af5068659429d4b4a4cee7e 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -33,8 +33,6 @@ describe('Working with sounds', () => { // Delete the sound await rightClickText('Meow', scope.soundsTab); await clickText('delete', scope.soundsTab); - await driver.switchTo().alert() - .accept(); // Add it back await clickXpath('//button[@aria-label="Choose a Sound"]'); diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js index 41f2eec3ab4220e83fc08f8936c35328367f3da4..5b99efcb62e0496312d31f15e8746b260ad204a1 100644 --- a/test/integration/sprites.test.js +++ b/test/integration/sprites.test.js @@ -55,8 +55,6 @@ describe('Working with sprites', () => { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await rightClickText('Sprite1', scope.spriteTile); await clickText('delete', scope.spriteTile); - await driver.switchTo().alert() - .accept(); // Confirm that the stage has been switched to await findByText('Stage selected: no motion blocks'); const logs = await getLogs(); diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx index 85032603d4b3a1712db3fb431a96d109dc40d7d7..25200a1fb5f8376bd7ade0d9b13ff98eaf990885 100644 --- a/test/unit/containers/sprite-selector-item.test.jsx +++ b/test/unit/containers/sprite-selector-item.test.jsx @@ -4,6 +4,7 @@ import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; +import {HAS_FONT_REGEXP} from '../../../src/containers/sprite-selector-item'; import CloseButton from '../../../src/components/close-button/close-button'; describe('SpriteSelectorItem Container', () => { @@ -48,22 +49,19 @@ describe('SpriteSelectorItem Container', () => { onDeleteButtonClick = jest.fn(); dispatchSetHoveredSprite = jest.fn(); selected = true; - // Mock window.confirm() which is called when the close button is clicked. - global.confirm = jest.fn(() => true); }); - test('should confirm if the user really wants to delete the sprite', () => { + test('should delete the sprite', () => { const wrapper = mountWithIntl(getContainer()); wrapper.find(CloseButton).simulate('click'); - expect(global.confirm).toHaveBeenCalled(); expect(onDeleteButtonClick).toHaveBeenCalledWith(1337); }); - test('should not delete the sprite if the user cancels', () => { - global.confirm = jest.fn(() => false); - const wrapper = mountWithIntl(getContainer()); - wrapper.find(CloseButton).simulate('click'); - expect(global.confirm).toHaveBeenCalled(); - expect(onDeleteButtonClick).not.toHaveBeenCalled(); + test('Has font regexp works', () => { + expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + + expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy(); }); }); diff --git a/webpack.config.js b/webpack.config.js index a44d90c48e246e1f45c450a665dbb025028b8b83..1013bb87a0857718222800cf06807fafc92f0271 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -177,7 +177,7 @@ module.exports = [ ]) }) ].concat( - process.env.NODE_ENV === 'production' ? ( + process.env.NODE_ENV === 'production' || process.env.BUILD_MODE === 'dist' ? ( // export as library defaultsDeep({}, base, { target: 'web',