diff --git a/src/components/ContentWrap.jsx b/src/components/ContentWrap.jsx
index da9327b..d984a5d 100644
--- a/src/components/ContentWrap.jsx
+++ b/src/components/ContentWrap.jsx
@@ -34,7 +34,6 @@ export default class ContentWrap extends Component {
this.logCount = 0;
window.onMessageFromConsole = this.onMessageFromConsole.bind(this);
-
window.previewException = this.previewException.bind(this);
// `clearConsole` is on window because it gets called from inside iframe also.
window.clearConsole = this.clearConsole.bind(this);
@@ -289,50 +288,25 @@ export default class ContentWrap extends Component {
]).then(() => this.setPreviewContent(true));
}
applyCodemirrorSettings(prefs) {
- if (!this.cm) {
- return;
+ if (window.consoleEl) {
+ window.consoleEl.querySelector(
+ '.CodeMirror'
+ ).style.fontSize = `${parseInt(prefs.fontSize, 10)}px`;
}
- htmlCodeEl.querySelector(
- '.CodeMirror'
- ).style.fontSize = cssCodeEl.querySelector(
- '.CodeMirror'
- ).style.fontSize = jsCodeEl.querySelector(
- '.CodeMirror'
- ).style.fontSize = `${parseInt(prefs.fontSize, 10)}px`;
- window.consoleEl.querySelector('.CodeMirror').style.fontSize = `${parseInt(
- prefs.fontSize,
- 10
- )}px`;
// Replace correct css file in LINK tags's href
- window.editorThemeLinkTag.href = `lib/codemirror/theme/${
- prefs.editorTheme
- }.css`;
+ if (prefs.editorTheme) {
+ window.editorThemeLinkTag.href = `lib/codemirror/theme/${
+ prefs.editorTheme
+ }.css`;
+ }
+
window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
/fontname/g,
(prefs.editorFont === 'other'
? prefs.editorCustomFont
: prefs.editorFont) || 'FiraCode'
);
- // window.customEditorFontInput.classList[
- // prefs.editorFont === 'other' ? 'remove' : 'add'
- // ]('hide');
- this.consoleCm.setOption('theme', prefs.editorTheme);
-
- ['html', 'js', 'css'].forEach(type => {
- this.cm[type].setOption('indentWithTabs', prefs.indentWith !== 'spaces');
- this.cm[type].setOption(
- 'blastCode',
- prefs.isCodeBlastOn ? { effect: 2, shake: false } : false
- );
- this.cm[type].setOption('indentUnit', +prefs.indentSize);
- this.cm[type].setOption('tabSize', +prefs.indentSize);
- this.cm[type].setOption('theme', prefs.editorTheme);
-
- this.cm[type].setOption('keyMap', prefs.keymap);
- this.cm[type].setOption('lineWrapping', prefs.lineWrap);
- this.cm[type].refresh();
- });
}
// Check all the code wrap if they are minimized or maximized
diff --git a/src/components/ContentWrapFiles.jsx b/src/components/ContentWrapFiles.jsx
new file mode 100644
index 0000000..dc8751b
--- /dev/null
+++ b/src/components/ContentWrapFiles.jsx
@@ -0,0 +1,640 @@
+import { h, Component } from 'preact';
+import UserCodeMirror from './UserCodeMirror';
+import { modes, HtmlModes, CssModes, JsModes } from '../codeModes';
+import { log, loadJS } from '../utils';
+
+import { linearizeFiles, assignFilePaths } from '../fileUtils';
+
+import { SplitPane } from './SplitPane';
+import { trackEvent } from '../analytics';
+import CodeMirror from '../CodeMirror';
+import { deferred } from '../deferred';
+import { SidePane } from './SidePane';
+import { Console } from './Console';
+
+const minCodeWrapSize = 33;
+
+/* global htmlCodeEl, jsCodeEl, cssCodeEl, logCountEl
+*/
+export default class ContentWrapFiles extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isConsoleOpen: false,
+ isCssSettingsModalOpen: false,
+ editorOptions: this.getEditorOptions()
+ };
+
+ this.fileBuffers = {};
+ this.updateTimer = null;
+ this.updateDelay = 500;
+ this.htmlMode = HtmlModes.HTML;
+ this.prefs = {};
+ this.codeInPreview = { html: null, css: null, js: null };
+ this.cmCodes = { html: props.currentItem.html, css: '', js: '' };
+ this.logCount = 0;
+
+ window.onMessageFromConsole = this.onMessageFromConsole.bind(this);
+ window.previewException = this.previewException.bind(this);
+ // `clearConsole` is on window because it gets called from inside iframe also.
+ window.clearConsole = this.clearConsole.bind(this);
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.state.isConsoleOpen !== nextState.isConsoleOpen ||
+ this.state.isCssSettingsModalOpen !== nextState.isCssSettingsModalOpen ||
+ this.state.mainSplitSizes !== nextState.mainSplitSizes ||
+ this.state.selectedFile !== nextState.selectedFile ||
+ this.props.currentLayoutMode !== nextProps.currentLayoutMode ||
+ this.props.currentItem !== nextProps.currentItem ||
+ this.props.prefs !== nextProps.prefs
+ );
+ }
+ componentWillUpdate(nextProps) {
+ if (
+ this.props.currentItem.createdOn !== nextProps.currentItem.createdOn ||
+ this.props.currentItem.id !== nextProps.currentItem.id
+ ) {
+ this.fileBuffers = {};
+ this.state.selectedFile = null;
+ }
+ }
+ componentDidUpdate() {
+ const { currentItem } = this.props;
+ const linearFiles = linearizeFiles(currentItem.files);
+
+ // Select a new file if nothing is selected already or the selected file exists no more.
+ if (
+ currentItem &&
+ currentItem.files &&
+ (!this.state.selectedFile ||
+ !linearFiles.includes(this.state.selectedFile))
+ ) {
+ this.fileSelectHandler(linearFiles[0]);
+ }
+ // HACK: becuase its a DOM manipulation
+ // window.logCountEl.textContent = this.logCount;
+ // log('🚀', 'didupdate', this.props.currentItem);
+ // if (this.isValidItem(this.props.currentItem)) {
+ // this.refreshEditor();
+ // }
+ }
+ componentDidMount() {
+ this.props.onRef(this);
+ }
+
+ getEditorOptions(fileName = '') {
+ let options = {
+ gutters: [
+ 'error-gutter',
+ 'CodeMirror-linenumbers',
+ 'CodeMirror-foldgutter'
+ ],
+ emmet: true
+ };
+ if (fileName.match(/\.css$/)) {
+ } else if (fileName.match(/\.js$/)) {
+ delete options.emmet;
+ } else if (fileName.match(/\.html$/)) {
+ // HTML
+ options = {
+ ...options,
+ noAutocomplete: true,
+ matchTags: { bothTags: true }
+ };
+ }
+
+ return options;
+ }
+
+ createEditorDoc(file) {
+ let mode;
+ if (file.name.match(/\.css$/)) {
+ mode = modes[CssModes.CSS];
+ } else if (file.name.match(/\.js$/)) {
+ mode = modes[JsModes.JS];
+ } else if (file.name.match(/\.html$/)) {
+ mode = modes[HtmlModes.HTML];
+ } else if (file.name.match(/\.md$/) || file.name.match(/\.markdown$/)) {
+ mode = modes[HtmlModes.MARKDOWN];
+ } else if (file.name.match(/\.sass$/)) {
+ mode = modes[CssModes.SASS];
+ } else if (file.name.match(/\.scss$/)) {
+ mode = modes[CssModes.SCSS];
+ }
+
+ CodeMirror.autoLoadMode(this.cm, mode.cmPath || mode.cmMode);
+
+ this.fileBuffers[file.name] = CodeMirror.Doc(
+ file.content || '',
+ mode.cmMode
+ );
+ }
+
+ onHtmlCodeChange(editor, change) {
+ this.cmCodes.html = editor.getValue();
+
+ this.props.onCodeChange(
+ this.state.selectedFile,
+ this.cmCodes.html,
+ change.origin !== 'setValue'
+ );
+ this.onCodeChange(editor, change);
+ }
+
+ onCodeChange(editor, change) {
+ clearTimeout(this.updateTimer);
+
+ this.updateTimer = setTimeout(() => {
+ // This is done so that multiple simultaneous setValue don't trigger too many preview refreshes
+ // and in turn too many file writes on a single file (eg. preview.html).
+ if (change.origin !== 'setValue') {
+ // Specifically checking for false so that the condition doesn't get true even
+ // on absent key - possible when the setting key hasn't been fetched yet.
+ if (this.prefs.autoPreview !== false) {
+ this.setPreviewContent();
+ }
+
+ // Track when people actually are working.
+ trackEvent.previewCount = (trackEvent.previewCount || 0) + 1;
+ if (trackEvent.previewCount === 4) {
+ trackEvent('fn', 'usingPreview');
+ }
+ }
+ }, this.updateDelay);
+ }
+
+ createPreviewFile(html, css, js) {
+ // Track if people have written code.
+ if (!trackEvent.hasTrackedCode && (html || css || js)) {
+ trackEvent('fn', 'hasCode');
+ trackEvent.hasTrackedCode = true;
+ }
+
+ var obj = {};
+ const duplicateFiles = JSON.parse(
+ JSON.stringify(this.props.currentItem.files)
+ );
+ const files = linearizeFiles(assignFilePaths(duplicateFiles, '/user'));
+
+ files.forEach(file => {
+ obj[file.path] = file.content || '';
+
+ // Add screenlog to index.html
+ if (file.name === 'index.html') {
+ obj[file.path] =
+ '' +
+ obj[file.path];
+ }
+ });
+
+ navigator.serviceWorker.controller.postMessage(obj);
+
+ if (this.detachedWindow) {
+ log('✉️ Sending message to detached window');
+ this.detachedWindow.postMessage({ contents: '/user/index.html' }, '*');
+ } else {
+ this.frame.src = '/user/index.html';
+ }
+ }
+ cleanupErrors() {
+ this.cm.clearGutter('error-gutter');
+ }
+
+ showErrors(lang, errors) {
+ var editor = this.cm;
+ errors.forEach(function(e) {
+ editor.operation(function() {
+ var n = document.createElement('div');
+ n.setAttribute('data-title', e.message);
+ n.classList.add('gutter-error-marker');
+ editor.setGutterMarker(e.lineNumber, 'error-gutter', n);
+ });
+ });
+ }
+
+ /**
+ * Generates the preview from the current code.
+ * @param {boolean} isForced Should refresh everything without any check or not
+ * @param {boolean} isManual Is this a manual preview request from user?
+ */
+ setPreviewContent(isForced, isManual) {
+ if (!this.props.prefs.autoPreview && !isManual) {
+ return;
+ }
+
+ if (!this.props.prefs.preserveConsoleLogs) {
+ this.clearConsole();
+ }
+ this.cleanupErrors();
+
+ var currentCode = {
+ html: this.cmCodes.html,
+ css: this.cmCodes.css,
+ js: this.cmCodes.js
+ };
+ log('🔎 setPreviewContent', isForced);
+ const targetFrame = this.detachedWindow
+ ? this.detachedWindow.document.querySelector('iframe')
+ : this.frame;
+
+ this.createPreviewFile();
+ // result.forEach(resultItem => {
+ // if (resultItem.errors) {
+ // this.showErrors(resultItem.errors.lang, resultItem.errors.data);
+ // }
+ // });
+
+ this.codeInPreview.html = currentCode.html;
+ this.codeInPreview.css = currentCode.css;
+ this.codeInPreview.js = currentCode.js;
+ }
+ isValidItem(item) {
+ return !!item.title;
+ }
+ refreshEditor() {
+ this.cmCodes.html = this.props.currentItem.html;
+ if (this.state.selectedFile) {
+ this.cm.setValue(this.state.selectedFile.content);
+ }
+ this.cm.refresh();
+ window.cm = this.cm;
+
+ this.clearConsole();
+
+ // Set preview only when all modes are updated so that preview doesn't generate on partially
+ // correct modes and also doesn't happen 3 times.
+ Promise.all([this.updateHtmlMode(this.props.currentItem.htmlMode)]).then(
+ () => this.setPreviewContent(true)
+ );
+ }
+ applyCodemirrorSettings(prefs) {
+ if (window.consoleEl) {
+ window.consoleEl.querySelector(
+ '.CodeMirror'
+ ).style.fontSize = `${parseInt(prefs.fontSize, 10)}px`;
+ }
+
+ // Replace correct css file in LINK tags's href
+ if (prefs.editorTheme) {
+ window.editorThemeLinkTag.href = `lib/codemirror/theme/${
+ prefs.editorTheme
+ }.css`;
+ }
+
+ window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
+ /fontname/g,
+ (prefs.editorFont === 'other'
+ ? prefs.editorCustomFont
+ : prefs.editorFont) || 'FiraCode'
+ );
+ }
+
+ // Check all the code wrap if they are minimized or maximized
+ updateCodeWrapCollapseStates() {
+ // This is debounced!
+ clearTimeout(this.updateCodeWrapCollapseStates.timeout);
+ this.updateCodeWrapCollapseStates.timeout = setTimeout(() => {
+ const { currentLayoutMode } = this.props;
+ const prop =
+ currentLayoutMode === 2 || currentLayoutMode === 5 ? 'width' : 'height';
+ [htmlCodeEl].forEach(function(el) {
+ const bounds = el.getBoundingClientRect();
+ const size = bounds[prop];
+ if (size < 100) {
+ el.classList.add('is-minimized');
+ } else {
+ el.classList.remove('is-minimized');
+ }
+ if (el.style[prop].indexOf(`100% - ${minCodeWrapSize * 2}px`) !== -1) {
+ el.classList.add('is-maximized');
+ } else {
+ el.classList.remove('is-maximized');
+ }
+ });
+ }, 50);
+ }
+
+ resetSplitting() {
+ this.setState({
+ mainSplitSizes: this.getMainSplitSizesToApply()
+ });
+ }
+ updateSplits() {
+ this.props.onSplitUpdate();
+ // Not using setState to avoid re-render
+ this.state.mainSplitSizes = this.props.currentItem.mainSizes;
+ }
+
+ // Returns the sizes of main code & preview panes.
+ getMainSplitSizesToApply() {
+ var mainSplitSizes;
+ const sidebarWidth = 200;
+ const { currentItem, currentLayoutMode } = this.props;
+ if (false && currentItem && currentItem.mainSizes) {
+ // For layout mode 3, main panes are reversed using flex-direction.
+ // So we need to apply the saved sizes in reverse order.
+ mainSplitSizes =
+ currentLayoutMode === 3
+ ? [currentItem.mainSizes[1], currentItem.mainSizes[0]]
+ : currentItem.mainSizes;
+ } else {
+ mainSplitSizes = [
+ `${sidebarWidth}px`,
+ `calc(50% - ${sidebarWidth / 2}px)`,
+ `calc(50% - ${sidebarWidth / 2}px)`
+ ];
+ }
+ return mainSplitSizes;
+ }
+
+ mainSplitDragEndHandler() {
+ if (this.props.prefs.refreshOnResize) {
+ // Running preview updation in next call stack, so that error there
+ // doesn't affect this dragend listener.
+ setTimeout(() => {
+ this.setPreviewContent(true);
+ }, 1);
+ }
+ this.updateSplits();
+ }
+
+ /**
+ * Loaded the code comiler based on the mode selected
+ */
+ handleModeRequirements(mode) {
+ const baseTranspilerPath = 'lib/transpilers';
+ // Exit if already loaded
+ var d = deferred();
+ if (modes[mode].hasLoaded) {
+ d.resolve();
+ return d.promise;
+ }
+
+ function setLoadedFlag() {
+ modes[mode].hasLoaded = true;
+ d.resolve();
+ }
+
+ if (mode === HtmlModes.JADE) {
+ loadJS(`${baseTranspilerPath}/jade.js`).then(setLoadedFlag);
+ } else if (mode === HtmlModes.MARKDOWN) {
+ loadJS(`${baseTranspilerPath}/marked.js`).then(setLoadedFlag);
+ } else if (mode === CssModes.LESS) {
+ loadJS(`${baseTranspilerPath}/less.min.js`).then(setLoadedFlag);
+ } else if (mode === CssModes.SCSS || mode === CssModes.SASS) {
+ loadJS(`${baseTranspilerPath}/sass.js`).then(function() {
+ window.sass = new Sass(`${baseTranspilerPath}/sass.worker.js`);
+ setLoadedFlag();
+ });
+ } else if (mode === CssModes.STYLUS) {
+ loadJS(`${baseTranspilerPath}/stylus.min.js`).then(setLoadedFlag);
+ } else if (mode === CssModes.ACSS) {
+ loadJS(`${baseTranspilerPath}/atomizer.browser.js`).then(setLoadedFlag);
+ } else if (mode === JsModes.COFFEESCRIPT) {
+ loadJS(`${baseTranspilerPath}/coffee-script.js`).then(setLoadedFlag);
+ } else if (mode === JsModes.ES6) {
+ loadJS(`${baseTranspilerPath}/babel.min.js`).then(setLoadedFlag);
+ } else if (mode === JsModes.TS) {
+ loadJS(`${baseTranspilerPath}/typescript.js`).then(setLoadedFlag);
+ } else {
+ d.resolve();
+ }
+
+ return d.promise;
+ }
+
+ updateHtmlMode(value) {
+ // this.props.onCodeModeChange('html', value);
+ // this.props.currentItem.htmlMode = value;
+ // this.cm.setOption('mode', modes[value].cmMode);
+ // CodeMirror.autoLoadMode(
+ // this.cm,
+ // modes[value].cmPath || modes[value].cmMode
+ // );
+ // return this.handleModeRequirements(value);
+ }
+ updateCssMode(value) {
+ // this.props.onCodeModeChange('css', value);
+ // this.props.currentItem.cssMode = value;
+ this.cm.setOption('mode', modes[value].cmMode);
+ this.cm.setOption('readOnly', modes[value].cmDisable);
+ /* window.cssSettingsBtn.classList[
+ modes[value].hasSettings ? 'remove' : 'add'
+ ]('hide'); */
+ CodeMirror.autoLoadMode(
+ this.cm,
+ modes[value].cmPath || modes[value].cmMode
+ );
+ return this.handleModeRequirements(value);
+ }
+ updateJsMode(value) {
+ this.cm.setOption('mode', modes[value].cmMode);
+ CodeMirror.autoLoadMode(
+ this.cm,
+ modes[value].cmPath || modes[value].cmMode
+ );
+ return this.handleModeRequirements(value);
+ }
+
+ getDemoFrame(callback) {
+ callback(this.frame);
+ }
+ editorFocusHandler(editor) {
+ this.props.onEditorFocus(editor);
+ }
+ fileSelectHandler(file) {
+ if (file.isFolder) {
+ this.props.onFolderSelect(file);
+ return;
+ }
+ this.setState({
+ editorOptions: this.getEditorOptions(file.name),
+ selectedFile: file
+ });
+ if (!this.fileBuffers[file.name]) {
+ this.createEditorDoc(file);
+ }
+ this.cm.swapDoc(this.fileBuffers[file.name]);
+
+ // var cmMode = 'html';
+ // if (file.name.match(/\.css$/)) {
+ // this.updateCssMode('css');
+ // } else if (file.name.match(/\.js$/)) {
+ // this.updateCssMode('js');
+ // } else {
+ // this.updateCssMode('html');
+ // }
+ // this.cm.setValue(file.content || '');
+ this.cm.focus();
+ }
+
+ updateLogCount() {
+ if (window.logCountEl) {
+ logCountEl.textContent = this.logCount;
+ }
+ }
+
+ onMessageFromConsole() {
+ /* eslint-disable no-param-reassign */
+ [...arguments].forEach(arg => {
+ if (
+ arg &&
+ arg.indexOf &&
+ arg.indexOf('filesystem:chrome-extension') !== -1
+ ) {
+ arg = arg.replace(
+ /filesystem:chrome-extension.*\.js:(\d+):*(\d*)/g,
+ 'script $1:$2'
+ );
+ }
+ try {
+ this.consoleCm.replaceRange(
+ arg +
+ ' ' +
+ ((arg + '').match(/\[object \w+]/) ? JSON.stringify(arg) : '') +
+ '\n',
+ {
+ line: Infinity
+ }
+ );
+ } catch (e) {
+ this.consoleCm.replaceRange('🌀\n', {
+ line: Infinity
+ });
+ }
+ this.consoleCm.scrollTo(0, Infinity);
+ this.logCount++;
+ });
+ this.updateLogCount();
+
+ /* eslint-enable no-param-reassign */
+ }
+
+ previewException(error) {
+ console.error('Possible infinite loop detected.', error.stack);
+ this.onMessageFromConsole('Possible infinite loop detected.', error.stack);
+ }
+
+ toggleConsole() {
+ this.setState({ isConsoleOpen: !this.state.isConsoleOpen });
+ trackEvent('ui', 'consoleToggle');
+ }
+ consoleHeaderDblClickHandler(e) {
+ if (!e.target.classList.contains('js-console__header')) {
+ return;
+ }
+ trackEvent('ui', 'consoleToggleDblClick');
+ this.toggleConsole();
+ }
+ clearConsole() {
+ this.consoleCm.setValue('');
+ this.logCount = 0;
+ this.updateLogCount();
+ }
+ clearConsoleBtnClickHandler() {
+ this.clearConsole();
+ trackEvent('ui', 'consoleClearBtnClick');
+ }
+
+ evalConsoleExpr(e) {
+ // Clear console on CTRL + L
+ if ((e.which === 76 || e.which === 12) && e.ctrlKey) {
+ this.clearConsole();
+ trackEvent('ui', 'consoleClearKeyboardShortcut');
+ } else if (e.which === 13) {
+ this.onMessageFromConsole('> ' + e.target.value);
+
+ /* eslint-disable no-underscore-dangle */
+ this.frame.contentWindow._wmEvaluate(e.target.value);
+
+ /* eslint-enable no-underscore-dangle */
+
+ e.target.value = '';
+ trackEvent('fn', 'evalConsoleExpr');
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ (this.cm = editor)}
+ onFocus={this.editorFocusHandler.bind(this)}
+ />
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/CreateNewModal.jsx b/src/components/CreateNewModal.jsx
index 7145860..8dc1699 100644
--- a/src/components/CreateNewModal.jsx
+++ b/src/components/CreateNewModal.jsx
@@ -7,6 +7,7 @@ export function CreateNewModal({
show,
closeHandler,
onBlankTemplateSelect,
+ onBlankFileTemplateSelect,
onTemplateSelect
}) {
return (
@@ -15,6 +16,9 @@ export function CreateNewModal({
+
Or choose from a template:
diff --git a/src/components/FileIcon.jsx b/src/components/FileIcon.jsx
new file mode 100644
index 0000000..46c2e8b
--- /dev/null
+++ b/src/components/FileIcon.jsx
@@ -0,0 +1,99 @@
+import { h } from 'preact';
+
+export function FileIcon({ file }) {
+ let path;
+ if (file.isFolder) {
+ if (!file.children.length) {
+ path = (
+
+ );
+ } else {
+ path = file.isCollapsed ? (
+
+ ) : (
+
+ );
+ }
+ } else {
+ const type = file.name.match(/.(\w+)$/)[1];
+ switch (type) {
+ case 'html':
+ path = (
+
+ );
+ break;
+
+ case 'js':
+ path = (
+
+ );
+ break;
+
+ case 'css':
+ path = (
+
+ );
+ break;
+
+ case 'md':
+ case 'markdown':
+ path = (
+
+ );
+ break;
+
+ case 'jpg':
+ case 'jpeg':
+ case 'svg':
+ case 'png':
+ path = (
+
+ );
+ break;
+ case 'json':
+ path = (
+
+ );
+ break;
+ default:
+ path = (
+
+ );
+ }
+ }
+ return (
+
+ );
+}
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
index 9d6d76a..ba76fa7 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.jsx
@@ -196,7 +196,7 @@ export default class Footer extends Component {