* 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
147 lines
3.4 KiB
JavaScript
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);
|