diff --git a/package.json b/package.json index 82d9dc575e093de0ebc3c88eafc24197bda403e2..d3585914f7bfab8ae13fe2d52bb92f522666e578 100644 --- a/package.json +++ b/package.json @@ -87,9 +87,9 @@ "react-redux": "5.0.7", "react-responsive": "5.0.0", "react-style-proptype": "3.2.1", - "react-tabs": "2.2.2", + "react-tabs": "2.3.0", "react-test-renderer": "16.2.0", - "react-tooltip": "3.6.1", + "react-tooltip": "3.8.0", "react-virtualized": "9.20.1", "redux": "3.7.2", "redux-mock-store": "^1.2.3", @@ -98,11 +98,11 @@ "scratch-audio": "0.1.0-prerelease.20180625202813", "scratch-blocks": "0.1.0-prerelease.1535662135", "scratch-l10n": "3.0.20180830210150", - "scratch-paint": "0.2.0-prerelease.20180831175055", + "scratch-paint": "0.2.0-prerelease.20180905220723", "scratch-render": "0.1.0-prerelease.20180824141819", "scratch-storage": "1.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", - "scratch-vm": "0.2.0-prerelease.20180830155110", + "scratch-vm": "0.2.0-prerelease.20180906133035", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index 0a271ea7a7838ba2a3500e02503c5bf8b41e24f7..9afac0e3891f6026874bcb0528c32b1661a1fbde 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -84,4 +84,8 @@ BrowserModal.propTypes = { onBack: PropTypes.func.isRequired }; -export default injectIntl(BrowserModal); +const WrappedBrowserModal = injectIntl(BrowserModal); + +WrappedBrowserModal.setAppElement = ReactModal.setAppElement; + +export default WrappedBrowserModal; diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 96fea86537fb95b886a43268dac70d77334d2e2f..71c623135093df1f503ff64c93a57183eb33e761 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -62,7 +62,6 @@ const GUIComponent = props => { costumeLibraryVisible, costumesTabVisible, enableCommunity, - hideIntro, importInfoVisible, intl, isPlayerOnly, @@ -116,7 +115,7 @@ const GUIComponent = props => { {...componentProps} > {previewInfoVisible ? ( - <PreviewModal hideIntro={hideIntro} /> + <PreviewModal /> ) : null} {loading ? ( <Loader /> @@ -282,7 +281,6 @@ GUIComponent.propTypes = { costumeLibraryVisible: PropTypes.bool, costumesTabVisible: PropTypes.bool, enableCommunity: PropTypes.bool, - hideIntro: PropTypes.bool, importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, isPlayerOnly: PropTypes.bool, diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 6e5d96070b77135abfeb28b64101928a4c00b4c6..f08e6bab746dd171f040e48e916663bbf438538b 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -72,10 +72,15 @@ class GUI extends React.Component { `Failed to load project from server [id=${window.location.hash}]: ${this.state.errorMessage}`); } const { + /* eslint-disable no-unused-vars */ + assetHost, + hideIntro, + projectData, + projectHost, + /* eslint-enable no-unused-vars */ children, fetchingProject, loadingStateVisible, - projectData, // eslint-disable-line no-unused-vars vm, ...componentProps } = this.props; @@ -94,6 +99,7 @@ class GUI extends React.Component { GUI.propTypes = { children: PropTypes.node, fetchingProject: PropTypes.bool, + hideIntro: PropTypes.bool, importInfoVisible: PropTypes.bool, loadingStateVisible: PropTypes.bool, onSeeCommunity: PropTypes.func, @@ -102,7 +108,7 @@ GUI.propTypes = { vm: PropTypes.instanceOf(VM) }; -const mapStateToProps = state => ({ +const mapStateToProps = (state, ownProps) => ({ activeTabIndex: state.scratchGui.editorTab.activeTabIndex, backdropLibraryVisible: state.scratchGui.modals.backdropLibrary, blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, @@ -113,7 +119,7 @@ const mapStateToProps = state => ({ isPlayerOnly: state.scratchGui.mode.isPlayerOnly, isRtl: state.locales.isRtl, loadingStateVisible: state.scratchGui.modals.loadingProject, - previewInfoVisible: state.scratchGui.modals.previewInfo, + previewInfoVisible: state.scratchGui.modals.previewInfo && !ownProps.hideIntro, targetIsStage: ( state.scratchGui.targets.stage && state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx index 7145cfa6acbb3170a85a4bc1c9d44673731fc64e..47b3cc3115ef6cfda1a38c31a4906566fc984498 100644 --- a/src/containers/preview-modal.jsx +++ b/src/containers/preview-modal.jsx @@ -6,8 +6,6 @@ import {connect} from 'react-redux'; import tabletFullScreen from '../lib/tablet-full-screen'; import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx'; -import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; -import supportedBrowser from '../lib/supported-browser'; import { closePreviewInfo, @@ -27,25 +25,6 @@ class PreviewModal extends React.Component { previewing: false }; } - - /** - * Conditionally returns an intro modal depending on the hideIntro prop - * @returns { React.Component | null } null if hideIntro is true, the intro modal component otherwise - */ - introIfShown () { - if (this.props.hideIntro) { - return null; // If hideIntro is true, the intro modal should not appear - } - - // otherwise, show the intro modal - return (<PreviewModalComponent - isRtl={this.props.isRtl} - previewing={this.state.previewing} - onCancel={this.handleCancel} - onTryIt={this.handleTryIt} - onViewProject={this.handleViewProject} - />); - } handleTryIt () { this.setState({previewing: true}); // try to run in fullscreen mode on tablets. @@ -59,18 +38,19 @@ class PreviewModal extends React.Component { this.props.onViewProject(); } render () { - return (supportedBrowser() ? - this.introIfShown() : - <BrowserModalComponent + return ( + <PreviewModalComponent isRtl={this.props.isRtl} - onBack={this.handleCancel} + previewing={this.state.previewing} + onCancel={this.handleCancel} + onTryIt={this.handleTryIt} + onViewProject={this.handleViewProject} /> ); } } PreviewModal.propTypes = { - hideIntro: PropTypes.bool, isRtl: PropTypes.bool, onTryIt: PropTypes.func, onViewProject: PropTypes.func diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index 2f70b44d251f52d5ccec5fb08ae6eb786bf79794..fe7ac023eab97c5df5aea8e5829a6769d28392b5 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -4,7 +4,6 @@ import {Provider} from 'react-redux'; import {createStore, combineReducers, compose} from 'redux'; import ConnectedIntlProvider from './connected-intl-provider.jsx'; -import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from '../reducers/gui'; import localesReducer, {initLocale, localesInitialState} from '../reducers/locales'; import {setPlayer, setFullScreen} from '../reducers/mode.js'; @@ -12,49 +11,76 @@ import {setPlayer, setFullScreen} from '../reducers/mode.js'; import locales from 'scratch-l10n'; import {detectLocale} from './detect-locale'; -import {ScratchPaintReducer} from 'scratch-paint'; - const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -const enhancer = composeEnhancers(guiMiddleware); /* * Higher Order Component to provide redux state. If an `intl` prop is provided * it will override the internal `intl` redux state * @param {React.Component} WrappedComponent - component to provide state for + * @param {boolean} localesOnly - only provide the locale state, not everything + * required by the GUI. Used to exclude excess state when + only rendering modals, not the GUI. * @returns {React.Component} component with redux and intl state provided */ -const AppStateHOC = function (WrappedComponent) { +const AppStateHOC = function (WrappedComponent, localesOnly) { class AppStateWrapper extends React.Component { constructor (props) { super(props); - let initializedGui = guiInitialState; - if (props.isFullScreen) { - initializedGui = initFullScreen(initializedGui); - } - if (props.isPlayerOnly) { - initializedGui = initPlayer(initializedGui); - } + let initialState = {}; + let reducers = {}; + let enhancer; let initializedLocales = localesInitialState; const locale = detectLocale(Object.keys(locales)); if (locale !== 'en') { initializedLocales = initLocale(initializedLocales, locale); } + if (localesOnly) { + // Used for instantiating minimal state for the unsupported + // browser modal + reducers = {locales: localesReducer}; + initialState = {locales: initializedLocales}; + enhancer = composeEnhancers(); + } else { + // You are right, this is gross. But it's necessary to avoid + // importing unneeded code that will crash unsupported browsers. + const guiRedux = require('../reducers/gui'); + const guiReducer = guiRedux.default; + const { + guiInitialState, + guiMiddleware, + initFullScreen, + initPlayer + } = guiRedux; + const {ScratchPaintReducer} = require('scratch-paint'); - const reducer = combineReducers({ - locales: localesReducer, - scratchGui: guiReducer, - scratchPaint: ScratchPaintReducer - }); - this.store = createStore( - reducer, - { + let initializedGui = guiInitialState; + if (props.isFullScreen) { + initializedGui = initFullScreen(initializedGui); + } + if (props.isPlayerOnly) { + initializedGui = initPlayer(initializedGui); + } + reducers = { + locales: localesReducer, + scratchGui: guiReducer, + scratchPaint: ScratchPaintReducer + }; + initialState = { locales: initializedLocales, scratchGui: initializedGui - }, - enhancer); + }; + enhancer = composeEnhancers(guiMiddleware); + } + const reducer = combineReducers(reducers); + this.store = createStore( + reducer, + initialState, + enhancer + ); } componentDidUpdate (prevProps) { + if (localesOnly) return; if (prevProps.isPlayerOnly !== this.props.isPlayerOnly) { this.store.dispatch(setPlayer(this.props.isPlayerOnly)); } diff --git a/src/playground/index.jsx b/src/playground/index.jsx index da2be7a27831b6179acb436d6b441f66e86c9dcf..ba0ada4da78fbc561cbd7eca365e8a73aa2b62c8 100644 --- a/src/playground/index.jsx +++ b/src/playground/index.jsx @@ -7,17 +7,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import analytics from '../lib/analytics'; -import GUI from '../containers/gui.jsx'; -import HashParserHOC from '../lib/hash-parser-hoc.jsx'; import AppStateHOC from '../lib/app-state-hoc.jsx'; +import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; +import supportedBrowser from '../lib/supported-browser'; import styles from './index.css'; -if (process.env.NODE_ENV === 'production' && typeof window === 'object') { - // Warn before navigating away - window.onbeforeunload = () => true; -} - // Register "base" page view analytics.pageview('/'); @@ -25,16 +20,15 @@ const appTarget = document.createElement('div'); appTarget.className = styles.app; document.body.appendChild(appTarget); -GUI.setAppElement(appTarget); -const WrappedGui = HashParserHOC(AppStateHOC(GUI)); - -// TODO a hack for testing the backpack, allow backpack host to be set by url param -const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); -const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; - -const backpackOptions = { - visible: true, - host: backpackHost -}; - -ReactDOM.render(<WrappedGui backpackOptions={backpackOptions} />, appTarget); +if (supportedBrowser()) { + // require needed here to avoid importing unsupported browser-crashing code + // at the top level + require('./render-gui.jsx').default(appTarget); + +} else { + BrowserModalComponent.setAppElement(appTarget); + const WrappedBrowserModalComponent = AppStateHOC(BrowserModalComponent, true /* localesOnly */); + const handleBack = () => {}; + // eslint-disable-next-line react/jsx-no-bind + ReactDOM.render(<WrappedBrowserModalComponent onBack={handleBack} />, appTarget); +} diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx new file mode 100644 index 0000000000000000000000000000000000000000..adec922b8c206c9211888da34a8c426362b9a10b --- /dev/null +++ b/src/playground/render-gui.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import AppStateHOC from '../lib/app-state-hoc.jsx'; +import GUI from '../containers/gui.jsx'; +import HashParserHOC from '../lib/hash-parser-hoc.jsx'; + +/* + * Render the GUI playground. This is a separate function because importing anything + * that instantiates the VM causes unsupported browsers to crash + * {object} appTarget - the DOM element to render to + */ +export default appTarget => { + GUI.setAppElement(appTarget); + const WrappedGui = HashParserHOC(AppStateHOC(GUI)); + + // TODO a hack for testing the backpack, allow backpack host to be set by url param + const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); + const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null; + + const backpackOptions = { + visible: true, + host: backpackHost + }; + if (process.env.NODE_ENV === 'production' && typeof window === 'object') { + // Warn before navigating away + window.onbeforeunload = () => true; + } + + ReactDOM.render(<WrappedGui backpackOptions={backpackOptions} />, appTarget); +};