Initial preview rendering
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"mocha": true,
|
||||
"node": true
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ public/js/main*
|
||||
public/js/commonFramework*
|
||||
public/js/sandbox*
|
||||
public/js/iFrameScripts*
|
||||
public/js/frame-runner*
|
||||
public/js/plugin*
|
||||
public/js/vendor*
|
||||
public/js/faux*
|
||||
|
70
client/frame-runner.js
Normal file
70
client/frame-runner.js
Normal file
@@ -0,0 +1,70 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var common = parent.__common;
|
||||
var Rx = parent.Rx;
|
||||
|
||||
common.getJsOutput = function evalJs(source = '') {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
return window.__err || 'source disabled';
|
||||
}
|
||||
let output;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
output = eval(source);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
window.__err = e;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
common.runTests$ = function runTests$({ tests = [], source }) {
|
||||
const editor = { getValue() { return source; } };
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
|
||||
// Iterate through the test one at a time
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(100)
|
||||
.map(({ text, testString }) => {
|
||||
const newTest = { text, testString };
|
||||
let test;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
test = eval(testString);
|
||||
/* eslint-enable no-eval */
|
||||
if (typeof test === 'function') {
|
||||
// maybe sync/promise/observable
|
||||
if (test.length === 0) {
|
||||
test();
|
||||
}
|
||||
// callback test
|
||||
if (test.length === 1) {
|
||||
console.log('callback test');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
newTest.err = e.message.split(':').shift();
|
||||
}
|
||||
return newTest;
|
||||
})
|
||||
// gather tests back into an array
|
||||
.toArray();
|
||||
};
|
||||
|
||||
// used when updating preview without running tests
|
||||
common.checkPreview$ = function checkPreview$(args) {
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
return Rx.Observable.just(args);
|
||||
};
|
||||
|
||||
// now that the runPreviewTest$ is defined
|
||||
// we set the subject to true
|
||||
// this will let the updatePreview
|
||||
// script now that we are ready.
|
||||
common.testFrameReady$.onNext(true);
|
||||
});
|
@@ -1,23 +1,15 @@
|
||||
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
|
||||
// the $ on the iframe window object is the same
|
||||
// as the one used on the main site, but
|
||||
// uses the iframe document as the context
|
||||
window.$(document).ready(function() {
|
||||
var _ = parent._;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var common = parent.__common;
|
||||
var Rx = parent.Rx;
|
||||
var chai = parent.chai;
|
||||
var assert = chai.assert;
|
||||
var tests = parent.tests;
|
||||
var common = parent.common;
|
||||
|
||||
common.getJsOutput = function evalJs(code = '') {
|
||||
common.getJsOutput = function evalJs(source = '') {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
return window.__err || 'code disabled';
|
||||
return window.__err || 'source disabled';
|
||||
}
|
||||
let output;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
output = eval(code);
|
||||
output = eval(source);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
window.__err = e;
|
||||
@@ -25,49 +17,42 @@ window.$(document).ready(function() {
|
||||
return output;
|
||||
};
|
||||
|
||||
common.runPreviewTests$ =
|
||||
function runPreviewTests$({
|
||||
tests = [],
|
||||
originalCode,
|
||||
...rest
|
||||
}) {
|
||||
const code = originalCode;
|
||||
const editor = { getValue() { return originalCode; } };
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
common.runTests$ = function runTests$({ tests = [], source }) {
|
||||
const editor = { getValue() { return source; } };
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
|
||||
// Iterate throught the test one at a time
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(100)
|
||||
.map(test => {
|
||||
const userTest = {};
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
eval(test);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
userTest.err = e.message.split(':').shift();
|
||||
} finally {
|
||||
if (!test.match(/message: /g)) {
|
||||
// assumes test does not contain arrays
|
||||
// This is a patch until all test fall into this pattern
|
||||
userTest.text = test
|
||||
.split(',')
|
||||
.pop();
|
||||
userTest.text = 'message: ' + userTest.text + '\');';
|
||||
} else {
|
||||
userTest.text = test;
|
||||
// Iterate through the test one at a time
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(100)
|
||||
.map(({ text, testString }) => {
|
||||
const newTest = { text, testString };
|
||||
let test;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
test = eval(testString);
|
||||
/* eslint-enable no-eval */
|
||||
if (typeof test === 'function') {
|
||||
// maybe sync/promise/observable
|
||||
if (test.length === 0) {
|
||||
test();
|
||||
}
|
||||
// callback test
|
||||
if (test.length === 1) {
|
||||
console.log('callback test');
|
||||
}
|
||||
}
|
||||
return userTest;
|
||||
})
|
||||
// gather tests back into an array
|
||||
.toArray()
|
||||
.map(tests => ({ ...rest, tests, originalCode }));
|
||||
};
|
||||
} catch (e) {
|
||||
newTest.err = e.message.split(':').shift();
|
||||
}
|
||||
return newTest;
|
||||
})
|
||||
// gather tests back into an array
|
||||
.toArray();
|
||||
};
|
||||
|
||||
// used when updating preview without running tests
|
||||
common.checkPreview$ = function checkPreview$(args) {
|
||||
@@ -81,5 +66,5 @@ window.$(document).ready(function() {
|
||||
// we set the subject to true
|
||||
// this will let the updatePreview
|
||||
// script now that we are ready.
|
||||
common.previewReady$.onNext(true);
|
||||
common.testFrameReady$.onNext(true);
|
||||
});
|
||||
|
@@ -51,7 +51,7 @@ createApp({
|
||||
serviceOptions,
|
||||
initialState,
|
||||
middlewares: [ routingMiddleware ],
|
||||
sagas,
|
||||
sagas: [...sagas ],
|
||||
sagaOptions,
|
||||
reducers: { routing },
|
||||
enhancers: [ devTools ]
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import loopProtect from 'loopProtect';
|
||||
|
||||
loopProtect.hit = function hit(line) {
|
||||
var err = 'Error: Exiting potential infinite loop at line ' +
|
||||
line +
|
||||
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
|
||||
'line. Beware that if you do have an infinite loop in your code' +
|
||||
'this will crash your browser.';
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
|
||||
export default function addLoopProtect() {
|
||||
const source = this;
|
||||
return source.map(files$ => files$.map(file => {
|
||||
if (file.extname === 'js') {
|
||||
file.contents = loopProtect(file.contents);
|
||||
}
|
||||
return file;
|
||||
}));
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import createTypes from '../../common/app/utils/create-types';
|
||||
const filterTypes = [
|
||||
execute
|
||||
];
|
||||
export default function executeChallengeSaga(action$, getState) {
|
||||
return action$
|
||||
.filter(({ type }) => filterTypes.some(_type => _type === type))
|
||||
.map(action => {
|
||||
if (action.type === execute) {
|
||||
const editors = getState().editors;
|
||||
}
|
||||
})
|
||||
}
|
@@ -1,14 +1,14 @@
|
||||
import { helpers, Observable } from 'rx';
|
||||
|
||||
const throwForJsHtml = {
|
||||
extname: /js|html/,
|
||||
ext: /js|html/,
|
||||
throwers: [
|
||||
{
|
||||
name: 'multiline-comment',
|
||||
description: 'Detect if a JS multi-line comment is left open',
|
||||
thrower: function checkForComments({ content }) {
|
||||
const openingComments = content.match(/\/\*/gi);
|
||||
const closingComments = content.match(/\*\//gi);
|
||||
thrower: function checkForComments({ contents }) {
|
||||
const openingComments = contents.match(/\/\*/gi);
|
||||
const closingComments = contents.match(/\*\//gi);
|
||||
if (
|
||||
openingComments &&
|
||||
(!closingComments || openingComments.length > closingComments.length)
|
||||
@@ -20,8 +20,8 @@ const throwForJsHtml = {
|
||||
name: 'nested-jQuery',
|
||||
description: 'Nested dollar sign calls breaks browsers',
|
||||
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
|
||||
thrower: function checkForNestedJquery({ content }) {
|
||||
if (content.match(this.detectUnsafeJQ)) {
|
||||
thrower: function checkForNestedJquery({ contents }) {
|
||||
if (contents.match(this.detectUnsafeJQ)) {
|
||||
throw new Error('Unsafe $($)');
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,10 @@ const throwForJsHtml = {
|
||||
name: 'unfinished-function',
|
||||
description: 'lonely function keywords breaks browsers',
|
||||
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
|
||||
thower: function checkForUnfinishedFunction({ content: code }) {
|
||||
thrower: function checkForUnfinishedFunction({ contents }) {
|
||||
if (
|
||||
code.match(/function/g) &&
|
||||
!code.match(this.detectFunctionCall)
|
||||
contents.match(/function/g) &&
|
||||
!contents.match(this.detectFunctionCall)
|
||||
) {
|
||||
throw new Error(
|
||||
'SyntaxError: Unsafe or unfinished function declaration'
|
||||
@@ -43,8 +43,8 @@ const throwForJsHtml = {
|
||||
name: 'unsafe console call',
|
||||
description: 'console call stops tests scripts from running',
|
||||
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
|
||||
thrower: function checkForUnsafeConsole({ content }) {
|
||||
if (content.match(this.detectUnsafeConsoleCall)) {
|
||||
thrower: function checkForUnsafeConsole({ contents }) {
|
||||
if (contents.match(this.detectUnsafeConsoleCall)) {
|
||||
throw new Error('Invalid if (null) console.log(1); detected');
|
||||
}
|
||||
}
|
||||
@@ -52,17 +52,17 @@ const throwForJsHtml = {
|
||||
]
|
||||
};
|
||||
|
||||
export default function pretester() {
|
||||
export default function throwers() {
|
||||
const source = this;
|
||||
return source.map(file$ => file$.flatMap(file => {
|
||||
if (!throwForJsHtml.extname.test(file.extname)) {
|
||||
if (!throwForJsHtml.ext.test(file.ext)) {
|
||||
return Observable.just(file);
|
||||
}
|
||||
return Observable.from(throwForJsHtml.throwers)
|
||||
.flatMap(({ thrower }) => {
|
||||
.flatMap(context => {
|
||||
try {
|
||||
let finalObs;
|
||||
const maybeObservableOrPromise = thrower(file);
|
||||
const maybeObservableOrPromise = context.thrower(file);
|
||||
if (helpers.isPromise(maybeObservableOrPromise)) {
|
||||
finalObs = Observable.fromPromise(maybeObservableOrPromise);
|
||||
} else if (Observable.isObservable(maybeObservableOrPromise)) {
|
37
client/new-framework/transformers.js
Normal file
37
client/new-framework/transformers.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Observable } from 'rx';
|
||||
import loopProtect from 'loop-protect';
|
||||
|
||||
loopProtect.hit = function hit(line) {
|
||||
var err = 'Error: Exiting potential infinite loop at line ' +
|
||||
line +
|
||||
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
|
||||
'line. Beware that if you do have an infinite loop in your code' +
|
||||
'this will crash your browser.';
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
const transformersForHtmlJS = {
|
||||
ext: /html|js/,
|
||||
transformers: [
|
||||
{
|
||||
name: 'add-loop-protect',
|
||||
transformer: function addLoopProtect(file) {
|
||||
file.contents = loopProtect(file.contents);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
|
||||
export default function transformers() {
|
||||
const source = this;
|
||||
return source.map(files$ => files$.flatMap(file => {
|
||||
if (!transformersForHtmlJS.ext.test(file.ext)) {
|
||||
return Observable.just(file);
|
||||
}
|
||||
return Observable.from(transformersForHtmlJS.transformers)
|
||||
.reduce((file, context) => context.transformer(file), file);
|
||||
}));
|
||||
}
|
98
client/sagas/execute-challenge-saga.js
Normal file
98
client/sagas/execute-challenge-saga.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { ajax$ } from '../../common/utils/ajax-stream';
|
||||
import throwers from '../new-framework/throwers';
|
||||
import transformers from '../new-framework/transformers';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
frameMain
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
import { setExt, updateContents } from '../../common/utils/polyvinyl';
|
||||
|
||||
// createFileStream(files: Dictionary[Path, PolyVinyl]) =>
|
||||
// Observable[...Observable[...PolyVinyl]]
|
||||
function createFileStream(files = {}) {
|
||||
return Observable.just(
|
||||
Observable.from(Object.keys(files)).map(key => files[key])
|
||||
);
|
||||
}
|
||||
|
||||
const jQuery = {
|
||||
src: '/bower_components/jquery/dist/jquery.js',
|
||||
script: true,
|
||||
type: 'global'
|
||||
};
|
||||
|
||||
const scriptCache = new Map();
|
||||
|
||||
function cacheScript({ src } = {}) {
|
||||
if (!src) {
|
||||
return Observable.throw(new Error('No source provided for script'));
|
||||
}
|
||||
if (scriptCache.has(src)) {
|
||||
return scriptCache.get(src);
|
||||
}
|
||||
const script$ = ajax$(src)
|
||||
.doOnNext(res => {
|
||||
console.log('status', res.status);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Request errror: ' + res.status);
|
||||
}
|
||||
})
|
||||
.map(({ response }) => response)
|
||||
.map(script => `<script>${script}</script>`)
|
||||
.catch(e => (console.error(e), Observable.just('')))
|
||||
.shareReplay();
|
||||
|
||||
scriptCache.set(src, script$);
|
||||
return script$;
|
||||
}
|
||||
|
||||
const frameRunner$ = cacheScript({ src: '/js/frame-runner.js' });
|
||||
|
||||
const htmlCatch = '\n<!-- -->';
|
||||
const jsCatch = '\n;/* */';
|
||||
|
||||
export default function executeChallengeSaga(action$, getState) {
|
||||
return action$
|
||||
.filter(({ type }) => type === types.executeChallenge)
|
||||
.debounce(750)
|
||||
.flatMapLatest(() => {
|
||||
const { files, required = [ jQuery ] } = getState().challengesApp;
|
||||
return createFileStream(files)
|
||||
::throwers()
|
||||
::transformers()
|
||||
// createbuild
|
||||
.flatMap(file$ => file$.reduce((build, file) => {
|
||||
let finalFile;
|
||||
if (file.ext === 'js') {
|
||||
finalFile = setExt('html', updateContents(
|
||||
`<script>${file.contents}${jsCatch}</script>`,
|
||||
file
|
||||
));
|
||||
} else if (file.ext === 'css') {
|
||||
finalFile = setExt('html', updateContents(
|
||||
`<style>${file.contents}</style>`,
|
||||
file
|
||||
));
|
||||
} else {
|
||||
finalFile = file;
|
||||
}
|
||||
return build + finalFile.contents + htmlCatch;
|
||||
}, ''))
|
||||
// add required scripts and links here
|
||||
.flatMap(build => {
|
||||
const header$ = Observable.from(required)
|
||||
.flatMap(required => {
|
||||
if (required.script) {
|
||||
return cacheScript(required);
|
||||
}
|
||||
return Observable.just('');
|
||||
})
|
||||
.reduce((header, required) => header + required, '');
|
||||
return Observable.combineLatest(header$, frameRunner$)
|
||||
.map(([ header, frameRunner ]) => header + build + frameRunner);
|
||||
})
|
||||
.map(build => frameMain(build));
|
||||
});
|
||||
}
|
51
client/sagas/frame-saga.js
Normal file
51
client/sagas/frame-saga.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BehaviorSubject } from 'rx';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
|
||||
// we use three different frames to make them all essentially pure functions
|
||||
const mainId = 'fcc-main-frame';
|
||||
/*
|
||||
const outputId = 'fcc-output-frame';
|
||||
const testId = 'fcc-test-frame';
|
||||
*/
|
||||
|
||||
function createFrame(document, id = mainId) {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = id;
|
||||
frame.setAttribute('style', 'display: none');
|
||||
document.body.appendChild(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
function refreshFrame(frame) {
|
||||
frame.src = 'about:blank';
|
||||
return frame;
|
||||
}
|
||||
|
||||
function getFrameDocument(document, id = mainId) {
|
||||
let frame = document.getElementById(id);
|
||||
if (!frame) {
|
||||
frame = createFrame(document, id);
|
||||
} else {
|
||||
refreshFrame(frame);
|
||||
}
|
||||
return frame.contentDocument || frame.contentWindow.document;
|
||||
}
|
||||
|
||||
function frameMain(build, document) {
|
||||
const main = getFrameDocument(document);
|
||||
main.open();
|
||||
main.write(build);
|
||||
main.close();
|
||||
}
|
||||
|
||||
export default function frameSaga(actions$, getState, { window, document }) {
|
||||
window.__common = {};
|
||||
window.__common.outputFrameReady$ = new BehaviorSubject(false);
|
||||
window.__common.testFrameReady$ = new BehaviorSubject(false);
|
||||
return actions$
|
||||
.filter(({ type }) => type === types.frameMain)
|
||||
.map(action => {
|
||||
frameMain(action.payload, document);
|
||||
return null;
|
||||
});
|
||||
}
|
@@ -3,11 +3,15 @@ import titleSaga from './title-saga';
|
||||
import localStorageSaga from './local-storage-saga';
|
||||
import hardGoToSaga from './hard-go-to-saga';
|
||||
import windowSaga from './window-saga';
|
||||
import executeChallengeSaga from './execute-challenge-saga';
|
||||
import frameSaga from './frame-saga';
|
||||
|
||||
export default [
|
||||
errSaga,
|
||||
titleSaga,
|
||||
localStorageSaga,
|
||||
hardGoToSaga,
|
||||
windowSaga
|
||||
windowSaga,
|
||||
executeChallengeSaga,
|
||||
frameSaga
|
||||
];
|
||||
|
@@ -8,23 +8,23 @@ import Editor from './Editor.jsx';
|
||||
import SidePanel from './Side-Panel.jsx';
|
||||
import Preview from './Preview.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import { updateFile } from '../../redux/actions';
|
||||
import { executeChallenge, updateFile } from '../../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
state => state.challengesApp.tests,
|
||||
state => state.challengesApp.files,
|
||||
state => state.challengesApp.path,
|
||||
({ challenge, showPreview, mode }, tests, files = {}, path = '') => ({
|
||||
content: files[path] && files[path].contents || '',
|
||||
file: files[path],
|
||||
state => state.challengesApp.key,
|
||||
({ challenge, showPreview, mode }, tests, files = {}, key = '') => ({
|
||||
content: files[key] && files[key].contents || '',
|
||||
file: files[key],
|
||||
showPreview,
|
||||
mode,
|
||||
tests
|
||||
})
|
||||
);
|
||||
|
||||
const bindableActions = { updateFile };
|
||||
const bindableActions = { executeChallenge, updateFile };
|
||||
|
||||
export class Challenge extends PureComponent {
|
||||
static displayName = 'Challenge';
|
||||
@@ -32,7 +32,9 @@ export class Challenge extends PureComponent {
|
||||
static propTypes = {
|
||||
showPreview: PropTypes.bool,
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
mode: PropTypes.string,
|
||||
updateFile: PropTypes.func,
|
||||
executeChallenge: PropTypes.func
|
||||
};
|
||||
|
||||
renderPreview(showPreview) {
|
||||
@@ -54,7 +56,8 @@ export class Challenge extends PureComponent {
|
||||
updateFile,
|
||||
file,
|
||||
mode,
|
||||
showPreview
|
||||
showPreview,
|
||||
executeChallenge
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
@@ -68,6 +71,7 @@ export class Challenge extends PureComponent {
|
||||
md={ showPreview ? 5 : 8 }>
|
||||
<Editor
|
||||
content={ content }
|
||||
executeChallenge={ executeChallenge }
|
||||
mode={ mode }
|
||||
updateFile={ content => updateFile(content, file) }
|
||||
/>
|
||||
|
@@ -36,6 +36,7 @@ export class Editor extends PureComponent {
|
||||
}
|
||||
static displayName = 'Editor';
|
||||
static propTypes = {
|
||||
executeChallenge: PropTypes.func,
|
||||
height: PropTypes.number,
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
@@ -47,6 +48,40 @@ export class Editor extends PureComponent {
|
||||
mode: 'javascript'
|
||||
};
|
||||
|
||||
createOptions = createSelector(
|
||||
state => state.options,
|
||||
state => state.executeChallenge,
|
||||
state => state.mode,
|
||||
(options, executeChallenge, mode) => ({
|
||||
...options,
|
||||
mode,
|
||||
extraKeys: {
|
||||
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) {
|
||||
if (cm.somethingSelected()) {
|
||||
return cm.indentSelection('subtract');
|
||||
}
|
||||
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||
return cm.replaceSelection(spaces);
|
||||
},
|
||||
'Ctrl-Enter': function() {
|
||||
executeChallenge();
|
||||
return false;
|
||||
},
|
||||
'Cmd-Enter': function() {
|
||||
executeChallenge();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
const { updateFile = (() => {}) } = this.props;
|
||||
this._subscription = this._editorContent$
|
||||
@@ -72,7 +107,7 @@ export class Editor extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, height, mode } = this.props;
|
||||
const { executeChallenge, content, height, mode } = this.props;
|
||||
const style = {};
|
||||
if (height) {
|
||||
style.height = height + 'px';
|
||||
@@ -84,7 +119,7 @@ export class Editor extends PureComponent {
|
||||
<NoSSR>
|
||||
<Codemirror
|
||||
onChange={ this.handleChange }
|
||||
options={{ ...options, mode }}
|
||||
options={ this.createOptions({ executeChallenge, mode, options }) }
|
||||
value={ content } />
|
||||
</NoSSR>
|
||||
</div>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
const mainId = 'fcc-main-frame';
|
||||
export default class extends PureComponent {
|
||||
static displayName = 'Preview';
|
||||
|
||||
@@ -14,7 +15,7 @@ export default class extends PureComponent {
|
||||
</div>
|
||||
<iframe
|
||||
className='iphone iframe-scroll'
|
||||
id='preview' />
|
||||
id={ mainId } />
|
||||
<div className='spacer' />
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,17 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
export default class extends PureComponent {
|
||||
import { executeChallenge } from '../../redux/actions';
|
||||
|
||||
const bindableActions = { executeChallenge };
|
||||
|
||||
export class ToolPanel extends PureComponent {
|
||||
static displayName = 'ToolPanel';
|
||||
|
||||
static propTypes = {
|
||||
executeChallenge: PropTypes.func
|
||||
};
|
||||
|
||||
render() {
|
||||
const { executeChallenge } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'>
|
||||
className='btn-big'
|
||||
onClick={ executeChallenge }>
|
||||
Run tests (ctrl + enter)
|
||||
</Button>
|
||||
<div className='button-spacer' />
|
||||
@@ -42,3 +53,5 @@ export default class extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, bindableActions)(ToolPanel);
|
||||
|
@@ -39,4 +39,11 @@ export const updateFile = createAction(
|
||||
types.updateFile,
|
||||
(content, file) => updateContents(content, file)
|
||||
);
|
||||
|
||||
export const updateFiles = createAction(types.updateFiles);
|
||||
|
||||
// rechallenge
|
||||
export const executeChallenge = createAction(types.executeChallenge);
|
||||
export const frameMain = createAction(types.frameMain);
|
||||
export const frameOutput = createAction(types.frameOutput);
|
||||
export const frameTests = createAction(types.frameTests);
|
||||
|
@@ -3,7 +3,13 @@ import { createPoly } from '../../../../utils/polyvinyl';
|
||||
|
||||
import types from './types';
|
||||
import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
|
||||
import { arrayToString, buildSeed, createTests, getPath } from '../utils';
|
||||
import {
|
||||
arrayToString,
|
||||
buildSeed,
|
||||
createTests,
|
||||
getPreFile,
|
||||
getFileKey
|
||||
} from '../utils';
|
||||
|
||||
const initialState = {
|
||||
challenge: '',
|
||||
@@ -24,7 +30,7 @@ const mainReducer = handleActions(
|
||||
...state,
|
||||
refresh: true,
|
||||
challenge: challenge.dashedName,
|
||||
path: getPath(challenge),
|
||||
key: getFileKey(challenge),
|
||||
tests: createTests(challenge)
|
||||
}),
|
||||
|
||||
@@ -57,12 +63,12 @@ const filesReducer = handleActions(
|
||||
{
|
||||
[types.updateFile]: (state, { payload: file }) => ({
|
||||
...state,
|
||||
[file.path]: file
|
||||
[file.key]: file
|
||||
}),
|
||||
[types.updateFiles]: (state, { payload: files }) => {
|
||||
return files
|
||||
.reduce((files, file) => {
|
||||
files[file.path] = file;
|
||||
files[file.key] = file;
|
||||
return files;
|
||||
}, { ...state });
|
||||
},
|
||||
@@ -77,11 +83,11 @@ const filesReducer = handleActions(
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
const path = getPath(challenge);
|
||||
const preFile = getPreFile(challenge);
|
||||
return {
|
||||
...state,
|
||||
[path]: createPoly({
|
||||
path,
|
||||
[preFile.key]: createPoly({
|
||||
...preFile,
|
||||
contents: buildSeed(challenge),
|
||||
head: arrayToString(challenge.head),
|
||||
tail: arrayToString(challenge.tail)
|
||||
|
@@ -17,5 +17,11 @@ export default createTypes([
|
||||
|
||||
// files
|
||||
'updateFile',
|
||||
'updateFiles'
|
||||
'updateFiles',
|
||||
|
||||
// rechallenge
|
||||
'executeChallenge',
|
||||
'frameMain',
|
||||
'frameOutput',
|
||||
'frameTests'
|
||||
], 'challenges');
|
||||
|
@@ -40,13 +40,21 @@ export function buildSeed({ challengeSeed = [] } = {}) {
|
||||
}
|
||||
|
||||
const pathsMap = {
|
||||
[HTML]: 'main.html',
|
||||
[JS]: 'main.js',
|
||||
[BONFIRE]: 'main.js'
|
||||
[HTML]: 'html',
|
||||
[JS]: 'js',
|
||||
[BONFIRE]: 'js'
|
||||
};
|
||||
|
||||
export function getPath({ challengeType }) {
|
||||
return pathsMap[challengeType] || 'main';
|
||||
export function getPreFile({ challengeType }) {
|
||||
return {
|
||||
name: 'index',
|
||||
ext: pathsMap[challengeType] || 'html',
|
||||
key: getFileKey({ challengeType })
|
||||
};
|
||||
}
|
||||
|
||||
export function getFileKey({ challengeType }) {
|
||||
return 'index' + (pathsMap[challengeType] || 'html');
|
||||
}
|
||||
|
||||
export function createTests({ tests = [] }) {
|
||||
|
@@ -1,25 +1,41 @@
|
||||
// originally base off of https://github.com/gulpjs/vinyl
|
||||
import path from 'path';
|
||||
import replaceExt from 'replace-ext';
|
||||
import invariant from 'invariant';
|
||||
|
||||
// interface PolyVinyl {
|
||||
// contents: String,
|
||||
// name: String,
|
||||
// ext: String,
|
||||
// path: String,
|
||||
// key: String,
|
||||
// head: String,
|
||||
// tail: String,
|
||||
// history: [...String],
|
||||
// error: Null|Object
|
||||
// }
|
||||
//
|
||||
// createPoly({
|
||||
// path: String,
|
||||
// name: String,
|
||||
// ext: String,
|
||||
// contents: String,
|
||||
// history?: [...String],
|
||||
// }) => PolyVinyl, throws
|
||||
export function createPoly({ path, contents, history, ...rest } = {}) {
|
||||
export function createPoly({
|
||||
name,
|
||||
ext,
|
||||
contents,
|
||||
history,
|
||||
...rest
|
||||
} = {}) {
|
||||
invariant(
|
||||
typeof path === 'string',
|
||||
'path must be a string but got %s',
|
||||
path
|
||||
typeof name === 'string',
|
||||
'name must be a string but got %s',
|
||||
name
|
||||
);
|
||||
|
||||
invariant(
|
||||
typeof ext === 'string',
|
||||
'ext must be a string, but was %s',
|
||||
ext
|
||||
);
|
||||
|
||||
invariant(
|
||||
@@ -30,9 +46,12 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
|
||||
|
||||
return {
|
||||
...rest,
|
||||
history: Array.isArray(history) ? history : [ path ],
|
||||
path: path,
|
||||
contents: contents,
|
||||
history: Array.isArray(history) ? history : [ name + ext ],
|
||||
name,
|
||||
ext,
|
||||
path: name + '.' + ext,
|
||||
key: name + ext,
|
||||
contents,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
@@ -41,7 +60,8 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
|
||||
export function isPoly(poly) {
|
||||
return poly &&
|
||||
typeof poly.contents === 'string' &&
|
||||
typeof poly.path === 'string' &&
|
||||
typeof poly.name === 'string' &&
|
||||
typeof poly.ext === 'string' &&
|
||||
Array.isArray(poly.history);
|
||||
}
|
||||
|
||||
@@ -69,23 +89,25 @@ export function updateContents(contents, poly) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getExt(poly) {
|
||||
export function setExt(ext, poly) {
|
||||
checkPoly(poly);
|
||||
invariant(
|
||||
!!poly.path,
|
||||
'No path specified! Can not get extname'
|
||||
);
|
||||
return path.extname(poly.path);
|
||||
}
|
||||
|
||||
export function setExt(extname, poly) {
|
||||
invariant(
|
||||
poly.path,
|
||||
'No path specified! Can not set extname',
|
||||
);
|
||||
const newPoly = {
|
||||
...poly,
|
||||
path: replaceExt(this.path, extname)
|
||||
ext,
|
||||
path: poly.name + '.' + ext,
|
||||
key: poly.name + ext
|
||||
};
|
||||
newPoly.history = [ ...poly.history, newPoly.path ];
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
export function setName(name, poly) {
|
||||
checkPoly(poly);
|
||||
const newPoly = {
|
||||
...poly,
|
||||
name,
|
||||
path: name + '.' + poly.ext,
|
||||
key: name + poly.ext
|
||||
};
|
||||
newPoly.history = [ ...poly.history, newPoly.path ];
|
||||
return newPoly;
|
||||
|
@@ -113,6 +113,7 @@ var paths = {
|
||||
js: [
|
||||
'client/main.js',
|
||||
'client/iFrameScripts.js',
|
||||
'client/frame-runner.js',
|
||||
'client/plugin.js'
|
||||
],
|
||||
|
||||
|
@@ -98,7 +98,6 @@
|
||||
"redux-actions": "^0.9.1",
|
||||
"redux-epic": "^0.1.1",
|
||||
"redux-form": "^5.2.3",
|
||||
"replace-ext": "0.0.1",
|
||||
"request": "^2.65.0",
|
||||
"reselect": "^2.0.2",
|
||||
"rx": "^4.0.0",
|
||||
|
@@ -39,7 +39,8 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
externals: {
|
||||
codemirror: 'CodeMirror'
|
||||
codemirror: 'CodeMirror',
|
||||
'loop-protect': 'loopProtect'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.DedupePlugin(),
|
||||
|
Reference in New Issue
Block a user