feat: add action row for challenges with ERMs on desktop (#39377)
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
69e3e138f6
commit
a1a051bd3a
13
client/src/templates/Challenges/classic/ActionRow.js
Normal file
13
client/src/templates/Challenges/classic/ActionRow.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import EditorTabs from './EditorTabs';
|
||||||
|
|
||||||
|
const ActionRow = () => (
|
||||||
|
<div className='action-row'>
|
||||||
|
<EditorTabs />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ActionRow.displayName = 'ActionRow';
|
||||||
|
|
||||||
|
export default ActionRow;
|
@ -1,12 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import EditorTabs from './EditorTabs';
|
||||||
|
import ActionRow from './ActionRow';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
challengeFile: PropTypes.shape({
|
challengeFiles: PropTypes.object,
|
||||||
key: PropTypes.string
|
|
||||||
}),
|
|
||||||
editor: PropTypes.element,
|
editor: PropTypes.element,
|
||||||
|
hasEditableBoundries: PropTypes.bool,
|
||||||
hasPreview: PropTypes.bool,
|
hasPreview: PropTypes.bool,
|
||||||
instructions: PropTypes.element,
|
instructions: PropTypes.element,
|
||||||
preview: PropTypes.element,
|
preview: PropTypes.element,
|
||||||
@ -24,17 +26,26 @@ const reflexProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class DesktopLayout extends Component {
|
class DesktopLayout extends Component {
|
||||||
|
getChallengeFile() {
|
||||||
|
const { challengeFiles } = this.props;
|
||||||
|
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
resizeProps,
|
resizeProps,
|
||||||
instructions,
|
instructions,
|
||||||
challengeFile,
|
|
||||||
editor,
|
editor,
|
||||||
testOutput,
|
testOutput,
|
||||||
hasPreview,
|
hasPreview,
|
||||||
preview
|
preview,
|
||||||
|
hasEditableBoundries
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const challengeFile = this.getChallengeFile();
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{hasEditableBoundries && <ActionRow />}
|
||||||
<ReflexContainer className='desktop-layout' orientation='vertical'>
|
<ReflexContainer className='desktop-layout' orientation='vertical'>
|
||||||
<ReflexElement flex={1} {...resizeProps}>
|
<ReflexElement flex={1} {...resizeProps}>
|
||||||
{instructions}
|
{instructions}
|
||||||
@ -44,7 +55,12 @@ class DesktopLayout extends Component {
|
|||||||
{challengeFile && (
|
{challengeFile && (
|
||||||
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
|
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
|
||||||
<ReflexElement flex={1} {...reflexProps} {...resizeProps}>
|
<ReflexElement flex={1} {...reflexProps} {...resizeProps}>
|
||||||
|
{
|
||||||
|
<Fragment>
|
||||||
|
{!hasEditableBoundries && <EditorTabs />}
|
||||||
{editor}
|
{editor}
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
<ReflexSplitter propagate={true} {...resizeProps} />
|
<ReflexSplitter propagate={true} {...resizeProps} />
|
||||||
<ReflexElement flex={0.25} {...reflexProps} {...resizeProps}>
|
<ReflexElement flex={0.25} {...reflexProps} {...resizeProps}>
|
||||||
@ -60,6 +76,7 @@ class DesktopLayout extends Component {
|
|||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
)}
|
)}
|
||||||
</ReflexContainer>
|
</ReflexContainer>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -435,7 +435,7 @@ class Editor extends Component {
|
|||||||
|
|
||||||
domNode.setAttribute('aria-hidden', true);
|
domNode.setAttribute('aria-hidden', true);
|
||||||
|
|
||||||
domNode.style.background = 'yellow';
|
domNode.style.background = 'lightYellow';
|
||||||
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
||||||
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
||||||
domNode.style.top = this.getViewZoneTop();
|
domNode.style.top = this.getViewZoneTop();
|
||||||
@ -459,8 +459,6 @@ class Editor extends Component {
|
|||||||
outputNode.style.zIndex = '10';
|
outputNode.style.zIndex = '10';
|
||||||
|
|
||||||
outputNode.setAttribute('aria-hidden', true);
|
outputNode.setAttribute('aria-hidden', true);
|
||||||
|
|
||||||
outputNode.style.background = 'var(--secondary-background)';
|
|
||||||
outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
|
||||||
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
|
||||||
outputNode.style.top = this.getOutputZoneTop();
|
outputNode.style.top = this.getOutputZoneTop();
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toggleVisibleEditor,
|
||||||
|
visibleEditorsSelector,
|
||||||
|
challengeFilesSelector
|
||||||
|
} from '../redux';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
challengeFiles: PropTypes.object.isRequired,
|
challengeFiles: PropTypes.object.isRequired,
|
||||||
toggleTab: PropTypes.func.isRequired,
|
toggleVisibleEditor: PropTypes.func.isRequired,
|
||||||
visibleEditors: PropTypes.shape({
|
visibleEditors: PropTypes.shape({
|
||||||
indexjs: PropTypes.bool,
|
indexjs: PropTypes.bool,
|
||||||
indexjsx: PropTypes.bool,
|
indexjsx: PropTypes.bool,
|
||||||
@ -12,13 +20,29 @@ const propTypes = {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
|
const mapStateToProps = createSelector(
|
||||||
|
visibleEditorsSelector,
|
||||||
|
challengeFilesSelector,
|
||||||
|
(visibleEditors, challengeFiles) => ({
|
||||||
|
visibleEditors,
|
||||||
|
challengeFiles
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleVisibleEditor
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditorTabs extends Component {
|
||||||
|
render() {
|
||||||
|
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
|
||||||
|
return (
|
||||||
<div className='monaco-editor-tabs'>
|
<div className='monaco-editor-tabs'>
|
||||||
{challengeFiles['indexjsx'] && (
|
{challengeFiles['indexjsx'] && (
|
||||||
<button
|
<button
|
||||||
aria-selected={visibleEditors.indexjsx}
|
aria-selected={visibleEditors.indexjsx}
|
||||||
className='monaco-editor-tab'
|
className='monaco-editor-tab'
|
||||||
onClick={() => toggleTab('indexjsx')}
|
onClick={() => toggleVisibleEditor('indexjsx')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
script.jsx
|
script.jsx
|
||||||
@ -28,7 +52,7 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
|
|||||||
<button
|
<button
|
||||||
aria-selected={visibleEditors.indexhtml}
|
aria-selected={visibleEditors.indexhtml}
|
||||||
className='monaco-editor-tab'
|
className='monaco-editor-tab'
|
||||||
onClick={() => toggleTab('indexhtml')}
|
onClick={() => toggleVisibleEditor('indexhtml')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
index.html
|
index.html
|
||||||
@ -38,7 +62,7 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
|
|||||||
<button
|
<button
|
||||||
aria-selected={visibleEditors.indexcss}
|
aria-selected={visibleEditors.indexcss}
|
||||||
className='monaco-editor-tab'
|
className='monaco-editor-tab'
|
||||||
onClick={() => toggleTab('indexcss')}
|
onClick={() => toggleVisibleEditor('indexcss')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
styles.css
|
styles.css
|
||||||
@ -48,7 +72,7 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
|
|||||||
<button
|
<button
|
||||||
aria-selected={visibleEditors.indexjs}
|
aria-selected={visibleEditors.indexjs}
|
||||||
className='monaco-editor-tab'
|
className='monaco-editor-tab'
|
||||||
onClick={() => toggleTab('indexjs')}
|
onClick={() => toggleVisibleEditor('indexjs')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
script.js
|
script.js
|
||||||
@ -56,8 +80,13 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EditorTabs.displayName = 'EditorTabs';
|
EditorTabs.displayName = 'EditorTabs';
|
||||||
EditorTabs.propTypes = propTypes;
|
EditorTabs.propTypes = propTypes;
|
||||||
|
|
||||||
export default EditorTabs;
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(EditorTabs);
|
||||||
|
@ -7,6 +7,7 @@ import ToolPanel from '../components/Tool-Panel';
|
|||||||
import { createStructuredSelector } from 'reselect';
|
import { createStructuredSelector } from 'reselect';
|
||||||
import { currentTabSelector, moveToTab } from '../redux';
|
import { currentTabSelector, moveToTab } from '../redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
import EditorTabs from './EditorTabs';
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentTab: currentTabSelector
|
currentTab: currentTabSelector
|
||||||
@ -66,6 +67,7 @@ class MobileLayout extends Component {
|
|||||||
{instructions}
|
{instructions}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
|
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
|
||||||
|
<EditorTabs />
|
||||||
{editor}
|
{editor}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>
|
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>
|
||||||
|
@ -3,7 +3,7 @@ import React, { Component, Suspense } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
|
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { isEmpty } from 'lodash';
|
import { getTargetEditor } from '../utils/getTargetEditor';
|
||||||
import { isDonationModalOpenSelector, userSelector } from '../../../redux';
|
import { isDonationModalOpenSelector, userSelector } from '../../../redux';
|
||||||
import {
|
import {
|
||||||
canFocusEditorSelector,
|
canFocusEditorSelector,
|
||||||
@ -13,13 +13,12 @@ import {
|
|||||||
saveEditorContent,
|
saveEditorContent,
|
||||||
setAccessibilityMode,
|
setAccessibilityMode,
|
||||||
setEditorFocusability,
|
setEditorFocusability,
|
||||||
|
visibleEditorsSelector,
|
||||||
updateFile
|
updateFile
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import './editor.css';
|
import './editor.css';
|
||||||
import { Loader } from '../../../components/helpers';
|
import { Loader } from '../../../components/helpers';
|
||||||
import EditorTabs from './EditorTabs';
|
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
import { toSortedArray } from '../../../../../utils/sort-files';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
canFocus: PropTypes.bool,
|
canFocus: PropTypes.bool,
|
||||||
@ -45,16 +44,31 @@ const propTypes = {
|
|||||||
setAccessibilityMode: PropTypes.func.isRequired,
|
setAccessibilityMode: PropTypes.func.isRequired,
|
||||||
setEditorFocusability: PropTypes.func,
|
setEditorFocusability: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
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(
|
const mapStateToProps = createSelector(
|
||||||
|
visibleEditorsSelector,
|
||||||
canFocusEditorSelector,
|
canFocusEditorSelector,
|
||||||
consoleOutputSelector,
|
consoleOutputSelector,
|
||||||
inAccessibilityModeSelector,
|
inAccessibilityModeSelector,
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({
|
(
|
||||||
|
visibleEditors,
|
||||||
|
canFocus,
|
||||||
|
output,
|
||||||
|
accessibilityMode,
|
||||||
|
open,
|
||||||
|
{ theme = 'default' }
|
||||||
|
) => ({
|
||||||
|
visibleEditors,
|
||||||
canFocus: open ? false : canFocus,
|
canFocus: open ? false : canFocus,
|
||||||
output,
|
output,
|
||||||
inAccessibilityMode: accessibilityMode,
|
inAccessibilityMode: accessibilityMode,
|
||||||
@ -70,15 +84,6 @@ const mapDispatchToProps = {
|
|||||||
updateFile
|
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 {
|
class MultifileEditor extends Component {
|
||||||
constructor(...props) {
|
constructor(...props) {
|
||||||
super(...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
|
// TODO: we might want to store the current editor here
|
||||||
this.focusOnEditor = this.focusOnEditor.bind(this);
|
this.focusOnEditor = this.focusOnEditor.bind(this);
|
||||||
}
|
}
|
||||||
@ -159,15 +157,6 @@ class MultifileEditor extends Component {
|
|||||||
// this._editor.focus();
|
// this._editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTab = newFileKey => {
|
|
||||||
this.setState(state => ({
|
|
||||||
visibleEditors: {
|
|
||||||
...state.visibleEditors,
|
|
||||||
[newFileKey]: !state.visibleEditors[newFileKey]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// this.setState({ fileKey: null });
|
// this.setState({ fileKey: null });
|
||||||
this.data = null;
|
this.data = null;
|
||||||
@ -180,9 +169,9 @@ class MultifileEditor extends Component {
|
|||||||
description,
|
description,
|
||||||
editorRef,
|
editorRef,
|
||||||
theme,
|
theme,
|
||||||
resizeProps
|
resizeProps,
|
||||||
|
visibleEditors
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { visibleEditors } = this.state;
|
|
||||||
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
|
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
|
||||||
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
|
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
|
||||||
// the in-editor description)
|
// 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
|
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
|
||||||
// the in-editor description)
|
// the in-editor description)
|
||||||
const targetEditor = getTargetEditor(challengeFiles);
|
const targetEditor = getTargetEditor(challengeFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReflexContainer
|
<ReflexContainer
|
||||||
orientation='horizontal'
|
orientation='horizontal'
|
||||||
{...reflexProps}
|
{...reflexProps}
|
||||||
{...resizeProps}
|
{...resizeProps}
|
||||||
>
|
>
|
||||||
<ReflexElement flex={0.1}>
|
<ReflexElement flex={10} {...reflexProps} {...resizeProps}>
|
||||||
<EditorTabs
|
|
||||||
challengeFiles={challengeFiles}
|
|
||||||
toggleTab={this.toggleTab.bind(this)}
|
|
||||||
visibleEditors={visibleEditors}
|
|
||||||
/>
|
|
||||||
</ReflexElement>
|
|
||||||
<ReflexElement flex={0.9} {...reflexProps} {...resizeProps}>
|
|
||||||
<ReflexContainer orientation='vertical'>
|
<ReflexContainer orientation='vertical'>
|
||||||
{visibleEditors.indexhtml && (
|
{visibleEditors.indexhtml && (
|
||||||
<ReflexElement {...reflexProps} {...resizeProps}>
|
<ReflexElement {...reflexProps} {...resizeProps}>
|
||||||
|
@ -5,7 +5,6 @@ import { createStructuredSelector } from 'reselect';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import { first } from 'lodash';
|
|
||||||
import Media from 'react-responsive';
|
import Media from 'react-responsive';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/Learn';
|
import LearnLayout from '../../../components/layouts/Learn';
|
||||||
@ -184,11 +183,6 @@ class ShowClassic extends Component {
|
|||||||
|
|
||||||
getVideoUrl = () => this.getChallenge().videoUrl;
|
getVideoUrl = () => this.getChallenge().videoUrl;
|
||||||
|
|
||||||
getChallengeFile() {
|
|
||||||
const { files } = this.props;
|
|
||||||
return first(Object.keys(files).map(key => files[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPreview() {
|
hasPreview() {
|
||||||
const { challengeType } = this.getChallenge();
|
const { challengeType } = this.getChallenge();
|
||||||
return (
|
return (
|
||||||
@ -229,6 +223,7 @@ class ShowClassic extends Component {
|
|||||||
containerRef={this.containerRef}
|
containerRef={this.containerRef}
|
||||||
description={description}
|
description={description}
|
||||||
editorRef={this.editorRef}
|
editorRef={this.editorRef}
|
||||||
|
hasEditableBoundries={this.hasEditableBoundries()}
|
||||||
resizeProps={this.resizeProps}
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
fields: { blockName },
|
fields: { blockName },
|
||||||
@ -265,8 +269,10 @@ class ShowClassic extends Component {
|
|||||||
executeChallenge,
|
executeChallenge,
|
||||||
pageContext: {
|
pageContext: {
|
||||||
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||||
}
|
},
|
||||||
|
files
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Hotkeys
|
<Hotkeys
|
||||||
editorRef={this.editorRef}
|
editorRef={this.editorRef}
|
||||||
@ -295,8 +301,9 @@ class ShowClassic extends Component {
|
|||||||
</Media>
|
</Media>
|
||||||
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
||||||
<DesktopLayout
|
<DesktopLayout
|
||||||
challengeFile={this.getChallengeFile()}
|
challengeFiles={files}
|
||||||
editor={this.renderEditor()}
|
editor={this.renderEditor()}
|
||||||
|
hasEditableBoundries={this.hasEditableBoundries()}
|
||||||
hasPreview={this.hasPreview()}
|
hasPreview={this.hasPreview()}
|
||||||
instructions={this.renderInstructionsPanel({
|
instructions={this.renderInstructionsPanel({
|
||||||
showToolPanel: true
|
showToolPanel: true
|
||||||
|
@ -57,3 +57,54 @@
|
|||||||
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monaco-editor-tabs {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 10px;
|
||||||
|
background-color: var(--primary-background);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-tab {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-left: none;
|
||||||
|
background-color: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.monaco-editor-tab:hover {
|
||||||
|
color: var(--quaternary-color);
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-tab:first-child {
|
||||||
|
border-left: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-tab-selected {
|
||||||
|
background-color: var(--primary-background);
|
||||||
|
border-bottom: 2px solid var(--primary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-tab[role='tab'][aria-selected='true'] {
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid var(--quaternary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row .monaco-editor-tabs {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row .monaco-editor-tabs .monaco-editor-tab {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
@ -1,53 +1,15 @@
|
|||||||
.monaco-editor-tabs {
|
#description {
|
||||||
display: flex;
|
background: var(--secondary-background);
|
||||||
padding: 0px 10px;
|
}
|
||||||
background-color: var(--primary-background);
|
.monaco-editor .margin-view-overlays .line-numbers,
|
||||||
border-bottom: 2px solid var(--primary-color);
|
.monaco-editor .margin-view-overlays .myLineDecoration + .line-numbers {
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.monaco-editor-tab {
|
[widgetid='my.overlay.widget'] {
|
||||||
position: relative;
|
padding: 10px;
|
||||||
top: 2px;
|
|
||||||
padding: 4px 16px;
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
border-left: none;
|
|
||||||
background-color: var(--secondary-background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.monaco-editor-tab:hover {
|
.vs .monaco-scrollable-element > .scrollbar > .slider {
|
||||||
color: var(--quaternary-color);
|
z-index: 11;
|
||||||
background-color: var(--quaternary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor-tab:first-child {
|
|
||||||
border-left: 2px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor-tab-selected {
|
|
||||||
background-color: var(--primary-background);
|
|
||||||
border-bottom: 2px solid var(--primary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor-tab[role='tab'][aria-selected='true'] {
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
color: var(--secondary-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.myInlineDecoration {
|
|
||||||
color: lightgray !important;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: oblique;
|
|
||||||
}
|
|
||||||
|
|
||||||
.myLineDecoration {
|
|
||||||
background: lightblue;
|
|
||||||
width: 5px !important;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.do-not-edit {
|
|
||||||
background: grey;
|
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import codeStorageEpic from './code-storage-epic';
|
|||||||
import { createExecuteChallengeSaga } from './execute-challenge-saga';
|
import { createExecuteChallengeSaga } from './execute-challenge-saga';
|
||||||
import { createCurrentChallengeSaga } from './current-challenge-saga';
|
import { createCurrentChallengeSaga } from './current-challenge-saga';
|
||||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||||
|
import { getTargetEditor } from '../utils/getTargetEditor';
|
||||||
import { completedChallengesSelector } from '../../../redux';
|
import { completedChallengesSelector } from '../../../redux';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export const backendNS = 'backendChallenge';
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
canFocusEditor: true,
|
canFocusEditor: true,
|
||||||
|
visibleEditors: {},
|
||||||
challengeFiles: {},
|
challengeFiles: {},
|
||||||
challengeMeta: {
|
challengeMeta: {
|
||||||
superBlock: '',
|
superBlock: '',
|
||||||
@ -86,6 +88,7 @@ export const types = createTypes(
|
|||||||
'moveToTab',
|
'moveToTab',
|
||||||
|
|
||||||
'setEditorFocusability',
|
'setEditorFocusability',
|
||||||
|
'toggleVisibleEditor',
|
||||||
'setAccessibilityMode',
|
'setAccessibilityMode',
|
||||||
|
|
||||||
'lastBlockChalSubmitted'
|
'lastBlockChalSubmitted'
|
||||||
@ -179,6 +182,7 @@ export const submitChallenge = createAction(types.submitChallenge);
|
|||||||
export const moveToTab = createAction(types.moveToTab);
|
export const moveToTab = createAction(types.moveToTab);
|
||||||
|
|
||||||
export const setEditorFocusability = createAction(types.setEditorFocusability);
|
export const setEditorFocusability = createAction(types.setEditorFocusability);
|
||||||
|
export const toggleVisibleEditor = createAction(types.toggleVisibleEditor);
|
||||||
export const setAccessibilityMode = createAction(types.setAccessibilityMode);
|
export const setAccessibilityMode = createAction(types.setAccessibilityMode);
|
||||||
|
|
||||||
export const lastBlockChalSubmitted = createAction(
|
export const lastBlockChalSubmitted = createAction(
|
||||||
@ -258,6 +262,8 @@ export const challengeDataSelector = state => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const canFocusEditorSelector = state => state[ns].canFocusEditor;
|
export const canFocusEditorSelector = state => state[ns].canFocusEditor;
|
||||||
|
export const visibleEditorsSelector = state => state[ns].visibleEditors;
|
||||||
|
|
||||||
export const inAccessibilityModeSelector = state =>
|
export const inAccessibilityModeSelector = state =>
|
||||||
state[ns].inAccessibilityMode;
|
state[ns].inAccessibilityMode;
|
||||||
|
|
||||||
@ -265,7 +271,8 @@ export const reducer = handleActions(
|
|||||||
{
|
{
|
||||||
[types.createFiles]: (state, { payload }) => ({
|
[types.createFiles]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
challengeFiles: payload
|
challengeFiles: payload,
|
||||||
|
visibleEditors: { [getTargetEditor(payload)]: true }
|
||||||
}),
|
}),
|
||||||
[types.updateFile]: (
|
[types.updateFile]: (
|
||||||
state,
|
state,
|
||||||
@ -399,6 +406,15 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
canFocusEditor: payload
|
canFocusEditor: payload
|
||||||
}),
|
}),
|
||||||
|
[types.toggleVisibleEditor]: (state, { payload }) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
visibleEditors: {
|
||||||
|
...state.visibleEditors,
|
||||||
|
[payload]: !state.visibleEditors[payload]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
[types.setAccessibilityMode]: (state, { payload }) => ({
|
[types.setAccessibilityMode]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
inAccessibilityMode: payload
|
inAccessibilityMode: payload
|
||||||
|
14
client/src/templates/Challenges/utils/getTargetEditor.js
Normal file
14
client/src/templates/Challenges/utils/getTargetEditor.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user