diff --git a/client/src/templates/Challenges/classic/Editor.js b/client/src/templates/Challenges/classic/Editor.js
index c8fab2f607f..d88c9a6f44b 100644
--- a/client/src/templates/Challenges/classic/Editor.js
+++ b/client/src/templates/Challenges/classic/Editor.js
@@ -98,12 +98,42 @@ class Editor extends Component {
constructor(...props) {
super(...props);
- this.state = {
- code: '',
- ext: '',
- fileKey: ''
+ // 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.
+
+ this.data = {
+ indexjs: {
+ model: null,
+ state: null
+ },
+ indexcss: {
+ model: null,
+ state: null
+ },
+ indexhtml: {
+ model: null,
+ state: null
+ }
};
+ // NOTE: for consitency with this.data (and this.options) currentFileKey
+ // is just a property, not state.
+
+ this.currentFileKey = 'indexhtml';
+
this.options = {
fontSize: '18px',
scrollBeyondLastLine: false,
@@ -140,15 +170,26 @@ class Editor extends Component {
}
editorWillMount = monaco => {
+ const { challengeFiles } = 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.
+
+ // If a model exists, there is no need to recreate it.
+ Object.keys(challengeFiles).forEach(key => {
+ this.data[key].model = this.data[key].model
+ ? this.data[key].model
+ : monaco.editor.createModel(
+ challengeFiles[key].contents,
+ modeMap[challengeFiles[key].ext]
+ );
+ });
+ return { model: this.data[this.currentFileKey].model };
};
editorDidMount = (editor, monaco) => {
- this.setState({
- code: this.props.challengeFiles.indexcss.contents,
- ext: this.props.challengeFiles.indexcss.ext,
- fileKey: this.props.challengeFiles.indexcss.key
- });
this._editor = editor;
editor.updateOptions({
accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
@@ -223,11 +264,26 @@ class Editor extends Component {
onChange = editorValue => {
const { updateFile } = this.props;
- const { fileKey } = this.state;
- updateFile({ key: fileKey, editorValue });
- this.setState({
- code: editorValue
- });
+ updateFile({ key: this.currentFileKey, editorValue });
+ };
+
+ changeTab = fileKey => {
+ this.currentFileKey = fileKey;
+ const editor = this._editor;
+ const currentState = editor.saveViewState();
+
+ const currentModel = editor.getModel();
+ if (currentModel === this.data.indexjs.model) {
+ this.data.indexjs.state = currentState;
+ } else if (currentModel === this.data.indexcss.model) {
+ this.data.indexcss.state = currentState;
+ } else if (currentModel === this.data.indexhtml.model) {
+ this.data.indexhtml.state = currentState;
+ }
+
+ editor.setModel(this.data[fileKey].model);
+ editor.restoreViewState(this.data[fileKey].state);
+ editor.focus();
};
componentDidUpdate(prevProps) {
@@ -236,28 +292,47 @@ class Editor extends Component {
}
}
+ componentWillUnmount() {
+ this.currentFileKey = null;
+ this.data = null;
+ }
+
render() {
- const { code, ext, fileKey } = this.state;
const { theme } = this.props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
+ // TODO: tabs should be dynamically created from the challengeFiles
+ // TODO: a11y fixes.
return (
}>