diff --git a/.travis.yml b/.travis.yml index 5805f6b68392113a5296715eaa5ffb7b402a054f..ba21af32461a84353b42c378e6583c44036f9c95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,15 +29,6 @@ before_deploy: export BEFORE_DEPLOY_RAN=true fi deploy: -- provider: script - on: - all_branches: true - skip_cleanup: true - script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -- provider: script - on: - all_branches: true - script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git - provider: npm on: branch: @@ -59,5 +50,14 @@ deploy: acl: public_read skip_cleanup: true local_dir: build +- provider: script + on: + all_branches: true + skip_cleanup: true + script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + all_branches: true + script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git after_deploy: - 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all' diff --git a/package.json b/package.json index 1cf1ce989618cd8fbc19c06aff68b30b717ce046..c18f84fc9460616e1efdc99097092ee9fd005a3c 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "lodash.pick": "4.4.0", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "postcss-import": "^11.0.0", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", "postcss-simple-vars": "^4.0.0", @@ -97,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.1533910749", - "scratch-l10n": "3.0.20180810134928", - "scratch-paint": "0.2.0-prerelease.20180809185222", + "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-svg-renderer": "0.2.0-prerelease.20180712223402", - "scratch-vm": "0.2.0-prerelease.20180809193416", + "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", + "scratch-vm": "0.2.0-prerelease.20180817143311", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.22.1", diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css index aa8840c4cc822574ff565273a858d52ced169227..a7f86ac4efe52677a2ef8bbf440c162a6f61f426 100644 --- a/src/components/browser-modal/browser-modal.css +++ b/src/components/browser-modal/browser-modal.css @@ -34,6 +34,10 @@ background-size: cover; } +[dir="rtl"] .illustration { + transform: scaleX(-1); +} + .body { background: $ui-white; padding: 1.5rem 2.25rem; diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index 457105727df41413ee3767d0d02cada6cdcaf1c3..0a271ea7a7838ba2a3500e02503c5bf8b41e24f7 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -22,62 +22,65 @@ const BrowserModal = ({intl, ...props}) => ( overlayClassName={styles.modalOverlay} onRequestClose={props.onBack} > - <Box className={styles.illustration} /> + <div dir={props.isRtl ? 'rtl' : 'ltr'} > + <Box className={styles.illustration} /> - <Box className={styles.body}> - <h2> - <FormattedMessage {...messages.label} /> - </h2> - <p> - { /* eslint-disable max-len */ } - <FormattedMessage - defaultMessage="We're very sorry, but Scratch 3.0 does not support Internet Explorer, Vivaldi, Opera or Silk. We recommend trying a newer browser such as Google Chrome, Mozilla Firefox, or Microsoft Edge." - description="Unsupported browser description" - id="gui.unsupportedBrowser.description" - /> - { /* eslint-enable max-len */ } - </p> - - <Box className={styles.buttonRow}> - <button - className={styles.backButton} - onClick={props.onBack} - > + <Box className={styles.body}> + <h2> + <FormattedMessage {...messages.label} /> + </h2> + <p> + { /* eslint-disable max-len */ } <FormattedMessage - defaultMessage="Back" - description="Button to go back in unsupported browser modal" - id="gui.unsupportedBrowser.back" + defaultMessage="We're very sorry, but Scratch 3.0 does not support Internet Explorer, Vivaldi, Opera or Silk. We recommend trying a newer browser such as Google Chrome, Mozilla Firefox, or Microsoft Edge." + description="Unsupported browser description" + id="gui.unsupportedBrowser.description" /> - </button> + { /* eslint-enable max-len */ } + </p> + + <Box className={styles.buttonRow}> + <button + className={styles.backButton} + onClick={props.onBack} + > + <FormattedMessage + defaultMessage="Back" + description="Button to go back in unsupported browser modal" + id="gui.unsupportedBrowser.back" + /> + </button> + </Box> + <div className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.unsupportedBrowser.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/3faq" + > + <FormattedMessage + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.unsupportedBrowser.previewfaqlinktext" + /> + </a> + ) + }} + /> + </div> </Box> - <div className={styles.faqLinkText}> - <FormattedMessage - defaultMessage="To learn more, go to the {previewFaqLink}." - description="Invitation to try 3.0 preview" - id="gui.unsupportedBrowser.previewfaq" - values={{ - previewFaqLink: ( - <a - className={styles.faqLink} - href="//scratch.mit.edu/3faq" - > - <FormattedMessage - defaultMessage="FAQ" - description="link to Scratch 3.0 FAQ page" - id="gui.unsupportedBrowser.previewfaqlinktext" - /> - </a> - ) - }} - /> - </div> - </Box> + </div> </ReactModal> ); BrowserModal.propTypes = { intl: intlShape.isRequired, + isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired }; diff --git a/src/components/cards/card.css b/src/components/cards/card.css index c16d9b26ec332cdd57c3dd70bf754749df703eda..439c8191f9d7a0fab002fbabb6f6240d91c4da27 100644 --- a/src/components/cards/card.css +++ b/src/components/cards/card.css @@ -187,15 +187,26 @@ height: 1rem; } -.help-icon { +[dir="ltr"] .help-icon { margin-right: 0.25rem; } -.close-icon { +[dir="rtl"] .help-icon { margin-left: 0.25rem; +} + +.close-icon { transform: rotate(45deg); } +[dir="ltr"] .close-icon { + margin-left: 0.25rem; +} + +[dir="rtl"] .close-icon { + margin-right: 0.25rem; +} + .see-all { display: flex; flex-direction: row; @@ -220,10 +231,14 @@ text-align: center; } -.see-all-button img { +[dir="ltr"] .see-all-button img { margin-left: 0.5rem; } +[dir="rtl"] .see-all-button img { + margin-right: 0.5rem; +} + .video-cover { width: 100%; height: 100%; diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx index 8e2c40ac3640b0d8cf9087f662f5e1cfab1a468d..c389f2923028022dbff2d758350ce5096bb397d7 100644 --- a/src/components/cards/cards.jsx +++ b/src/components/cards/cards.jsx @@ -5,8 +5,8 @@ import Draggable from 'react-draggable'; import styles from './card.css'; -import nextIcon from './icon--next.svg'; -import prevIcon from './icon--prev.svg'; +import rightArrow from './icon--next.svg'; +import leftArrow from './icon--prev.svg'; import helpIcon from '../../lib/assets/icon--tutorials.svg'; import closeIcon from '../close-button/icon--close.svg'; @@ -98,32 +98,32 @@ ImageStep.propTypes = { title: PropTypes.node.isRequired }; -const NextPrevButtons = ({onNextStep, onPrevStep}) => ( +const NextPrevButtons = ({isRtl, onNextStep, onPrevStep}) => ( <Fragment> {onNextStep ? ( <div> - <div className={styles.rightCard} /> + <div className={isRtl ? styles.leftCard : styles.rightCard} /> <div - className={styles.rightButton} + className={isRtl ? styles.leftButton : styles.rightButton} onClick={onNextStep} > <img draggable={false} - src={nextIcon} + src={isRtl ? leftArrow : rightArrow} /> </div> </div> ) : null} {onPrevStep ? ( <div> - <div className={styles.leftCard} /> + <div className={isRtl ? styles.rightCard : styles.leftCard} /> <div - className={styles.leftButton} + className={isRtl ? styles.rightButton : styles.leftButton} onClick={onPrevStep} > <img draggable={false} - src={prevIcon} + src={isRtl ? rightArrow : leftArrow} /> </div> </div> @@ -132,6 +132,7 @@ const NextPrevButtons = ({onNextStep, onPrevStep}) => ( ); NextPrevButtons.propTypes = { + isRtl: PropTypes.bool, onNextStep: PropTypes.func, onPrevStep: PropTypes.func }; @@ -201,51 +202,76 @@ PreviewsStep.propTypes = { }; const Cards = props => { - if (props.activeDeckId === null) return; + const { + activeDeckId, + content, + dragging, + isRtl, + onActivateDeckFactory, + onCloseCards, + onDrag, + onStartDrag, + onEndDrag, + onShowAll, + onNextStep, + onPrevStep, + step, + ...posProps + } = props; + let {x, y} = posProps; - const steps = props.content[props.activeDeckId].steps; + if (activeDeckId === null) return; + + if (x === 0 && y === 0) { + // initialize positions + x = isRtl ? -292 : 292; + y = 365; + } + + const steps = content[activeDeckId].steps; return ( <Draggable bounds="parent" - position={{x: props.x, y: props.y}} - onDrag={props.onDrag} - onStart={props.onStartDrag} - onStop={props.onEndDrag} + position={{x: x, y: y}} + onDrag={onDrag} + onStart={onStartDrag} + onStop={onEndDrag} > <div className={styles.cardContainer}> <div className={styles.card}> <CardHeader - step={props.step} + step={step} totalSteps={steps.length} - onCloseCards={props.onCloseCards} - onShowAll={props.onShowAll} + onCloseCards={onCloseCards} + onShowAll={onShowAll} /> <div className={styles.stepBody}> - {steps[props.step].deckIds ? ( + {steps[step].deckIds ? ( <PreviewsStep - content={props.content} - deckIds={steps[props.step].deckIds} - onActivateDeckFactory={props.onActivateDeckFactory} - onShowAll={props.onShowAll} + content={content} + deckIds={steps[step].deckIds} + onActivateDeckFactory={onActivateDeckFactory} + onShowAll={onShowAll} /> ) : ( - steps[props.step].video ? ( + steps[step].video ? ( <VideoStep - dragging={props.dragging} - video={steps[props.step].video} + dragging={dragging} + video={steps[step].video} /> ) : ( <ImageStep - image={steps[props.step].image} - title={steps[props.step].title} + image={steps[step].image} + title={steps[step].title} /> ) )} </div> <NextPrevButtons - onNextStep={props.step < steps.length - 1 ? props.onNextStep : null} - onPrevStep={props.step > 0 ? props.onPrevStep : null} + isRtl={isRtl} + onNextStep={step < steps.length - 1 ? onNextStep : null} + onPrevStep={step > 0 ? onPrevStep : null} /> </div> </div> @@ -268,6 +294,7 @@ Cards.propTypes = { }) }), dragging: PropTypes.bool.isRequired, + isRtl: PropTypes.bool, onActivateDeckFactory: PropTypes.func.isRequired, onCloseCards: PropTypes.func.isRequired, onDrag: PropTypes.func, diff --git a/src/components/coming-soon/coming-soon.css b/src/components/coming-soon/coming-soon.css index 091d2dd344c4bba864447041cd0c08b5ee07f5d6..900173e420dd9b042964c924b75e38f94898e8de 100644 --- a/src/components/coming-soon/coming-soon.css +++ b/src/components/coming-soon/coming-soon.css @@ -62,8 +62,15 @@ } .coming-soon-image { - margin-left: .125rem; width: 1.25rem; height: 1.25rem; vertical-align: middle; } + +[dir="ltr"] .coming-soon-image { + margin-left: .125rem; +} + +[dir="rtl"] .coming-soon-image { + margin-right: .125rem; +} diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 1924bd8c4080c2ce3208de1c2fb211492a0fbd79..46cf41d2b79baf011980c08bef45b07eac1e9611 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -186,6 +186,9 @@ */ display: flex; flex-direction: column; + /* pad entire wrapper to the left and right; allow children to fill width */ + padding-left: $space; + padding-right: $space; } .stage-and-target-wrapper.large { @@ -209,9 +212,6 @@ flex-basis: 0; padding-top: $space; - padding-left: $space; - padding-right: $space; - min-height: 0; /* this makes it work in Firefox */ /* diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index c7859c5aa54aec12c417b36cc28cd1aaf668e78e..96fea86537fb95b886a43268dac70d77334d2e2f 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -125,7 +125,7 @@ const GUIComponent = props => { <ImportModal /> ) : null} {isRendererSupported ? null : ( - <WebGlModal /> + <WebGlModal isRtl={isRtl} /> )} {tipsLibraryVisible ? ( <TipsLibrary /> diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css index b265efae02fc5ed6c7348a4422d9b84e9e474fa0..7da215c015b83017e9be1c1ad95c57bf7cf8f37a 100644 --- a/src/components/import-modal/import-modal.css +++ b/src/components/import-modal/import-modal.css @@ -82,6 +82,10 @@ $sides: 20rem; justify-content: flex-start; } +[dir="rtl"] .header-item-close img { + transform: scaleX(-1); +} + .body { background: $ui-white; padding: 1.5rem 2.25rem; diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx index 0fa3aac864978b182c294cc4ce0de911ef652e65..1028d5ddc20f0490a19bfecbf82526869a5a2d27 100644 --- a/src/components/import-modal/import-modal.jsx +++ b/src/components/import-modal/import-modal.jsx @@ -42,106 +42,112 @@ const ImportModal = ({intl, ...props}) => ( overlayClassName={styles.modalOverlay} onRequestClose={props.onCancel} > - <Box> - <div className={styles.header}> - <div - className={classNames( - styles.headerItem, - styles.headerItemClose - )} - > - <CloseButton - buttonType="back" - size={CloseButton.SIZE_LARGE} - onClick={props.onGoBack} - /> - </div> - <div - className={classNames( - styles.headerItem, - styles.headerItemTitle - )} - > - <h2> - {intl.formatMessage({...messages.title})} - </h2> - </div> - <div className={classNames(styles.headerItem, styles.headerItemFilter)}> - {null} + <div dir={props.isRtl ? 'rtl' : 'ltr'} > + <Box> + <div className={styles.header}> + <div + className={classNames( + styles.headerItem, + styles.headerItemClose + )} + > + <CloseButton + buttonType="back" + size={CloseButton.SIZE_LARGE} + onClick={props.onGoBack} + /> + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemTitle + )} + > + <h2> + {intl.formatMessage({...messages.title})} + </h2> + </div> + <div className={classNames(styles.headerItem, styles.headerItemFilter)}> + {null} + </div> </div> - </div> - </Box> + </Box> - <Box className={styles.body}> - <p> - {intl.formatMessage({...messages.formDescription})} - </p> - <Box - className={classNames(styles.inputRow, - (props.hasValidationError ? styles.badInputContainer : styles.okInputContainer)) - } - > - <input - autoFocus - placeholder={props.placeholder} - value={props.inputValue} - onChange={props.onChange} - onKeyPress={props.onKeyPress} - /> - <button - className={styles.okButton} - title="viewproject" - onClick={props.onViewProject} + <Box className={styles.body}> + <p> + {intl.formatMessage({...messages.formDescription})} + </p> + <Box + className={classNames(styles.inputRow, + (props.hasValidationError ? styles.badInputContainer : styles.okInputContainer)) + } > - <FormattedMessage - defaultMessage="View" - description="Label for button to load a scratch 2.0 project" - id="gui.importModal.viewproject" + <input + autoFocus + placeholder={props.placeholder} + value={props.inputValue} + onChange={props.onChange} + onKeyPress={props.onKeyPress} /> - </button> - </Box> - {props.hasValidationError ? - <Box className={styles.errorRow}> - <p> + <button + className={styles.okButton} + title={intl.formatMessage({ + defaultMessage: 'View Project', + description: 'Tooltip for View button', + id: 'gui.importModal.viewprojecttooltip' + })} + onClick={props.onViewProject} + > <FormattedMessage - {...messages[`${props.errorMessage}`]} + defaultMessage="View" + description="Label for button to load a scratch 2.0 project" + id="gui.importModal.viewproject" /> - </p> - </Box> : null - } - <Box className={styles.buttonRow}> - <button - onClick={props.onGoBack} - > + </button> + </Box> + {props.hasValidationError ? + <Box className={styles.errorRow}> + <p> + <FormattedMessage + {...messages[`${props.errorMessage}`]} + /> + </p> + </Box> : null + } + <Box className={styles.buttonRow}> + <button + onClick={props.onGoBack} + > + <FormattedMessage + defaultMessage="Go Back" + description="Label for button to back out of importing a project" + id="gui.importInfo.goback" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> <FormattedMessage - defaultMessage="Go Back" - description="Label for button to back out of importing a project" - id="gui.importInfo.goback" + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.importInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/3faq" + > + <FormattedMessage + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.importInfo.previewfaqlinktext" + /> + </a> + ) + }} /> - </button> - </Box> - <Box className={styles.faqLinkText}> - <FormattedMessage - defaultMessage="To learn more, go to the {previewFaqLink}." - description="Invitation to try 3.0 preview" - id="gui.importInfo.previewfaq" - values={{ - previewFaqLink: ( - <a - className={styles.faqLink} - href="//scratch.mit.edu/3faq" - > - <FormattedMessage - defaultMessage="FAQ" - description="link to Scratch 3.0 FAQ page" - id="gui.importInfo.previewfaqlinktext" - /> - </a> - ) - }} - /> + </Box> </Box> - </Box> + </div> </ReactModal> ); diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 635b18012b2a5e88bc5e7f56372da3d102a2eec0..c1ae3f9fa2144059547841170b555535081062f8 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -66,6 +66,7 @@ align-self: center; position: relative; align-items: center; + white-space: nowrap; height: $menu-bar-height; } @@ -168,11 +169,18 @@ } .dropdown-caret-icon { - margin-left: .5rem; width: 0.5rem; height: 0.5rem; } +[dir="ltr"] .dropdown-caret-icon { + margin-left: .5rem; +} + +[dir="rtl"] .dropdown-caret-icon { + margin-right: .5rem; +} + .disabled { opacity: 0.5; } diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 28530131fb3e25ae7f01d5d1d6d06fbf76c13ab2..11562411086b5cb47d97300f424d6494a1fd7000 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -91,10 +91,11 @@ MenuBarItemTooltip.propTypes = { place: PropTypes.oneOf(['top', 'bottom', 'left', 'right']) }; -const MenuItemTooltip = ({id, children, className}) => ( +const MenuItemTooltip = ({id, isRtl, children, className}) => ( <ComingSoonTooltip className={classNames(styles.comingSoon, className)} - place="right" + isRtl={isRtl} + place={isRtl ? 'left' : 'right'} tooltipClassName={styles.comingSoonTooltip} tooltipId={id} > @@ -105,7 +106,8 @@ const MenuItemTooltip = ({id, children, className}) => ( MenuItemTooltip.propTypes = { children: PropTypes.node, className: PropTypes.string, - id: PropTypes.string + id: PropTypes.string, + isRtl: PropTypes.bool }; const MenuBarMenu = ({ @@ -143,9 +145,9 @@ class MenuBar extends React.Component { this.props.onClickLanguage(e); } } - handleRestoreOption (restoreFun /* eslint-disable-line no-unused-vars */) { + handleRestoreOption (restoreFun) { return () => { - // restoreFun(); TODO re-enable this when validation issues are fixed + restoreFun(); this.props.onRequestCloseEdit(); }; } @@ -189,6 +191,7 @@ class MenuBar extends React.Component { </div> <MenuBarMenu open={this.props.languageMenuOpen} + place={this.props.isRtl ? 'left' : 'right'} onRequestClose={this.props.onRequestCloseLanguage} > <LanguageSelector /> @@ -211,9 +214,13 @@ class MenuBar extends React.Component { </div> <MenuBarMenu open={this.props.fileMenuOpen} + place={this.props.isRtl ? 'left' : 'right'} onRequestClose={this.props.onRequestCloseFile} > - <MenuItemTooltip id="new"> + <MenuItemTooltip + id="new" + isRtl={this.props.isRtl} + > <MenuItem> <FormattedMessage defaultMessage="New" @@ -223,7 +230,10 @@ class MenuBar extends React.Component { </MenuItem> </MenuItemTooltip> <MenuSection> - <MenuItemTooltip id="save"> + <MenuItemTooltip + id="save" + isRtl={this.props.isRtl} + > <MenuItem> <FormattedMessage defaultMessage="Save now" @@ -232,7 +242,10 @@ class MenuBar extends React.Component { /> </MenuItem> </MenuItemTooltip> - <MenuItemTooltip id="copy"> + <MenuItemTooltip + id="copy" + isRtl={this.props.isRtl} + > <MenuItem> <FormattedMessage defaultMessage="Save as a copy" @@ -285,29 +298,28 @@ class MenuBar extends React.Component { </div> <MenuBarMenu open={this.props.editMenuOpen} + place={this.props.isRtl ? 'left' : 'right'} onRequestClose={this.props.onRequestCloseEdit} > - <MenuItemTooltip id="restore"> - <DeletionRestorer>{(handleRestore, {restorable /* eslint-disable-line no-unused-vars, max-len */, deletedItem}) => ( - <MenuItem - className={classNames(styles.disabled)} - 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" - /> - } - </MenuItem> - )}</DeletionRestorer> - </MenuItemTooltip> + <DeletionRestorer>{(handleRestore, {restorable, deletedItem}) => ( + <MenuItem + 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" + /> + } + </MenuItem> + )}</DeletionRestorer> <MenuSection> <TurboMode>{(toggleTurboMode, {turboMode}) => ( <MenuItem onClick={toggleTurboMode}> @@ -429,7 +441,7 @@ class MenuBar extends React.Component { </MenuBarItemTooltip> <MenuBarItemTooltip id="account-nav" - place="left" + place={this.props.isRtl ? 'right' : 'left'} > <div className={classNames( @@ -462,6 +474,7 @@ MenuBar.propTypes = { enableCommunity: PropTypes.bool, fileMenuOpen: PropTypes.bool, intl: intlShape, + isRtl: PropTypes.bool, languageMenuOpen: PropTypes.bool, onClickEdit: PropTypes.func, onClickFile: PropTypes.func, @@ -476,6 +489,7 @@ MenuBar.propTypes = { const mapStateToProps = state => ({ fileMenuOpen: fileMenuOpen(state), editMenuOpen: editMenuOpen(state), + isRtl: state.locales.isRtl, languageMenuOpen: languageMenuOpen(state) }); diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css index d6cf6dc4568fd5178ea0849bbd92551838d14bf9..6e17cb6ea0817f2baebc957608831e7ecd8ced7e 100644 --- a/src/components/preview-modal/preview-modal.css +++ b/src/components/preview-modal/preview-modal.css @@ -74,17 +74,28 @@ color: white; } -.button-row button + button { +[dir="ltr"] .button-row button + button { margin-left: 0.5rem; } +[dir="rtl"] .button-row button + button { + margin-right: 0.5rem; +} + .cat-icon { - margin-left: .125rem; width: 1.5rem; height: 1.5rem; vertical-align: middle; } +[dir="ltr"] .cat-icon { + margin-left: .125rem; +} + +[dir="rtl"] .cat-icon { + margin-right: .125rem; +} + .faq-link-text { margin: 2rem 0 .5rem 0; font-size: .875rem; diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx index b9c803b767782939b86193725d4ff6dd09b919d1..2c946bb422b78db3f5131f3409ab68254163e296 100644 --- a/src/components/preview-modal/preview-modal.jsx +++ b/src/components/preview-modal/preview-modal.jsx @@ -28,93 +28,111 @@ const PreviewModal = ({intl, ...props}) => ( overlayClassName={styles.modalOverlay} onRequestClose={props.onTryIt} > - <Box className={styles.illustration} /> + <div dir={props.isRtl ? 'rtl' : 'ltr'} > + <Box className={styles.illustration} /> - <Box className={styles.body}> - <h2> - <FormattedMessage - defaultMessage="Welcome to the Scratch 3.0 Beta" - description="Header for Beta Info Modal" - id="gui.previewInfo.betawelcome" - /> - </h2> - <p> - <FormattedMessage - defaultMessage="We're working on the next generation of Scratch. We're excited for you to try it!" - description="Invitation to try 3.0 Beta" - id="gui.previewInfo.invitation" - /> - </p> - - <Box className={styles.buttonRow}> - <button - className={styles.noButton} - onClick={props.onCancel} - > + <Box className={styles.body}> + <h2> + <FormattedMessage + defaultMessage="Welcome to the Scratch 3.0 Beta" + description="Header for Beta Info Modal" + id="gui.previewInfo.betawelcome" + /> + </h2> + <p> + { /* eslint-disable max-len */ } <FormattedMessage - defaultMessage="Not Now" - description="Label for button to back out of trying Scratch 3.0 Beta" - id="gui.previewInfo.notnow" + defaultMessage="We're working on the next generation of Scratch. We're excited for you to try it!" + description="Invitation to try 3.0 Beta" + id="gui.previewInfo.invitation" /> - </button> - <button - className={styles.okButton} - title="tryit" - onClick={props.onTryIt} - > + { /* eslint-enable max-len */ } + </p> + + <Box className={styles.buttonRow}> + <button + className={styles.noButton} + title={intl.formatMessage({ + defaultMessage: 'Not Now', + description: 'Tooltip for Not Now button', + id: 'gui.previewModal.notnowtooltip' + })} + onClick={props.onCancel} + > + <FormattedMessage + defaultMessage="Not Now" + description="Label for button to back out of trying Scratch 3.0 Beta" + id="gui.previewInfo.notnow" + /> + </button> + <button + className={styles.okButton} + title={intl.formatMessage({ + defaultMessage: 'Try It', + description: 'Tooltip for Try It button', + id: 'gui.previewModal.tryittooltip' + })} + onClick={props.onTryIt} + > + <FormattedMessage + defaultMessage="Try It! {caticon}" + description="Label for button to try Scratch 3.0 Beta" + id="gui.previewModal.tryit" + values={{ + caticon: ( + <img + className={styles.catIcon} + src={catIcon} + /> + ) + }} + /> + </button> + <button + className={styles.viewProjectButton} + title={intl.formatMessage({ + defaultMessage: 'View 2.0 Project', + description: 'Tooltip for View 2.0 Project button', + id: 'gui.previewModal.viewprojecttooltip' + })} + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View 2.0 Project" + description="Label for button to import a 2.0 project" + id="gui.previewModal.viewproject" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> <FormattedMessage - defaultMessage="Try It! {caticon}" - description="Label for button to try Scratch 3.0 Beta" - id="gui.previewModal.tryit" + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 Beta" + id="gui.previewInfo.previewfaq" values={{ - caticon: ( - <img - className={styles.catIcon} - src={catIcon} - /> + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/3faq" + > + <FormattedMessage + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.previewInfo.previewfaqlinktext" + /> + </a> ) }} /> - </button> - <button - className={styles.viewProjectButton} - title="viewproject" - onClick={props.onViewProject} - > - <FormattedMessage - defaultMessage="View 2.0 Project" - description="Label for button to import a 2.0 project" - id="gui.previewModal.viewproject" - /> - </button> - </Box> - <Box className={styles.faqLinkText}> - <FormattedMessage - defaultMessage="To learn more, go to the {previewFaqLink}." - description="Invitation to try 3.0 Beta" - id="gui.previewInfo.previewfaq" - values={{ - previewFaqLink: ( - <a - className={styles.faqLink} - href="//scratch.mit.edu/3faq" - > - <FormattedMessage - defaultMessage="FAQ" - description="link to Scratch 3.0 FAQ page" - id="gui.previewInfo.previewfaqlinktext" - /> - </a> - ) - }} - /> + </Box> </Box> - </Box> + </div> </ReactModal> ); PreviewModal.propTypes = { intl: intlShape.isRequired, + isRtl: PropTypes.bool, onCancel: PropTypes.func.isRequired, onTryIt: PropTypes.func.isRequired, onViewProject: PropTypes.func.isRequired diff --git a/src/components/stage-header/stage-header.css b/src/components/stage-header/stage-header.css index 397c679da34811e8170a422d6f24dc336a619db4..dd2dc2c65ca642338358d681f572a1b04796fd14 100644 --- a/src/components/stage-header/stage-header.css +++ b/src/components/stage-header/stage-header.css @@ -22,7 +22,8 @@ flex-shrink: 0; align-items: center; height: $stage-menu-height; - padding: $space; + padding-top: $space; + padding-bottom: $space; } .stage-size-row { diff --git a/src/components/stage-wrapper/stage-wrapper.css b/src/components/stage-wrapper/stage-wrapper.css index 29ca66aa2923ec3c3b3076a524e4216cfde71832..9f6ab5c6149ecc6c398ce1a0594d7696dd964cf2 100644 --- a/src/components/stage-wrapper/stage-wrapper.css +++ b/src/components/stage-wrapper/stage-wrapper.css @@ -6,9 +6,6 @@ } .stage-canvas-wrapper { - padding-left: $space; - padding-right: $space; - /* Hides negative space between edge of rounded corners + container, when selected */ user-select: none; } diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index 003947533cafe82ff22178ce1aaa35d1e3a2e88c..7ce4d55d9f6a953541564e6e12b51fc1fed9be78 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -11,7 +11,7 @@ /* Attach border radius directly to canvas to prevent needing overflow:hidden; */ border-radius: $space; - border: 1px solid $ui-black-transparent; + border: $stage-standard-border-width solid $ui-black-transparent; /* @todo: This is for overriding the value being set somewhere. Where is it being set? */ background-color: transparent; @@ -51,15 +51,17 @@ bottom: 0; z-index: $z-index-stage-wrapper-overlay; background-color: $ui-white; + /* spacing between stage and control bar (on the top), or between + stage and window edges (on left/right/bottom) */ + padding: $stage-full-screen-stage-padding; } +/* wraps only main content of overlay player, not monitors */ .stage-overlay-content { outline: none; margin: auto; - border: 3px solid rgb(126, 133, 151); + border: $stage-full-screen-border-width solid rgb(126, 133, 151); padding: 0; - margin-top: 3px; - margin-bottom: 3px; border-radius: $space; overflow: hidden; @@ -76,6 +78,21 @@ position: absolute; } +/* adjust monitors when stage is standard size: +shift them down and right to compensate for the stage's border */ +.stage-wrapper .monitor-wrapper { + top: $stage-standard-border-width; + left: $stage-standard-border-width; +} + +/* adjust monitors when stage is full screen: +.stage-wrapper-overlay uses position: fixed instead of relative, so we need +to adjust for the border using a different method */ +.stage-wrapper-overlay .monitor-wrapper { + padding-top: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width); + padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width); +} + .monitor-wrapper, .color-picker-wrapper, .queston-wrapper { position: absolute; top: 0; diff --git a/src/components/webgl-modal/webgl-modal.css b/src/components/webgl-modal/webgl-modal.css index f1b6ff6efee57e7a40a5ad1a1ebc3325e8167dcd..10eed71f1d4667f7ea166508cd3b3a7cbbf8502c 100644 --- a/src/components/webgl-modal/webgl-modal.css +++ b/src/components/webgl-modal/webgl-modal.css @@ -34,6 +34,10 @@ background-size: cover; } +[dir="rtl"] .illustration { + transform: scaleX(-1); +} + .body { background: $ui-white; padding: 1.5rem 2.25rem; diff --git a/src/components/webgl-modal/webgl-modal.jsx b/src/components/webgl-modal/webgl-modal.jsx index f0c3c6c27529eb3ac8ab782131898698634459b7..faf5ac55171925ca5ba40b2869272f59f52858e2 100644 --- a/src/components/webgl-modal/webgl-modal.jsx +++ b/src/components/webgl-modal/webgl-modal.jsx @@ -22,76 +22,79 @@ const WebGlModal = ({intl, ...props}) => ( overlayClassName={styles.modalOverlay} onRequestClose={props.onBack} > - <Box className={styles.illustration} /> + <div dir={props.isRtl ? 'rtl' : 'ltr'}> + <Box className={styles.illustration} /> - <Box className={styles.body}> - <h2> - <FormattedMessage {...messages.label} /> - </h2> - <p> - { /* eslint-disable max-len */ } - <FormattedMessage - defaultMessage="Unfortunately it looks like your browser or computer {webGlLink}. This technology is needed for Scratch 3.0 to run." - description="WebGL missing message" - id="gui.webglModal.description" - values={{ - webGlLink: ( - <a - className={styles.faqLink} - href="https://en.wikipedia.org/wiki/WebGL#Support" - > - <FormattedMessage - defaultMessage="does not support WebGL" - description="link part of your browser does not support WebGL message" - id="gui.webglModal.webgllink" - /> - </a> - ) - }} - /> - { /* eslint-enable max-len */ } - </p> - - <Box className={styles.buttonRow}> - <button - className={styles.backButton} - onClick={props.onBack} - > + <Box className={styles.body}> + <h2> + <FormattedMessage {...messages.label} /> + </h2> + <p> + { /* eslint-disable max-len */ } <FormattedMessage - defaultMessage="Back" - description="Label for button go back when browser is unsupported" - id="gui.webglModal.back" + defaultMessage="Unfortunately it looks like your browser or computer {webGlLink}. This technology is needed for Scratch 3.0 to run." + description="WebGL missing message" + id="gui.webglModal.description" + values={{ + webGlLink: ( + <a + className={styles.faqLink} + href="https://en.wikipedia.org/wiki/WebGL#Support" + > + <FormattedMessage + defaultMessage="does not support WebGL" + description="link part of your browser does not support WebGL message" + id="gui.webglModal.webgllink" + /> + </a> + ) + }} /> - </button> + { /* eslint-enable max-len */ } + </p> + + <Box className={styles.buttonRow}> + <button + className={styles.backButton} + onClick={props.onBack} + > + <FormattedMessage + defaultMessage="Back" + description="Label for button go back when browser is unsupported" + id="gui.webglModal.back" + /> + </button> + </Box> + <div className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Scratch 3.0 FAQ description" + id="gui.webglModal.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/3faq" + > + <FormattedMessage + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.webglModal.previewfaqlinktext" + /> + </a> + ) + }} + /> + </div> </Box> - <div className={styles.faqLinkText}> - <FormattedMessage - defaultMessage="To learn more, go to the {previewFaqLink}." - description="Scratch 3.0 FAQ description" - id="gui.webglModal.previewfaq" - values={{ - previewFaqLink: ( - <a - className={styles.faqLink} - href="//scratch.mit.edu/3faq" - > - <FormattedMessage - defaultMessage="FAQ" - description="link to Scratch 3.0 FAQ page" - id="gui.webglModal.previewfaqlinktext" - /> - </a> - ) - }} - /> - </div> - </Box> + </div> </ReactModal> ); WebGlModal.propTypes = { intl: intlShape.isRequired, + isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired }; diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index e7303c145fb969ff5e226f68ff51b768f3b2da5b..e90fe1dff10b4d767596e7509b7ac1cb4d5106cb 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -27,6 +27,7 @@ class Backpack extends React.Component { 'handleDrop', 'handleToggle', 'handleDelete', + 'getBackpackAssetURL', 'refreshContents' ]); this.state = { @@ -44,11 +45,14 @@ class Backpack extends React.Component { if (props.host && !storage._hasAddedBackpackSource) { storage.addWebSource( [storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound], - asset => `${props.host}/${asset.assetId}.${asset.dataFormat}` + this.getBackpackAssetURL ); storage._hasAddedBackpackSource = true; } } + getBackpackAssetURL (asset) { + return `${this.props.host}/${asset.assetId}.${asset.dataFormat}`; + } handleToggle () { const newState = !this.state.expanded; this.setState({expanded: newState, offset: 0}); @@ -132,8 +136,8 @@ const getTokenAndUsername = state => { // Look for the session state provided by scratch-www if (state.session && state.session.session) { return { - token: state.session.session.token, - username: state.session.session.username + token: state.session.session.user.token, + username: state.session.session.user.username }; } // Otherwise try to pull testing params out of the URL, or return nulls diff --git a/src/containers/cards.jsx b/src/containers/cards.jsx index 5dc081f7b96f883cdbe07cd05ec1d23ce6c2f76c..2e4b92e5af01252ec88d1ae1c47dbb7a3c219663 100644 --- a/src/containers/cards.jsx +++ b/src/containers/cards.jsx @@ -23,6 +23,7 @@ const mapStateToProps = state => ({ step: state.scratchGui.cards.step, x: state.scratchGui.cards.x, y: state.scratchGui.cards.y, + isRtl: state.locales.isRtl, dragging: state.scratchGui.cards.dragging }); diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx index 52e522d5aaea290d3775515d18400023e4f15f13..fbb15f3b998d1deb80cb309146c1dfa48d88a593 100644 --- a/src/containers/error-boundary.jsx +++ b/src/containers/error-boundary.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; import bowser from 'bowser'; import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; import CrashMessageComponent from '../components/crash-message/crash-message.jsx'; @@ -57,7 +58,10 @@ class ErrorBoundary extends React.Component { if (supportedBrowser()) { return <CrashMessageComponent onReload={this.handleReload} />; } - return <BrowserModalComponent onBack={this.handleBack} />; + return (<BrowserModalComponent + isRtl={this.props.isRtl} + onBack={this.handleBack} + />); } return this.props.children; } @@ -65,7 +69,15 @@ class ErrorBoundary extends React.Component { ErrorBoundary.propTypes = { action: PropTypes.string.isRequired, // Used for defining tracking action - children: PropTypes.node + children: PropTypes.node, + isRtl: PropTypes.bool }; -export default ErrorBoundary; +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +// no-op function to prevent dispatch prop being passed to component +const mapDispatchToProps = () => {}; + +export default connect(mapStateToProps, mapDispatchToProps)(ErrorBoundary); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index ffa099496fc5c3d10587fdb66f3f547e6622f127..cdcb87c8985b31498c8e5eee61c54067225901fe 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -72,10 +72,12 @@ class GUI extends React.Component { `Failed to load project from server [id=${window.location.hash}]: ${this.state.errorMessage}`); } const { + assetHost, // eslint-disable-line no-unused-vars children, fetchingProject, loadingStateVisible, projectData, // eslint-disable-line no-unused-vars + projectHost, // eslint-disable-line no-unused-vars vm, ...componentProps } = this.props; @@ -92,7 +94,7 @@ class GUI extends React.Component { } GUI.propTypes = { - ...GUIComponent.propTypes, + children: PropTypes.node, fetchingProject: PropTypes.bool, importInfoVisible: PropTypes.bool, loadingStateVisible: PropTypes.bool, @@ -102,8 +104,6 @@ GUI.propTypes = { vm: PropTypes.instanceOf(VM) }; -GUI.defaultProps = GUIComponent.defaultProps; - const mapStateToProps = state => ({ activeTabIndex: state.scratchGui.editorTab.activeTabIndex, backdropLibraryVisible: state.scratchGui.modals.backdropLibrary, diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx index 0731098f1563c2fda54258c807babc0872ee4638..1c75929cf560a17b6d81c1e59457c7ce99371dc5 100644 --- a/src/containers/import-modal.jsx +++ b/src/containers/import-modal.jsx @@ -73,6 +73,7 @@ class ImportModal extends React.Component { errorMessage={this.state.errorMessage} hasValidationError={this.state.hasValidationError} inputValue={this.state.inputValue} + isRtl={this.props.isRtl} placeholder="scratch.mit.edu/projects/123456789" onCancel={this.handleCancel} onChange={this.handleChange} @@ -85,12 +86,15 @@ class ImportModal extends React.Component { } ImportModal.propTypes = { + isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, onViewProject: PropTypes.func }; -const mapStateToProps = () => ({}); +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); const mapDispatchToProps = dispatch => ({ onBack: () => { diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx index a53f398ee70d19b33504e7267fff104fa291986f..7145cfa6acbb3170a85a4bc1c9d44673731fc64e 100644 --- a/src/containers/preview-modal.jsx +++ b/src/containers/preview-modal.jsx @@ -39,12 +39,12 @@ class PreviewModal extends React.Component { // otherwise, show the intro modal return (<PreviewModalComponent + isRtl={this.props.isRtl} previewing={this.state.previewing} onCancel={this.handleCancel} onTryIt={this.handleTryIt} onViewProject={this.handleViewProject} />); - } handleTryIt () { this.setState({previewing: true}); @@ -62,6 +62,7 @@ class PreviewModal extends React.Component { return (supportedBrowser() ? this.introIfShown() : <BrowserModalComponent + isRtl={this.props.isRtl} onBack={this.handleCancel} /> ); @@ -70,11 +71,14 @@ class PreviewModal extends React.Component { PreviewModal.propTypes = { hideIntro: PropTypes.bool, + isRtl: PropTypes.bool, onTryIt: PropTypes.func, onViewProject: PropTypes.func }; -const mapStateToProps = () => ({}); +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); const mapDispatchToProps = dispatch => ({ onTryIt: () => { diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index 785f9d08f49359bc6a6c60f89262b5a177d58f36..a0197ad6dd9ecfc09e61c5a783878395eef7e509 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -2,6 +2,7 @@ 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'; @@ -11,6 +12,14 @@ import SpriteSelectorItemComponent from '../components/sprite-selector-item/spri 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' + } +}); + class SpriteSelectorItem extends React.Component { constructor (props) { super(props); @@ -75,9 +84,8 @@ class SpriteSelectorItem extends React.Component { } handleDelete (e) { e.stopPropagation(); // To prevent from bubbling back to handleClick - // @todo add i18n here // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to delete this?')) { + if (window.confirm(this.props.intl.formatMessage(messages.deleteSpriteConfirmation))) { this.props.onDeleteButtonClick(this.props.id); } } @@ -136,6 +144,7 @@ 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, @@ -159,7 +168,8 @@ const mapDispatchToProps = dispatch => ({ onDrag: data => dispatch(updateAssetDrag(data)) }); + export default connect( mapStateToProps, mapDispatchToProps -)(SpriteSelectorItem); +)(injectIntl(SpriteSelectorItem)); diff --git a/src/containers/webgl-modal.jsx b/src/containers/webgl-modal.jsx index 02e43bf01db100c611003e517d015e9f45c6291c..d6633f30394ae74a6c2845547d165b4c9df116ee 100644 --- a/src/containers/webgl-modal.jsx +++ b/src/containers/webgl-modal.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import WebGlModalComponent from '../components/webgl-modal/webgl-modal.jsx'; @@ -9,10 +10,15 @@ class WebGlModal extends React.Component { render () { return ( <WebGlModalComponent + isRtl={this.props.isRtl} onBack={this.handleCancel} /> ); } } +WebGlModal.propTypes = { + isRtl: PropTypes.bool +}; + export default WebGlModal; diff --git a/src/css/units.css b/src/css/units.css index ef34280983f2c54b1fa67c9ecf50e00b141396dc..59d9a2e7cdd244f7bb3beaf8482adce58a6a3380 100644 --- a/src/css/units.css +++ b/src/css/units.css @@ -1,3 +1,6 @@ +/* make sure to keep these in sync with other constants, +e.g. STAGE_DIMENSION_DEFAULTS in lib/screen-utils.js */ + $space: 0.5rem; $sprites-per-row: 5; @@ -9,6 +12,10 @@ $stage-menu-height: 2.75rem; $library-header-height: 3.125rem; $library-filter-bar-height: 2.5rem; +$stage-standard-border-width: 0.0625rem; +$stage-full-screen-border-width: 0.1875rem; +$stage-full-screen-stage-padding: 0.1875rem; + $form-radius: calc($space / 2); /* layout contants from `layout-constants.js` */ diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js index f0c51f14040ced7823c226521dd792b4cf639f65..9453f197709fee3e1b06dbcb2c98d90f50ab1e49 100644 --- a/src/lib/backpack-api.js +++ b/src/lib/backpack-api.js @@ -12,7 +12,7 @@ const getBackpackContents = ({ }) => new Promise((resolve, reject) => { xhr({ method: 'GET', - uri: `${host}${username}?limit=${limit}&offset=${offset}`, + uri: `${host}/${username}?limit=${limit}&offset=${offset}`, headers: {'x-token': token}, json: true }, (error, response) => { @@ -43,7 +43,7 @@ const saveBackpackObject = ({ }) => new Promise((resolve, reject) => { xhr({ method: 'POST', - uri: `${host}${username}`, + uri: `${host}/${username}`, headers: {'x-token': token}, json: {type, mime, name, body, thumbnail} }, (error, response) => { @@ -62,7 +62,7 @@ const deleteBackpackObject = ({ }) => new Promise((resolve, reject) => { xhr({ method: 'DELETE', - uri: `${host}${username}/${id}`, + uri: `${host}/${username}/${id}`, headers: {'x-token': token} }, (error, response) => { if (error || response.statusCode !== 200) { diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index cb8501af42ab532cb153a6a68efbbec185adf682..22a1aa5d1ce4c9666b6b87acde4084285053b62c 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -19,6 +19,9 @@ const ProjectLoaderHOC = function (WrappedComponent) { projectData: null, fetchingProject: false }; + storage.setProjectHost(props.projectHost); + storage.setAssetHost(props.assetHost); + } componentDidMount () { if (this.props.projectId || this.props.projectId === 0) { @@ -26,6 +29,12 @@ const ProjectLoaderHOC = function (WrappedComponent) { } } componentWillUpdate (nextProps) { + if (this.props.projectHost !== nextProps.projectHost) { + storage.setProjectHost(nextProps.projectHost); + } + if (this.props.assetHost !== nextProps.assetHost) { + storage.setAssetHost(nextProps.assetHost); + } if (this.props.projectId !== nextProps.projectId) { this.setState({fetchingProject: true}, () => { this.updateProject(nextProps.projectId); @@ -67,9 +76,13 @@ const ProjectLoaderHOC = function (WrappedComponent) { } } ProjectLoaderComponent.propTypes = { + assetHost: PropTypes.string, + projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }; ProjectLoaderComponent.defaultProps = { + assetHost: 'https://assets.scratch.mit.edu', + projectHost: 'https://projects.scratch.mit.edu', projectId: 0 }; diff --git a/src/lib/screen-utils.js b/src/lib/screen-utils.js index e233993e26155a8f493363c7a6424422d2e1e054..afef90d84c845119d881c11eef4e27c4f3a0b2ae 100644 --- a/src/lib/screen-utils.js +++ b/src/lib/screen-utils.js @@ -10,8 +10,13 @@ import layout, {STAGE_DISPLAY_SCALES, STAGE_SIZE_MODES, STAGE_DISPLAY_SIZES} fro */ const STAGE_DIMENSION_DEFAULTS = { - spacingBorderAdjustment: 9, - menuHeightAdjustment: 40 + // referencing css/units.css, + // spacingBorderAdjustment = 2 * $full-screen-top-bottom-margin + + // 2 * $full-screen-border-width + fullScreenSpacingBorderAdjustment: 12, + // referencing css/units.css, + // menuHeightAdjustment = $stage-menu-height + menuHeightAdjustment: 44 }; /** @@ -48,7 +53,7 @@ const getStageDimensions = (stageSize, isFullScreen) => { if (isFullScreen) { stageDimensions.height = window.innerHeight - STAGE_DIMENSION_DEFAULTS.menuHeightAdjustment - - STAGE_DIMENSION_DEFAULTS.spacingBorderAdjustment; + STAGE_DIMENSION_DEFAULTS.fullScreenSpacingBorderAdjustment; stageDimensions.width = stageDimensions.height + (stageDimensions.height / 3); diff --git a/src/lib/storage.js b/src/lib/storage.js index be7bcef1e6df21e80093f7e713554bcf38364547..9c49d81703f96aa67e6509013d91df0f0231e911 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -2,9 +2,6 @@ import ScratchStorage from 'scratch-storage'; import defaultProjectAssets from './default-project'; -const PROJECT_SERVER = 'https://projects.scratch.mit.edu'; -const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu'; - /** * Wrapper for ScratchStorage which adds default web sources. * @todo make this more configurable @@ -12,29 +9,36 @@ const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu'; class Storage extends ScratchStorage { constructor () { super(); + defaultProjectAssets.forEach(asset => this.cache( + this.AssetType[asset.assetType], + this.DataFormat[asset.dataFormat], + asset.data, + asset.id + )); this.addWebSource( [this.AssetType.Project], - projectAsset => { - const [projectId, revision] = projectAsset.assetId.split('.'); - return revision ? - `${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` : - `${PROJECT_SERVER}/internalapi/project/${projectId}/get/`; - } + this.getProjectURL.bind(this) ); this.addWebSource( [this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound], - asset => `${ASSET_SERVER}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/` + this.getAssetURL.bind(this) ); this.addWebSource( [this.AssetType.Sound], asset => `static/extension-assets/scratch3_music/${asset.assetId}.${asset.dataFormat}` ); - defaultProjectAssets.forEach(asset => this.cache( - this.AssetType[asset.assetType], - this.DataFormat[asset.dataFormat], - asset.data, - asset.id - )); + } + setProjectHost (projectHost) { + this.projectHost = projectHost; + } + getProjectURL (projectAsset) { + return `${this.projectHost}/internalapi/project/${projectAsset.assetId}/get/`; + } + setAssetHost (assetHost) { + this.assetHost = assetHost; + } + getAssetURL (asset) { + return `${this.assetHost}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`; } } diff --git a/src/reducers/cards.js b/src/reducers/cards.js index ba1c50d8e28af9d3e7a3c234417ccdc09e0b3f07..37319cbea06d82c0e2ac65e97d7a211eb5e18db9 100644 --- a/src/reducers/cards.js +++ b/src/reducers/cards.js @@ -16,8 +16,8 @@ const initialState = { content: decks, activeDeckId: null, step: 0, - x: 292, - y: 365, + x: 0, + y: 0, dragging: false }; diff --git a/test/integration/backpack.test.js b/test/integration/backpack.test.js index 9694e67ba2cc4b3aca21d96a2c0145285ec0405b..cfc78613ccdb6ab4fa71d029842b7df26543b03c 100644 --- a/test/integration/backpack.test.js +++ b/test/integration/backpack.test.js @@ -24,7 +24,7 @@ describe('Working with the how-to library', () => { test('Backpack is "Coming Soon" without backpack host param', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); // Check that the backpack header is visible and wrapped in a coming soon tooltip await clickText('Backpack', '*[@data-for="backpack-tooltip"]'); const logs = await getLogs(); @@ -33,7 +33,7 @@ describe('Working with the how-to library', () => { test('Backpack can be expanded with backpack host param', async () => { await loadUri(`${uri}?backpack_host=some-value`); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); // Try activating the backpack from the costumes tab to make sure it isn't pushed off await clickText('Costumes'); diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js index e7fcf0b827f94f5f8baedb3237a4a4728e6eaa20..83b72620bcc04dad457f157071a54016f74381aa 100644 --- a/test/integration/blocks.test.js +++ b/test/integration/blocks.test.js @@ -29,7 +29,7 @@ describe('Working with the blocks', () => { test('Blocks report when clicked in the toolbox', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Code'); await clickText('Operators', scope.blocksTab); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -41,7 +41,7 @@ describe('Working with the blocks', () => { test('Switching sprites updates the block menus', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Sound', scope.blocksTab); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation // "Meow" sound block should be visible @@ -58,7 +58,7 @@ describe('Working with the blocks', () => { test('Creating variables', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Code'); await clickText('Variables', scope.blocksTab); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -92,7 +92,7 @@ describe('Working with the blocks', () => { test('Creating a list', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Code'); await clickText('Variables', scope.blocksTab); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -127,7 +127,7 @@ describe('Working with the blocks', () => { test('Custom procedures', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('My Blocks'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a Block'); @@ -146,7 +146,7 @@ describe('Working with the blocks', () => { test('Adding an extension', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickXpath('//button[@title="Add Extension"]'); await clickText('Pen'); diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index 23790842d8d84dc8928a05fbdeda5db361de1bc0..7aa3b969fc2e152cd461c1c649f392e5fc00dfc9 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -27,7 +27,7 @@ describe('Working with costumes', () => { test('Adding a costume through the library', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); await clickXpath('//button[@aria-label="Choose a Costume"]'); const el = await findByXpath("//input[@placeholder='Search']"); @@ -40,7 +40,7 @@ describe('Working with costumes', () => { test('Adding a costume by surprise button', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); await driver.actions().mouseMove(el) @@ -53,7 +53,7 @@ describe('Working with costumes', () => { test('Adding a costume by paint button', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); await driver.actions().mouseMove(el) @@ -66,7 +66,7 @@ describe('Working with costumes', () => { test('Duplicating a costume', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); await rightClickText('costume1', scope.costumesTab); @@ -82,7 +82,7 @@ describe('Working with costumes', () => { test('Adding a backdrop', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickXpath('//button[@aria-label="Choose a Backdrop"]'); const el = await findByXpath("//input[@placeholder='Search']"); await el.sendKeys('blue'); @@ -93,7 +93,7 @@ describe('Working with costumes', () => { test('Converting bitmap/vector in paint editor', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); // Convert the first costume to bitmap. @@ -117,7 +117,7 @@ describe('Working with costumes', () => { test('Undo/redo in the paint editor', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); await clickText('costume1', scope.costumesTab); await clickText('Convert to Bitmap', scope.costumesTab); diff --git a/test/integration/how-tos.test.js b/test/integration/how-tos.test.js index 5b9b76e4b74cb0163066010a3bd91da03a8992b3..dcdccb4994706fec851695f136e4e51c3f0567eb 100644 --- a/test/integration/how-tos.test.js +++ b/test/integration/how-tos.test.js @@ -25,7 +25,7 @@ describe('Working with the how-to library', () => { test('Choosing a how-to', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); await clickXpath('//*[@aria-label="Tutorials"]'); await clickText('Getting Started'); // Modal should close diff --git a/test/integration/localization.test.js b/test/integration/localization.test.js index 101d3ce8613bd27e89b4e69ddfdfdbe483cfae6f..abd8ee4676db49ad620e6aae480aff6cefbea363 100644 --- a/test/integration/localization.test.js +++ b/test/integration/localization.test.js @@ -24,7 +24,7 @@ describe('Localization', () => { test('Localization', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickXpath('//*[@aria-label="language selector"]'); await clickText('English'); await clickText('Deutsch'); diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js index cb0477ec9317a9fbb2fc0ca91c3e564b1b525ad2..62199cdcbb0cf2885b969f00a082d9c54b4245b2 100644 --- a/test/integration/project-loading.test.js +++ b/test/integration/project-loading.test.js @@ -38,7 +38,7 @@ describe('Loading scratch gui', () => { const el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); const projectId = '96708228'; await el.sendKeys(`scratch.mit.edu/projects/${projectId}`); - await clickXpath('//button[@title="viewproject"]'); + await clickXpath('//button[@title="View Project"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -52,11 +52,11 @@ describe('Loading scratch gui', () => { await clickText('View 2.0 Project'); let el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); await el.sendKeys('thisisnotaurl'); - await clickXpath('//button[@title="viewproject"]'); + await clickXpath('//button[@title="View Project"]'); el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); await el.clear(); await el.sendKeys('scratch.mit.edu/projects/96708228'); - await clickXpath('//button[@title="viewproject"]'); + await clickXpath('//button[@title="View Project"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -71,7 +71,7 @@ describe('Loading scratch gui', () => { const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -93,7 +93,7 @@ describe('Loading scratch gui', () => { .setSize(1920, 1080); const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Full Screen Control"]'); await clickXpath('//img[@title="Go"]'); diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index aa3790f9508c251438bec724f2f3f813fecaa440..7c306a5eee2c9c05c2d16d1c9ed8a1a489f5370c 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -27,7 +27,7 @@ describe('Working with sounds', () => { test('Adding a sound through the library', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Sounds'); // Delete the sound @@ -65,7 +65,7 @@ describe('Working with sounds', () => { test('Adding a sound by surprise button', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Sounds'); const el = await findByXpath('//button[@aria-label="Choose a Sound"]'); await driver.actions().mouseMove(el) @@ -78,7 +78,7 @@ describe('Working with sounds', () => { test('Duplicating a sound', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Sounds'); await rightClickText('Meow', scope.soundsTab); @@ -95,7 +95,7 @@ describe('Working with sounds', () => { // Regression test for gui issue #1320 test('Switching sprites with different numbers of sounds', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); // Add a sound so this sprite has 2 sounds. await clickText('Sounds'); diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js index 816d06a1ebf246eeb7aa67729412c64bd99c3694..41f2eec3ab4220e83fc08f8936c35328367f3da4 100644 --- a/test/integration/sprites.test.js +++ b/test/integration/sprites.test.js @@ -28,7 +28,7 @@ describe('Working with sprites', () => { test('Adding a sprite through the library', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await clickText('Costumes'); await clickXpath('//button[@aria-label="Choose a Sprite"]'); await clickText('Apple', scope.modal); // Closes modal @@ -39,7 +39,7 @@ describe('Working with sprites', () => { test('Adding a sprite by surprise button', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); await driver.actions().mouseMove(el) .perform(); @@ -51,7 +51,7 @@ describe('Working with sprites', () => { test('Deleting only sprite does not crash', async () => { await loadUri(uri); - await clickXpath('//button[@title="tryit"]'); + await clickXpath('//button[@title="Try It"]'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await rightClickText('Sprite1', scope.spriteTile); await clickText('delete', scope.spriteTile);