diff --git a/package.json b/package.json index b4b134061f6a0f1f025ef917eb064e811a8bdc80..7485a7a3afd28083f4aedca06cda0b9cd145cd12 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "minilog": "3.1.0", "mkdirp": "^0.5.1", "postcss-import": "^11.0.0", - "postcss-loader": "^2.1.4", + "postcss-import": "^12.0.0", + "postcss-loader": "^3.0.0", "postcss-simple-vars": "^4.0.0", "prop-types": "^15.5.10", "raf": "^3.4.0", @@ -96,16 +97,16 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1532446271", - "scratch-l10n": "3.0.20180719145856", - "scratch-paint": "0.2.0-prerelease.20180718183615", - "scratch-render": "0.1.0-prerelease.20180724152606", + "scratch-blocks": "0.1.0-prerelease.1533835159", + "scratch-l10n": "3.0.20180803171042", + "scratch-paint": "0.2.0-prerelease.20180809150043", + "scratch-render": "0.1.0-prerelease.20180808184135", "scratch-storage": "0.5.1", "scratch-svg-renderer": "0.2.0-prerelease.20180712223402", - "scratch-vm": "0.2.0-prerelease.20180724192502", + "scratch-vm": "^0.2.0-prerelease.20180808205317", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", - "style-loader": "^0.21.0", + "style-loader": "^0.22.1", "svg-to-image": "1.1.3", "text-encoding": "0.6.4", "to-style": "1.3.3", diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index 8f39946c128d445c5dbd98acbade5ac10553f1c2..e82103ca10a2d62e78912a13100355a75ed90cc3 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -1,4 +1,5 @@ @import "../../css/colors.css"; +@import "../../css/z-index.css"; $main-button-size: 2.75rem; $more-button-size: 2.25rem; @@ -44,7 +45,7 @@ button::-moz-focus-inner { width: $main-button-size; height: $main-button-size; box-shadow: 0 0 0 4px $motion-transparent; - z-index: 20; /* TODO reorder layout to prevent z-index need */ + z-index: $z-index-add-button; transition: transform, box-shadow 0.5s; } @@ -58,6 +59,10 @@ button::-moz-focus-inner { height: calc($main-button-size - 1rem); } +[dir="rtl"] .main-icon { + transform: scaleX(-1); +} + .more-buttons-outer { /* Need to use two divs to set different overflow x/y @@ -71,6 +76,7 @@ button::-moz-focus-inner { border-top-right-radius: $more-button-size; width: $more-button-size; margin-left: calc(($main-button-size - $more-button-size) / 2); + margin-right: calc(($main-button-size - $more-button-size) / 2); position: absolute; bottom: calc($main-button-size); @@ -150,7 +156,7 @@ button::-moz-focus-inner { border-radius: .25rem !important; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; - z-index: 100 !important; + z-index: $z-index-tooltip !important; } $arrow-size: 0.5rem; diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 2725c502b421007f85c7ec4d8621ff213f35b162..90555dfd8c1fde5ed886b81daa3f716d25d3d053 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -101,6 +101,7 @@ class ActionMenu extends React.Component { img: mainImg, title: mainTitle, moreButtons, + tooltipPlace, onClick } = this.props; @@ -134,7 +135,7 @@ class ActionMenu extends React.Component { className={styles.tooltip} effect="solid" id={mainTooltipId} - place="left" + place={tooltipPlace || 'left'} /> <div className={styles.moreButtonsOuter}> <div className={styles.moreButtons}> @@ -142,7 +143,7 @@ class ActionMenu extends React.Component { fileAccept, fileChange, fileInput}, keyId) => { const isComingSoon = !handleClick; const hasFileInput = fileInput; - const tooltipId = title; + const tooltipId = `${mainTooltipId}-${title}`; return ( <div key={`${tooltipId}-${keyId}`}> <button @@ -174,7 +175,7 @@ class ActionMenu extends React.Component { })} effect="solid" id={tooltipId} - place="left" + place={tooltipPlace || 'left'} /> </div> ); @@ -198,7 +199,8 @@ ActionMenu.propTypes = { fileInput: PropTypes.func // Optional, only for file upload })), onClick: PropTypes.func.isRequired, - title: PropTypes.node.isRequired + title: PropTypes.node.isRequired, + tooltipPlace: PropTypes.string }; export default ActionMenu; diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 132f7d71dd68ec3883f9e2d6dafc2b8679148c21..535c38b0be684e8ac55d3cec8446cf758cd00833 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -32,6 +32,7 @@ $fade-out-distance: 100px; position: absolute; bottom: 0; left: 0; + right:0; background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1)); height: $fade-out-distance; width: 100%; diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 0f7b0b89f7ea02a0f0e4fd2b6472debc75f82784..04cdc293798d3d2217f9eaed7299505db951c3a1 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -17,7 +17,7 @@ const dragTypeMap = { sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => ( +const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, onToggle, onDelete}) => ( <div className={styles.backpackContainer}> <div className={styles.backpackHeader} @@ -45,7 +45,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on {expanded ? ( <div className={styles.backpackList} - ref={dropAreaRef} + ref={containerRef} > {error ? ( <div className={styles.statusMessage}> @@ -104,6 +104,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on ); Backpack.propTypes = { + containerRef: PropTypes.func, contents: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, thumbnailUrl: PropTypes.string, @@ -111,7 +112,6 @@ Backpack.propTypes = { name: PropTypes.string })), dragOver: PropTypes.bool, - dropAreaRef: PropTypes.func, error: PropTypes.bool, expanded: PropTypes.bool, loading: PropTypes.bool, diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index bb5c82fcffab08ea5c860ea746f8ebc9d0a6c71e..457105727df41413ee3767d0d02cada6cdcaf1c3 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -60,12 +60,12 @@ const BrowserModal = ({intl, ...props}) => ( previewFaqLink: ( <a className={styles.faqLink} - href="//scratch.mit.edu/preview-faq" + href="//scratch.mit.edu/3faq" > <FormattedMessage - defaultMessage="Preview FAQ" - description="link to Scratch 3.0 preview FAQ page" - id="gui.unsupportedBrowser.previewfaqlink" + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.unsupportedBrowser.previewfaqlinktext" /> </a> ) diff --git a/src/components/button/button.css b/src/components/button/button.css index 9ce746c52140b73be1ba889fae9c028ab7cfe2bb..9ba2fbd93b4b48dd489f6249e2cafd8abcfc2eec 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -11,10 +11,17 @@ } .icon { - margin-right: .5rem; height: 1.5rem; } +[dir="ltr"] .icon { + margin-right: .5rem; +} + +[dir="rtl"] .icon { + margin-left: .5rem; +} + .content { white-space: nowrap; } diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css index 4ea962fbafd68bc7ecdf3736d96930070a46f0de..7522fbb2ec286f0491d62d1fd6a67a7ebb5b2ed4 100644 --- a/src/components/camera-modal/camera-modal.css +++ b/src/components/camera-modal/camera-modal.css @@ -135,6 +135,10 @@ $main-button-size: 2.75rem; color: $ui-white; } +[dir="rtl"] .retake-button img { + transform: scaleX(-1); +} + @keyframes flash { 0% { opacity: 1; } 100% { opacity: 0; } diff --git a/src/components/camera-modal/camera-modal.jsx b/src/components/camera-modal/camera-modal.jsx index f115a6f8a75f1c77286f15365137509313a8783f..8c499c3624922e7d84ac1b00e75cf8f9205c29c0 100644 --- a/src/components/camera-modal/camera-modal.jsx +++ b/src/components/camera-modal/camera-modal.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Box from '../box/box.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import styles from './camera-modal.css'; import backIcon from './icon--back.svg'; import cameraIcon from '../action-menu/icon--camera.svg'; @@ -79,7 +79,7 @@ const CameraModal = ({intl, ...props}) => ( {props.capture ? <Box className={styles.buttonRow}> <button - className={styles.cancelButton} + className={styles.retakeButton} key="retake-button" onClick={props.onBack} > diff --git a/src/components/coming-soon/coming-soon.css b/src/components/coming-soon/coming-soon.css index bdd7f87eb4f031a9155aec4a0fd3f6affda1ad0d..091d2dd344c4bba864447041cd0c08b5ee07f5d6 100644 --- a/src/components/coming-soon/coming-soon.css +++ b/src/components/coming-soon/coming-soon.css @@ -16,7 +16,7 @@ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; font-size: 1rem !important; line-height: 1.25rem !important; - z-index: $z-index-coming-soon !important; + z-index: $z-index-tooltip !important; } .coming-soon:after { diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index b1e99f37a3cf5222a7a2da7b2cfd2d0730186611..aec4ce630d51e7bf1bcc352d0b13855366d25b15 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -51,10 +51,14 @@ align-items: center; } -.device-tile-image { +[dir="ltr"] .device-tile-image { margin-right: 0.5rem; } +[dir="rtl"] .device-tile-image { + margin-left: 0.5rem; +} + .device-tile-name-wrapper { display: flex; flex-direction: column; @@ -89,9 +93,16 @@ align-items: flex-end; width: 22px; height: 16px; +} + +[dir="ltr"] .signal-strength-meter { margin-right: 1rem; } +[dir="rtl"] .signal-strength-meter { + margin-left: 1rem; +} + .signal-bar { width: 4px; border-radius: 4px; @@ -122,6 +133,14 @@ animation: spin 4s linear infinite; } +[dir="ltr"] .radar { + margin-right: .5rem; +} + +[dir="rtl"] .radar { + margin-left: .5rem; +} + @keyframes spin { 100% { transform: rotate(360deg); @@ -145,6 +164,7 @@ position: absolute; top: -5px; right: -15px; + left: -15px; padding: 5px 5px; background-color: $motion-primary; border-radius: 100%; @@ -215,14 +235,26 @@ margin-left: 3rem; } +[dir="ltr"] .scratch-link-help-step { + margin-left: 3rem; +} + +[dir="rtl"] .scratch-link-help-step { + margin-right: 3rem; +} + .scratch-link-icon { max-width: 50px; } -.help-step-image { +[dir="ltr"] .help-step-image { margin-right: 0.5rem; } +[dir="rtl"] .help-step-image { + margin-left: 0.5rem; +} + .help-step-number { background: $pen-primary; border-radius: 100%; @@ -231,11 +263,18 @@ align-items: center; color: $ui-white; font-weight: bold; - margin-right: 0.5rem; min-width: 2rem; height: 2rem; } +[dir="ltr"] .help-step-number { + margin-right: 0.5rem; +} + +[dir="rtl"] .help-step-number { + margin-left: 0.5rem; +} + .button-row { font-weight: bolder; text-align: center; @@ -281,14 +320,26 @@ border-bottom-left-radius: 0; } -.button-icon-right { +[dir="ltr"] .button-icon-right { margin-left: 0.5rem; } +[dir="rtl"] .button-icon-right { + margin-right: 0.5rem; +} -.button-icon-left { +[dir="ltr"] .button-icon-left { margin-right: 0.5rem; } +[dir="rtl"] .button-icon-left { + margin-left: 0.5rem; +} + +/* reverse back arrow icon for RTL, don't reverse other connection icons */ +[dir="rtl"] .button-icon-back { + transform: scaleX(-1); +} + .red-button { background: $red-primary; } diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx index 577bcd2f71b2969a08883b8c7e8f33f73eb4ff64..6fb9cd4ff98c924e05d0ea5f35ebbf3aa7f1f348 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -3,7 +3,7 @@ import React from 'react'; import keyMirror from 'keymirror'; import Box from '../box/box.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import ScanningStep from '../../containers/scanning-step.jsx'; import AutoScanningStep from '../../containers/auto-scanning-step.jsx'; diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx index 194e95678fed2fae311e583c35f0a676acc58aa8..a5a5ce1b70085d66bfd351599673da58da171821 100644 --- a/src/components/connection-modal/error-step.jsx +++ b/src/components/connection-modal/error-step.jsx @@ -1,5 +1,6 @@ import {FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; @@ -39,7 +40,7 @@ const ErrorStep = props => ( onClick={props.onScanning} > <img - className={styles.buttonIconLeft} + className={classNames(styles.buttonIconLeft, styles.buttonIconBack)} src={backIcon} /> <FormattedMessage diff --git a/src/components/connection-modal/unavailable-step.jsx b/src/components/connection-modal/unavailable-step.jsx index 975a66debe7d0b14b25a87c632c04a494775ac4d..ae89404a1952d8ccded43103dc0b1b6af50cf036 100644 --- a/src/components/connection-modal/unavailable-step.jsx +++ b/src/components/connection-modal/unavailable-step.jsx @@ -1,5 +1,6 @@ import {FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; @@ -64,7 +65,7 @@ const UnavailableStep = props => ( onClick={props.onScanning} > <img - className={styles.buttonIconLeft} + className={classNames(styles.buttonIconLeft, styles.buttonIconBack)} src={backIcon} /> <FormattedMessage diff --git a/src/components/custom-procedures/custom-procedures.css b/src/components/custom-procedures/custom-procedures.css index 6c8967a12d513023f337c3b58d5070281a761170..1716baab306a5b5b4a877831aaff8ee8dfbe5dda 100644 --- a/src/components/custom-procedures/custom-procedures.css +++ b/src/components/custom-procedures/custom-procedures.css @@ -91,6 +91,10 @@ 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; +} diff --git a/src/components/custom-procedures/custom-procedures.jsx b/src/components/custom-procedures/custom-procedures.jsx index 81756e762ee9e8ffd64492a35b59b2849aada0a7..7d2f3e1a5352afb855d6f5e59bae83164e37ff6a 100644 --- a/src/components/custom-procedures/custom-procedures.jsx +++ b/src/components/custom-procedures/custom-procedures.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import Box from '../box/box.jsx'; import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; diff --git a/src/components/custom-procedures/icon--label.svg b/src/components/custom-procedures/icon--label.svg index 2c2d52a66da350abe5947ffdc139a1eacb2dbbf8..c46c2f00cc8c247ff412ecfa41d0af0e4298465d 100644 Binary files a/src/components/custom-procedures/icon--label.svg and b/src/components/custom-procedures/icon--label.svg differ diff --git a/src/components/direction-picker/dial.css b/src/components/direction-picker/dial.css new file mode 100644 index 0000000000000000000000000000000000000000..ce9b7daea87f1f91f59437877c0504255b758666 --- /dev/null +++ b/src/components/direction-picker/dial.css @@ -0,0 +1,41 @@ +@import "../../css/colors.css"; + +.container { + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + user-select: none; +} + +.dial-container { + position: relative; +} + +.dial-face, .dial-handle, .gauge { + position: absolute; + top: 0; + left: 0; + overflow: visible; +} + +.dial-face { + width: 100%; +} + +$dial-size: 40px; + +.dial-handle { + cursor: pointer; + width: $dial-size; + height: $dial-size; + /* Use margin to make positioning via top/left easier */ + margin-left: calc($dial-size / -2); + margin-top: calc($dial-size / -2); +} + +.gauge-path { + fill: $motion-transparent; + stroke: $motion-primary; + stroke-width: 1px; +} diff --git a/src/components/direction-picker/dial.jsx b/src/components/direction-picker/dial.jsx new file mode 100644 index 0000000000000000000000000000000000000000..df97fda7fa4cc03cdf61f2b4fc52f6dfcd05c814 --- /dev/null +++ b/src/components/direction-picker/dial.jsx @@ -0,0 +1,156 @@ +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; +import React from 'react'; +import {getEventXY} from '../../lib/touch-utils'; + +import styles from './dial.css'; + +import dialFace from './icon--dial.svg'; +import dialHandle from './icon--handle.svg'; + +class Dial extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleMouseDown', + 'handleMouseMove', + 'containerRef', + 'handleRef', + 'unbindMouseEvents' + ]); + } + + componentDidMount () { + // Manually add touch/mouse handlers so that preventDefault can be used + // to prevent scrolling on touch. + // Tracked as a react issue https://github.com/facebook/react/issues/6436 + this.handleElement.addEventListener('mousedown', this.handleMouseDown); + this.handleElement.addEventListener('touchstart', this.handleMouseDown); + } + + componentWillUnmount () { + this.unbindMouseEvents(); + this.handleElement.removeEventListener('mousedown', this.handleMouseDown); + this.handleElement.removeEventListener('touchstart', this.handleMouseDown); + } + + /** + * Get direction from dial center to mouse move event. + * @param {Event} e - Mouse move event. + * @returns {number} Direction in degrees, clockwise, 90=horizontal. + */ + directionToMouseEvent (e) { + const {x: mx, y: my} = getEventXY(e); + const bbox = this.containerElement.getBoundingClientRect(); + const cy = bbox.top + (bbox.height / 2); + const cx = bbox.left + (bbox.width / 2); + const angle = Math.atan2(my - cy, mx - cx); + const degrees = angle * (180 / Math.PI); + return degrees + 90; // To correspond with scratch coordinate system + } + + /** + * Create SVG path data string for the dial "gauge", the overlaid arc slice. + * @param {number} radius - The radius of the dial. + * @param {number} direction - Direction in degrees, clockwise, 90=horizontal. + * @returns {string} Path data string for the gauge. + */ + gaugePath (radius, direction) { + const rads = (direction) * (Math.PI / 180); + const path = []; + path.push(`M ${radius} 0`); + path.push(`L ${radius} ${radius}`); + path.push(`L ${radius + (radius * Math.sin(rads))} ${radius - (radius * Math.cos(rads))}`); + path.push(`A ${radius} ${radius} 0 0 ${direction < 0 ? 1 : 0} ${radius} 0`); + path.push(`Z`); + return path.join(' '); + } + + handleMouseMove (e) { + this.props.onChange(this.directionToMouseEvent(e) + this.directionOffset); + e.preventDefault(); + } + + unbindMouseEvents () { + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mouseup', this.unbindMouseEvents); + window.removeEventListener('touchmove', this.handleMouseMove); + window.removeEventListener('touchend', this.unbindMouseEvents); + } + + handleMouseDown (e) { + // Because the drag handle is not a single point, there is some initial + // difference between the current sprite direction and the direction to the mouse + // Store this offset to prevent jumping when the mouse is moved. + this.directionOffset = this.props.direction - this.directionToMouseEvent(e); + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mouseup', this.unbindMouseEvents); + window.addEventListener('touchmove', this.handleMouseMove); + window.addEventListener('touchend', this.unbindMouseEvents); + e.preventDefault(); + } + + containerRef (el) { + this.containerElement = el; + } + + handleRef (el) { + this.handleElement = el; + } + + render () { + const {direction, radius} = this.props; + return ( + <div className={styles.container}> + <div + className={styles.dialContainer} + ref={this.containerRef} + style={{ + width: `${radius * 2}px`, + height: `${radius * 2}px` + }} + > + <img + className={styles.dialFace} + draggable={false} + src={dialFace} + /> + <svg + className={styles.gauge} + height={radius * 2} + width={radius * 2} + > + <path + className={styles.gaugePath} + d={this.gaugePath(radius, direction)} + /> + </svg> + <img + className={styles.dialHandle} + draggable={false} + ref={this.handleRef} + src={dialHandle} + style={{ + top: `${radius - (radius * Math.cos(direction * (Math.PI / 180)))}px`, + left: `${radius + (radius * Math.sin(direction * (Math.PI / 180)))}px`, + transform: `rotate(${direction}deg)` + }} + /> + </div> + </div> + ); + } +} + +Dial.propTypes = { + direction: PropTypes.number, + onChange: PropTypes.func.isRequired, + radius: PropTypes.number +}; + +Dial.defaultProps = { + direction: 90, // degrees + radius: 56 // px +}; + +export default Dial; diff --git a/src/components/direction-picker/direction-picker.css b/src/components/direction-picker/direction-picker.css new file mode 100644 index 0000000000000000000000000000000000000000..501ff02171425a3a229e56b789823a748b2d4554 --- /dev/null +++ b/src/components/direction-picker/direction-picker.css @@ -0,0 +1,32 @@ +@import "../../css/colors.css"; + +.button-row { + display: flex; + flex-direction: row; + justify-content: center; + +} + +.icon-button { + margin: 0.25rem; + border: none; + background: none; + outline: none; + cursor: pointer; + user-select: none; +} + +.icon-button:active > img { + width: 20px; + height: 20px; + transform: scale(1.15); +} + +.icon-button > img { + transition: transform 0.1s; + filter: grayscale(100%); +} + +.icon-button.active > img { + filter: none; +} diff --git a/src/components/direction-picker/direction-picker.jsx b/src/components/direction-picker/direction-picker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..626957381abf5d42680e4580ce49d557cde06a49 --- /dev/null +++ b/src/components/direction-picker/direction-picker.jsx @@ -0,0 +1,142 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Popover from 'react-popover'; +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +import Label from '../forms/label.jsx'; +import Input from '../forms/input.jsx'; +import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import Dial from './dial.jsx'; + +import styles from './direction-picker.css'; + +import allAroundIcon from './icon--all-around.svg'; +import leftRightIcon from './icon--left-right.svg'; +import dontRotateIcon from './icon--dont-rotate.svg'; + +const BufferedInput = BufferedInputHOC(Input); + +const directionLabel = ( + <FormattedMessage + defaultMessage="Direction" + description="Sprite info direction label" + id="gui.SpriteInfo.direction" + /> +); + +const RotationStyles = { + ALL_AROUND: 'all around', + LEFT_RIGHT: 'left-right', + DONT_ROTATE: "don't rotate" +}; + +const messages = defineMessages({ + allAround: { + id: 'gui.directionPicker.rotationStyles.allAround', + description: 'Button to change to the all around rotation style', + defaultMessage: 'All Around' + }, + leftRight: { + id: 'gui.directionPicker.rotationStyles.leftRight', + description: 'Button to change to the left-right rotation style', + defaultMessage: 'Left/Right' + }, + dontRotate: { + id: 'gui.directionPicker.rotationStyles.dontRotate', + description: 'Button to change to the dont rotate rotation style', + defaultMessage: 'Do not rotate' + } +}); + +const DirectionPicker = props => ( + <Label + secondary + text={directionLabel} + > + <Popover + body={ + <div> + <Dial + direction={props.direction} + onChange={props.onChangeDirection} + /> + <div className={styles.buttonRow}> + <button + className={classNames(styles.iconButton, { + [styles.active]: props.rotationStyle === RotationStyles.ALL_AROUND + })} + title={props.intl.formatMessage(messages.allAround)} + onClick={props.onClickAllAround} + > + <img + draggable={false} + src={allAroundIcon} + /> + </button> + <button + className={classNames(styles.iconButton, { + [styles.active]: props.rotationStyle === RotationStyles.LEFT_RIGHT + })} + title={props.intl.formatMessage(messages.leftRight)} + onClick={props.onClickLeftRight} + > + <img + draggable={false} + src={leftRightIcon} + /> + </button> + <button + className={classNames(styles.iconButton, { + [styles.active]: props.rotationStyle === RotationStyles.DONT_ROTATE + })} + title={props.intl.formatMessage(messages.dontRotate)} + onClick={props.onClickDontRotate} + > + <img + draggable={false} + src={dontRotateIcon} + /> + </button> + </div> + </div> + } + isOpen={props.popoverOpen} + preferPlace="above" + onOuterAction={props.onClosePopover} + > + <BufferedInput + small + disabled={props.disabled} + label={directionLabel} + tabIndex="0" + type="text" + value={props.disabled ? '' : props.direction} + onFocus={props.onOpenPopover} + onSubmit={props.onChangeDirection} + /> + </Popover> + </Label> + +); + +DirectionPicker.propTypes = { + direction: PropTypes.number, + disabled: PropTypes.bool.isRequired, + intl: intlShape, + onChangeDirection: PropTypes.func.isRequired, + onClickAllAround: PropTypes.func.isRequired, + onClickDontRotate: PropTypes.func.isRequired, + onClickLeftRight: PropTypes.func.isRequired, + onClosePopover: PropTypes.func.isRequired, + onOpenPopover: PropTypes.func.isRequired, + popoverOpen: PropTypes.bool.isRequired, + rotationStyle: PropTypes.string +}; + +const WrappedDirectionPicker = injectIntl(DirectionPicker); + +export { + WrappedDirectionPicker as default, + RotationStyles +}; diff --git a/src/components/direction-picker/icon--all-around.svg b/src/components/direction-picker/icon--all-around.svg new file mode 100644 index 0000000000000000000000000000000000000000..2412c0b2327c3db488418d978ad382fa72335702 Binary files /dev/null and b/src/components/direction-picker/icon--all-around.svg differ diff --git a/src/components/direction-picker/icon--dial.svg b/src/components/direction-picker/icon--dial.svg new file mode 100644 index 0000000000000000000000000000000000000000..d4aa8ed8205a7d51ee4c78086e409c2ef3248ff5 Binary files /dev/null and b/src/components/direction-picker/icon--dial.svg differ diff --git a/src/components/direction-picker/icon--dont-rotate.svg b/src/components/direction-picker/icon--dont-rotate.svg new file mode 100644 index 0000000000000000000000000000000000000000..4796c03af2562424f84487a4d8522ea16f20537c Binary files /dev/null and b/src/components/direction-picker/icon--dont-rotate.svg differ diff --git a/src/components/direction-picker/icon--handle.svg b/src/components/direction-picker/icon--handle.svg new file mode 100644 index 0000000000000000000000000000000000000000..8e5fee6e0ba3c7edd111b8a52e8fd58cb6239ddd Binary files /dev/null and b/src/components/direction-picker/icon--handle.svg differ diff --git a/src/components/direction-picker/icon--left-right.svg b/src/components/direction-picker/icon--left-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..4525bd6e917188b4f348b4215d4f00a15955f78d Binary files /dev/null and b/src/components/direction-picker/icon--left-right.svg differ diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css index b67e60795c29a100c7e67982b8dafeb8bb46c28b..fffef4812eca008d8d3e3dc30c9a540e75192eef 100644 --- a/src/components/filter/filter.css +++ b/src/components/filter/filter.css @@ -18,13 +18,22 @@ .filter-icon { position: absolute; top: 0; - left: 0; height: 1rem; width: 1rem; +} + +[dir="ltr"] .filter-icon { + left: 0; margin: 0.75rem 0.75rem 0.75rem 1rem; } +[dir="rtl"] .filter-icon { + right: 0; + margin: 0.75rem 1rem 0.75rem 0.75rem; + transform: scaleX(-1); +} + .filter:focus-within { box-shadow: 0 0 0 .25rem $motion-transparent; } @@ -36,7 +45,6 @@ opacity: 0; position: absolute; top: 0; - right: 0; display: flex; justify-content: center; @@ -53,6 +61,14 @@ transition: opacity 0.05s linear; } +[dir="ltr"] .x-icon-wrapper { + right: 0; +} + +[dir="rtl"] .x-icon-wrapper { + left: 0; +} + /* Shown state */ @@ -96,13 +112,27 @@ font-size: 0.75rem; letter-spacing: 0.15px; cursor: text; +} + +[dir="ltr"] .filter-input { padding: .625rem 2rem .625rem 3rem; } +[dir="rtl"] .filter-input { + padding: .625rem 3rem .625rem 2rem; +} + .filter-input::placeholder { opacity: .5; - padding: 0 0 0 0.25rem; color: $text-primary; font-size: 0.875rem; letter-spacing: 0.15px; } + +[dir="ltr"] .filter-input::placeholder { + padding: 0 0 0 0.25rem; +} + +[dir="rtl"] .filter-input::placeholder { + padding: 0 0.25rem 0 0; +} diff --git a/src/components/forms/label.css b/src/components/forms/label.css index 759a731d37efca6effb443534f24ed17e30bec5c..14691f1056a672bb91b5b1912c47661440b36566 100644 --- a/src/components/forms/label.css +++ b/src/components/forms/label.css @@ -9,13 +9,20 @@ .input-label, .input-label-secondary { font-size: 0.625rem; - margin-right: .5rem; user-select: none; cursor: default; white-space: nowrap; } +[dir="ltr"] .input-label, .input-label-secondary { + margin-right: .5rem; +} + +[dir="rtl"] .input-label, .input-label-secondary { + margin-left: .5rem; +} + .input-label { font-weight: bold; } diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index e40b025735171530f8006dbbfd589e48879a82e3..1924bd8c4080c2ce3208de1c2fb211492a0fbd79 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -63,7 +63,6 @@ flex-grow: 1; height: 80%; margin-bottom: 0; - margin-left: -0.5rem; border-radius: 1rem 1rem 0 0; border: 1px solid $ui-black-transparent; @@ -82,9 +81,24 @@ white-space: nowrap; } +[dir="ltr"] .tab { + margin-left: -0.5rem; +} + +[dir="rtl"] .tab { + margin-right: -0.5rem; +} + +[dir="ltr"] .tab:nth-of-type(1) { + margin-left: 0; +} + +[dir="rtl"] .tab:nth-of-type(1) { + margin-right: 0; +} + /* Use z-indices to force left-on-top for tabs */ .tab:nth-of-type(1) { - margin-left: 0; z-index: 3; } .tab:nth-of-type(2) { @@ -106,11 +120,19 @@ } .tab img { - margin-right: 0.125rem; width: 1.375rem; filter: grayscale(100%); } +[dir="ltr"] .tab img { + margin-right: 0.125rem; +} + +[dir="rtl"] .tab img { + margin-left: 0.125rem; + transform: scaleX(-1); +} + .tab.is-selected img { filter: none; } @@ -191,7 +213,7 @@ padding-right: $space; min-height: 0; /* this makes it work in Firefox */ - + /* For making the sprite-selector a scrollable pane @todo: Not working in Safari @@ -206,6 +228,7 @@ position: absolute; bottom: 0; left: 0; + right: 0; z-index: $z-index-extension-button; background: $motion-primary; @@ -240,6 +263,10 @@ $fade-out-distance: 15px; height: 1.75rem; } +[dir="rtl"] .extension-button-icon { + transform: scaleX(-1); +} + .extension-button > div { margin-top: 0; } diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 8d251bd14e17538e5f38da679d8ddd3eb752757d..c3b7cff716b6b07fd874c64abfa68035a593fca5 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -65,6 +65,7 @@ const GUIComponent = props => { importInfoVisible, intl, isPlayerOnly, + isRtl, loading, onExtensionButtonClick, onActivateCostumesTab, @@ -110,6 +111,7 @@ const GUIComponent = props => { ) : ( <Box className={styles.pageWrapper} + dir={isRtl ? 'rtl' : 'ltr'} {...componentProps} > {previewInfoVisible ? ( @@ -282,6 +284,7 @@ GUIComponent.propTypes = { importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, isPlayerOnly: PropTypes.bool, + isRtl: PropTypes.bool, loading: PropTypes.bool, onActivateCostumesTab: PropTypes.func, onActivateSoundsTab: PropTypes.func, diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx index ad42929e7d6c24afced4c2febf2bda976ea27f2b..0fa3aac864978b182c294cc4ce0de911ef652e65 100644 --- a/src/components/import-modal/import-modal.jsx +++ b/src/components/import-modal/import-modal.jsx @@ -16,6 +16,12 @@ const messages = defineMessages({ description: 'Scratch 2.0 import modal label - for accessibility' }, formDescription: { + defaultMessage: + 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Beta will not be saved.', + description: 'Import project message', + id: 'gui.importInfo.betamessage' + }, + previewFormDescription: { defaultMessage: 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.', description: 'Import project message', @@ -123,12 +129,12 @@ const ImportModal = ({intl, ...props}) => ( previewFaqLink: ( <a className={styles.faqLink} - href="//scratch.mit.edu/preview-faq" + href="//scratch.mit.edu/3faq" > <FormattedMessage - defaultMessage="Preview FAQ" - description="link to Scratch 3.0 preview FAQ page" - id="gui.importInfo.previewfaqlink" + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.importInfo.previewfaqlinktext" /> </a> ) diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 08bd6b5561446df26a4968c64d559ce1d0d3f2bc..24117372b46f1524a3bcf46c4189b34e3a020d33 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -14,6 +14,7 @@ import ProjectLoader from '../../containers/project-loader.jsx'; import Menu from '../../containers/menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import ProjectSaver from '../../containers/project-saver.jsx'; +import TurboMode from '../../containers/turbo-mode.jsx'; import {openTipsLibrary} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; @@ -297,15 +298,23 @@ class MenuBar extends React.Component { </MenuItem> </MenuItemTooltip> <MenuSection> - <MenuItemTooltip id="turbo"> - <MenuItem> - <FormattedMessage - defaultMessage="Turbo mode" - description="Menu bar item for toggling turbo mode" - id="gui.menuBar.turboMode" - /> + <TurboMode>{(toggleTurboMode, {turboMode}) => ( + <MenuItem onClick={toggleTurboMode}> + {turboMode ? ( + <FormattedMessage + defaultMessage="Turn off Turbo Mode" + description="Menu bar item for turning off turbo mode" + id="gui.menuBar.turboModeOff" + /> + ) : ( + <FormattedMessage + defaultMessage="Turn on Turbo Mode" + description="Menu bar item for turning on turbo mode" + id="gui.menuBar.turboModeOn" + /> + )} </MenuItem> - </MenuItemTooltip> + )}</TurboMode> </MenuSection> </MenuBarMenu> </div> @@ -376,7 +385,7 @@ class MenuBar extends React.Component { <div className={classNames(styles.menuBarItem, styles.feedbackButtonWrapper)}> <a className={styles.feedbackLink} - href="https://scratch.mit.edu/discuss/topic/299791/" + href="https://scratch.mit.edu/discuss/57/" rel="noopener noreferrer" target="_blank" > diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css index 3b318f40ddd7b0697e33d90aa31b505c519a0394..1bb4f196343e94658a56c140447ee91ce4a739aa 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -100,13 +100,24 @@ $sides: 20rem; user-select: none; letter-spacing: 0.4px; cursor: default; +} + +[dir="ltr"] .header-item-title { margin: 0 -$sides 0 0; } -.full-screen .header-item-title { +[dir="rtl"] .header-item-title { + margin: 0 0 0 -$sides; +} + +.full-screen [dir="ltr"] .header-item-title { margin: 0 0 0 -$sides; } +.full-screen [dir="rtl"] .header-item-title { + margin: 0 -$sides 0 0; +} + .header-item-close { flex-basis: $sides; justify-content: flex-end; @@ -120,17 +131,36 @@ $sides: 20rem; .back-button { font-weight: normal; + padding-right: 0; padding-left: 0; } +[dir="rtl"] .back-button img { + transform: scaleX(-1); +} + .header-item-help { padding: 0; - margin-right: -4.75rem; z-index: 1; } +[dir="ltr"] .header-item-help { + margin-right: -4.75rem; +} + +[dir="rtl"] .header-item-help { + margin-left: -4.75rem; +} + .help-button { font-weight: normal; - padding-right: 0; font-size: 0.75rem; } + +[dir="ltr"] .help-button { + padding-right: 0; +} + +[dir="rtl"] .help-button { + padding-left: 0; +} diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index acce21993d86b23137104f5f7bddc1215c1d26bf..32fe0502854d4676a2b617f5ccb04f46660a00d0 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -24,6 +24,7 @@ const ModalComponent = props => ( onRequestClose={props.onRequestClose} > <Box + dir={props.isRtl ? 'rtl' : 'ltr'} direction="column" grow={1} > @@ -103,6 +104,7 @@ ModalComponent.propTypes = { fullScreen: PropTypes.bool, headerClassName: PropTypes.string, headerImage: PropTypes.string, + isRtl: PropTypes.bool, onHelp: PropTypes.func, onRequestClose: PropTypes.func }; diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx index 5772b518539a7541f30859a3aef6540eef030878..b9c803b767782939b86193725d4ff6dd09b919d1 100644 --- a/src/components/preview-modal/preview-modal.jsx +++ b/src/components/preview-modal/preview-modal.jsx @@ -12,6 +12,11 @@ const messages = defineMessages({ id: 'gui.previewInfo.label', defaultMessage: 'Try Scratch 3.0', description: 'Scratch 3.0 modal label - for accessibility' + }, + previewWelcome: { + defaultMessage: 'Welcome to the Scratch 3.0 Beta', + description: 'Header for Preview Info Modal', + id: 'gui.previewInfo.welcome' } }); @@ -28,15 +33,15 @@ const PreviewModal = ({intl, ...props}) => ( <Box className={styles.body}> <h2> <FormattedMessage - defaultMessage="Welcome to the Scratch 3.0 Preview" - description="Header for Preview Info Modal" - id="gui.previewInfo.welcome" + 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 preview" + description="Invitation to try 3.0 Beta" id="gui.previewInfo.invitation" /> </p> @@ -48,7 +53,7 @@ const PreviewModal = ({intl, ...props}) => ( > <FormattedMessage defaultMessage="Not Now" - description="Label for button to back out of trying Scratch 3.0 preview" + description="Label for button to back out of trying Scratch 3.0 Beta" id="gui.previewInfo.notnow" /> </button> @@ -59,7 +64,7 @@ const PreviewModal = ({intl, ...props}) => ( > <FormattedMessage defaultMessage="Try It! {caticon}" - description="Label for button to try Scratch 3.0 preview" + description="Label for button to try Scratch 3.0 Beta" id="gui.previewModal.tryit" values={{ caticon: ( @@ -86,18 +91,18 @@ const PreviewModal = ({intl, ...props}) => ( <Box className={styles.faqLinkText}> <FormattedMessage defaultMessage="To learn more, go to the {previewFaqLink}." - description="Invitation to try 3.0 preview" + description="Invitation to try 3.0 Beta" id="gui.previewInfo.previewfaq" values={{ previewFaqLink: ( <a className={styles.faqLink} - href="//scratch.mit.edu/preview-faq" + href="//scratch.mit.edu/3faq" > <FormattedMessage - defaultMessage="Preview FAQ" - description="link to Scratch 3.0 preview FAQ page" - id="gui.previewInfo.previewfaqlink" + defaultMessage="FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.previewInfo.previewfaqlinktext" /> </a> ) diff --git a/src/components/preview-modal/welcome.png b/src/components/preview-modal/welcome.png index 520b619919b1b9dfabcef138fc16fc64aebee9e6..1172df932b425ae538bcfe2d23430373310e325e 100644 Binary files a/src/components/preview-modal/welcome.png and b/src/components/preview-modal/welcome.png differ diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index 78e3ecb27c6b3d2583528ed1dc6b1df6f36dca6e..ae523e188013ad9c79433ed908f4623665551a50 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -60,10 +60,14 @@ 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; +} + .more-options { border-top: 1px dashed hsla(0, 0%, 0%, .25); overflow: visible; diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 3e5ec7950fc1be25a4e5cc89297a9828237fd649..42de1599939801b8896374f032525be7c77982a9 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -4,7 +4,7 @@ import React from 'react'; import Box from '../box/box.jsx'; import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import styles from './prompt.css'; diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx index 40167172deac8f7167c88d2046b89a92d5162886..8f76b506f33e8e38c00869c0a2d06bf6f6fcc471 100644 --- a/src/components/record-modal/playback-step.jsx +++ b/src/components/record-modal/playback-step.jsx @@ -87,7 +87,7 @@ const PlaybackStep = props => ( </Box> <Box className={styles.buttonRow}> <button - className={styles.cancelButton} + className={styles.rerecordButton} onClick={props.onBack} > <img diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css index 79ce9c606a995d69642eabc76b064ba06cc50aa1..ae9d8afcc0d0b2d8b5235b7c86b30c5fa3bb15b6 100644 --- a/src/components/record-modal/record-modal.css +++ b/src/components/record-modal/record-modal.css @@ -118,3 +118,7 @@ opacity: 0.2; transition: 0.1s; } + +[dir="rtl"] .rerecord-button img { + transform: scaleX(-1); +} diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx index 2adc9c14774b9fcf8992c78a53a531c26328429d..5f4086c1c60a35f7a46e011d49248f3849d93de6 100644 --- a/src/components/record-modal/record-modal.jsx +++ b/src/components/record-modal/record-modal.jsx @@ -4,7 +4,7 @@ import Box from '../box/box.jsx'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import RecordingStep from '../../containers/recording-step.jsx'; import PlaybackStep from '../../containers/playback-step.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import styles from './record-modal.css'; const messages = defineMessages({ diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 7703802e307b871ecfb073f12458126d3b8b43f6..c6e620001af95c4762242a6f1d0f914c02d0d561 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -14,6 +14,10 @@ align-items: center; } +[dir="rtl"] .row-reverse { + flex-direction: row-reverse; +} + .row + .row { margin-top: calc(2 * $space); } @@ -23,12 +27,28 @@ flex-direction: row; } -.input-group + .input-group { +[dir="ltr"] .input-group + .input-group { margin-left: calc(2 * $space); } -.input-group { +[dir="rtl"] .input-group + .input-group { + margin-right: calc(2 * $space); +} + +[dir="ltr"] .input-group { + padding-right: calc(2 * $space); + border-right: 1px dashed $ui-black-transparent; +} + +[dir="rtl"] .input-group { + padding-left: calc(2 * $space); + border-left: 1px dashed $ui-black-transparent; +} + +[dir="rtl"] .row-reverse > .input-group { + padding-left: 0; padding-right: calc(2 * $space); + border-left: none; border-right: 1px dashed $ui-black-transparent; } @@ -87,26 +107,31 @@ $border-radius: 0.25rem; /*min-width: 1.5rem;*/ } +[dir="rtl"] .undo-icon, [dir="rtl"] .redo-icon { + transform: scaleX(-1); +} + .trim-button { display: flex; align-items: center; color: $text-primary; font-size: 0.625rem; - margin-left: 1rem; user-select: none; } +[dir="ltr"] .trim-button { + margin-left: 1rem; +} + +[dir="rtl"] .trim-button { + margin-right: 1rem; +} + .trim-button > img { width: 1.25rem; margin-bottom: -0.375rem; } -.input-group-right { - flex-grow: 1; - display: flex; - flex-direction: row-reverse; -} - .effect-button { flex-basis: 60px; color: $text-primary; @@ -124,26 +149,47 @@ $border-radius: 0.25rem; margin-bottom: -0.375rem; } -.button-group { +[dir="ltr"] .button-group { margin-left: 1rem; } +[dir="rtl"] .button-group { + margin-right: 1rem; +} + .button-group .button { border-radius: 0; +} + +[dir="ltr"] .button-group .button { border-left: none; } +[dir="rtl"] .button-group .button { + border-right: none; +} -.button-group .button:last-of-type { +[dir="ltr"] .button-group .button:last-of-type { border-top-right-radius: $border-radius; border-bottom-right-radius: $border-radius; } -.button-group .button:first-of-type { +[dir="ltr"] .button-group .button:first-of-type { border-left: 1px solid $ui-black-transparent; border-top-left-radius: $border-radius; border-bottom-left-radius: $border-radius; } +[dir="rtl"] .button-group .button:last-of-type { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} + +[dir="rtl"] .button-group .button:first-of-type { + border-right: 1px solid $ui-black-transparent; + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; +} + .button:disabled > img { opacity: 0.25; } diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index a4aafcd9155968b46d30d8022092a3da11cc58ae..f97338e06a55bf75e0bceda2ddc33d8a9be1070b 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -122,6 +122,7 @@ const SoundEditor = props => ( onClick={props.onUndo} > <img + className={styles.undoIcon} draggable={false} src={undoIcon} /> @@ -133,6 +134,7 @@ const SoundEditor = props => ( onClick={props.onRedo} > <img + className={styles.redoIcon} draggable={false} src={redoIcon} /> @@ -168,7 +170,7 @@ const SoundEditor = props => ( /> </div> </div> - <div className={styles.row}> + <div className={classNames(styles.row, styles.rowReverse)}> <div className={styles.inputGroup}> {props.playhead ? ( <button diff --git a/src/components/sprite-info/sprite-info.css b/src/components/sprite-info/sprite-info.css index c835843994ce4ffb477a742fdd48c6461c288992..179b13965c5268ee367aa6fed7d2ec7abad98e3f 100644 --- a/src/components/sprite-info/sprite-info.css +++ b/src/components/sprite-info/sprite-info.css @@ -59,27 +59,45 @@ cursor: default; } -.radio-left { +.radio-first { border: 1px solid $ui-black-transparent; +} + +[dir="ltr"] .radio-first { border-top-left-radius: $form-radius; border-bottom-left-radius: $form-radius; } -.radio-left:focus { +[dir="rtl"] .radio-first { + border-top-right-radius: $form-radius; + border-bottom-right-radius: $form-radius; +} + +.radio-first:focus { border-color: $motion-primary; box-shadow: inset 0 0 0 -2px $ui-black-transparent; } -.radio-right { +.radio-last { border-bottom: 1px solid $ui-black-transparent; border-top: 1px solid $ui-black-transparent; +} + +[dir="ltr"] .radio-last { border-right: 1px solid $ui-black-transparent; border-left: 1px solid $ui-black-transparent; border-top-right-radius: $form-radius; border-bottom-right-radius: $form-radius; } -.radio-right:focus { +[dir="rtl"] .radio-last { + border-left: 1px solid $ui-black-transparent; + border-right: 1px solid $ui-black-transparent; + border-top-left-radius: $form-radius; + border-bottom-left-radius: $form-radius; +} + +.radio-last:focus { border-color: $motion-primary; box-shadow: inset 0 0 0 -2px $ui-black-transparent; } diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index 37e18735a8c6b2bddbba1443f356b820f50ca321..f4e3773b96ff002f25092bd74426cc5238527625 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -6,6 +6,8 @@ import Box from '../box/box.jsx'; import Label from '../forms/label.jsx'; import Input from '../forms/input.jsx'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import DirectionPicker from '../../containers/direction-picker.jsx'; + import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js'; @@ -29,6 +31,7 @@ const messages = defineMessages({ class SpriteInfo extends React.Component { shouldComponentUpdate (nextProps) { return ( + this.props.rotationStyle !== nextProps.rotationStyle || this.props.direction !== nextProps.direction || this.props.disabled !== nextProps.disabled || this.props.name !== nextProps.name || @@ -65,13 +68,6 @@ class SpriteInfo extends React.Component { id="gui.SpriteInfo.size" /> ); - const directionLabel = ( - <FormattedMessage - defaultMessage="Direction" - description="Sprite info direction label" - id="gui.SpriteInfo.direction" - /> - ); const spriteNameInput = ( <BufferedInput @@ -180,7 +176,7 @@ class SpriteInfo extends React.Component { <div className={classNames( styles.radio, - styles.radioLeft, + styles.radioFirst, styles.iconWrapper, { [styles.isActive]: this.props.visible && !this.props.disabled, @@ -199,7 +195,7 @@ class SpriteInfo extends React.Component { <div className={classNames( styles.radio, - styles.radioRight, + styles.radioLast, styles.iconWrapper, { [styles.isActive]: !this.props.visible && !this.props.disabled, @@ -234,20 +230,13 @@ class SpriteInfo extends React.Component { </Label> </div> <div className={classNames(styles.group, styles.largerInput)}> - <Label - secondary - text={directionLabel} - > - <BufferedInput - small - disabled={this.props.disabled} - label={directionLabel} - tabIndex="0" - type="text" - value={this.props.disabled ? '' : this.props.direction} - onSubmit={this.props.onChangeDirection} - /> - </Label> + <DirectionPicker + direction={this.props.direction} + disabled={this.props.disabled} + rotationStyle={this.props.rotationStyle} + onChangeDirection={this.props.onChangeDirection} + onChangeRotationStyle={this.props.onChangeRotationStyle} + /> </div> </div> </Box> @@ -265,6 +254,7 @@ SpriteInfo.propTypes = { name: PropTypes.string, onChangeDirection: PropTypes.func, onChangeName: PropTypes.func, + onChangeRotationStyle: PropTypes.func, onChangeSize: PropTypes.func, onChangeX: PropTypes.func, onChangeY: PropTypes.func, @@ -272,6 +262,7 @@ SpriteInfo.propTypes = { onClickVisible: PropTypes.func, onPressNotVisible: PropTypes.func, onPressVisible: PropTypes.func, + rotationStyle: PropTypes.string, size: PropTypes.oneOfType([ PropTypes.string, PropTypes.number diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css index 24bf17395a71fec24c218446c88762d8ad3cb1a9..d29d15c82c0777cf8eecec14ae027972715cc506 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.css +++ b/src/components/sprite-selector-item/sprite-selector-item.css @@ -80,15 +80,29 @@ .delete-button { position: absolute; top: 0.125rem; - right: 0.125rem; z-index: 1; } +[dir="ltr"] .delete-button { + right: 0.125rem; +} + +[dir="rtl"] .delete-button { + left: 0.125rem; +} + .number { position: absolute; top: 0.15rem; - left: 0.15rem; font-size: 0.625rem; font-weight: bold; z-index: 2; } + +[dir="ltr"] .number { + left: 0.15rem; +} + +[dir="rtl"] .number { + right: 0.15rem; +} diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx index a29912b79385183e8c49e0ac819ba5288ec3e761..df42c6c28e94024d15c58e2d388ea4c3ce608fbf 100644 --- a/src/components/sprite-selector/sprite-list.jsx +++ b/src/components/sprite-selector/sprite-list.jsx @@ -53,8 +53,11 @@ const SpriteList = function (props) { // Note the absence of the self-sharing check: a sprite can share assets with itself. // This is a quirk of 2.0, but seems worth leaving possible, it // allows quick (albeit unusual) duplication of assets. - isRaised = isRaised || draggingType === DragConstants.COSTUME || - draggingType === DragConstants.SOUND; + isRaised = isRaised || [ + DragConstants.COSTUME, + DragConstants.SOUND, + DragConstants.BACKPACK_COSTUME, + DragConstants.BACKPACK_SOUND].includes(draggingType); return ( <SortableAsset diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index 70895211757bba3a752569a9969aaf21c783cc8e..da89efab0433d86fe091e452f56c06f3fb496e9e 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -68,8 +68,14 @@ .add-button { position: absolute; bottom: 0.75rem; +} + +[dir="ltr"] .add-button { right: 1rem; - z-index: $z-index-add-button; +} + +[dir="rtl"] .add-button { + left: 1rem; } .raised { diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index e6948c5b7d85c08720a24625425e560c345811aa..5b55c200998b4076e14da558e2504ce0b2c4323a 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; - import Box from '../box/box.jsx'; import SpriteInfo from '../../containers/sprite-info.jsx'; import SpriteList from './sprite-list.jsx'; import ActionMenu from '../action-menu/action-menu.jsx'; import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants'; +import RtlLocales from '../../lib/rtl-locales'; import styles from './sprite-selector.css'; @@ -47,6 +47,7 @@ const SpriteSelectorComponent = function (props) { intl, onChangeSpriteDirection, onChangeSpriteName, + onChangeSpriteRotationStyle, onChangeSpriteSize, onChangeSpriteVisibility, onChangeSpriteX, @@ -84,6 +85,7 @@ const SpriteSelectorComponent = function (props) { direction={selectedSprite.direction} disabled={spriteInfoDisabled} name={selectedSprite.name} + rotationStyle={selectedSprite.rotationStyle} size={selectedSprite.size} stageSize={stageSize} visible={selectedSprite.visible} @@ -91,6 +93,7 @@ const SpriteSelectorComponent = function (props) { y={selectedSprite.y} onChangeDirection={onChangeSpriteDirection} onChangeName={onChangeSpriteName} + onChangeRotationStyle={onChangeSpriteRotationStyle} onChangeSize={onChangeSpriteSize} onChangeVisibility={onChangeSpriteVisibility} onChangeX={onChangeSpriteX} @@ -137,6 +140,7 @@ const SpriteSelectorComponent = function (props) { } ]} title={intl.formatMessage(messages.addSpriteFromLibrary)} + tooltipPlace={RtlLocales.indexOf(intl.locale) === -1 ? 'left' : 'right'} onClick={onNewSpriteClick} /> </Box> @@ -152,6 +156,7 @@ SpriteSelectorComponent.propTypes = { intl: intlShape.isRequired, onChangeSpriteDirection: PropTypes.func, onChangeSpriteName: PropTypes.func, + onChangeSpriteRotationStyle: PropTypes.func, onChangeSpriteSize: PropTypes.func, onChangeSpriteVisibility: PropTypes.func, onChangeSpriteX: PropTypes.func, diff --git a/src/components/stage-header/stage-header.css b/src/components/stage-header/stage-header.css index 93ef5b4408a1d701fc624a0f41b9ad994844dd58..397c679da34811e8170a422d6f24dc336a619db4 100644 --- a/src/components/stage-header/stage-header.css +++ b/src/components/stage-header/stage-header.css @@ -31,9 +31,16 @@ .stage-size-toggle-group { display: flex; +} + +[dir="ltr"] .stage-size-toggle-group { margin-right: .2rem; } +[dir="rtl"] .stage-size-toggle-group { + margin-left: .2rem; +} + .stage-button { display: block; border: 1px solid $ui-black-transparent; @@ -51,13 +58,24 @@ height: 100%; } -.stage-button-right { +[dir="ltr"] .stage-button-first { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +[dir="ltr"] .stage-button-last { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; } -.stage-button-left { +[dir="rtl"] .stage-button-first { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +[dir="rtl"] .stage-button-last { + border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; } diff --git a/src/components/stage-header/stage-header.jsx b/src/components/stage-header/stage-header.jsx index 21a156a95a6add7ff7caa60b21aa23c28b04b6d7..43414179b7b4d4f26454b0ef92d8ed16e6af28be 100644 --- a/src/components/stage-header/stage-header.jsx +++ b/src/components/stage-header/stage-header.jsx @@ -96,7 +96,7 @@ const StageHeaderComponent = function (props) { <Button className={classNames( styles.stageButton, - styles.stageButtonLeft, + styles.stageButtonFirst, (stageSizeMode === STAGE_SIZE_MODES.small) ? null : styles.stageButtonToggledOff )} onClick={onSetStageSmall} @@ -113,7 +113,7 @@ const StageHeaderComponent = function (props) { <Button className={classNames( styles.stageButton, - styles.stageButtonRight, + styles.stageButtonLast, (stageSizeMode === STAGE_SIZE_MODES.large) ? null : styles.stageButtonToggledOff )} onClick={onSetStageLarge} diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css index a168759901196783d90a0714fbb578941e028d9d..2669a6de576a4487686e466cfdae7d0ba87349f9 100644 --- a/src/components/stage-selector/stage-selector.css +++ b/src/components/stage-selector/stage-selector.css @@ -1,6 +1,5 @@ @import "../../css/units.css"; @import "../../css/colors.css"; -@import "../../css/z-index.css"; $header-height: calc($stage-menu-height - 2px); @@ -96,7 +95,6 @@ $header-height: calc($stage-menu-height - 2px); .add-button { position: absolute; bottom: 0.75rem; - z-index: $z-index-add-button } .raised, .raised .header { diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index f5de8b7a89b1c0b2b4fb784b51f5fb66251eee2a..c40620be07c57b5c465605ff64b6b6ff51e4e374 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -39,6 +39,8 @@ const messages = defineMessages({ const StageSelector = props => { const { backdropCount, + containerRef, + dragOver, fileInputRef, intl, selected, @@ -59,9 +61,10 @@ const StageSelector = props => { <Box className={classNames(styles.stageSelector, { [styles.isSelected]: selected, - [styles.raised]: raised, + [styles.raised]: raised || dragOver, [styles.receivedBlocks]: receivedBlocks })} + componentRef={containerRef} onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} @@ -125,6 +128,8 @@ const StageSelector = props => { StageSelector.propTypes = { backdropCount: PropTypes.number.isRequired, + containerRef: PropTypes.func, + dragOver: PropTypes.bool, fileInputRef: PropTypes.func, intl: intlShape.isRequired, onBackdropFileUpload: PropTypes.func, diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index c620c8504c13a1dbd2034a9a2d3a72f80e3d8b85..565c9881cf405e6cb3856eb38671e69995a2ac2f 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -23,6 +23,7 @@ const TargetPane = ({ spriteLibraryVisible, onChangeSpriteDirection, onChangeSpriteName, + onChangeSpriteRotationStyle, onChangeSpriteSize, onChangeSpriteVisibility, onChangeSpriteX, @@ -60,6 +61,7 @@ const TargetPane = ({ stageSize={stageSize} onChangeSpriteDirection={onChangeSpriteDirection} onChangeSpriteName={onChangeSpriteName} + onChangeSpriteRotationStyle={onChangeSpriteRotationStyle} onChangeSpriteSize={onChangeSpriteSize} onChangeSpriteVisibility={onChangeSpriteVisibility} onChangeSpriteX={onChangeSpriteX} @@ -128,6 +130,7 @@ TargetPane.propTypes = { }), onChangeSpriteDirection: PropTypes.func, onChangeSpriteName: PropTypes.func, + onChangeSpriteRotationStyle: PropTypes.func, onChangeSpriteSize: PropTypes.func, onChangeSpriteVisibility: PropTypes.func, onChangeSpriteX: PropTypes.func, diff --git a/src/components/webgl-modal/webgl-modal.jsx b/src/components/webgl-modal/webgl-modal.jsx index 0a7b753aae47d4ed746ac1da18fc8fe2ad3e0ebf..f0c3c6c27529eb3ac8ab782131898698634459b7 100644 --- a/src/components/webgl-modal/webgl-modal.jsx +++ b/src/components/webgl-modal/webgl-modal.jsx @@ -74,12 +74,12 @@ const WebGlModal = ({intl, ...props}) => ( previewFaqLink: ( <a className={styles.faqLink} - href="//scratch.mit.edu/preview-faq" + href="//scratch.mit.edu/3faq" > <FormattedMessage - defaultMessage="preview FAQ" + defaultMessage="FAQ" description="link to Scratch 3.0 FAQ page" - id="gui.webglModal.previewfaqlink" + id="gui.webglModal.previewfaqlinktext" /> </a> ) diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index aedf2f35da1c84187386aae3e5132d7fbfad9372..e7303c145fb969ff5e226f68ff51b768f3b2da5b 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -11,11 +11,15 @@ import { spritePayload } from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; +import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import {connect} from 'react-redux'; import storage from '../lib/storage'; import VM from 'scratch-vm'; +const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; +const DroppableBackpack = DropAreaHOC(dragTypes)(BackpackComponent); + class Backpack extends React.Component { constructor (props) { super(props); @@ -23,8 +27,7 @@ class Backpack extends React.Component { 'handleDrop', 'handleToggle', 'handleDelete', - 'refreshContents', - 'setRef' + 'refreshContents' ]); this.state = { dragOver: false, @@ -46,29 +49,6 @@ class Backpack extends React.Component { storage._hasAddedBackpackSource = true; } } - componentWillReceiveProps (newProps) { - const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; - // If `dragging` becomes true, record the drop area rectangle - if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { - this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); - // If `dragging` becomes false, call the drop handler - } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) { - this.handleDrop(this.props.dragInfo); - this.setState({dragOver: false}); - } - - // If a drag is in progress (currentOffset) and it matches the relevant drag types, - // test if the drag is within the drop area rect and set the state accordingly. - if (this.dropAreaRect && newProps.dragInfo.currentOffset && dragTypes.includes(newProps.dragInfo.dragType)) { - const {x, y} = newProps.dragInfo.currentOffset; - const {top, right, bottom, left} = this.dropAreaRect; - if (x > left && x < right && y > top && y < bottom) { - this.setState({dragOver: true}); - } else { - this.setState({dragOver: false}); - } - } - } handleToggle () { const newState = !this.state.expanded; this.setState({expanded: newState, offset: 0}); @@ -126,19 +106,15 @@ class Backpack extends React.Component { }); } } - setRef (ref) { - this.ref = ref; - } render () { return ( - <BackpackComponent + <DroppableBackpack contents={this.state.contents} - dragOver={this.state.dragOver} - dropAreaRef={this.setRef} error={this.state.error} expanded={this.state.expanded} loading={this.state.loading} onDelete={this.handleDelete} + onDrop={this.handleDrop} onToggle={this.props.host ? this.handleToggle : null} /> ); @@ -146,15 +122,6 @@ class Backpack extends React.Component { } Backpack.propTypes = { - dragInfo: PropTypes.shape({ - currentOffset: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number - }), - dragType: PropTypes.string, - dragging: PropTypes.bool, - index: PropTypes.number - }), host: PropTypes.string, token: PropTypes.string, username: PropTypes.string, @@ -181,7 +148,6 @@ const getTokenAndUsername = state => { const mapStateToProps = state => Object.assign( { - dragInfo: state.scratchGui.assetDrag, vm: state.scratchGui.vm }, getTokenAndUsername(state) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index d5dcc5400f3a0d0a0aa8c9562a7f90478c671a84..8a6bec5fd7578beff79912dea9b6dd5cc52595ed 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -77,7 +77,7 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {toolbox: this.props.toolboxXML} + {rtl: this.props.isRtl, toolbox: this.props.toolboxXML} ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -405,6 +405,7 @@ class Blocks extends React.Component { options, stageSize, vm, + isRtl, isVisible, onActivateColorPicker, updateToolboxState, @@ -464,6 +465,7 @@ Blocks.propTypes = { anyModalVisible: PropTypes.bool, customProceduresVisible: PropTypes.bool, extensionLibraryVisible: PropTypes.bool, + isRtl: PropTypes.bool, isVisible: PropTypes.bool, locale: PropTypes.string, messages: PropTypes.objectOf(PropTypes.string), @@ -538,6 +540,7 @@ const mapStateToProps = state => ({ state.scratchGui.mode.isFullScreen ), extensionLibraryVisible: state.scratchGui.modals.extensionLibrary, + isRtl: state.locales.isRtl, locale: state.locales.locale, messages: state.locales.messages, toolboxXML: state.scratchGui.toolbox.toolboxXML, diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx index 90017c047dc3031219139ef655f3d5a5a45667b2..276d4f72a886e6df3b5f150d1cdd443e5eee9956 100644 --- a/src/containers/connection-modal.jsx +++ b/src/containers/connection-modal.jsx @@ -3,6 +3,7 @@ import React from 'react'; import bindAll from 'lodash.bindall'; import ConnectionModalComponent, {PHASES} from '../components/connection-modal/connection-modal.jsx'; import VM from 'scratch-vm'; +import analytics from '../lib/analytics'; class ConnectionModal extends React.Component { constructor (props) { @@ -16,18 +17,13 @@ class ConnectionModal extends React.Component { 'handleHelp' ]); this.state = { - phase: PHASES.scanning + phase: props.vm.getPeripheralIsConnected(props.extensionId) ? + PHASES.connected : PHASES.scanning }; } componentDidMount () { this.props.vm.on('PERIPHERAL_CONNECTED', this.handleConnected); this.props.vm.on('PERIPHERAL_ERROR', this.handleError); - - // Check if we're already connected - if (this.props.vm.getPeripheralIsConnected(this.props.extensionId)) { - this.handleConnected(); - } - } componentWillUnmount () { this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleConnected); @@ -43,6 +39,11 @@ class ConnectionModal extends React.Component { this.setState({ phase: PHASES.connecting }); + analytics.event({ + category: 'extensions', + action: 'connecting', + label: this.props.extensionId + }); } handleDisconnect () { this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready'); @@ -68,6 +69,11 @@ class ConnectionModal extends React.Component { this.setState({ phase: PHASES.error }); + analytics.event({ + category: 'extensions', + action: 'connecting error', + label: this.props.extensionId + }); } } handleConnected () { @@ -75,9 +81,19 @@ class ConnectionModal extends React.Component { this.setState({ phase: PHASES.connected }); + analytics.event({ + category: 'extensions', + action: 'connected', + label: this.props.extensionId + }); } handleHelp () { window.open(this.props.helpLink, '_blank'); + analytics.event({ + category: 'extensions', + action: 'help', + label: this.props.extensionId + }); } render () { return ( diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx index 1b4637d9900f3b929eb0f9a810d6dd830b5bbdfc..7cc52870fea7dad1e58b168d406019a4a4549162 100644 --- a/src/containers/controls.jsx +++ b/src/containers/controls.jsx @@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; +import {connect} from 'react-redux'; import analytics from '../lib/analytics'; import ControlsComponent from '../components/controls/controls.jsx'; @@ -11,34 +12,13 @@ class Controls extends React.Component { super(props); bindAll(this, [ 'handleGreenFlagClick', - 'handleStopAllClick', - 'onProjectRunStart', - 'onProjectRunStop' + 'handleStopAllClick' ]); - this.state = { - projectRunning: false, - turbo: false - }; - } - componentDidMount () { - this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart); - this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop); - } - componentWillUnmount () { - this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart); - this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop); - } - onProjectRunStart () { - this.setState({projectRunning: true}); - } - onProjectRunStop () { - this.setState({projectRunning: false}); } handleGreenFlagClick (e) { e.preventDefault(); if (e.shiftKey) { - this.setState({turbo: !this.state.turbo}); - this.props.vm.setTurboMode(!this.state.turbo); + this.props.vm.setTurboMode(!this.props.turbo); } else { this.props.vm.greenFlag(); analytics.event({ @@ -58,13 +38,15 @@ class Controls extends React.Component { render () { const { vm, // eslint-disable-line no-unused-vars + projectRunning, + turbo, ...props } = this.props; return ( <ControlsComponent {...props} - active={this.state.projectRunning} - turbo={this.state.turbo} + active={projectRunning} + turbo={turbo} onGreenFlagClick={this.handleGreenFlagClick} onStopAllClick={this.handleStopAllClick} /> @@ -73,7 +55,14 @@ class Controls extends React.Component { } Controls.propTypes = { + projectRunning: PropTypes.bool.isRequired, + turbo: PropTypes.bool.isRequired, vm: PropTypes.instanceOf(VM) }; -export default Controls; +const mapStateToProps = state => ({ + projectRunning: state.scratchGui.vmStatus.running, + turbo: state.scratchGui.vmStatus.turbo +}); + +export default connect(mapStateToProps)(Controls); diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx index 879800aae880313299ab53601e6a1f9f0f667d54..525422720a8324b69d47ccad362943f7f8a6e988 100644 --- a/src/containers/custom-procedures.jsx +++ b/src/containers/custom-procedures.jsx @@ -32,7 +32,8 @@ class CustomProcedures extends React.Component { this.blocks = blocksRef; const workspaceConfig = defaultsDeep({}, CustomProcedures.defaultOptions, - this.props.options + this.props.options, + {rtl: this.props.isRtl} ); // @todo This is a hack to make there be no toolbox. @@ -117,6 +118,7 @@ class CustomProcedures extends React.Component { } CustomProcedures.propTypes = { + isRtl: PropTypes.bool, mutator: PropTypes.instanceOf(Element), onRequestClose: PropTypes.func.isRequired, options: PropTypes.shape({ @@ -147,6 +149,7 @@ CustomProcedures.defaultProps = { }; const mapStateToProps = state => ({ + isRtl: state.locales.isRtl, mutator: state.scratchGui.customProcedures.mutator }); diff --git a/src/containers/direction-picker.jsx b/src/containers/direction-picker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a0643d706c8aaaa9600a08aea4ecd2c986ba5659 --- /dev/null +++ b/src/containers/direction-picker.jsx @@ -0,0 +1,62 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import DirectionComponent, {RotationStyles} from '../components/direction-picker/direction-picker.jsx'; + +class DirectionPicker extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleOpenPopover', + 'handleClosePopover', + 'handleClickLeftRight', + 'handleClickDontRotate', + 'handleClickAllAround' + ]); + this.state = { + popoverOpen: false + }; + } + handleOpenPopover () { + this.setState({popoverOpen: true}); + } + handleClosePopover () { + this.setState({popoverOpen: false}); + } + handleClickAllAround () { + this.props.onChangeRotationStyle(RotationStyles.ALL_AROUND); + } + handleClickLeftRight () { + this.props.onChangeRotationStyle(RotationStyles.LEFT_RIGHT); + } + handleClickDontRotate () { + this.props.onChangeRotationStyle(RotationStyles.DONT_ROTATE); + } + render () { + return ( + <DirectionComponent + direction={this.props.direction} + disabled={this.props.disabled} + popoverOpen={this.state.popoverOpen && !this.props.disabled} + rotationStyle={this.props.rotationStyle} + onChangeDirection={this.props.onChangeDirection} + onClickAllAround={this.handleClickAllAround} + onClickDontRotate={this.handleClickDontRotate} + onClickLeftRight={this.handleClickLeftRight} + onClosePopover={this.handleClosePopover} + onOpenPopover={this.handleOpenPopover} + /> + ); + } +} + +DirectionPicker.propTypes = { + direction: PropTypes.number, + disabled: PropTypes.bool, + onChangeDirection: PropTypes.func, + onChangeRotationStyle: PropTypes.func, + rotationStyle: PropTypes.string +}; + +export default DirectionPicker; diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 8db598c85fd28f2f1c6e66fdbae55468399123c4..ffa099496fc5c3d10587fdb66f3f547e6622f127 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -113,6 +113,7 @@ const mapStateToProps = state => ({ costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, importInfoVisible: state.scratchGui.modals.importInfo, isPlayerOnly: state.scratchGui.mode.isPlayerOnly, + isRtl: state.locales.isRtl, loadingStateVisible: state.scratchGui.modals.loadingProject, previewInfoVisible: state.scratchGui.modals.previewInfo, targetIsStage: ( diff --git a/src/containers/modal.jsx b/src/containers/modal.jsx index 67b14c9ff4494b1e5f354f7befb0b6b0faa9946b..30dc4277b56a8a7aabcf2dd204d3be9c14c5b072 100644 --- a/src/containers/modal.jsx +++ b/src/containers/modal.jsx @@ -1,6 +1,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import {connect} from 'react-redux'; import ModalComponent from '../components/modal/modal.jsx'; @@ -47,8 +48,15 @@ class Modal extends React.Component { Modal.propTypes = { id: PropTypes.string.isRequired, + isRtl: PropTypes.bool, onRequestClose: PropTypes.func, onRequestOpen: PropTypes.func }; -export default Modal; +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(Modal); diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx index 99c233c7d8e7ea7b8a28f497274fe85da966948d..e5a86fcc067417198d55724646f85f1ebecebfa9 100644 --- a/src/containers/stage-selector.jsx +++ b/src/containers/stage-selector.jsx @@ -7,6 +7,8 @@ import {connect} from 'react-redux'; import {openBackdropLibrary} from '../reducers/modals'; import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; import {setHoveredSprite} from '../reducers/hovered-target'; +import DragConstants from '../lib/drag-constants'; +import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx'; @@ -14,6 +16,15 @@ import backdropLibraryContent from '../lib/libraries/backdrops.json'; import costumeLibraryContent from '../lib/libraries/costumes.json'; import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js'; +const dragTypes = [ + DragConstants.COSTUME, + DragConstants.SOUND, + DragConstants.BACKPACK_COSTUME, + DragConstants.BACKPACK_SOUND +]; + +const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent); + class StageSelector extends React.Component { constructor (props) { super(props); @@ -27,6 +38,7 @@ class StageSelector extends React.Component { 'handleBackdropUpload', 'handleMouseEnter', 'handleMouseLeave', + 'handleDrop', 'setFileInput' ]); } @@ -75,6 +87,22 @@ class StageSelector extends React.Component { handleMouseLeave () { this.props.dispatchSetHoveredSprite(null); } + handleDrop (dragInfo) { + if (dragInfo.dragType === DragConstants.COSTUME) { + this.props.vm.shareCostumeToTarget(dragInfo.index, this.props.id); + } else if (dragInfo.dragType === DragConstants.SOUND) { + this.props.vm.shareSoundToTarget(dragInfo.index, this.props.id); + } else if (dragInfo.dragType === DragConstants.BACKPACK_COSTUME) { + this.props.vm.addCostume(dragInfo.payload.body, { + name: dragInfo.payload.name + }, this.props.id); + } else if (dragInfo.dragType === DragConstants.BACKPACK_SOUND) { + this.props.vm.addSound({ + md5: dragInfo.payload.body, + name: dragInfo.payload.name + }, this.props.id); + } + } setFileInput (input) { this.fileInput = input; } @@ -82,16 +110,16 @@ class StageSelector extends React.Component { const componentProps = omit(this.props, [ 'assetId', 'dispatchSetHoveredSprite', 'id', 'onActivateTab', 'onSelect']); return ( - <StageSelectorComponent + <DroppableStage fileInputRef={this.setFileInput} onBackdropFileUpload={this.handleBackdropUpload} onBackdropFileUploadClick={this.handleFileUploadClick} onClick={this.handleClick} + onDrop={this.handleDrop} onEmptyBackdropClick={this.handleEmptyBackdrop} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onSurpriseBackdropClick={this.handleSurpriseBackdrop} - {...componentProps} /> ); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index a116b3c0788127d6743e7555ecb1cdb34016d97d..870b9716440211dab3249f4393a25095dbdb58fa 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -20,6 +20,7 @@ class TargetPane extends React.Component { super(props); bindAll(this, [ 'handleBlockDragEnd', + 'handleChangeSpriteRotationStyle', 'handleChangeSpriteDirection', 'handleChangeSpriteName', 'handleChangeSpriteSize', @@ -48,6 +49,9 @@ class TargetPane extends React.Component { handleChangeSpriteDirection (direction) { this.props.vm.postSpriteInfo({direction}); } + handleChangeSpriteRotationStyle (rotationStyle) { + this.props.vm.postSpriteInfo({rotationStyle}); + } handleChangeSpriteName (name) { this.props.vm.renameSprite(this.props.editingTarget, name); } @@ -152,6 +156,18 @@ class TargetPane extends React.Component { this.props.vm.shareCostumeToTarget(dragInfo.index, targetId); } else if (targetId && dragInfo.dragType === DragConstants.SOUND) { this.props.vm.shareSoundToTarget(dragInfo.index, targetId); + } else if (dragInfo.dragType === DragConstants.BACKPACK_COSTUME) { + // In scratch 2, this only creates a new sprite from the costume. + // We may be able to handle both kinds of drops, depending on where + // the drop happens. For now, just add the costume. + this.props.vm.addCostume(dragInfo.payload.body, { + name: dragInfo.payload.name + }, targetId); + } else if (dragInfo.dragType === DragConstants.BACKPACK_SOUND) { + this.props.vm.addSound({ + md5: dragInfo.payload.body, + name: dragInfo.payload.name + }, targetId); } } } @@ -167,6 +183,7 @@ class TargetPane extends React.Component { fileInputRef={this.setFileInput} onChangeSpriteDirection={this.handleChangeSpriteDirection} onChangeSpriteName={this.handleChangeSpriteName} + onChangeSpriteRotationStyle={this.handleChangeSpriteRotationStyle} onChangeSpriteSize={this.handleChangeSpriteSize} onChangeSpriteVisibility={this.handleChangeSpriteVisibility} onChangeSpriteX={this.handleChangeSpriteX} diff --git a/src/containers/turbo-mode.jsx b/src/containers/turbo-mode.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f7ee5878e956c4e6cbec6e255a0a447562832962 --- /dev/null +++ b/src/containers/turbo-mode.jsx @@ -0,0 +1,60 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; + +/** + * Turbo Mode component passes toggleTurboMode function to its child. + * It also includes `turboMode` in the props passed to the children. + * It expects this child to be a function with the signature + * function (toggleTurboMode, {turboMode, ...props}) {} + * The component can then be used to attach turbo mode setting functionality + * to any other component: + * + * <TurboMode>{(toggleTurboMode, props) => ( + * <MyCoolComponent + * turboEnabled={props.turboMode} + * onClick={toggleTurboMode} + * {...props} + * /> + * )}</TurboMode> + */ +class TurboMode extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'toggleTurboMode' + ]); + } + toggleTurboMode () { + this.props.vm.setTurboMode(!this.props.turboMode); + } + render () { + const { + /* eslint-disable no-unused-vars */ + children, + vm, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + return this.props.children(this.toggleTurboMode, props); + } +} + +TurboMode.propTypes = { + children: PropTypes.func, + turboMode: PropTypes.bool, + vm: PropTypes.shape({ + setTurboMode: PropTypes.func + }) +}; + +const mapStateToProps = state => ({ + vm: state.scratchGui.vm, + turboMode: state.scratchGui.vmStatus.turbo +}); + +export default connect( + mapStateToProps, + () => ({}) // omit dispatch prop +)(TurboMode); diff --git a/src/css/z-index.css b/src/css/z-index.css index b3368fc2bd638ef4763bf495171b41a7f41d7c53..26893f08fa5147930328992359d709777bef0ec7 100644 --- a/src/css/z-index.css +++ b/src/css/z-index.css @@ -8,8 +8,8 @@ $z-index-extension-button: 50; /* Force extension button above the ScratchBlocks $z-index-menu-bar: 50; /* blocklyToolboxDiv is 40 */ $z-index-monitor: 100; -$z-index-coming-soon: 110; $z-index-add-button: 120; +$z-index-tooltip: 130; /* tooltips should go over add buttons if they overlap */ $z-index-card: 490; $z-index-loader: 500; diff --git a/src/lib/drop-area-hoc.jsx b/src/lib/drop-area-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9f44293e88cdaab98dcd80b0a8cdd4fde915b646 --- /dev/null +++ b/src/lib/drop-area-hoc.jsx @@ -0,0 +1,88 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import omit from 'lodash.omit'; +import {connect} from 'react-redux'; + +const DropAreaHOC = function (dragTypes) { + return function (WrappedComponent) { + class DropAreaWrapper extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setRef' + ]); + + this.state = { + dragOver: false + }; + + this.ref = null; + this.containerBox = null; + } + + componentWillReceiveProps (newProps) { + // If `dragging` becomes true, record the drop area rectangle + if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { + this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); + // If `dragging` becomes false, call the drop handler + } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) { + this.props.onDrop(this.props.dragInfo); + this.setState({dragOver: false}); + } + + // If a drag is in progress (currentOffset) and it matches the relevant drag types, + // test if the drag is within the drop area rect and set the state accordingly. + if (this.dropAreaRect && newProps.dragInfo.currentOffset && + dragTypes.includes(newProps.dragInfo.dragType)) { + const {x, y} = newProps.dragInfo.currentOffset; + const {top, right, bottom, left} = this.dropAreaRect; + if (x > left && x < right && y > top && y < bottom) { + this.setState({dragOver: true}); + } else { + this.setState({dragOver: false}); + } + } + } + setRef (el) { + this.ref = el; + } + render () { + const componentProps = omit(this.props, ['onDrop', 'dragInfo']); + return ( + <WrappedComponent + containerRef={this.setRef} + dragOver={this.state.dragOver} + {...componentProps} + /> + ); + } + } + + DropAreaWrapper.propTypes = { + dragInfo: PropTypes.shape({ + currentOffset: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number + }), + dragType: PropTypes.string, + dragging: PropTypes.bool, + index: PropTypes.number + }), + onDrop: PropTypes.func + }; + + const mapStateToProps = state => ({ + dragInfo: state.scratchGui.assetDrag + }); + + const mapDispatchToProps = () => ({}); + + return connect( + mapStateToProps, + mapDispatchToProps + )(DropAreaWrapper); + }; +}; + +export default DropAreaHOC; diff --git a/src/lib/rtl-locales.js b/src/lib/rtl-locales.js new file mode 100644 index 0000000000000000000000000000000000000000..b48ff75997f906abc0a11530e74e2a4ecc4116a6 --- /dev/null +++ b/src/lib/rtl-locales.js @@ -0,0 +1,3 @@ +// TODO: this probably should be coming from scratch-l10n +// Tracking in https://github.com/LLK/scratch-l10n/issues/32 +export default ['he']; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 28f578b371b89bb717cf8dffa01c4a752530d464..b2256b82a4ace77d8d2bec3a426bb02852cd3464 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -8,6 +8,7 @@ import {connect} from 'react-redux'; import {updateTargets} from '../reducers/targets'; import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; +import {setRunningState, setTurboState} from '../reducers/vm-status'; /* * Higher Order Component to manage events emitted by the VM @@ -31,6 +32,10 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('targetsUpdate', this.props.onTargetsUpdate); this.props.vm.on('MONITORS_UPDATE', this.props.onMonitorsUpdate); this.props.vm.on('BLOCK_DRAG_UPDATE', this.props.onBlockDragUpdate); + this.props.vm.on('TURBO_MODE_ON', this.props.onTurboModeOn); + this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff); + this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); + this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); } componentDidMount () { if (this.props.attachKeyboardEvents) { @@ -96,7 +101,11 @@ const vmListenerHOC = function (WrappedComponent) { onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, onMonitorsUpdate: PropTypes.func.isRequired, + onProjectRunStart: PropTypes.func.isRequired, + onProjectRunStop: PropTypes.func.isRequired, onTargetsUpdate: PropTypes.func.isRequired, + onTurboModeOff: PropTypes.func.isRequired, + onTurboModeOn: PropTypes.func.isRequired, username: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; @@ -117,7 +126,11 @@ const vmListenerHOC = function (WrappedComponent) { }, onBlockDragUpdate: areBlocksOverGui => { dispatch(updateBlockDrag(areBlocksOverGui)); - } + }, + onProjectRunStart: () => dispatch(setRunningState(true)), + onProjectRunStop: () => dispatch(setRunningState(false)), + onTurboModeOn: () => dispatch(setTurboState(true)), + onTurboModeOff: () => dispatch(setTurboState(false)) }); return connect( mapStateToProps, diff --git a/src/playground/player.jsx b/src/playground/player.jsx index a0894fe39dea782401e1b2806f3ddf55b9d267e9..6c2833f1b8108d08022e9d6c85bac9357f1f497f 100644 --- a/src/playground/player.jsx +++ b/src/playground/player.jsx @@ -18,7 +18,7 @@ if (process.env.NODE_ENV === 'production' && typeof window === 'object') { import styles from './player.css'; -const Player = ({isPlayerOnly, onSeeInside}) => ( +const Player = ({isPlayerOnly, onSeeInside, projectId}) => ( <Box className={classNames({ [styles.stageOnly]: isPlayerOnly @@ -28,13 +28,15 @@ const Player = ({isPlayerOnly, onSeeInside}) => ( <GUI enableCommunity isPlayerOnly={isPlayerOnly} + projectId={projectId} /> </Box> ); Player.propTypes = { isPlayerOnly: PropTypes.bool, - onSeeInside: PropTypes.func + onSeeInside: PropTypes.func, + projectId: PropTypes.string }; const mapStateToProps = state => ({ diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 7d98ae2f3f74a757bc101910bbabe6fd491fc192..2c5642416a52b1e65a4324fee3ad0019ffb52e8b 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -15,6 +15,7 @@ import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; import toolboxReducer, {toolboxInitialState} from './toolbox'; import vmReducer, {vmInitialState} from './vm'; +import vmStatusReducer, {vmStatusInitialState} from './vm-status'; import throttle from 'redux-throttle'; const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true}))); @@ -35,7 +36,8 @@ const guiInitialState = { monitorLayout: monitorLayoutInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, - vm: vmInitialState + vm: vmInitialState, + vmStatus: vmStatusInitialState }; const initPlayer = function (currentState) { @@ -75,7 +77,8 @@ const guiReducer = combineReducers({ monitorLayout: monitorLayoutReducer, targets: targetReducer, toolbox: toolboxReducer, - vm: vmReducer + vm: vmReducer, + vmStatus: vmStatusReducer }); export { diff --git a/src/reducers/locales.js b/src/reducers/locales.js index fda8f9dc36d14dac09f72fad62f704617b87e0cf..14ca3bd37b17971432645eeafc684cc244a63270 100644 --- a/src/reducers/locales.js +++ b/src/reducers/locales.js @@ -2,6 +2,7 @@ import {addLocaleData} from 'react-intl'; import {localeData} from 'scratch-l10n'; import editorMessages from 'scratch-l10n/locales/editor-msgs'; +import RtlLocales from '../lib/rtl-locales'; addLocaleData(localeData); @@ -9,6 +10,7 @@ const UPDATE_LOCALES = 'scratch-gui/locales/UPDATE_LOCALES'; const SELECT_LOCALE = 'scratch-gui/locales/SELECT_LOCALE'; const initialState = { + isRtl: false, locale: 'en', messagesByLocale: editorMessages, messages: editorMessages.en @@ -19,12 +21,14 @@ const reducer = function (state, action) { switch (action.type) { case SELECT_LOCALE: return Object.assign({}, state, { + isRtl: RtlLocales.indexOf(action.locale) !== -1, locale: action.locale, messagesByLocale: state.messagesByLocale, messages: state.messagesByLocale[action.locale] }); case UPDATE_LOCALES: return Object.assign({}, state, { + isRtl: state.isRtl, locale: state.locale, messagesByLocale: action.messagesByLocale, messages: action.messagesByLocale[state.locale] @@ -53,6 +57,7 @@ const initLocale = function (currentState, locale) { {}, currentState, { + isRtl: RtlLocales.indexOf(locale) !== -1, locale: locale, messagesByLocale: currentState.messagesByLocale, messages: currentState.messagesByLocale[locale] diff --git a/src/reducers/vm-status.js b/src/reducers/vm-status.js new file mode 100644 index 0000000000000000000000000000000000000000..8e965199232d322d386ce53c3f57fb243be6151c --- /dev/null +++ b/src/reducers/vm-status.js @@ -0,0 +1,44 @@ +const SET_RUNNING_STATE = 'scratch-gui/vm-status/SET_RUNNING_STATE'; +const SET_TURBO_STATE = 'scratch-gui/vm-status/SET_TURBO_STATE'; + +const initialState = { + running: false, + turbo: false +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_RUNNING_STATE: + return Object.assign({}, state, { + running: action.running + }); + case SET_TURBO_STATE: + return Object.assign({}, state, { + turbo: action.turbo + }); + default: + return state; + } +}; + +const setRunningState = function (running) { + return { + type: SET_RUNNING_STATE, + running: running + }; +}; + +const setTurboState = function (turbo) { + return { + type: SET_TURBO_STATE, + turbo: turbo + }; +}; + +export { + reducer as default, + initialState as vmStatusInitialState, + setRunningState, + setTurboState +}; diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index ce2d6a533e35a43b07ae7557b64b0d96ebadccf5..2d979e4486221ed08b770c2a572572d4f2e8f498 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -44,6 +44,9 @@ class SeleniumHelper { args.push('--headless'); } chromeCapabilities.set('chromeOptions', {args}); + chromeCapabilities.setLoggingPrefs({ + performance: 'ALL' + }); this.driver = new webdriver.Builder() .forBrowser('chrome') .withCapabilities(chromeCapabilities) diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js index f745dc24d2c0101582e5901d581d7dd901f717d7..ecaeb56f2fa36f9bd83095563395f72a7759426f 100644 --- a/test/integration/examples.test.js +++ b/test/integration/examples.test.js @@ -35,6 +35,14 @@ describe('player example', () => { await clickXpath('//img[@title="Stop"]'); const logs = await getLogs(); await expect(logs).toEqual([]); + const projectRequests = await driver.manage().logs() + .get('performance') + .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) + .filter(m => m.method === 'Network.requestWillBeSent') + .map(m => m.params.request.url) + .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/') + ); + await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']); }); }); @@ -58,6 +66,14 @@ describe('blocks example', () => { await clickXpath('//img[@title="Stop"]'); const logs = await getLogs(); await expect(logs).toEqual([]); + const projectRequests = await driver.manage().logs() + .get('performance') + .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) + .filter(m => m.method === 'Network.requestWillBeSent') + .map(m => m.params.request.url) + .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/') + ); + await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']); }); test('Change categories', async () => { diff --git a/test/smoke/browser.test.js b/test/smoke/browser.test.js index aa894a05578bcebb74f335469ed5e6a2d85710f8..941b3eef3bd7e34b0165c2002898d4b170fc4b73 100644 --- a/test/smoke/browser.test.js +++ b/test/smoke/browser.test.js @@ -8,7 +8,7 @@ const { // Make the default timeout longer, Sauce tests take ~30s jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 1000; // eslint-disable-line -const SUPPORTED_MESSAGE = 'Welcome to the Scratch 3.0 Preview'; +const SUPPORTED_MESSAGE = 'Welcome to the Scratch 3.0 Beta'; const UNSUPPORTED_MESSAGE = 'Scratch 3.0 does not support Internet Explorer'; // Driver configs can be generated with the Sauce Platform Configurator diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 0279a418b36f6ede1d60b9f936eeebf03ecf5aaa..93ebc2cf68c794ad31bbf93e6f61b6ec5ac440b5 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -39,6 +39,7 @@ exports[`Sound Editor Component matches snapshot 1`] = ` title="Undo" > <img + className={undefined} draggable={false} src="test-file-stub" /> @@ -50,6 +51,7 @@ exports[`Sound Editor Component matches snapshot 1`] = ` title="Redo" > <img + className={undefined} draggable={false} src="test-file-stub" /> @@ -334,7 +336,7 @@ exports[`Sound Editor Component matches snapshot 1`] = ` </div> </div> <div - className={undefined} + className="" > <div className={undefined}