diff --git a/client/src/templates/Challenges/classic/ActionRow.js b/client/src/templates/Challenges/classic/ActionRow.js new file mode 100644 index 0000000000..c53f5ff740 --- /dev/null +++ b/client/src/templates/Challenges/classic/ActionRow.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import EditorTabs from './EditorTabs'; + +const ActionRow = () => ( +
+ +
+); + +ActionRow.displayName = 'ActionRow'; + +export default ActionRow; diff --git a/client/src/templates/Challenges/classic/DesktopLayout.js b/client/src/templates/Challenges/classic/DesktopLayout.js index ccdeb4bc99..32dd8c8627 100644 --- a/client/src/templates/Challenges/classic/DesktopLayout.js +++ b/client/src/templates/Challenges/classic/DesktopLayout.js @@ -1,12 +1,14 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import PropTypes from 'prop-types'; +import { first } from 'lodash'; +import EditorTabs from './EditorTabs'; +import ActionRow from './ActionRow'; const propTypes = { - challengeFile: PropTypes.shape({ - key: PropTypes.string - }), + challengeFiles: PropTypes.object, editor: PropTypes.element, + hasEditableBoundries: PropTypes.bool, hasPreview: PropTypes.bool, instructions: PropTypes.element, preview: PropTypes.element, @@ -24,42 +26,57 @@ const reflexProps = { }; class DesktopLayout extends Component { + getChallengeFile() { + const { challengeFiles } = this.props; + return first(Object.keys(challengeFiles).map(key => challengeFiles[key])); + } + render() { const { resizeProps, instructions, - challengeFile, editor, testOutput, hasPreview, - preview + preview, + hasEditableBoundries } = this.props; + + const challengeFile = this.getChallengeFile(); return ( - - - {instructions} - - - - {challengeFile && ( - - - {editor} - - - - {testOutput} - - - )} - - {hasPreview && } - {hasPreview && ( - - {preview} + + {hasEditableBoundries && } + + + {instructions} - )} - + + + {challengeFile && ( + + + { + + {!hasEditableBoundries && } + {editor} + + } + + + + {testOutput} + + + )} + + {hasPreview && } + {hasPreview && ( + + {preview} + + )} + + ); } } diff --git a/client/src/templates/Challenges/classic/Editor.js b/client/src/templates/Challenges/classic/Editor.js index 775e89921a..a7c9a38025 100644 --- a/client/src/templates/Challenges/classic/Editor.js +++ b/client/src/templates/Challenges/classic/Editor.js @@ -435,7 +435,7 @@ class Editor extends Component { domNode.setAttribute('aria-hidden', true); - domNode.style.background = 'yellow'; + domNode.style.background = 'lightYellow'; domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px'; domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; domNode.style.top = this.getViewZoneTop(); @@ -459,8 +459,6 @@ class Editor extends Component { outputNode.style.zIndex = '10'; outputNode.setAttribute('aria-hidden', true); - - outputNode.style.background = 'var(--secondary-background)'; outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px'; outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; outputNode.style.top = this.getOutputZoneTop(); diff --git a/client/src/templates/Challenges/classic/EditorTabs.js b/client/src/templates/Challenges/classic/EditorTabs.js index 03f65cc391..73b942d242 100644 --- a/client/src/templates/Challenges/classic/EditorTabs.js +++ b/client/src/templates/Challenges/classic/EditorTabs.js @@ -1,9 +1,17 @@ -import React from 'react'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; + +import { + toggleVisibleEditor, + visibleEditorsSelector, + challengeFilesSelector +} from '../redux'; const propTypes = { challengeFiles: PropTypes.object.isRequired, - toggleTab: PropTypes.func.isRequired, + toggleVisibleEditor: PropTypes.func.isRequired, visibleEditors: PropTypes.shape({ indexjs: PropTypes.bool, indexjsx: PropTypes.bool, @@ -12,52 +20,73 @@ const propTypes = { }) }; -const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => ( -
- {challengeFiles['indexjsx'] && ( - - )} - {challengeFiles['indexhtml'] && ( - - )} - {challengeFiles['indexcss'] && ( - - )} - {challengeFiles['indexjs'] && ( - - )} -
+const mapStateToProps = createSelector( + visibleEditorsSelector, + challengeFilesSelector, + (visibleEditors, challengeFiles) => ({ + visibleEditors, + challengeFiles + }) ); +const mapDispatchToProps = { + toggleVisibleEditor +}; + +class EditorTabs extends Component { + render() { + const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props; + return ( +
+ {challengeFiles['indexjsx'] && ( + + )} + {challengeFiles['indexhtml'] && ( + + )} + {challengeFiles['indexcss'] && ( + + )} + {challengeFiles['indexjs'] && ( + + )} +
+ ); + } +} + EditorTabs.displayName = 'EditorTabs'; EditorTabs.propTypes = propTypes; -export default EditorTabs; +export default connect( + mapStateToProps, + mapDispatchToProps +)(EditorTabs); diff --git a/client/src/templates/Challenges/classic/MobileLayout.js b/client/src/templates/Challenges/classic/MobileLayout.js index e64bd7d5e0..363ef55d74 100644 --- a/client/src/templates/Challenges/classic/MobileLayout.js +++ b/client/src/templates/Challenges/classic/MobileLayout.js @@ -7,6 +7,7 @@ import ToolPanel from '../components/Tool-Panel'; import { createStructuredSelector } from 'reselect'; import { currentTabSelector, moveToTab } from '../redux'; import { bindActionCreators } from 'redux'; +import EditorTabs from './EditorTabs'; const mapStateToProps = createStructuredSelector({ currentTab: currentTabSelector @@ -66,6 +67,7 @@ class MobileLayout extends Component { {instructions} + {editor} diff --git a/client/src/templates/Challenges/classic/MultifileEditor.js b/client/src/templates/Challenges/classic/MultifileEditor.js index 3aab3e74ec..884429590c 100644 --- a/client/src/templates/Challenges/classic/MultifileEditor.js +++ b/client/src/templates/Challenges/classic/MultifileEditor.js @@ -3,7 +3,7 @@ import React, { Component, Suspense } from 'react'; import { connect } from 'react-redux'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { createSelector } from 'reselect'; -import { isEmpty } from 'lodash'; +import { getTargetEditor } from '../utils/getTargetEditor'; import { isDonationModalOpenSelector, userSelector } from '../../../redux'; import { canFocusEditorSelector, @@ -13,13 +13,12 @@ import { saveEditorContent, setAccessibilityMode, setEditorFocusability, + visibleEditorsSelector, updateFile } from '../redux'; import './editor.css'; import { Loader } from '../../../components/helpers'; -import EditorTabs from './EditorTabs'; import Editor from './Editor'; -import { toSortedArray } from '../../../../../utils/sort-files'; const propTypes = { canFocus: PropTypes.bool, @@ -45,16 +44,31 @@ const propTypes = { setAccessibilityMode: PropTypes.func.isRequired, setEditorFocusability: PropTypes.func, theme: PropTypes.string, - updateFile: PropTypes.func.isRequired + updateFile: PropTypes.func.isRequired, + visibleEditors: PropTypes.shape({ + indexjs: PropTypes.bool, + indexjsx: PropTypes.bool, + indexcss: PropTypes.bool, + indexhtml: PropTypes.bool + }) }; const mapStateToProps = createSelector( + visibleEditorsSelector, canFocusEditorSelector, consoleOutputSelector, inAccessibilityModeSelector, isDonationModalOpenSelector, userSelector, - (canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({ + ( + visibleEditors, + canFocus, + output, + accessibilityMode, + open, + { theme = 'default' } + ) => ({ + visibleEditors, canFocus: open ? false : canFocus, output, inAccessibilityMode: accessibilityMode, @@ -70,15 +84,6 @@ const mapDispatchToProps = { updateFile }; -function getTargetEditor(challengeFiles) { - let targetEditor = Object.values(challengeFiles).find( - ({ editableRegionBoundaries }) => !isEmpty(editableRegionBoundaries) - )?.key; - - // fallback for when there is no editable region. - return targetEditor || toSortedArray(challengeFiles)[0].key; -} - class MultifileEditor extends Component { constructor(...props) { super(...props); @@ -137,13 +142,6 @@ class MultifileEditor extends Component { } }; - const { challengeFiles } = this.props; - const targetEditor = getTargetEditor(challengeFiles); - - this.state = { - visibleEditors: { [targetEditor]: true } - }; - // TODO: we might want to store the current editor here this.focusOnEditor = this.focusOnEditor.bind(this); } @@ -159,15 +157,6 @@ class MultifileEditor extends Component { // this._editor.focus(); } - toggleTab = newFileKey => { - this.setState(state => ({ - visibleEditors: { - ...state.visibleEditors, - [newFileKey]: !state.visibleEditors[newFileKey] - } - })); - }; - componentWillUnmount() { // this.setState({ fileKey: null }); this.data = null; @@ -180,9 +169,9 @@ class MultifileEditor extends Component { description, editorRef, theme, - resizeProps + resizeProps, + visibleEditors } = this.props; - const { visibleEditors } = this.state; const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; // TODO: the tabs mess up the rendering (scroll doesn't work properly and // the in-editor description) @@ -207,21 +196,13 @@ class MultifileEditor extends Component { // TODO: the tabs mess up the rendering (scroll doesn't work properly and // the in-editor description) const targetEditor = getTargetEditor(challengeFiles); - return ( - - - - + {visibleEditors.indexhtml && ( diff --git a/client/src/templates/Challenges/classic/Show.js b/client/src/templates/Challenges/classic/Show.js index 4aeb9975fa..ec8017a29f 100644 --- a/client/src/templates/Challenges/classic/Show.js +++ b/client/src/templates/Challenges/classic/Show.js @@ -5,7 +5,6 @@ import { createStructuredSelector } from 'reselect'; import { connect } from 'react-redux'; import Helmet from 'react-helmet'; import { graphql } from 'gatsby'; -import { first } from 'lodash'; import Media from 'react-responsive'; import LearnLayout from '../../../components/layouts/Learn'; @@ -184,11 +183,6 @@ class ShowClassic extends Component { getVideoUrl = () => this.getChallenge().videoUrl; - getChallengeFile() { - const { files } = this.props; - return first(Object.keys(files).map(key => files[key])); - } - hasPreview() { const { challengeType } = this.getChallenge(); return ( @@ -229,6 +223,7 @@ class ShowClassic extends Component { containerRef={this.containerRef} description={description} editorRef={this.editorRef} + hasEditableBoundries={this.hasEditableBoundries()} resizeProps={this.resizeProps} /> ) @@ -255,6 +250,15 @@ class ShowClassic extends Component { ); } + hasEditableBoundries() { + const { files } = this.props; + return Object.values(files).some( + file => + file.editableRegionBoundaries && + file.editableRegionBoundaries.length === 2 + ); + } + render() { const { fields: { blockName }, @@ -265,8 +269,10 @@ class ShowClassic extends Component { executeChallenge, pageContext: { challengeMeta: { introPath, nextChallengePath, prevChallengePath } - } + }, + files } = this.props; + return ( .scrollbar > .slider { + z-index: 11; } diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 9fb862c262..52f88c666d 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -12,6 +12,7 @@ import codeStorageEpic from './code-storage-epic'; import { createExecuteChallengeSaga } from './execute-challenge-saga'; import { createCurrentChallengeSaga } from './current-challenge-saga'; import { challengeTypes } from '../../../../utils/challengeTypes'; +import { getTargetEditor } from '../utils/getTargetEditor'; import { completedChallengesSelector } from '../../../redux'; import { isEmpty } from 'lodash'; @@ -20,6 +21,7 @@ export const backendNS = 'backendChallenge'; const initialState = { canFocusEditor: true, + visibleEditors: {}, challengeFiles: {}, challengeMeta: { superBlock: '', @@ -86,6 +88,7 @@ export const types = createTypes( 'moveToTab', 'setEditorFocusability', + 'toggleVisibleEditor', 'setAccessibilityMode', 'lastBlockChalSubmitted' @@ -179,6 +182,7 @@ export const submitChallenge = createAction(types.submitChallenge); export const moveToTab = createAction(types.moveToTab); export const setEditorFocusability = createAction(types.setEditorFocusability); +export const toggleVisibleEditor = createAction(types.toggleVisibleEditor); export const setAccessibilityMode = createAction(types.setAccessibilityMode); export const lastBlockChalSubmitted = createAction( @@ -258,6 +262,8 @@ export const challengeDataSelector = state => { }; export const canFocusEditorSelector = state => state[ns].canFocusEditor; +export const visibleEditorsSelector = state => state[ns].visibleEditors; + export const inAccessibilityModeSelector = state => state[ns].inAccessibilityMode; @@ -265,7 +271,8 @@ export const reducer = handleActions( { [types.createFiles]: (state, { payload }) => ({ ...state, - challengeFiles: payload + challengeFiles: payload, + visibleEditors: { [getTargetEditor(payload)]: true } }), [types.updateFile]: ( state, @@ -399,6 +406,15 @@ export const reducer = handleActions( ...state, canFocusEditor: payload }), + [types.toggleVisibleEditor]: (state, { payload }) => { + return { + ...state, + visibleEditors: { + ...state.visibleEditors, + [payload]: !state.visibleEditors[payload] + } + }; + }, [types.setAccessibilityMode]: (state, { payload }) => ({ ...state, inAccessibilityMode: payload diff --git a/client/src/templates/Challenges/utils/getTargetEditor.js b/client/src/templates/Challenges/utils/getTargetEditor.js new file mode 100644 index 0000000000..950ab5153f --- /dev/null +++ b/client/src/templates/Challenges/utils/getTargetEditor.js @@ -0,0 +1,14 @@ +import { toSortedArray } from '../../../../../utils/sort-files'; +import { isEmpty } from 'lodash'; + +export function getTargetEditor(challengeFiles) { + if (isEmpty(challengeFiles)) return null; + else { + let targetEditor = Object.values(challengeFiles).find( + ({ editableRegionBoundaries }) => !isEmpty(editableRegionBoundaries) + )?.key; + + // fallback for when there is no editable region. + return targetEditor || toSortedArray(challengeFiles)[0].key; + } +}