feat: multiple concurrent editors

This commit is contained in:
Oliver Eyton-Williams
2020-07-27 15:20:19 +02:00
committed by Mrugesh Mohapatra
parent 2e7a2424c1
commit 02aff4d400
4 changed files with 440 additions and 191 deletions

View File

@ -1,4 +1,4 @@
import React, { Component, Suspense } from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -16,8 +16,6 @@ import {
updateFile
} from '../redux';
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
import { Loader } from '../../../components/helpers';
import { toSortedArray } from '../../../../../utils/sort-files';
import './editor.css';
@ -25,6 +23,7 @@ const MonacoEditor = Loadable(() => import('react-monaco-editor'));
const propTypes = {
canFocus: PropTypes.bool,
// TODO: use shape
challengeFiles: PropTypes.object,
containerRef: PropTypes.any.isRequired,
contents: PropTypes.string,
@ -37,6 +36,10 @@ const propTypes = {
initialEditorContent: PropTypes.string,
initialExt: PropTypes.string,
output: PropTypes.arrayOf(PropTypes.string),
resizeProps: PropTypes.shape({
onStopResize: PropTypes.func,
onResize: PropTypes.func
}),
saveEditorContent: PropTypes.func.isRequired,
setAccessibilityMode: PropTypes.func.isRequired,
setEditorFocusability: PropTypes.func,
@ -118,9 +121,6 @@ class Editor extends Component {
// available files into that order. i.e. if it's just one file it will
// automatically be first, but if there's jsx and js (for some reason) it
// will be [jsx, js].
// this.state = {
// fileKey: 'indexhtml'
// };
// NOTE: This looks like it should be react state. However we need
// to access monaco.editor to create the models and store the state and that
@ -129,52 +129,22 @@ class Editor extends Component {
// As a result it was unclear how to link up the editor's lifecycle with
// react's lifecycle. Simply storing the models and state here and letting
// the editor control them seems to be the best solution.
// TODO: IS THIS STILL TRUE NOW EACH EDITOR IS AN ISLAND, ENTIRE OF ITSELF?
// TODO: is there any point in initializing this? It should be fine with
// this.data = {indexjs:{}, indexcss:{}, indexhtml:{}, indexjsx: {}}
// this.data = {}
this.data = {
indexjs: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexcss: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexhtml: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexjsx: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
}
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
};
const { challengeFiles } = this.props;
// NOTE: the ARIA state is controlled by fileKey, so changes to it must
// trigger a re-render. Hence state:
this.state = {
fileKey: toSortedArray(challengeFiles)[0].key
};
this.options = {
fontSize: '18px',
@ -214,53 +184,54 @@ class Editor extends Component {
editorWillMount = monaco => {
this._monaco = monaco;
const { challengeFiles } = this.props;
const { challengeFiles, fileKey } = this.props;
defineMonacoThemes(monaco);
// If a model is not provided, then the editor 'owns' the model it creates
// and will dispose of that model if it is replaced. Since we intend to
// swap and reuse models, we have to create our own models to prevent
// disposal.
Object.keys(challengeFiles).forEach(key => {
// If a model exists, there is no need to recreate it.
const model =
this.data[key].model ||
monaco.editor.createModel(
challengeFiles[key].contents,
modeMap[challengeFiles[key].ext]
);
this.data[key].model = model;
// TODO: For now, I'm keeping the 'data' machinery, but it'll probably go
const editableRegion = [...challengeFiles[key].editableRegionBoundaries];
const model =
this.data.model ||
monaco.editor.createModel(
challengeFiles[fileKey].contents,
modeMap[challengeFiles[fileKey].ext]
);
this.data.model = model;
if (editableRegion.length === 2)
this.decorateForbiddenRanges(key, editableRegion);
});
return { model: this.data[this.state.fileKey].model };
const editableRegion = [
...challengeFiles[fileKey].editableRegionBoundaries
];
if (editableRegion.length === 2)
this.decorateForbiddenRanges(editableRegion);
return { model: this.data.model };
};
// Updates the model if the contents has changed. This is only necessary for
// changes coming from outside the editor (such as code resets).
updateEditorValues = () => {
const { challengeFiles } = this.props;
Object.keys(challengeFiles).forEach(key => {
const newContents = challengeFiles[key].contents;
if (this.data[key].model.getValue() !== newContents) {
this.data[key].model.setValue(newContents);
}
});
const { challengeFiles, fileKey } = this.props;
const newContents = challengeFiles[fileKey].contents;
if (this.data.model.getValue() !== newContents) {
this.data.model.setValue(newContents);
}
};
editorDidMount = (editor, monaco) => {
this._editor = editor;
const { challengeFiles } = this.props;
const { fileKey } = this.state;
const { challengeFiles, fileKey } = this.props;
editor.updateOptions({
accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
});
// Users who are using screen readers should not have to move focus from
// the editor to the description every time they open a challenge.
if (this.props.canFocus && !this.props.inAccessibilityMode) {
// TODO: only one Editor should be calling for focus at once.
editor.focus();
} else this.focusOnHotkeys();
editor.addAction({
@ -382,7 +353,6 @@ class Editor extends Component {
};
viewZoneCallback = changeAccessor => {
const { fileKey } = this.state;
// TODO: is there any point creating this here? I know it's cached, but
// would it not be better just sourced from the overlayWidget?
const domNode = this.createDescription();
@ -391,7 +361,7 @@ class Editor extends Component {
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
// TODO: set via onComputedHeight?
this.data[fileKey].viewZoneHeight = domNode.offsetHeight;
this.data.viewZoneHeight = domNode.offsetHeight;
var background = document.createElement('div');
background.style.background = 'lightgreen';
@ -407,12 +377,11 @@ class Editor extends Component {
this._editor.layoutOverlayWidget(this._overlayWidget)
};
this.data[fileKey].viewZoneId = changeAccessor.addZone(viewZone);
this.data.viewZoneId = changeAccessor.addZone(viewZone);
};
// TODO: this is basically the same as viewZoneCallback, so DRY them out.
outputZoneCallback = changeAccessor => {
const { fileKey } = this.state;
// TODO: is there any point creating this here? I know it's cached, but
// would it not be better just sourced from the overlayWidget?
const outputNode = this.createOutputNode();
@ -421,7 +390,7 @@ class Editor extends Component {
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
// TODO: set via onComputedHeight?
this.data[fileKey].outputZoneHeight = outputNode.offsetHeight;
this.data.outputZoneHeight = outputNode.offsetHeight;
var background = document.createElement('div');
background.style.background = 'lightpink';
@ -437,7 +406,7 @@ class Editor extends Component {
this._editor.layoutOverlayWidget(this._outputWidget)
};
this.data[fileKey].outputZoneId = changeAccessor.addZone(viewZone);
this.data.outputZoneId = changeAccessor.addZone(viewZone);
};
createDescription() {
@ -516,7 +485,7 @@ class Editor extends Component {
onChange = editorValue => {
const { updateFile } = this.props;
// TODO: use fileKey everywhere?
const { fileKey: key } = this.state;
const { fileKey: key } = this.props;
// TODO: now that we have getCurrentEditableRegion, should the overlays
// follow that directly? We could subscribe to changes to that and redraw if
// those imply that the positions have changed (i.e. if the content height
@ -530,28 +499,6 @@ class Editor extends Component {
updateFile({ key, editorValue, editableRegionBoundaries });
};
changeTab = newFileKey => {
const { challengeFiles } = this.props;
this.setState({ fileKey: newFileKey });
const editor = this._editor;
const currentState = editor.saveViewState();
const currentModel = editor.getModel();
for (const key in this.data) {
if (currentModel === this.data[key].model) {
this.data[key].state = currentState;
}
}
editor.setModel(this.data[newFileKey].model);
editor.restoreViewState(this.data[newFileKey].state);
editor.focus();
const editableBoundaries = [
...challengeFiles[newFileKey].editableRegionBoundaries
];
this.showEditableRegion(editableBoundaries);
};
showEditableRegion(editableBoundaries) {
if (editableBoundaries.length !== 2) return;
// this is a heuristic: if the cursor is at the start of the page, chances
@ -597,7 +544,7 @@ class Editor extends Component {
// currently is. (see getLineAfterViewZone)
// TODO: DRY this and getOutputZoneTop out.
getViewZoneTop() {
const heightDelta = this.data[this.state.fileKey].viewZoneHeight || 0;
const heightDelta = this.data.viewZoneHeight || 0;
const top =
this._editor.getTopForLineNumber(this.getLineAfterViewZone()) -
@ -609,7 +556,7 @@ class Editor extends Component {
}
getOutputZoneTop() {
const heightDelta = this.data[this.state.fileKey].outputZoneHeight || 0;
const heightDelta = this.data.outputZoneHeight || 0;
const top =
this._editor.getTopForLineNumber(this.getLineAfterEditableRegion()) -
@ -624,19 +571,15 @@ class Editor extends Component {
// the region it should cover instead.
// TODO: DRY
getLineAfterViewZone() {
const { fileKey } = this.state;
return (
this.data[fileKey].model.getDecorationRange(
this.data[fileKey].startEditDecId
).endLineNumber + 1
this.data.model.getDecorationRange(this.data.startEditDecId)
.endLineNumber + 1
);
}
getLineAfterEditableRegion() {
const { fileKey } = this.state;
return this.data[fileKey].model.getDecorationRange(
this.data[fileKey].endEditDecId
).startLineNumber;
return this.data.model.getDecorationRange(this.data.endEditDecId)
.startLineNumber;
}
translateRange = (range, lineDelta) => {
@ -661,14 +604,13 @@ class Editor extends Component {
};
};
getCurrentEditableRegion = key => {
const model = this.data[key].model;
getCurrentEditableRegion = () => {
const model = this.data.model;
// TODO: this is a little low-level, but we should bail if there is no
// editable region defined.
if (!this.data[key].startEditDecId || !this.data[key].endEditDecId)
return null;
const firstRange = model.getDecorationRange(this.data[key].startEditDecId);
const secondRange = model.getDecorationRange(this.data[key].endEditDecId);
if (!this.data.startEditDecId || !this.data.endEditDecId) return null;
const firstRange = model.getDecorationRange(this.data.startEditDecId);
const secondRange = model.getDecorationRange(this.data.endEditDecId);
const { startLineNumber, endLineNumber } = this.getLinesBetweenRanges(
firstRange,
secondRange
@ -681,8 +623,8 @@ class Editor extends Component {
return new this._monaco.Range(startLineNumber, 1, endLineNumber, endColumn);
};
decorateForbiddenRanges(key, editableRegion) {
const model = this.data[key].model;
decorateForbiddenRanges(editableRegion) {
const model = this.data.model;
const forbiddenRanges = [
[1, editableRegion[0]],
[editableRegion[1], model.getLineCount()]
@ -693,7 +635,7 @@ class Editor extends Component {
});
// the first range should expand at the top
this.data[key].startEditDecId = this.highlightLines(
this.data.startEditDecId = this.highlightLines(
this._monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
model,
ranges[0]
@ -706,7 +648,7 @@ class Editor extends Component {
);
// the second range should expand at the bottom
this.data[key].endEditDecId = this.highlightLines(
this.data.endEditDecId = this.highlightLines(
this._monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter,
model,
ranges[1]
@ -871,15 +813,15 @@ class Editor extends Component {
// we only need to handle the special case of the second region being
// pulled up, the first region already behaves correctly.
this.data[key].endEditDecId = preventOverlap(this.data[key].endEditDecId);
this.data.endEditDecId = preventOverlap(this.data.endEditDecId);
// TODO: do the same for the description widget
// this has to be handle differently, because we care about the END
// of the zone, not the START
handleDescriptionZoneChange(this.data[key].startEditDecId);
handleHintsZoneChange(this.data[key].endEditDecId);
warnUser(this.data[key].startEditDecId);
warnUser(this.data[key].endEditDecId);
handleDescriptionZoneChange(this.data.startEditDecId);
handleHintsZoneChange(this.data.endEditDecId);
warnUser(this.data.startEditDecId);
warnUser(this.data.endEditDecId);
});
}
@ -908,7 +850,7 @@ class Editor extends Component {
componentDidUpdate(prevProps) {
// TODO: re-organise into something less nested
const { fileKey } = this.state;
// If a challenge is reset, it needs to communicate that change to the
// editor. This looks for changes any challenge files and updates if needed.
if (this.props.challengeFiles !== prevProps.challengeFiles) {
@ -924,7 +866,6 @@ class Editor extends Component {
// (shownHint,maybe) and have that persist through previews. But, for
// now:
if (output) {
console.log('OUTPUT', output);
if (output[0]) {
document.getElementById('test-status').innerHTML = output[0];
}
@ -933,17 +874,19 @@ class Editor extends Component {
document.getElementById('test-output').innerHTML = output[1];
}
if (this.data[fileKey].startEditDecId) {
if (this.data.startEditDecId) {
this.updateOutputZone();
}
}
}
// TODO: to get the 'dimensions' prop, this needs to be a inside a
// ReflexElement
if (this.props.dimensions !== prevProps.dimensions) {
// Order matters here. The view zones need to know the new editor
// dimensions in order to render correctly.
this._editor.layout();
if (this.data[fileKey].startEditDecId) {
if (this.data.startEditDecId) {
this.updateViewZone();
this.updateOutputZone();
}
@ -954,86 +897,34 @@ class Editor extends Component {
// TODO: DRY (there's going to be a lot of that)
updateOutputZone() {
this._editor.changeViewZones(changeAccessor => {
changeAccessor.removeZone(this.data[this.state.fileKey].outputZoneId);
changeAccessor.removeZone(this.data.outputZoneId);
this.outputZoneCallback(changeAccessor);
});
}
updateViewZone() {
this._editor.changeViewZones(changeAccessor => {
changeAccessor.removeZone(this.data[this.state.fileKey].viewZoneId);
changeAccessor.removeZone(this.data.viewZoneId);
this.viewZoneCallback(changeAccessor);
});
}
componentWillUnmount() {
this.setState({ fileKey: null });
this.data = null;
}
render() {
const { challengeFiles, theme } = this.props;
const { theme } = this.props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: tabs should be dynamically created from the challengeFiles
// TODO: is the key necessary? Try switching themes without it.
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description)
return (
<Suspense fallback={<Loader timeout={600} />}>
<span className='notranslate'>
<div className='monaco-editor-tabs'>
{challengeFiles['indexjsx'] && (
<button
aria-selected={this.state.fileKey === 'indexjsx'}
className='monaco-editor-tab'
onClick={() => this.changeTab('indexjsx')}
role='tab'
>
script.jsx
</button>
)}
{challengeFiles['indexhtml'] && (
<button
aria-selected={this.state.fileKey === 'indexhtml'}
className='monaco-editor-tab'
onClick={() => this.changeTab('indexhtml')}
role='tab'
>
index.html
</button>
)}
{challengeFiles['indexjs'] && (
<button
aria-selected={this.state.fileKey === 'indexjs'}
className='monaco-editor-tab'
onClick={() => this.changeTab('indexjs')}
role='tab'
>
script.js
</button>
)}
{challengeFiles['indexcss'] && (
<button
aria-selected={this.state.fileKey === 'indexcss'}
className='monaco-editor-tab'
onClick={() => this.changeTab('indexcss')}
role='tab'
>
styles.css
</button>
)}
</div>
<MonacoEditor
editorDidMount={this.editorDidMount}
editorWillMount={this.editorWillMount}
key={`${editorTheme}`}
onChange={this.onChange}
options={this.options}
theme={editorTheme}
/>
</span>
</Suspense>
<MonacoEditor
editorDidMount={this.editorDidMount}
editorWillMount={this.editorWillMount}
onChange={this.onChange}
options={this.options}
theme={editorTheme}
/>
);
}
}

View File

@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
challengeFiles: PropTypes.object.isRequired,
toggleTab: PropTypes.func.isRequired,
visibleEditors: PropTypes.shape({
indexjs: PropTypes.bool,
indexjsx: PropTypes.bool,
indexcss: PropTypes.bool,
indexhtml: PropTypes.bool
})
};
const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
<div className='monaco-editor-tabs'>
{challengeFiles['indexjsx'] && (
<button
aria-selected={visibleEditors.indexjsx}
className='monaco-editor-tab'
onClick={() => toggleTab('indexjsx')}
role='tab'
>
script.jsx
</button>
)}
{challengeFiles['indexhtml'] && (
<button
aria-selected={visibleEditors.indexhtml}
className='monaco-editor-tab'
onClick={() => toggleTab('indexhtml')}
role='tab'
>
index.html
</button>
)}
{challengeFiles['indexcss'] && (
<button
aria-selected={visibleEditors.indexcss}
className='monaco-editor-tab'
onClick={() => toggleTab('indexcss')}
role='tab'
>
styles.css
</button>
)}
{challengeFiles['indexjs'] && (
<button
aria-selected={visibleEditors.indexjs}
className='monaco-editor-tab'
onClick={() => toggleTab('indexjs')}
role='tab'
>
script.js
</button>
)}
</div>
);
EditorTabs.displayName = 'EditorTabs';
EditorTabs.propTypes = propTypes;
export default EditorTabs;

View File

@ -0,0 +1,294 @@
import PropTypes from 'prop-types';
import React, { Component, Suspense } from 'react';
import { connect } from 'react-redux';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import { createSelector } from 'reselect';
import { isDonationModalOpenSelector, userSelector } from '../../../redux';
import {
canFocusEditorSelector,
consoleOutputSelector,
executeChallenge,
inAccessibilityModeSelector,
saveEditorContent,
setAccessibilityMode,
setEditorFocusability,
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,
// TODO: use shape
challengeFiles: PropTypes.object,
containerRef: PropTypes.any.isRequired,
contents: PropTypes.string,
description: PropTypes.string,
dimensions: PropTypes.object,
editorRef: PropTypes.any.isRequired,
executeChallenge: PropTypes.func.isRequired,
ext: PropTypes.string,
fileKey: PropTypes.string,
inAccessibilityMode: PropTypes.bool.isRequired,
initialEditorContent: PropTypes.string,
initialExt: PropTypes.string,
output: PropTypes.arrayOf(PropTypes.string),
resizeProps: PropTypes.shape({
onStopResize: PropTypes.func,
onResize: PropTypes.func
}),
saveEditorContent: PropTypes.func.isRequired,
setAccessibilityMode: PropTypes.func.isRequired,
setEditorFocusability: PropTypes.func,
theme: PropTypes.string,
updateFile: PropTypes.func.isRequired
};
const mapStateToProps = createSelector(
canFocusEditorSelector,
consoleOutputSelector,
inAccessibilityModeSelector,
isDonationModalOpenSelector,
userSelector,
(canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({
canFocus: open ? false : canFocus,
output,
inAccessibilityMode: accessibilityMode,
theme
})
);
const mapDispatchToProps = {
executeChallenge,
saveEditorContent,
setAccessibilityMode,
setEditorFocusability,
updateFile
};
class MultifileEditor extends Component {
constructor(...props) {
super(...props);
// TENATIVE PLAN: create a typical order [html/jsx, css, js], put the
// available files into that order. i.e. if it's just one file it will
// automatically be first, but if there's jsx and js (for some reason) it
// will be [jsx, js].
// this.state = {
// fileKey: 'indexhtml'
// };
// NOTE: This looks like it should be react state. However we need
// to access monaco.editor to create the models and store the state and that
// is only available in the react-monaco-editor component's lifecycle hooks
// and not react's lifecyle hooks.
// As a result it was unclear how to link up the editor's lifecycle with
// react's lifecycle. Simply storing the models and state here and letting
// the editor control them seems to be the best solution.
// TODO: is there any point in initializing this? It should be fine with
// this.data = {indexjs:{}, indexcss:{}, indexhtml:{}, indexjsx: {}}
this.data = {
indexjs: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexcss: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexhtml: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
},
indexjsx: {
model: null,
state: null,
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
viewZoneHeight: null
}
};
const { challengeFiles } = this.props;
const targetEditor = toSortedArray(challengeFiles)[0].key;
this.state = {
visibleEditors: { [targetEditor]: true }
};
// TODO: we might want to store the current editor here
this.focusOnEditor = this.focusOnEditor.bind(this);
}
focusOnHotkeys() {
if (this.props.containerRef.current) {
this.props.containerRef.current.focus();
}
}
focusOnEditor() {
// TODO: it should focus one of the editors
// this._editor.focus();
}
toggleTab = newFileKey => {
this.setState(state => ({
visibleEditors: {
...state.visibleEditors,
[newFileKey]: !state.visibleEditors[newFileKey]
}
}));
};
componentWillUnmount() {
// this.setState({ fileKey: null });
this.data = null;
}
render() {
const {
challengeFiles,
containerRef,
description,
editorRef,
theme,
resizeProps
} = 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)
// TODO: the splitters should appear between editors, so logically this
// would be best as
// editors.map(props => <EditorWrapper ...props>).join(<ReflexSplitter>)
// ...probably! As long as we can put keys in the right places.
const reflexProps = {
propagateDimensions: true,
renderOnResize: true,
renderOnResizeRate: 20
};
// TODO: this approach (||ing the visibleEditors) isn't great.
const splitCSS = visibleEditors.indexhtml && visibleEditors.indexcss;
const splitJS =
visibleEditors.indexcss ||
(visibleEditors.indexhtml && visibleEditors.indexjs);
// TODO: tabs should be dynamically created from the challengeFiles
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description)
return (
<ReflexContainer
orientation='horizontal'
{...reflexProps}
{...resizeProps}
>
<ReflexElement flex={0.1}>
<EditorTabs
challengeFiles={challengeFiles}
toggleTab={this.toggleTab.bind(this)}
visibleEditors={visibleEditors}
/>
</ReflexElement>
<ReflexElement flex={0.9} {...reflexProps} {...resizeProps}>
<ReflexContainer orientation='vertical'>
{visibleEditors.indexhtml && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Suspense fallback={<Loader timeout={600} />}>
<span className='notranslate'>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
// TODO: only pass description to the editor with the
// editable region
description={description}
fileKey='indexhtml'
key='indexhtml'
ref={editorRef}
resizeProps={resizeProps}
theme={editorTheme}
/>
</span>
</Suspense>
</ReflexElement>
)}
{splitCSS && <ReflexSplitter propagate={true} {...resizeProps} />}
{visibleEditors.indexcss && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Suspense fallback={<Loader timeout={600} />}>
<span className='notranslate'>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
// TODO: only pass description to the editor with the
// editable region
description={'this sentence is not here'}
fileKey='indexcss'
key='indexcss'
resizeProps={resizeProps}
theme={editorTheme}
/>
</span>
</Suspense>
</ReflexElement>
)}
{splitJS && <ReflexSplitter propagate={true} {...resizeProps} />}
{visibleEditors.indexjs && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Suspense fallback={<Loader timeout={600} />}>
<span className='notranslate'>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
// TODO: only pass description to the editor with the
// editable region
description={'neither is this one'}
fileKey='indexjs'
key='indexjs'
resizeProps={resizeProps}
theme={editorTheme}
/>
</span>
</Suspense>
</ReflexElement>
)}
</ReflexContainer>
</ReflexElement>
</ReflexContainer>
);
}
}
MultifileEditor.displayName = 'MultifileEditor';
MultifileEditor.propTypes = propTypes;
// NOTE: withRef gets replaced by forwardRef in react-redux 6,
// https://github.com/reduxjs/react-redux/releases/tag/v6.0.0
export default connect(
mapStateToProps,
mapDispatchToProps,
null,
{ withRef: true }
)(MultifileEditor);

View File

@ -9,7 +9,7 @@ import { first } from 'lodash';
import Media from 'react-responsive';
import LearnLayout from '../../../components/layouts/Learn';
import Editor from './Editor';
import MultifileEditor from './MultifileEditor';
import Preview from '../components/Preview';
import SidePanel from '../components/Side-Panel';
import Output from '../components/Output';
@ -224,11 +224,12 @@ class ShowClassic extends Component {
const { description } = this.getChallenge();
return (
files && (
<Editor
<MultifileEditor
challengeFiles={files}
containerRef={this.containerRef}
description={description}
ref={this.editorRef}
editorRef={this.editorRef}
resizeProps={this.resizeProps}
/>
)
);