diff --git a/config/webpack.config.js b/config/webpack.config.js index 691db3b9f1c..c8e75f0dc95 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -54,7 +54,6 @@ const config = { target: 'web', - mainFields: ['browser', 'module', 'jsnext:main', 'main'], output: getOutput(), @@ -138,11 +137,16 @@ const config = { ], }, resolve: { + mainFields: ['browser', 'module', 'jsnext:main', 'main'], modules: [ 'node_modules', ], extensions: ['.js', '.json'], + + alias: { + moment: 'moment/moment.js', + }, }, plugins: [ diff --git a/src/app/components/buttons/Button.js b/src/app/components/buttons/Button.js index bf14ce75e09..d1e7a58197b 100644 --- a/src/app/components/buttons/Button.js +++ b/src/app/components/buttons/Button.js @@ -6,6 +6,7 @@ const styles = props => ` transition: 0.3s ease all; text-transform: uppercase; text-decoration: none; + line-height: 1; background-color: ${props.disabled ? props.theme.background2.darken(0.1)() : props.theme.secondary()}; color: ${props.disabled ? props.theme.background2.lighten(1.5)() : 'white'}; ${(() => { diff --git a/src/app/pages/SandboxView/Editor/Content/View/EditorPreview.js b/src/app/pages/SandboxView/Editor/Content/View/EditorPreview.js index 285592b4b04..b5202ff5ffa 100644 --- a/src/app/pages/SandboxView/Editor/Content/View/EditorPreview.js +++ b/src/app/pages/SandboxView/Editor/Content/View/EditorPreview.js @@ -13,7 +13,7 @@ import type { ModuleTab } from '../../../../../store/reducers/views/sandbox'; import CodeEditor from './subviews/CodeEditor'; import Preview from './subviews/Preview'; import { directoriesBySandboxSelector } from '../../../../../store/entities/directories/selector'; -import { modulesBySandboxSelector, singleModuleSelector } from '../../../../../store/entities/modules/selector'; +import { modulesBySandboxSelector, singleModuleSelector, modulePathSelector } from '../../../../../store/entities/modules/selector'; import { singleSourceSelector } from '../../../../../store/entities/sources/selector'; import type { Sandbox } from '../../../../../store/entities/sandboxes/index'; @@ -33,6 +33,7 @@ type Props = { sandbox: ?Sandbox; moduleActions: moduleEntity.actions; sourceActions: sourceEntity.actions; + modulePath: ?string; }; type State = { @@ -51,6 +52,7 @@ const mapStateToProps = (state, props) => ({ boilerplates: boilerplatesBySandboxSelector(state, { id: props.sandbox.id }), module: singleModuleSelector(state, { id: props.tab ? props.tab.moduleId : null }), source: singleSourceSelector(state, { id: props.sandbox.source }), + modulePath: modulePathSelector(state, { id: props.tab ? props.tab.moduleId : null }), }); const mapDispatchToProps = dispatch => ({ moduleActions: bindActionCreators(moduleEntity.actions, dispatch), @@ -67,9 +69,17 @@ class EditorPreview extends React.PureComponent { startResizing = () => this.setState({ resizing: true }); stopResizing = () => this.setState({ resizing: false }); + saveCode = () => { + const { module, moduleActions } = this.props; + + if (module == null) return; + + moduleActions.saveCode(module.id); + }; + render() { const { source, modules, directories, boilerplates, - moduleActions, module, sourceActions } = this.props; + moduleActions, module, sourceActions, modulePath } = this.props; if (module == null || source == null) return null; return ( @@ -81,7 +91,7 @@ class EditorPreview extends React.PureComponent { defaultSize="50%" minSize={360} primary="second" - paneStyle={{ height: 'calc(100% - 35px)' }} + paneStyle={{ height: '100%' }} > diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/Header.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/Header.js new file mode 100644 index 00000000000..6ddb065c473 --- /dev/null +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/Header.js @@ -0,0 +1,45 @@ +import React from 'react'; +import styled from 'styled-components'; + +import SaveIcon from 'react-icons/lib/md/save'; +import Button from '../../../../../../../components/buttons/Button'; + +const Container = styled.div` + display: flex; + background-color: ${props => props.theme.background}; + box-shadow: 0 3px 3px ${props => props.theme.background2}; + color: ${props => props.theme.white}; + padding: 0.5rem 1rem; + height: 3rem; + box-sizing: border-box; + justify-content: space-between; + vertical-align: middle; + align-items: center; +`; + +const Path = styled.span` + color: ${props => props.theme.background.lighten(1.25)}; + padding-right: 0.1rem; +`; + + +type Props = { + title: string; + path: string; + saveComponent?: () => void; +}; + +export default ({ path, title, saveComponent }: Props) => ( + + +
+ {path} + {title} +
+ + +
+); diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/index.js similarity index 91% rename from src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor.js rename to src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/index.js index 58a5f426a5d..cd31917f5a2 100644 --- a/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor.js +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/CodeEditor/index.js @@ -17,7 +17,8 @@ import 'codemirror/addon/fold/foldcode'; import 'codemirror/addon/fold/foldgutter'; import 'codemirror/addon/fold/brace-fold'; -import theme from '../../../../../../../common/theme'; +import theme from '../../../../../../../../common/theme'; +import Header from './Header'; const documentCache = {}; @@ -25,8 +26,11 @@ type Props = { code: ?string; error: ?Object; id: string; + title: string; + modulePath: string; changeCode: (id: string, code: string) => void; - saveCode: (id: string) => void; + saveCode: () => void; + canSave: boolean; }; const Container = styled.div` @@ -54,14 +58,6 @@ const ErrorMessage = styled.div` color: ${props => props.theme.red}; `; -const TopMessage = styled.div` - flex: 0 0 auto; - padding: 0.5rem 1rem; - font-size: 14px; - color: ${props => props.theme.background.lighten(1.5)}; - vertical-align: middle; -`; - const handleError = (cm, currentError, nextError, nextCode, nextId) => { if (currentError || nextError) { if (currentError && nextError && @@ -97,8 +93,7 @@ export default class CodeEditor extends React.PureComponent { window.addEventListener('keydown', (event: KeyboardEvent) => { if (event.ctrlKey || event.metaKey) { if (event.key === 's' || event.keyCode === 83) { - const { id } = this.props; - this.props.saveCode(id); + this.props.saveCode(); event.preventDefault(); return false; } @@ -108,7 +103,9 @@ export default class CodeEditor extends React.PureComponent { } shouldComponentUpdate(nextProps: Props) { - return nextProps.id !== this.props.id || nextProps.error !== this.props.error; + return nextProps.id !== this.props.id || + nextProps.error !== this.props.error || + this.props.canSave !== nextProps.canSave; } componentWillReceiveProps(nextProps: Props) { @@ -170,15 +167,10 @@ export default class CodeEditor extends React.PureComponent { codemirror: typeof CodeMirror; render() { - const { error } = this.props; + const { error, title, saveCode, canSave, modulePath } = this.props; return ( - - - - Last update: 5 seconds ago - - +
diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/AddressBar.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/AddressBar.js new file mode 100644 index 00000000000..74a5327274c --- /dev/null +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/AddressBar.js @@ -0,0 +1,63 @@ +import React from 'react'; +import styled from 'styled-components'; +import theme from '../../../../../../../../common/theme'; + +const TEXT_COLOR = theme.gray.darken(0.2)(); + +const Container = styled.div` + position: relative; + color: ${TEXT_COLOR}; + vertical-align: middle; +`; + +const Input = styled.input` + padding: 0.2rem 1rem; + color: ${TEXT_COLOR}; + width: 100%; + box-sizing: border-box; +`; + +const Slash = styled.span` + position: absolute; + padding: 0.3rem 0.75rem; + top: 0; + bottom: 0; + left: 0; + vertical-align: middle; + line-height: 1.15; +`; + +type Props = { + url: string; + onChange: (url: string) => void; + onConfirm: () => void; +}; + +export default class extends React.PureComponent { + props: Props; + + onChange = (evt) => { + const { onChange } = this.props; + + onChange(evt.target.value); + }; + + handleKeyDown = (e) => { + const { onConfirm } = this.props; + + if (e.keyCode === 13) { + // Enter + onConfirm(); + } + } + + render() { + const { url = '' } = this.props; + return ( + + / + + + ); + } +} diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Navigator.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Navigator.js new file mode 100644 index 00000000000..7feee248c0d --- /dev/null +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Navigator.js @@ -0,0 +1,78 @@ +import React from 'react'; +import styled from 'styled-components'; + +import LeftIcon from 'react-icons/lib/fa/angle-left'; +import RightIcon from 'react-icons/lib/fa/angle-right'; +import RefreshIcon from 'react-icons/lib/md/refresh'; + +import AddressBar from './AddressBar'; +import Switch from './Switch'; + +const Container = styled.div` + display: flex; + background-color: #f2f2f2; + padding: 0.5rem; + align-items: center; + line-height: 1; + box-shadow: 0 1px 3px #ddd; +`; + +const Icons = styled.div` + display: flex; +`; + +const Icon = styled.div` + display: inline-block; + color: ${props => (props.disabled ? props.theme.gray : props.theme.gray.darken(0.3))}; + font-size: 1.5rem; + line-height: 0.5; + margin: 0 0.1rem; + vertical-align: middle; + text-align: center; + + ${props => !props.disabled && ` + &:hover { + background-color: #e2e2e2; + cursor: pointer; + }`} +`; + +const AddressBarContainer = styled.div` + width: 100%; + box-sizing: border-box; + margin: 0 0.5rem; +`; + +type Props = { + url: string, + onChange: (text: string) => void; + onConfirm: () => void; + onBack?: () => void; + onForward?: () => void; + onRefresh?: () => void; + isProjectView: boolean; + toggleProjectView: () => void; +}; + +export default ({ + url, + onChange, + onConfirm, + onBack, + onForward, + onRefresh, + isProjectView, + toggleProjectView, +}: Props) => ( + + + + + + + + + + + +); diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Switch.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Switch.js new file mode 100644 index 00000000000..53f0fd4b1c5 --- /dev/null +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/Switch.js @@ -0,0 +1,56 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Container = styled.div` + transition: 0.3s ease all; + position: relative; + background-color: ${props => (props.right ? props.theme.primary : props.theme.secondary)}; + font-size: 1rem; + width: 7rem; + border-radius: 50px; + font-size: .875rem; + color: ${props => (props.right ? props.theme.primaryText : props.theme.secondary.darken(0.5))}; + border: 1px solid #ccc; + padding: calc(0.5rem - 1px); + height: 0.9rem; + cursor: pointer; + + &:before, &:after { + position: absolute; + top: 50%; + margin-top: -.5em; + line-height: 1; + } +`; + +const Text = styled.span` + position: absolute; + ${props => props.position}: 0.75rem; + opacity: ${props => (props.right ? 1 : 0)}; +`; + +const Dot = styled.div` + transition: inherit; + position: absolute; + border-radius: 50%; + height: 1.5rem; + width: 1.5rem; + left: 0.2rem; + transform: translateX(${props => (props.right ? '4.2rem' : '0')}); + top: 0.1rem; + background-color: ${props => (props.right ? props.theme.primaryText : props.theme.secondary.darken(0.5))}; + box-shadow: 0 0 4px ${props => (props.right ? props.theme.primaryText : props.theme.secondary.darken(0.5))};; +`; + +type Props = { + right: boolean; + onClick: () => void; +}; + +export default ({ right, onClick }: Props) => ( + + Project + Current + + +); diff --git a/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview.js b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/index.js similarity index 51% rename from src/app/pages/SandboxView/Editor/Content/View/subviews/Preview.js rename to src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/index.js index 584dd9753a1..d37d62de668 100644 --- a/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview.js +++ b/src/app/pages/SandboxView/Editor/Content/View/subviews/Preview/index.js @@ -4,11 +4,13 @@ import styled from 'styled-components'; import { debounce } from 'lodash'; -import type { Module } from '../../../../../../store/entities/modules/'; -import type { Source } from '../../../../../../store/entities/sources/'; -import type { Directory } from '../../../../../../store/entities/directories/index'; -import type { Boilerplate } from '../../../../../../store/entities/boilerplates'; -import { host } from '../../../../../../utils/url-generator'; +import type { Module } from '../../../../../../../store/entities/modules/'; +import type { Source } from '../../../../../../../store/entities/sources/'; +import type { Directory } from '../../../../../../../store/entities/directories/index'; +import type { Boilerplate } from '../../../../../../../store/entities/boilerplates'; +import { frameUrl } from '../../../../../../../utils/url-generator'; +import Navigator from './Navigator'; +import { isMainModule } from '../../../../../../../store/entities/modules/index'; const Container = styled.div` position: absolute; @@ -45,6 +47,11 @@ type Props = { type State = { frameInitialized: boolean; + url: string; + history: Array; + historyPosition: number; + urlInAddressBar: string; + isProjectView: boolean; }; export default class Preview extends React.PureComponent { @@ -54,6 +61,10 @@ export default class Preview extends React.PureComponent { this.setError = debounce(this.setError, 500); this.state = { frameInitialized: false, + history: [], + historyPosition: 0, + urlInAddressBar: '', + isProjectView: true, }; } @@ -98,12 +109,16 @@ export default class Preview extends React.PureComponent { this.setError.cancel(); // To reset the debounce, but still quickly remove errors this.props.setError(this.props.module.id, null); + } else if (type === 'urlchange') { + const url = e.data.url.replace('/', ''); + this.commitUrl(url); } } - } + }; executeCode = () => { const { modules, directories, boilerplates, bundle = {}, module } = this.props; + const { isProjectView } = this.state; if (bundle.manifest == null) { if (!bundle.processing && !bundle.error) { @@ -112,22 +127,86 @@ export default class Preview extends React.PureComponent { return; } + const mainModule = isProjectView ? modules.filter(isMainModule)[0] : module; + requestAnimationFrame(() => { document.getElementById('sandbox').contentWindow.postMessage({ + type: 'compile', boilerplates, - module, + module: mainModule, + changedModule: module, modules, directories, manifest: bundle.manifest, url: bundle.url, }, '*'); }); - } + }; setError = (e: ?{ message: string; line: number }) => { this.props.setError(this.props.module.id, e); + }; + + updateUrl = (url: string) => { + this.setState({ urlInAddressBar: url }); + }; + + sendUrl = () => { + const { urlInAddressBar } = this.state; + + document.getElementById('sandbox').src = frameUrl(urlInAddressBar); + this.commitUrl(urlInAddressBar); + } + + handleRefresh = () => { + const { history, historyPosition } = this.state; + + document.getElementById('sandbox').src = frameUrl(history[historyPosition]); + this.setState({ + urlInAddressBar: history[historyPosition], + }); } + handleBack = () => { + document.getElementById('sandbox').contentWindow.postMessage({ + type: 'urlback', + }, '*'); + + const { historyPosition, history } = this.state; + this.setState({ + historyPosition: this.state.historyPosition - 1, + urlInAddressBar: history[historyPosition - 1], + }); + }; + + handleForward = () => { + document.getElementById('sandbox').contentWindow.postMessage({ + type: 'urlforward', + }, '*'); + + const { historyPosition, history } = this.state; + this.setState({ + historyPosition: this.state.historyPosition + 1, + urlInAddressBar: history[historyPosition + 1], + }); + }; + + commitUrl = (url: string) => { + const { history, historyPosition } = this.state; + history.length = historyPosition + 1; + this.setState({ + history: [...history, url], + historyPosition: historyPosition + 1, + urlInAddressBar: url, + }); + }; + + toggleProjectView = () => { + this.setState({ isProjectView: !this.state.isProjectView }, () => { + this.executeCode(); + }); + }; + props: Props; state: State; element: ?Element; @@ -136,8 +215,10 @@ export default class Preview extends React.PureComponent { render() { const { bundle = {} } = this.props; + const { historyPosition, history, urlInAddressBar, isProjectView } = this.state; + + const url = urlInAddressBar || ''; - const location = document.location; if (bundle.processing) { return ( @@ -148,9 +229,19 @@ export default class Preview extends React.PureComponent { return ( + 0 && this.handleBack} + onForward={historyPosition < history.length - 1 && this.handleForward} + onRefresh={this.handleRefresh} + isProjectView={isProjectView} + toggleProjectView={this.toggleProjectView} + /> diff --git a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/DirectoryChildren.js b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/DirectoryChildren.js index 93b688389f9..9e6ed86c611 100644 --- a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/DirectoryChildren.js +++ b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/DirectoryChildren.js @@ -6,6 +6,7 @@ import DirectoryEntry from './'; import type { Module } from '../../../../../../store/entities/modules'; import type { Directory } from '../../../../../../store/entities/directories'; import { validateTitle } from '../../../../../../store/entities/modules/validator'; +import { isMainModule } from '../../../../../../store/entities/modules/index'; type Props = { depth: number; @@ -51,6 +52,7 @@ export default class DirectoryChildren extends React.PureComponent { ))} {modules.map((m) => { const isActive = m.id === currentModuleId; + const mainModule = isMainModule(m); return ( ); })} diff --git a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/Entry/index.js b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/Entry/index.js index f1552ced7c7..25c4263a9c2 100644 --- a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/Entry/index.js +++ b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/Entry/index.js @@ -35,6 +35,7 @@ type Props = { hasChildren?: boolean; openModuleTab: (id: string) => void; root: ?boolean; + isMainModule: boolean; }; type State = { @@ -75,12 +76,18 @@ class Entry extends React.PureComponent { }; openContextMenu = (event: MouseEvent) => { + const { id, isMainModule, onCreateModuleClick, + onCreateDirectoryClick, rename, deleteEntry } = this.props; + + if (isMainModule) { + return; + } + event.preventDefault(); this.setState({ selected: true, }); - const { id, onCreateModuleClick, onCreateDirectoryClick, rename, deleteEntry } = this.props; const items = [onCreateModuleClick && { title: 'New Module', action: onCreateModuleClick, @@ -149,7 +156,7 @@ class Entry extends React.PureComponent { } const entrySource = { - canDrag: props => !!props.id, + canDrag: props => !!props.id && !props.isMainModule, beginDrag: (props) => { if (props.closeTree) props.closeTree(); return { id: props.id, directory: props.type === 'directory' }; diff --git a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/index.js b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/index.js index 23e62e045e3..4661d7a6aef 100644 --- a/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/index.js +++ b/src/app/pages/SandboxView/Editor/Workspace/CodeEditor/DirectoryEntry/index.js @@ -172,8 +172,8 @@ class DirectoryEntry extends React.PureComponent { onClick={this.toggleOpen} renameValidator={this.validateDirectoryTitle} rename={root - ? this.renameSandbox : - directoryActions.renameDirectory + ? this.renameSandbox + : directoryActions.renameDirectory } onCreateModuleClick={this.onCreateModuleClick} onCreateDirectoryClick={this.onCreateDirectoryClick} diff --git a/src/app/pages/SandboxView/Sandbox.js b/src/app/pages/SandboxView/Sandbox.js index 5a0f8efc923..03ab4786fb4 100644 --- a/src/app/pages/SandboxView/Sandbox.js +++ b/src/app/pages/SandboxView/Sandbox.js @@ -77,6 +77,7 @@ class SandboxPage extends React.PureComponent { // Reset sandbox view info if sandbox changes if (!this.props.sandbox || (sandbox && sandbox.id !== this.props.currentSandboxId)) { this.props.sandboxViewActions.setCurrentSandbox(sandbox.id); + this.props.sandboxViewActions.openDefaultModuleTab(); } } diff --git a/src/app/store/actions/views/sandbox.js b/src/app/store/actions/views/sandbox.js index f3eefcbbfe5..39a0f7f2742 100644 --- a/src/app/store/actions/views/sandbox.js +++ b/src/app/store/actions/views/sandbox.js @@ -1,17 +1,28 @@ +import { currentSandboxIdSelector } from '../../selectors/views/sandbox-selector'; +import { defaultModuleSelector } from '../../entities/modules/selector'; + export const SET_TAB = 'SET_TAB'; export const CLOSE_TAB = 'CLOSE_TAB'; export const OPEN_MODULE_TAB = 'OPEN_MODULE_TAB'; export const RESET_SANDBOX_VIEW = 'RESET_SANDBOX_VIEW'; export const SET_CURRENT_SANDBOX = 'SET_CURRENT_SANDBOX'; +const openModuleTab = id => ({ + type: OPEN_MODULE_TAB, + moduleId: id, + view: 'EditorPreview', +}); + export default { - openModuleTab: id => ({ - type: OPEN_MODULE_TAB, - moduleId: id, - view: 'EditorPreview', - }), + openModuleTab, + openDefaultModuleTab: () => (dispatch, getState) => { + const sandboxId = currentSandboxIdSelector(getState()); + const defaultModule = defaultModuleSelector(getState(), { sandboxId }); + if (defaultModule) { + dispatch(openModuleTab(defaultModule.id)); + } + }, setTab: id => ({ type: SET_TAB, id }), - closeTab: id => ({ type: CLOSE_TAB, id }), reset: () => ({ type: RESET_SANDBOX_VIEW }), setCurrentSandbox: sandboxId => ({ type: SET_CURRENT_SANDBOX, sandboxId }), }; diff --git a/src/app/store/entities/modules/index.js b/src/app/store/entities/modules/index.js index 925cbc00e52..ff1022ebd16 100644 --- a/src/app/store/entities/modules/index.js +++ b/src/app/store/entities/modules/index.js @@ -26,6 +26,10 @@ export type Module = { const actions = createActions(schema); +export function isMainModule(module) { + return module.directoryId == null && module.title === 'index.js'; +} + export default createEntity(schema, { actions, reducer, diff --git a/src/app/store/entities/modules/selector.js b/src/app/store/entities/modules/selector.js index d9df10ffeaf..58cf0d6380d 100644 --- a/src/app/store/entities/modules/selector.js +++ b/src/app/store/entities/modules/selector.js @@ -3,14 +3,14 @@ import { values } from 'lodash'; import { singleSandboxSelector } from '../sandboxes/selector'; import resolveModule from '../../../../sandbox/utils/resolve-module'; -import { directoriesBySandboxSelector } from '../directories/selector'; +import { directoriesBySandboxSelector, directoriesSelector } from '../directories/selector'; import { entriesInDirectorySelector } from '../../selectors/entry-selectors'; -import {currentTabSelector} from '../../selectors/views/sandbox-selector'; +import { currentTabSelector } from '../../selectors/views/sandbox-selector'; +import { isMainModule } from './index'; export const modulesSelector = state => state.entities.modules; -export const defaultModuleSelector = state => modulesSelector(state).default; export const singleModuleSelector = (state, { id }) => ( - modulesSelector(state)[id] || defaultModuleSelector(state) + modulesSelector(state)[id] ); export const modulesBySandboxSelector = createSelector( @@ -23,6 +23,10 @@ export const modulesBySandboxSelector = createSelector( }, ); +export const defaultModuleSelector = (state, { sandboxId }) => ( + modulesBySandboxSelector(state, { id: sandboxId }).filter(isMainModule)[0] +); + export const currentModuleSelector = createSelector( currentTabSelector, modulesSelector, @@ -47,6 +51,25 @@ export const moduleByPathSelector = createSelector( }, ); +export const modulePathSelector = createSelector( + modulesSelector, + directoriesSelector, + (_, { id }) => id, + (modules, directories, id) => { + const module = modules[id]; + + if (!module) return ''; + + let directory = directories[module.directoryId]; + let path = '/'; + while (directory != null) { + path = `/${directory.title}${path}`; + directory = directories[directory.directoryId]; + } + return path; + }, +); + export const modulesInDirectorySelector = createSelector( (_, { id }) => id, (_, { sourceId }) => sourceId, diff --git a/src/app/store/reducers/views/sandbox.js b/src/app/store/reducers/views/sandbox.js index e26c631ae8e..7da6cd141d0 100644 --- a/src/app/store/reducers/views/sandbox.js +++ b/src/app/store/reducers/views/sandbox.js @@ -1,6 +1,4 @@ // @flow -import { findIndex } from 'lodash'; - import * as actions from '../../actions/views/sandbox'; type CustomTab = { @@ -56,31 +54,6 @@ function singleSandboxReducer(state: SandboxState = initialSandboxState, action: return newState; } - case actions.SET_TAB: { - return { - ...state, - currentTab: action.id, - }; - } - case actions.CLOSE_TAB: { - const newTabs = state.tabs.filter(t => t.id !== action.id); - const newCurrentTab = () => { - if (state.currentTab === action.id) { - const oldTabPosition = findIndex(state.tabs, t => t.id === action.id); - const newCurrentTabPosition = Math.max(0, oldTabPosition - 1); - - if (newTabs[newCurrentTabPosition]) return newTabs[newCurrentTabPosition].id; - return null; - } - return state.currentTab; - }; - - return { - ...state, - tabs: newTabs, - currentTab: newCurrentTab(), - }; - } case actions.RESET_SANDBOX_VIEW: return initialState; default: @@ -91,8 +64,6 @@ function singleSandboxReducer(state: SandboxState = initialSandboxState, action: export default function sandboxReducer(state: State = initialState, action: any) { switch (action.type) { case actions.OPEN_MODULE_TAB: - case actions.SET_TAB: - case actions.CLOSE_TAB: case actions.RESET_SANDBOX_VIEW: if (state.currentSandboxId == null) { return state; diff --git a/src/app/utils/url-generator.js b/src/app/utils/url-generator.js index e4fa7c6ffe6..66da6ccbcf8 100644 --- a/src/app/utils/url-generator.js +++ b/src/app/utils/url-generator.js @@ -5,6 +5,10 @@ export const sandboxUrl = sandbox => ( : `/anonymous/${sandbox.slug}` ); +export const frameUrl = (append = '') => ( + `${location.protocol}//sandbox.${host()}/${append}` +); + export const editModuleUrl = sandbox => ( `${sandboxUrl(sandbox)}/code` ); diff --git a/src/common/theme.js b/src/common/theme.js index 9ed390f616e..b1a84fe3069 100644 --- a/src/common/theme.js +++ b/src/common/theme.js @@ -51,6 +51,7 @@ const theme = createTheme({ // secondary: '#DC3522', secondary: '#6CAEDD', white: '#E0E0E0', + gray: '#C0C0C0', black: '#74757D', redBackground: '#400000', red: '#F27777', diff --git a/src/sandbox/eval/index.js b/src/sandbox/eval/index.js index 6a371260e69..00dcf601841 100644 --- a/src/sandbox/eval/index.js +++ b/src/sandbox/eval/index.js @@ -3,7 +3,7 @@ import type { Module } from '../../app/store/entities/modules/'; import type { Directory } from '../../app/store/entities/directories/index'; -import evalJS from './js'; +import evalJS, { deleteCache as deleteJSCache } from './js'; import evalHTML from './html'; import evalCSS from './css'; @@ -24,6 +24,12 @@ function doEval(mainModule, modules, directories, manifest, depth) { return evalJS(mainModule, modules, directories, manifest, depth); } +export function deleteCache(module: Module) { + if (module.title.includes('.js')) { + deleteJSCache(module); + } +} + const evalModule = ( mainModule: Module, modules: Array, diff --git a/src/sandbox/eval/js.js b/src/sandbox/eval/js.js index 98b8d3f6487..bb4ae254145 100644 --- a/src/sandbox/eval/js.js +++ b/src/sandbox/eval/js.js @@ -4,6 +4,10 @@ import resolveModule from '../utils/resolve-module'; const moduleCache = new Map(); +export function deleteCache(module) { + moduleCache.delete(module.id); +} + const compileCode = (code: string = '', moduleName: string = 'unknown') => { try { return transform(code, { diff --git a/src/sandbox/index.js b/src/sandbox/index.js index 6a71a0d8220..bd61694fded 100644 --- a/src/sandbox/index.js +++ b/src/sandbox/index.js @@ -1,6 +1,6 @@ import delay from './utils/delay'; import buildError from './utils/error-message-builder'; -import evalModule from './eval'; +import evalModule, { deleteCache } from './eval'; import { getBoilerplates, evalBoilerplates, findBoilerplate } from './boilerplates'; let errorHappened = false; @@ -11,15 +11,23 @@ async function addDependencyBundle() { const script = document.createElement('script'); script.setAttribute('src', url); script.setAttribute('async', false); - document.body.appendChild(script); + document.head.appendChild(script); while (window.dependencies == null) { await delay(100); } } -window.addEventListener('message', async (message) => { - const { modules, directories, boilerplates, module, manifest, url: newUrl } = message.data; +async function compile(message) { + const { + modules, + directories, + boilerplates, + module, + manifest, + url: newUrl, + changedModule, + } = message.data; if (fetching) return; @@ -38,7 +46,7 @@ window.addEventListener('message', async (message) => { try { document.body.innerHTML = ''; - document.head.innerHTML = ''; + deleteCache(changedModule); const compiledModule = evalModule(module, modules, directories, manifest); const boilerplate = findBoilerplate(module); @@ -56,6 +64,39 @@ window.addEventListener('message', async (message) => { error: buildError(e), }, '*'); } +} + +window.addEventListener('message', async (message) => { + if (message.data.type === 'compile') { + await compile(message); + } else if (message.data.type === 'urlback') { + history.back(); + } else if (message.data.type === 'urlforward') { + history.forward(); + } }); window.parent.postMessage('Ready!', '*'); + +function setupHistoryListeners() { + const pushState = window.history.pushState; + window.history.pushState = function (state) { + if (typeof history.onpushstate === 'function') { + window.history.onpushstate({ state }); + } + // ... whatever else you want to do + // maybe call onhashchange e.handler + return pushState.apply(window.history, arguments); + }; + + history.onpushstate = (e) => { + setTimeout(() => { + window.parent.postMessage({ + type: 'urlchange', + url: document.location.pathname + location.search, + }, '*'); + }); + }; +} + +setupHistoryListeners();