diff --git a/src/backend/tracers/js/worker.js b/src/backend/tracers/js/worker.js index 0b3ad35ea760a8c20c2ff45e30deda876f634020..06a841cc1431885620f7a2f28fe35ae3e555dae3 100644 --- a/src/backend/tracers/js/worker.js +++ b/src/backend/tracers/js/worker.js @@ -7,7 +7,7 @@ const sandbox = code => { eval(code); }; -onmessage = e => { // TODO: stop after the first delay() on the initial run +onmessage = e => { const lines = e.data.split('\n').map((line, i) => line.replace(/(.+\. *delay *)(\( *\))/g, `$1(${i})`)); const { code } = Babel.transform(lines.join('\n'), { presets: ['es2015'] }); sandbox(code); diff --git a/src/frontend/common/util.js b/src/frontend/common/util.js index bd5fa70da1d7c64dd602268660d172b136320702..a284690097fc45af18ce01a43feb42df7039146e 100644 --- a/src/frontend/common/util.js +++ b/src/frontend/common/util.js @@ -30,6 +30,14 @@ const createProjectFile = (name, content) => createFile(name, content, [{ const createUserFile = (name, content) => createFile(name, content, undefined); +const isSaved = ({ titles, files, lastTitles, lastFiles }) => { + const serialize = (titles, files) => JSON.stringify({ + titles, + files: files.map(({ name, content }) => ({ name, content })), + }); + return serialize(titles, files) === serialize(lastTitles, lastFiles); +}; + export { classes, distance, @@ -38,4 +46,5 @@ export { createFile, createProjectFile, createUserFile, + isSaved, }; diff --git a/src/frontend/components/App/index.jsx b/src/frontend/components/App/index.jsx index ed1e36d94ba1254636ee673d2f048daf7493fcd6..3358ea549bc6259e7eb58dc8f50bdd90ef508fa2 100644 --- a/src/frontend/components/App/index.jsx +++ b/src/frontend/components/App/index.jsx @@ -3,10 +3,7 @@ import Cookies from 'js-cookie'; import { connect } from 'react-redux'; import Promise from 'bluebird'; import { Helmet } from 'react-helmet'; -import AutosizeInput from 'react-input-autosize'; import queryString from 'query-string'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import faPlus from '@fortawesome/fontawesome-free-solid/faPlus'; import { BaseComponent, CodeEditor, @@ -32,7 +29,6 @@ class App extends BaseComponent { this.state = { navigatorOpened: true, workspaceWeights: [1, 2, 2], - editorTabIndex: -1, }; this.codeEditorRef = React.createRef(); @@ -79,11 +75,12 @@ class App extends BaseComponent { toggleHistoryBlock(enable = !this.unblock) { if (enable) { + const { saved } = this.props.current; const warningMessage = 'Are you sure you want to discard changes?'; - window.onbeforeunload = () => this.isSaved() ? undefined : warningMessage; + window.onbeforeunload = () => saved ? undefined : warningMessage; this.unblock = this.props.history.block((location) => { if (location.pathname === this.props.location.pathname) return; - if (!this.isSaved()) return warningMessage; + if (!saved) return warningMessage; }); } else { window.onbeforeunload = undefined; @@ -189,11 +186,11 @@ class App extends BaseComponent { selectDefaultTab() { const { ext } = this.props.env; const { files } = this.props.current; - let editorTabIndex = files.findIndex(file => extension(file.name) === 'json'); - if (!~editorTabIndex) files.findIndex(file => extension(file.name) === ext); - if (!~editorTabIndex) editorTabIndex = files.findIndex(file => exts.includes(extension(file.name))); - if (!~editorTabIndex) editorTabIndex = Math.min(0, files.length - 1); - this.handleChangeEditorTabIndex(editorTabIndex); + const editingFile = files.find(file => extension(file.name) === 'json') || + files.find(file => extension(file.name) === ext) || + files.find(file => exts.includes(extension(file.name))) || + files[files.length - 1]; + this.props.setEditingFile(editingFile); } handleChangeWorkspaceWeights(workspaceWeights) { @@ -201,67 +198,15 @@ class App extends BaseComponent { this.codeEditorRef.current.getWrappedInstance().handleResize(); } - handleChangeEditorTabIndex(editorTabIndex) { - const { files } = this.props.current; - if (editorTabIndex === files.length) this.handleAddFile(); - this.setState({ editorTabIndex }); - this.props.shouldBuild(); - } - - handleAddFile() { - const { ext } = this.props.env; - const { files } = this.props.current; - const language = languages.find(language => language.ext === ext); - const file = { ...language.skeleton }; - let count = 0; - while (files.some(existingFile => existingFile.name === file.name)) file.name = `code-${++count}.${ext}`; - this.props.addFile(file); - } - - handleRenameFile(e) { - const { value } = e.target; - const { editorTabIndex } = this.state; - this.props.renameFile(editorTabIndex, value); - } - - handleDeleteFile() { - const { editorTabIndex } = this.state; - const { files } = this.props.current; - this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2)); - this.props.deleteFile(editorTabIndex); - } - toggleNavigatorOpened(navigatorOpened = !this.state.navigatorOpened) { this.setState({ navigatorOpened }); } - isSaved() { - const { titles, files, lastTitles, lastFiles } = this.props.current; - const serialize = (titles, files) => JSON.stringify({ - titles, - files: files.map(({ name, content }) => ({ name, content })), - }); - return serialize(titles, files) === serialize(lastTitles, lastFiles); - } - render() { - const { navigatorOpened, workspaceWeights, editorTabIndex } = this.state; + const { navigatorOpened, workspaceWeights } = this.state; - const { files, titles, description } = this.props.current; - const saved = this.isSaved(); + const { titles, description, saved } = this.props.current; const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`; - const file = files[editorTabIndex]; - - const editorTitles = files.map(file => file.name); - if (file) { - editorTitles[editorTabIndex] = ( - e.stopPropagation()} onChange={e => this.handleRenameFile(e)} /> - ); - } - editorTitles.push( - , - ); return (
@@ -269,17 +214,16 @@ class App extends BaseComponent { {title} -
this.toggleNavigatorOpened()} saved={saved} - navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()} file={file} +
this.toggleNavigatorOpened()} + navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()} ignoreHistoryBlock={this.ignoreHistoryBlock} /> this.handleChangeWorkspaceWeights(weights)}> - this.handleChangeEditorTabIndex(tabIndex)}> - this.handleDeleteFile()} /> + + diff --git a/src/frontend/components/App/stylesheet.scss b/src/frontend/components/App/stylesheet.scss index 34d3b5a6b27dfd7e0ed71f1d36b533cffff42f6f..d001baf19f23e0d21bd8e6dd7d009cd1714aaa8f 100644 --- a/src/frontend/components/App/stylesheet.scss +++ b/src/frontend/components/App/stylesheet.scss @@ -60,16 +60,6 @@ button { } .editor_tab_container { - .input_title { - input { - &:hover, - &:focus { - margin: -4px; - padding: 4px; - background-color: $theme-normal; - } - } - } } } diff --git a/src/frontend/components/CodeEditor/index.jsx b/src/frontend/components/CodeEditor/index.jsx index c354fd2f9cba1712c0775cdfe3172fcff115923b..c50ad8bfc57907345dfcaae543a3e6e0341e463a 100644 --- a/src/frontend/components/CodeEditor/index.jsx +++ b/src/frontend/components/CodeEditor/index.jsx @@ -8,7 +8,7 @@ import { languages } from '/common/config'; import { Button, Ellipsis, FoldableAceEditor } from '/components'; import styles from './stylesheet.scss'; -@connect(({ env, player }) => ({ env, player }), actions, null, { withRef: true }) +@connect(({ current, env, player }) => ({ current, env, player }), actions, null, { withRef: true }) class CodeEditor extends React.Component { constructor(props) { super(props); @@ -16,24 +16,19 @@ class CodeEditor extends React.Component { this.aceEditorRef = React.createRef(); } - handleChangeCode(code) { - const { file } = this.props; - this.props.modifyFile({ ...file, content: code }); - if (extension(file.name) === 'md') this.props.shouldBuild(); - } - handleResize() { this.aceEditorRef.current.editor.resize(); } render() { - const { className, file, onClickDelete } = this.props; + const { className } = this.props; + const { editingFile } = this.props.current; const { user } = this.props.env; - const { lineIndicator, buildAt } = this.props.player; + const { lineIndicator } = this.props.player; - if (!file) return null; + if (!editingFile) return null; - const fileExt = extension(file.name); + const fileExt = extension(editingFile.name); const language = languages.find(language => language.ext === fileExt); const mode = language ? language.mode : fileExt === 'md' ? 'markdown' : @@ -49,7 +44,7 @@ class CodeEditor extends React.Component { theme="tomorrow_night_eighties" name="code_editor" editorProps={{ $blockScrolling: true }} - onChange={code => this.handleChangeCode(code)} + onChange={code => this.props.modifyFile(editingFile, code)} markers={lineIndicator ? [{ startRow: lineIndicator.lineNumber, startCol: 0, @@ -60,12 +55,11 @@ class CodeEditor extends React.Component { inFront: true, _key: lineIndicator.cursor, }] : []} - foldAt={buildAt} - value={file.content} /> + value={editingFile.content} />
Contributed by { - (file.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => ( + (editingFile.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => (
diff --git a/src/frontend/components/FoldableAceEditor/index.jsx b/src/frontend/components/FoldableAceEditor/index.jsx index e2d4145cb0d2fccd1a1f1ce672c718dd3c186d6d..5f9f0a9eec02737b4c89636854330d220307fcfe 100644 --- a/src/frontend/components/FoldableAceEditor/index.jsx +++ b/src/frontend/components/FoldableAceEditor/index.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import AceEditor from 'react-ace'; import 'brace/mode/plain_text'; import 'brace/mode/markdown'; @@ -8,19 +9,23 @@ import 'brace/mode/c_cpp'; import 'brace/mode/java'; import 'brace/theme/tomorrow_night_eighties'; import 'brace/ext/searchbox'; +import { actions } from '/reducers'; +@connect(({ current }) => ({ current }), actions) class FoldableAceEditor extends AceEditor { componentDidMount() { super.componentDidMount(); - this.foldTracers(); + const { shouldBuild } = this.props.current; + if (shouldBuild) this.foldTracers(); } componentDidUpdate(prevProps, prevState, snapshot) { super.componentDidUpdate(prevProps, prevState, snapshot); - if (prevProps.foldAt !== this.props.foldAt) { - this.foldTracers(); + const { editingFile, shouldBuild } = this.props.current; + if (editingFile !== prevProps.current.editingFile) { + if (shouldBuild) this.foldTracers(); } } diff --git a/src/frontend/components/Header/index.jsx b/src/frontend/components/Header/index.jsx index 5ce620ddd38449890decf606143dff1226348d05..618cba3562659e81f6111278ba71e43274a8a7fa 100644 --- a/src/frontend/components/Header/index.jsx +++ b/src/frontend/components/Header/index.jsx @@ -110,8 +110,8 @@ class Header extends BaseComponent { } render() { - const { className, onClickTitleBar, navigatorOpened, saved, file } = this.props; - const { scratchPaper, titles } = this.props.current; + const { className, onClickTitleBar, navigatorOpened } = this.props; + const { scratchPaper, titles, saved } = this.props.current; const { ext, user } = this.props.env; const permitted = this.hasPermission(); @@ -174,7 +174,7 @@ class Header extends BaseComponent {
- + ); diff --git a/src/frontend/components/Player/index.jsx b/src/frontend/components/Player/index.jsx index cb8bef723902d7683ee44ebfb68906b7d95e1c41..a9884725bdb7e8b4fe2e7a456e4f070b22ca1aed 100644 --- a/src/frontend/components/Player/index.jsx +++ b/src/frontend/components/Player/index.jsx @@ -13,7 +13,7 @@ import { actions } from '/reducers'; import { BaseComponent, Button, ProgressBar } from '/components'; import styles from './stylesheet.scss'; -@connect(({ player }) => ({ player }), actions) +@connect(({ current, player }) => ({ current, player }), actions) class Player extends BaseComponent { constructor(props) { super(props); @@ -30,15 +30,14 @@ class Player extends BaseComponent { } componentDidMount() { - const { file } = this.props; - this.build(file); + const { editingFile, shouldBuild } = this.props.current; + if (shouldBuild) this.build(editingFile); } componentWillReceiveProps(nextProps) { - const { file } = nextProps; - const { buildAt } = nextProps.player; - if (buildAt !== this.props.player.buildAt) { - this.build(file); + const { editingFile, shouldBuild } = nextProps.current; + if (editingFile !== this.props.current.editingFile) { + if (shouldBuild) this.build(editingFile); } } @@ -145,13 +144,15 @@ class Player extends BaseComponent { } render() { - const { className, file } = this.props; + const { className } = this.props; + const { editingFile } = this.props.current; const { chunks, cursor } = this.props.player; const { speed, playing, building } = this.state; return (
- { diff --git a/src/frontend/components/TabContainer/index.jsx b/src/frontend/components/TabContainer/index.jsx index 1298186db90b798f67f75592ec2bc0c334663dff..dc28ad76fe64cdb2e7c542532299e426a2164bcc 100644 --- a/src/frontend/components/TabContainer/index.jsx +++ b/src/frontend/components/TabContainer/index.jsx @@ -1,26 +1,50 @@ import React from 'react'; +import { connect } from 'react-redux'; +import AutosizeInput from 'react-input-autosize'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import faPlus from '@fortawesome/fontawesome-free-solid/faPlus'; import { classes } from '/common/util'; +import { actions } from '/reducers'; +import { languages } from '/common/config'; import styles from './stylesheet.scss'; +@connect(({ current, env }) => ({ current, env }), actions) class TabContainer extends React.Component { + handleAddFile() { + const { ext } = this.props.env; + const { files } = this.props.current; + const language = languages.find(language => language.ext === ext); + const newFile = { ...language.skeleton }; + let count = 0; + while (files.some(file => file.name === newFile.name)) newFile.name = `code-${++count}.${ext}`; + this.props.addFile(newFile); + } + render() { - const { className, children, titles, tabIndex, onChangeTabIndex } = this.props; + const { className, children } = this.props; + const { editingFile, files } = this.props.current; return (
{ - titles.map((title, i) => { - const selected = i === tabIndex; - return ( -
onChangeTabIndex(i)}> - {title} -
- ); - }) + files.map((file, i) => file === editingFile ? ( +
this.props.setEditingFile(file)}> + e.stopPropagation()} + onChange={e => this.props.renameFile(file, e.target.value)} /> +
+ ) : ( +
this.props.setEditingFile(file)}> + {file.name} +
+ )) } +
this.handleAddFile()}> + +
diff --git a/src/frontend/components/TabContainer/stylesheet.scss b/src/frontend/components/TabContainer/stylesheet.scss index ab7a368ee903b0588e186992bc4bc26de70e2401..a70b65c420297c4c4d28ba00d06634972eb9b1b7 100644 --- a/src/frontend/components/TabContainer/stylesheet.scss +++ b/src/frontend/components/TabContainer/stylesheet.scss @@ -25,6 +25,17 @@ margin: 0; border-bottom: 1px solid $theme-light; + .input_title { + input { + &:hover, + &:focus { + margin: -4px; + padding: 4px; + background-color: $theme-normal; + } + } + } + &.selected { border-left: 1px solid $theme-light; border-right: 1px solid $theme-light; @@ -56,4 +67,4 @@ background-color: $theme-dark; overflow: hidden; } -} \ No newline at end of file +} diff --git a/src/frontend/reducers/current.js b/src/frontend/reducers/current.js index 26af7f520dcea623a5d9c5ab5eee2ed9600fba7d..d0652472981d9020b8267d5a3518ab41421007c1 100644 --- a/src/frontend/reducers/current.js +++ b/src/frontend/reducers/current.js @@ -1,43 +1,34 @@ import { combineActions, createAction, handleActions } from 'redux-actions'; import { README_MD } from '/files'; +import { extension, isSaved } from '/common/util'; const prefix = 'CURRENT'; const setHome = createAction(`${prefix}/SET_HOME`, () => defaultState); -const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => { - const titles = [categoryName, algorithmName]; - return { - algorithm: { categoryKey, algorithmKey }, - scratchPaper: undefined, - titles, - files, - lastTitles: titles, - lastFiles: files, - description, - }; -}); -const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => { - const titles = ['Scratch Paper', title]; - return { - algorithm: undefined, - scratchPaper: { login, gistId }, - titles, - files, - lastTitles: titles, - lastFiles: files, - description: homeDescription, - }; -}); +const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => ({ + algorithm: { categoryKey, algorithmKey }, + titles: [categoryName, algorithmName], + files, + description, +})); +const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => ({ + scratchPaper: { login, gistId }, + titles: ['Scratch Paper', title], + files, + description: homeDescription, +})); +const setEditingFile = createAction(`${prefix}/SET_EDITING_FILE`, file => ({ file })); const modifyTitle = createAction(`${prefix}/MODIFY_TITLE`, title => ({ title })); const addFile = createAction(`${prefix}/ADD_FILE`, file => ({ file })); -const modifyFile = createAction(`${prefix}/MODIFY_FILE`, file => ({ file })); -const deleteFile = createAction(`${prefix}/DELETE_FILE`, index => ({ index })); -const renameFile = createAction(`${prefix}/RENAME_FILE`, (index, name) => ({ index, name })); +const renameFile = createAction(`${prefix}/RENAME_FILE`, (file, name) => ({ file, name })); +const modifyFile = createAction(`${prefix}/MODIFY_FILE`, (file, content) => ({ file, content })); +const deleteFile = createAction(`${prefix}/DELETE_FILE`, file => ({ file })); export const actions = { setHome, setAlgorithm, setScratchPaper, + setEditingFile, modifyTitle, addFile, modifyFile, @@ -59,6 +50,9 @@ const defaultState = { lastTitles: homeTitles, lastFiles: homeFiles, description: homeDescription, + editingFile: undefined, + shouldBuild: true, + saved: true, }; export default handleActions({ @@ -66,35 +60,85 @@ export default handleActions({ setHome, setAlgorithm, setScratchPaper, - )]: (state, { payload }) => ({ - ...state, - ...payload, - }), + )]: (state, { payload }) => { + const { algorithm, scratchPaper, titles, files, description } = payload; + return { + ...state, + algorithm, + scratchPaper, + titles, + files, + lastTitles: titles, + lastFiles: files, + description, + editingFile: undefined, + shouldBuild: true, + saved: true, + }; + }, + [setEditingFile]: (state, { payload }) => { + const { file } = payload; + return { + ...state, + editingFile: file, + shouldBuild: true, + }; + }, [modifyTitle]: (state, { payload }) => { const { title } = payload; - return { + const newState = { ...state, titles: [state.titles[0], title], }; + return { + ...newState, + saved: isSaved(newState), + }; }, [addFile]: (state, { payload }) => { const { file } = payload; - const files = [...state.files, file]; - return { ...state, files }; + const newState = { + ...state, + files: [...state.files, file], + editingFile: file, + shouldBuild: true, + }; + return { + ...newState, + saved: isSaved(newState), + }; }, - [modifyFile]: (state, { payload }) => { - const { file } = payload; - const files = state.files.map(oldFile => oldFile.name === file.name ? file : oldFile); - return { ...state, files }; + [combineActions( + renameFile, + modifyFile, + )]: (state, { payload }) => { + const { file, ...update } = payload; + const editingFile = { ...file, ...update }; + const newState = { + ...state, + files: state.files.map(oldFile => oldFile === file ? editingFile : oldFile), + editingFile, + shouldBuild: extension(editingFile.name) === 'md', + }; + return { + ...newState, + saved: isSaved(newState), + }; }, [deleteFile]: (state, { payload }) => { - const { index } = payload; - const files = state.files.filter((file, i) => i !== index); - return { ...state, files }; - }, - [renameFile]: (state, { payload }) => { - const { index, name } = payload; - const files = state.files.map((file, i) => i === index ? { ...file, name } : file); - return { ...state, files }; + const { file } = payload; + const index = state.files.indexOf(file); + const files = state.files.filter(oldFile => oldFile !== file); + const editingFile = files[Math.min(index, files.length - 1)]; + const newState = { + ...state, + files, + editingFile, + shouldBuild: true, + }; + return { + ...newState, + saved: isSaved(newState), + }; }, }, defaultState); diff --git a/src/frontend/reducers/player.js b/src/frontend/reducers/player.js index ce37dd1d269dbe96966057857529bc8c7503096d..370d04ea6e9946ce66fb0f3d59fc9581eb8f8166 100644 --- a/src/frontend/reducers/player.js +++ b/src/frontend/reducers/player.js @@ -2,20 +2,17 @@ import { combineActions, createAction, handleActions } from 'redux-actions'; const prefix = 'PLAYER'; -const shouldBuild = createAction(`${prefix}/SHOULD_BUILD`, () => ({ buildAt: Date.now() })); const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks })); const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor })); const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator })); export const actions = { - shouldBuild, setChunks, setCursor, setLineIndicator, }; const defaultState = { - buildAt: 0, chunks: [], cursor: 0, lineIndicator: undefined, @@ -23,7 +20,6 @@ const defaultState = { export default handleActions({ [combineActions( - shouldBuild, setChunks, setCursor, setLineIndicator,