Files
freeCodeCamp/common/app/routes/Challenges/views/Modern/Editor.jsx
Berkeley Martinez dced96da8e feat: react challenges (#16099)
* chore(packages): Update redux utils

* feat(Panes): Invert control of panes map creation

* feat(Modern): Add view

* feat(Panes): Decouple panes from Challenges

* fix(Challenges): Decouple challenge views from panes map

* fix(Challenge/views): PanesMap => mapStateToPanesMap

This clarifies what these functions are doing

* fix(Challenges): Add view type

* fix(Panes): Remove unneeded panes container

* feat(Panes): Invert control of pane content render

This decouples the Panes from the content they render, allowing for
greater flexibility.

* feat(Modern): Add side panel

This is common between modern and classic

* feat(seed): Array to string file content

* fix(files): Modern files should be polyvinyls

* feat(Modern): Create editors per file

* fix(seed/React): Incorrect keyfile name

* feat(Modern): Highligh jsx correctly

This adds highlighting for jsx. Unfortunately, this disables linting for
non-javascript files as jshint will only work for those

* feat(rechallenge): Add jsx ext to babel transformer

* feat(seed): Normalize challenge files head/tail/content

* refactor(rechallenge/build): Rename function

* fix(code-storage): Pull in files from localStorage

* feat(Modern/React): Add Enzyme to test runner

This enables testing of React challenges

* feat(Modern): Add submission type

* refactor(Panes): Rename panes map update action
2017-11-29 17:44:51 -06:00

147 lines
3.4 KiB
JavaScript

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import Codemirror from 'react-codemirror';
import NoSSR from 'react-no-ssr';
import MouseTrap from 'mousetrap';
import ns from './ns.json';
import CodeMirrorSkeleton from '../../Code-Mirror-Skeleton.jsx';
import {
executeChallenge,
modernEditorUpdated,
challengeMetaSelector
} from '../../redux';
import { createFileSelector } from '../../../../files';
const envProps = typeof window !== 'undefined' ? Object.keys(window) : [];
const options = {
lint: {
esversion: 6,
predef: envProps
},
lineNumbers: true,
mode: 'javascript',
runnable: true,
matchBrackets: true,
autoCloseBrackets: true,
scrollbarStyle: 'null',
lineWrapping: true,
gutters: [ 'CodeMirror-lint-markers' ]
};
const mapStateToProps = createSelector(
createFileSelector((_, { fileKey }) => fileKey || ''),
challengeMetaSelector,
(
file,
{ mode }
) => ({
content: file.contents || '// Happy Coding!',
file: file,
mode: file.ext || mode || 'javascript'
})
);
const mapDispatchToProps = {
executeChallenge,
modernEditorUpdated
};
const propTypes = {
content: PropTypes.string,
executeChallenge: PropTypes.func.isRequired,
fileKey: PropTypes.string,
mode: PropTypes.string,
modernEditorUpdated: PropTypes.func.isRequired
};
export class Editor extends PureComponent {
createOptions = createSelector(
state => state.executeChallenge,
state => state.mode,
(executeChallenge, mode) => ({
...options,
mode,
// JSHint only works with javascript
// we will need to switch to eslint to make this work with jsx
lint: mode === 'javascript' ? options.lint : false,
extraKeys: {
Esc() {
document.activeElement.blur();
},
Tab(cm) {
if (cm.somethingSelected()) {
return cm.indentSelection('add');
}
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
return cm.replaceSelection(spaces);
},
'Shift-Tab': function(cm) {
return cm.indentSelection('subtract');
},
'Ctrl-Enter': function() {
executeChallenge();
return false;
},
'Cmd-Enter': function() {
executeChallenge();
return false;
},
'Ctrl-/': function(cm) {
cm.toggleComment();
},
'Cmd-/': function(cm) {
cm.toggleComment();
}
}
})
);
componentDidMount() {
MouseTrap.bind('e', () => {
this.refs.editor.focus();
}, 'keyup');
}
componentWillUnmount() {
MouseTrap.unbind('e', 'keyup');
}
render() {
const {
content,
modernEditorUpdated,
executeChallenge,
fileKey,
mode
} = this.props;
return (
<div
className={ `${ns}-editor` }
role='main'
>
<NoSSR onSSR={ <CodeMirrorSkeleton content={ content } /> }>
<Codemirror
onChange={ content => modernEditorUpdated(fileKey, content) }
options={ this.createOptions({ executeChallenge, mode }) }
ref='editor'
value={ content }
/>
</NoSSR>
</div>
);
}
}
Editor.displayName = 'Editor';
Editor.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Editor);