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) => (
+
+
+
+
+
+
+);
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();