mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-11 17:16:26 +02:00
@ -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;
|
||||
}
|
||||
htmlCodeEl.querySelector(
|
||||
'.CodeMirror'
|
||||
).style.fontSize = cssCodeEl.querySelector(
|
||||
'.CodeMirror'
|
||||
).style.fontSize = jsCodeEl.querySelector(
|
||||
if (window.consoleEl) {
|
||||
window.consoleEl.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
|
||||
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
|
||||
|
640
src/components/ContentWrapFiles.jsx
Normal file
640
src/components/ContentWrapFiles.jsx
Normal file
@ -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] =
|
||||
'<script src="' +
|
||||
(chrome.extension
|
||||
? chrome.extension.getURL('lib/screenlog.js')
|
||||
: `${location.origin}${
|
||||
window.DEBUG ? '' : BASE_PATH
|
||||
}/lib/screenlog.js`) +
|
||||
'"></script>' +
|
||||
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 (
|
||||
<SplitPane
|
||||
class="content-wrap flex flex-grow"
|
||||
sizes={this.state.mainSplitSizes}
|
||||
minSize={150}
|
||||
style=""
|
||||
direction={
|
||||
this.props.currentLayoutMode === 2 ? 'vertical' : 'horizontal'
|
||||
}
|
||||
onDragEnd={this.mainSplitDragEndHandler.bind(this)}
|
||||
>
|
||||
<div id="js-sidebar">
|
||||
<SidePane
|
||||
files={this.props.currentItem.files || []}
|
||||
selectedFile={this.state.selectedFile}
|
||||
onFileSelect={this.fileSelectHandler.bind(this)}
|
||||
onAddFile={this.props.onAddFile}
|
||||
onRemoveFile={this.props.onRemoveFile}
|
||||
onRenameFile={this.props.onRenameFile}
|
||||
onFileDrop={this.props.onFileDrop}
|
||||
/>
|
||||
</div>
|
||||
<div class="code-side" id="js-code-side">
|
||||
<div
|
||||
data-code-wrap-id="0"
|
||||
id="htmlCodeEl"
|
||||
data-type="html"
|
||||
class="code-wrap"
|
||||
onTransitionEnd={this.updateCodeWrapCollapseStates.bind(this)}
|
||||
>
|
||||
<div
|
||||
class="js-code-wrap__header code-wrap__header"
|
||||
title="Double click to toggle code pane"
|
||||
>
|
||||
<label class="btn-group" dropdow title="Click to change">
|
||||
{this.state.selectedFile ? this.state.selectedFile.name : ''}
|
||||
</label>
|
||||
<div class="code-wrap__header-right-options">
|
||||
<a
|
||||
class="js-code-collapse-btn code-wrap__header-btn code-wrap__collapse-btn"
|
||||
title="Toggle code pane"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UserCodeMirror
|
||||
options={this.state.editorOptions}
|
||||
prefs={this.props.prefs}
|
||||
onChange={this.onHtmlCodeChange.bind(this)}
|
||||
onCreation={editor => (this.cm = editor)}
|
||||
onFocus={this.editorFocusHandler.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-side" id="js-demo-side" style="">
|
||||
<iframe
|
||||
ref={el => (this.frame = el)}
|
||||
src="/user/index.html"
|
||||
frameborder="0"
|
||||
id="demo-frame"
|
||||
allowfullscreen
|
||||
/>
|
||||
<Console
|
||||
isConsoleOpen={this.state.isConsoleOpen}
|
||||
onConsoleHeaderDblClick={this.consoleHeaderDblClickHandler.bind(
|
||||
this
|
||||
)}
|
||||
onClearConsoleBtnClick={this.clearConsoleBtnClickHandler.bind(this)}
|
||||
toggleConsole={this.toggleConsole.bind(this)}
|
||||
onEvalInputKeyup={this.evalConsoleExpr.bind(this)}
|
||||
onReady={el => (this.consoleCm = el)}
|
||||
/>
|
||||
</div>
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ export function CreateNewModal({
|
||||
show,
|
||||
closeHandler,
|
||||
onBlankTemplateSelect,
|
||||
onBlankFileTemplateSelect,
|
||||
onTemplateSelect
|
||||
}) {
|
||||
return (
|
||||
@ -15,6 +16,9 @@ export function CreateNewModal({
|
||||
<button className="btn" onClick={onBlankTemplateSelect}>
|
||||
Start a blank creation
|
||||
</button>
|
||||
<button className="btn" onClick={onBlankFileTemplateSelect}>
|
||||
Start a blank files creation
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
Or choose from a template:
|
||||
|
99
src/components/FileIcon.jsx
Normal file
99
src/components/FileIcon.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
export function FileIcon({ file }) {
|
||||
let path;
|
||||
if (file.isFolder) {
|
||||
if (!file.children.length) {
|
||||
path = (
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
path = file.isCollapsed ? (
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const type = file.name.match(/.(\w+)$/)[1];
|
||||
switch (type) {
|
||||
case 'html':
|
||||
path = (
|
||||
<path
|
||||
fill="rgb(225, 187, 21)"
|
||||
d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'js':
|
||||
path = (
|
||||
<path
|
||||
fill="rgb(255, 165, 0)"
|
||||
d="M3,3H21V21H3V3M7.73,18.04C8.13,18.89 8.92,19.59 10.27,19.59C11.77,19.59 12.8,18.79 12.8,17.04V11.26H11.1V17C11.1,17.86 10.75,18.08 10.2,18.08C9.62,18.08 9.38,17.68 9.11,17.21L7.73,18.04M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'css':
|
||||
path = (
|
||||
<path
|
||||
fill="rgb(95, 158, 160)"
|
||||
d="M5,3L4.35,6.34H17.94L17.5,8.5H3.92L3.26,11.83H16.85L16.09,15.64L10.61,17.45L5.86,15.64L6.19,14H2.85L2.06,18L9.91,21L18.96,18L20.16,11.97L20.4,10.76L21.94,3H5Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'md':
|
||||
case 'markdown':
|
||||
path = (
|
||||
<path
|
||||
fill="skyblue"
|
||||
d="M2,16V8H4L7,11L10,8H12V16H10V10.83L7,13.83L4,10.83V16H2M16,8H19V12H21.5L17.5,16.5L13.5,12H16V8Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'svg':
|
||||
case 'png':
|
||||
path = (
|
||||
<path
|
||||
fill="crimson"
|
||||
d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'json':
|
||||
path = (
|
||||
<path
|
||||
fill="orange"
|
||||
d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
path = (
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<svg class="sidebar__file-icon" viewBox="0 0 24 24">
|
||||
{path}
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -196,7 +196,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.props.saveHtmlBtnClickHandler}
|
||||
id="saveHtmlBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Save as HTML file"
|
||||
>
|
||||
<svg viewBox="0 0 24 24">
|
||||
@ -219,7 +219,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.props.codepenBtnClickHandler}
|
||||
id="codepenBtn"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile"
|
||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Edit on CodePen"
|
||||
>
|
||||
<svg>
|
||||
@ -243,7 +243,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 1)}
|
||||
id="layoutBtn1"
|
||||
class="mode-btn hide-on-mobile"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Switch to layout with preview on right"
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
||||
@ -253,7 +253,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 2)}
|
||||
id="layoutBtn2"
|
||||
class="mode-btn hide-on-mobile"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Switch to layout with preview on bottom"
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
@ -263,7 +263,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 3)}
|
||||
id="layoutBtn3"
|
||||
class="mode-btn hide-on-mobile"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Switch to layout with preview on left"
|
||||
>
|
||||
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
||||
@ -273,7 +273,7 @@ export default class Footer extends Component {
|
||||
<button
|
||||
onClick={this.layoutBtnClickhandler.bind(this, 5)}
|
||||
id="layoutBtn5"
|
||||
class="mode-btn hide-on-mobile"
|
||||
class="mode-btn hide-on-mobile hide-in-file-mode"
|
||||
aria-label="Switch to layout with all vertical panes"
|
||||
>
|
||||
<svg viewBox="0 0 100 100">
|
||||
|
@ -49,6 +49,14 @@ export function ItemTile({
|
||||
</div>
|
||||
) : null}
|
||||
<h3 class="saved-item-tile__title">{item.title}</h3>
|
||||
{item.files ? (
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
{item.updatedOn ? (
|
||||
<div class="saved-item-tile__meta">
|
||||
|
@ -27,6 +27,7 @@ export function MainHeader(props) {
|
||||
</svg>Run
|
||||
</button>
|
||||
|
||||
{!this.props.isFileMode && (
|
||||
<Button
|
||||
onClick={props.addLibraryBtnHandler}
|
||||
data-event-category="ui"
|
||||
@ -43,6 +44,7 @@ export function MainHeader(props) {
|
||||
{props.externalLibCount}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<button
|
||||
class="btn--dark flex flex-v-center hint--rounded hint--bottom-left"
|
||||
|
@ -33,6 +33,7 @@ export default class Settings extends Component {
|
||||
return true;
|
||||
}
|
||||
render() {
|
||||
const { prefs } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h1>Settings</h1>
|
||||
@ -47,7 +48,7 @@ export default class Settings extends Component {
|
||||
type="radio"
|
||||
name="indentation"
|
||||
value="spaces"
|
||||
checked={this.props.prefs.indentation === 'spaces'}
|
||||
checked={prefs.indentWith === 'spaces'}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
data-setting="indentWith"
|
||||
/>{' '}
|
||||
@ -58,7 +59,7 @@ export default class Settings extends Component {
|
||||
type="radio"
|
||||
name="indentation"
|
||||
value="tabs"
|
||||
checked={this.props.prefs.indentation === 'tabs'}
|
||||
checked={prefs.indentWith === 'tabs'}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
data-setting="indentWith"
|
||||
/>{' '}
|
||||
@ -70,14 +71,14 @@ export default class Settings extends Component {
|
||||
<input
|
||||
type="range"
|
||||
class="va-m ml-1"
|
||||
value={this.props.prefs.indentSize}
|
||||
value={prefs.indentSize}
|
||||
min="1"
|
||||
max="7"
|
||||
list="indentationSizeList"
|
||||
data-setting="indentSize"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<span id="indentationSizeValueEl">{this.props.prefs.indentSize}</span>
|
||||
<span id="indentationSizeValueEl">{prefs.indentSize}</span>
|
||||
<datalist id="indentationSizeList">
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
@ -99,7 +100,7 @@ export default class Settings extends Component {
|
||||
<select
|
||||
style="flex:1;margin-left:20px"
|
||||
data-setting="htmlMode"
|
||||
value={this.props.prefs.htmlMode}
|
||||
value={prefs.htmlMode}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
>
|
||||
<option value="html">HTML</option>
|
||||
@ -109,7 +110,7 @@ export default class Settings extends Component {
|
||||
<select
|
||||
style="flex:1;margin-left:20px"
|
||||
data-setting="cssMode"
|
||||
value={this.props.prefs.cssMode}
|
||||
value={prefs.cssMode}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
>
|
||||
<option value="css">CSS</option>
|
||||
@ -122,7 +123,7 @@ export default class Settings extends Component {
|
||||
<select
|
||||
style="flex:1;margin-left:20px"
|
||||
data-setting="jsMode"
|
||||
value={this.props.prefs.jsMode}
|
||||
value={prefs.jsMode}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
>
|
||||
<option value="js">JS</option>
|
||||
@ -136,7 +137,7 @@ export default class Settings extends Component {
|
||||
<select
|
||||
style="flex:1;margin:0 20px"
|
||||
data-setting="editorTheme"
|
||||
value={this.props.prefs.editorTheme}
|
||||
value={prefs.editorTheme}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
>
|
||||
{editorThemes.map(theme => (
|
||||
@ -149,7 +150,7 @@ export default class Settings extends Component {
|
||||
<select
|
||||
style="flex:1;margin:0 20px"
|
||||
data-setting="editorFont"
|
||||
value={this.props.prefs.editorFont}
|
||||
value={prefs.editorFont}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
>
|
||||
<option value="FiraCode">Fira Code</option>
|
||||
@ -159,11 +160,11 @@ export default class Settings extends Component {
|
||||
<option disabled="disabled">----</option>
|
||||
<option value="other">Other font from system</option>
|
||||
</select>
|
||||
{this.props.prefs.editorFont === 'other' && (
|
||||
{prefs.editorFont === 'other' && (
|
||||
<input
|
||||
id="customEditorFontInput"
|
||||
type="text"
|
||||
value={this.props.prefs.editorCustomFont}
|
||||
value={prefs.editorCustomFont}
|
||||
placeholder="Custom font name here"
|
||||
data-setting="editorCustomFont"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
@ -175,7 +176,7 @@ export default class Settings extends Component {
|
||||
<input
|
||||
style="width:70px"
|
||||
type="number"
|
||||
value={this.props.prefs.fontSize}
|
||||
value={prefs.fontSize}
|
||||
data-setting="fontSize"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>{' '}
|
||||
@ -188,7 +189,7 @@ export default class Settings extends Component {
|
||||
type="radio"
|
||||
name="keymap"
|
||||
value="sublime"
|
||||
checked={this.props.prefs.keymap === 'sublime'}
|
||||
checked={prefs.keymap === 'sublime'}
|
||||
data-setting="keymap"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>{' '}
|
||||
@ -199,7 +200,7 @@ export default class Settings extends Component {
|
||||
type="radio"
|
||||
name="keymap"
|
||||
value="vim"
|
||||
checked={this.props.prefs.keymap === 'vim'}
|
||||
checked={prefs.keymap === 'vim'}
|
||||
data-setting="keymap"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>{' '}
|
||||
@ -212,49 +213,49 @@ export default class Settings extends Component {
|
||||
name="lineWrap"
|
||||
title="Toggle wrapping of long sentences onto new line"
|
||||
label="Line wrap"
|
||||
pref={this.props.prefs.lineWrap}
|
||||
pref={prefs.lineWrap}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="refreshOnResize"
|
||||
title="Your Preview will refresh when you resize the preview split"
|
||||
label="Refresh preview on resize"
|
||||
pref={this.props.prefs.refreshOnResize}
|
||||
pref={prefs.refreshOnResize}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="autoComplete"
|
||||
title="Turns on the auto-completion suggestions as you type"
|
||||
label="Auto-complete suggestions"
|
||||
pref={this.props.prefs.autoComplete}
|
||||
pref={prefs.autoComplete}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="autoPreview"
|
||||
title="Refreshes the preview as you code. Otherwise use the Run button"
|
||||
label="Auto-preview"
|
||||
pref={this.props.prefs.autoPreview}
|
||||
pref={prefs.autoPreview}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="autoSave"
|
||||
title="Auto-save keeps saving your code at regular intervals after you hit the first save manually"
|
||||
label="Auto-save"
|
||||
pref={this.props.prefs.autoSave}
|
||||
pref={prefs.autoSave}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="preserveLastCode"
|
||||
title="Loads the last open creation when app starts"
|
||||
label="Preserve last written code"
|
||||
pref={this.props.prefs.preserveLastCode}
|
||||
pref={prefs.preserveLastCode}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="replaceNewTab"
|
||||
title="Turning this on will start showing Web Maker in every new tab you open"
|
||||
label="Replace new tab page"
|
||||
pref={this.props.prefs.replaceNewTab}
|
||||
pref={prefs.replaceNewTab}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
showWhenExtension
|
||||
/>
|
||||
@ -262,14 +263,14 @@ export default class Settings extends Component {
|
||||
name="preserveConsoleLogs"
|
||||
title="Preserves the console logs across your preview refreshes"
|
||||
label="Preserve console logs"
|
||||
pref={this.props.prefs.preserveConsoleLogs}
|
||||
pref={prefs.preserveConsoleLogs}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
<CheckboxSetting
|
||||
name="lightVersion"
|
||||
title="Switch to lighter version for better performance. Removes things like blur etc."
|
||||
label="Fast/light version"
|
||||
pref={this.props.prefs.lightVersion}
|
||||
pref={prefs.lightVersion}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
@ -283,7 +284,7 @@ export default class Settings extends Component {
|
||||
title="Enjoy wonderful particle blasts while you type"
|
||||
label="Code blast!"
|
||||
name="isCodeBlastOn"
|
||||
pref={this.props.prefs.isCodeBlastOn}
|
||||
pref={prefs.isCodeBlastOn}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
|
||||
@ -291,7 +292,7 @@ export default class Settings extends Component {
|
||||
title="Get ready to build some games at JS13KGames"
|
||||
label="Js13kGames Mode"
|
||||
name="isJs13kModeOn"
|
||||
pref={this.props.prefs.isJs13kModeOn}
|
||||
pref={prefs.isJs13kModeOn}
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>
|
||||
</p>
|
||||
@ -307,7 +308,7 @@ export default class Settings extends Component {
|
||||
Maximum time allowed in a loop iteration{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={this.props.prefs.infiniteLoopTimeout}
|
||||
value={prefs.infiniteLoopTimeout}
|
||||
data-setting="infiniteLoopTimeout"
|
||||
onChange={this.updateSetting.bind(this)}
|
||||
/>{' '}
|
||||
|
295
src/components/SidePane.jsx
Normal file
295
src/components/SidePane.jsx
Normal file
@ -0,0 +1,295 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { FileIcon } from './FileIcon';
|
||||
import { getParentPath, getFileFromPath } from '../fileUtils';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
const ESCAPE_KEY = 27;
|
||||
|
||||
function File({
|
||||
file,
|
||||
selectedFile,
|
||||
fileBeingRenamed,
|
||||
onFileSelect,
|
||||
onRenameBtnClick,
|
||||
onRemoveBtnClick,
|
||||
onNameInputBlur,
|
||||
onNameInputKeyUp,
|
||||
onFileDrop
|
||||
}) {
|
||||
function focusInput(el) {
|
||||
el &&
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 1);
|
||||
}
|
||||
function dragStartHandler(e) {
|
||||
e.dataTransfer.setData('text/plain', file.path);
|
||||
}
|
||||
function dragOverHandler(e) {
|
||||
if (file.isFolder) {
|
||||
e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
e.currentTarget.classList.add('is-being-dragged-over');
|
||||
e.currentTarget.style.outline = '1px dashed';
|
||||
}
|
||||
}
|
||||
function dragLeaveHandler(e) {
|
||||
if (file.isFolder) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.outline = null;
|
||||
}
|
||||
}
|
||||
function dropHandler(e) {
|
||||
e.stopPropagation();
|
||||
if (file.isFolder) {
|
||||
e.preventDefault();
|
||||
onFileDrop(e.dataTransfer.getData('text/plain'), file);
|
||||
e.currentTarget.style.outline = null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onDragOver={dragOverHandler}
|
||||
onDragLeave={dragLeaveHandler}
|
||||
onDrop={dropHandler}
|
||||
>
|
||||
{file === fileBeingRenamed ? (
|
||||
<input
|
||||
type="text"
|
||||
ref={focusInput}
|
||||
value={file.name}
|
||||
onBlur={onNameInputBlur}
|
||||
onKeyUp={onNameInputKeyUp}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
class={`sidebar__file ${selectedFile === file ? 'selected' : ''}`}
|
||||
type="button"
|
||||
draggable={file.name !== 'index.html'}
|
||||
onDragStart={dragStartHandler}
|
||||
onClick={onFileSelect.bind(null, file)}
|
||||
>
|
||||
<div class="flex flex-v-center">
|
||||
<FileIcon file={file} />
|
||||
{file.name}
|
||||
</div>
|
||||
<div class="sidebar__file-options">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--dark"
|
||||
onClick={onRenameBtnClick.bind(null, file)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style="vertical-align:middle;width:14px;height:14px"
|
||||
>
|
||||
<path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--dark"
|
||||
onClick={onRemoveBtnClick.bind(null, file)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style="vertical-align:middle;width:14px;height:14px"
|
||||
>
|
||||
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Folder(props) {
|
||||
return (
|
||||
<div>
|
||||
<File {...props} file={props.file} />
|
||||
<div class="sidebar__folder-wrap" data-collapsed={props.file.isCollapsed}>
|
||||
{props.file.children.map(
|
||||
childFile =>
|
||||
childFile.isFolder ? (
|
||||
<Folder {...props} file={childFile} />
|
||||
) : (
|
||||
<File {...props} file={childFile} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class SidePane extends Component {
|
||||
addFileButtonClickHandler() {
|
||||
this.setState({ isAddingFile: true });
|
||||
}
|
||||
addFolderButtonClickHandler() {
|
||||
this.setState({ isAddingFolder: true });
|
||||
}
|
||||
/**
|
||||
* Checks if the passed filename already exists and if so, warns the user.
|
||||
* Also it passes false if the validation fails.
|
||||
* @param {string} newFileName New file name to validate
|
||||
*/
|
||||
warnForExistingFileName(newFileName) {
|
||||
// We also check for fileBeingRenamed !== file because when a file being renamed is
|
||||
// asked to be set as same name, then that should not match and warn here.
|
||||
let newPath = this.state.fileBeingRenamed
|
||||
? `${getParentPath(this.state.fileBeingRenamed.path)}/${newFileName}`
|
||||
: newFileName;
|
||||
// remove first slash
|
||||
newPath = newPath.replace(/^\//, '');
|
||||
const match = getFileFromPath(this.props.files, newPath);
|
||||
|
||||
if (match.file && this.state.fileBeingRenamed !== match.file) {
|
||||
alert(`A file with name ${newFileName} already exists.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
addFile(e) {
|
||||
// This gets called twice when enter is pressed, because blur also fires.
|
||||
// So check `isAddingFile` before proceeding.
|
||||
if (!this.state.isAddingFile && !this.state.isAddingFolder) {
|
||||
return;
|
||||
}
|
||||
const newFileName = e.target.value;
|
||||
if (!this.warnForExistingFileName(newFileName)) {
|
||||
return;
|
||||
}
|
||||
if (newFileName) {
|
||||
this.props.onAddFile(newFileName, this.state.isAddingFolder);
|
||||
}
|
||||
this.setState({ isAddingFile: false, isAddingFolder: false });
|
||||
}
|
||||
newFileNameInputKeyDownHandler(e) {
|
||||
if (e.which === ENTER_KEY) {
|
||||
this.addFile(e);
|
||||
} else if (e.which === ESCAPE_KEY) {
|
||||
this.setState({ isAddingFile: false, isAddingFolder: false });
|
||||
}
|
||||
}
|
||||
removeFileClickHandler(file, e) {
|
||||
e.stopPropagation();
|
||||
const answer = confirm(`Are you sure you want to delete "${file.name}"?`);
|
||||
if (answer) {
|
||||
this.props.onRemoveFile(file.path);
|
||||
}
|
||||
}
|
||||
renameFile(e) {
|
||||
// This gets called twice when enter is pressed, because blur also fires.
|
||||
if (!e.target || !this.state.fileBeingRenamed) {
|
||||
return;
|
||||
}
|
||||
const newFileName = e.target.value;
|
||||
if (!this.warnForExistingFileName(newFileName)) {
|
||||
return;
|
||||
}
|
||||
if (newFileName) {
|
||||
this.props.onRenameFile(this.state.fileBeingRenamed.path, newFileName);
|
||||
}
|
||||
this.setState({ fileBeingRenamed: null });
|
||||
}
|
||||
renameFileNameInputKeyUpHandler(e) {
|
||||
if (e.which === ENTER_KEY) {
|
||||
this.renameFile(e);
|
||||
} else if (e.which === ESCAPE_KEY) {
|
||||
this.setState({ fileBeingRenamed: null });
|
||||
}
|
||||
}
|
||||
renameFileClickHandler(file, e) {
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
fileBeingRenamed: file
|
||||
});
|
||||
}
|
||||
|
||||
dragOverHandler(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
dropHandler(e) {
|
||||
e.preventDefault();
|
||||
// Object with `children` key is to simulate a folder structure for root folder
|
||||
this.props.onFileDrop(e.dataTransfer.getData('text/plain'), {
|
||||
children: this.props.files
|
||||
});
|
||||
// e.currentTarget.style.outline = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { files, onFileSelect, selectedFile, onRemoveFile } = this.props;
|
||||
const moreProps = {
|
||||
onRemoveBtnClick: this.removeFileClickHandler.bind(this),
|
||||
onRenameBtnClick: this.renameFileClickHandler.bind(this),
|
||||
onNameInputBlur: this.renameFile.bind(this),
|
||||
onNameInputKeyUp: this.renameFileNameInputKeyUpHandler.bind(this),
|
||||
fileBeingRenamed: this.state.fileBeingRenamed
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class="sidebar"
|
||||
onDragOver={this.dragOverHandler.bind(this)}
|
||||
onDrop={this.dropHandler.bind(this)}
|
||||
>
|
||||
<div class="flex jc-sb" style="padding: 5px 4px">
|
||||
Files
|
||||
<div class="flex flex-v-center">
|
||||
<button
|
||||
type="button"
|
||||
class="btn--dark"
|
||||
onClick={this.addFileButtonClickHandler.bind(this)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style="vertical-align:middle;width:14px;height:14px"
|
||||
>
|
||||
<path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,15V12H9V15H6V17H9V20H11V17H14V15H11Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn--dark"
|
||||
onClick={this.addFolderButtonClickHandler.bind(this)}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style="vertical-align:middle;width:14px;height:14px"
|
||||
>
|
||||
<path d="M10,4L12,6H20A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M15,9V12H12V14H15V17H17V14H20V12H17V9H15Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.isAddingFile || this.state.isAddingFolder ? (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
ref={el => {
|
||||
el &&
|
||||
setTimeout(() => {
|
||||
el.focus();
|
||||
}, 1);
|
||||
}}
|
||||
onBlur={this.addFile.bind(this)}
|
||||
onKeyUp={this.newFileNameInputKeyDownHandler.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{files.map(
|
||||
file =>
|
||||
file.isFolder ? (
|
||||
<Folder {...this.props} {...moreProps} file={file} />
|
||||
) : (
|
||||
<File {...this.props} {...moreProps} file={file} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -38,27 +38,58 @@ emmet(CodeMirror);
|
||||
export default class UserCodeMirror extends Component {
|
||||
componentDidMount() {
|
||||
this.initEditor();
|
||||
this.textarea.parentNode.querySelector(
|
||||
'.CodeMirror'
|
||||
).style.fontSize = `${parseInt(this.props.prefs.fontSize, 10)}px`;
|
||||
}
|
||||
shouldComponentUpdate() {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.prefs !== this.props.prefs) {
|
||||
const { prefs } = nextProps;
|
||||
console.log('updating CM prefs', prefs);
|
||||
|
||||
this.cm.setOption('indentWithTabs', prefs.indentWith !== 'spaces');
|
||||
this.cm.setOption(
|
||||
'blastCode',
|
||||
prefs.isCodeBlastOn ? { effect: 2, shake: false } : false
|
||||
);
|
||||
this.cm.setOption('theme', prefs.editorTheme);
|
||||
|
||||
this.cm.setOption('indentUnit', +prefs.indentSize);
|
||||
this.cm.setOption('tabSize', +prefs.indentSize);
|
||||
|
||||
this.cm.setOption('keyMap', prefs.keymap);
|
||||
this.cm.setOption('lineWrapping', prefs.lineWrap);
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.parentNode.querySelector(
|
||||
'.CodeMirror'
|
||||
).style.fontSize = `${parseInt(prefs.fontSize, 10)}px`;
|
||||
}
|
||||
|
||||
this.cm.refresh();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
initEditor() {
|
||||
const options = this.props.options;
|
||||
const { options, prefs } = this.props;
|
||||
this.cm = CodeMirror.fromTextArea(this.textarea, {
|
||||
mode: options.mode,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
lineWrapping: !!prefs.lineWrap,
|
||||
autofocus: options.autofocus || false,
|
||||
autoCloseBrackets: true,
|
||||
autoCloseTags: true,
|
||||
matchBrackets: true,
|
||||
matchTags: options.matchTags || false,
|
||||
tabMode: 'indent',
|
||||
keyMap: 'sublime',
|
||||
theme: 'monokai',
|
||||
keyMap: prefs.keyMap || 'sublime',
|
||||
theme: prefs.editorTheme || 'monokai',
|
||||
lint: !!options.lint,
|
||||
tabSize: 2,
|
||||
tabSize: +prefs.indentSize || 2,
|
||||
indentWithTabs: prefs.indentWith !== 'spaces',
|
||||
indentUnit: +prefs.indentSize,
|
||||
foldGutter: true,
|
||||
styleActiveLine: true,
|
||||
gutters: options.gutters || [],
|
||||
@ -91,7 +122,7 @@ export default class UserCodeMirror extends Component {
|
||||
const input = $('[data-setting=indentWith]:checked');
|
||||
if (
|
||||
!editor.somethingSelected() &&
|
||||
(!input || input.value === 'spaces')
|
||||
(!prefs.indentWith || prefs.indentWith === 'spaces')
|
||||
) {
|
||||
// softtabs adds spaces. This is required because by default tab key will put tab, but we want
|
||||
// to indent with spaces if `spaces` is preferred mode of indentation.
|
||||
@ -111,10 +142,11 @@ export default class UserCodeMirror extends Component {
|
||||
this.cm.addKeyMap({
|
||||
'Ctrl-Space': 'autocomplete'
|
||||
});
|
||||
if (!options.noAutocomplete) {
|
||||
this.cm.on('inputRead', (editor, input) => {
|
||||
// Process further If this has autocompletition on and also the global
|
||||
// autocomplete setting is on.
|
||||
if (!this.props.options.noAutocomplete && this.props.prefs.autoComplete) {
|
||||
if (
|
||||
!this.props.prefs.autoComplete ||
|
||||
input.origin !== '+input' ||
|
||||
input.text[0] === ';' ||
|
||||
input.text[0] === ',' ||
|
||||
@ -125,8 +157,8 @@ export default class UserCodeMirror extends Component {
|
||||
CodeMirror.commands.autocomplete(this.cm, null, {
|
||||
completeSingle: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
this.props.onCreation(this.cm);
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,10 @@
|
||||
*/
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import '../service-worker-registration';
|
||||
import { MainHeader } from './MainHeader.jsx';
|
||||
import ContentWrap from './ContentWrap.jsx';
|
||||
import ContentWrapFiles from './ContentWrapFiles.jsx';
|
||||
import Footer from './Footer.jsx';
|
||||
import SavedItemPane from './SavedItemPane.jsx';
|
||||
import AddLibrary from './AddLibrary.jsx';
|
||||
@ -21,6 +22,13 @@ import {
|
||||
getCompleteHtml,
|
||||
getFilenameFromUrl
|
||||
} from '../utils';
|
||||
import {
|
||||
linearizeFiles,
|
||||
assignFilePaths,
|
||||
getFileFromPath,
|
||||
removeFileAtPath,
|
||||
doesFileExistInFolder
|
||||
} from '../fileUtils';
|
||||
import { itemService } from '../itemService';
|
||||
import '../db';
|
||||
import { Notifications } from './Notifications';
|
||||
@ -194,7 +202,7 @@ export default class App extends Component {
|
||||
this.createNewItem();
|
||||
}
|
||||
Object.assign(this.state.prefs, result);
|
||||
this.setState({ prefs: this.state.prefs });
|
||||
this.setState({ prefs: { ...this.state.prefs } });
|
||||
this.updateSetting();
|
||||
});
|
||||
|
||||
@ -231,6 +239,22 @@ export default class App extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
incrementUnsavedChanges() {
|
||||
this.setState({ unsavedEditCount: this.state.unsavedEditCount + 1 });
|
||||
|
||||
if (
|
||||
this.state.unsavedEditCount % UNSAVED_WARNING_COUNT === 0 &&
|
||||
this.state.unsavedEditCount >= UNSAVED_WARNING_COUNT
|
||||
) {
|
||||
window.saveBtn.classList.add('animated');
|
||||
window.saveBtn.classList.add('wobble');
|
||||
window.saveBtn.addEventListener('animationend', () => {
|
||||
window.saveBtn.classList.remove('animated');
|
||||
window.saveBtn.classList.remove('wobble');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateProfileUi() {
|
||||
if (this.state.user) {
|
||||
document.body.classList.add('is-logged-in');
|
||||
@ -264,9 +288,9 @@ export default class App extends Component {
|
||||
alertsService.add(`"${sourceItem.title}" was forked`);
|
||||
trackEvent('fn', 'itemForked');
|
||||
}
|
||||
createNewItem() {
|
||||
var d = new Date();
|
||||
this.setCurrentItem({
|
||||
createNewItem(isFileMode = false) {
|
||||
const d = new Date();
|
||||
let item = {
|
||||
title:
|
||||
'Untitled ' +
|
||||
d.getDate() +
|
||||
@ -276,12 +300,38 @@ export default class App extends Component {
|
||||
d.getHours() +
|
||||
':' +
|
||||
d.getMinutes(),
|
||||
createdOn: +d,
|
||||
content: ''
|
||||
};
|
||||
if (isFileMode) {
|
||||
item = {
|
||||
...item,
|
||||
files: assignFilePaths([
|
||||
{ name: 'index.html', content: '' },
|
||||
{
|
||||
name: 'styles',
|
||||
isFolder: true,
|
||||
children: [{ name: 'style.css', content: '' }]
|
||||
},
|
||||
{ name: 'script.js', content: '' },
|
||||
{
|
||||
name: 'tempo',
|
||||
isFolder: true,
|
||||
children: [{ name: 'main.css', content: '' }]
|
||||
}
|
||||
])
|
||||
};
|
||||
} else {
|
||||
item = {
|
||||
...item,
|
||||
html: '',
|
||||
css: '',
|
||||
js: '',
|
||||
externalLibs: { js: '', css: '' },
|
||||
layoutMode: this.state.currentLayoutMode
|
||||
}).then(() => this.refreshEditor());
|
||||
};
|
||||
}
|
||||
this.setCurrentItem(item).then(() => this.refreshEditor());
|
||||
alertsService.add('New item created');
|
||||
}
|
||||
openItem(item) {
|
||||
@ -317,10 +367,12 @@ export default class App extends Component {
|
||||
setCurrentItem(item) {
|
||||
const d = deferred();
|
||||
// TODO: remove later
|
||||
if (!item.files) {
|
||||
item.htmlMode =
|
||||
item.htmlMode || this.state.prefs.htmlMode || HtmlModes.HTML;
|
||||
item.cssMode = item.cssMode || this.state.prefs.cssMode || CssModes.CSS;
|
||||
item.jsMode = item.jsMode || this.state.prefs.jsMode || JsModes.JS;
|
||||
}
|
||||
|
||||
this.setState({ currentItem: item }, d.resolve);
|
||||
|
||||
@ -470,7 +522,9 @@ export default class App extends Component {
|
||||
isKeyboardShortcutsModalOpen: !this.state.isKeyboardShortcutsModalOpen
|
||||
});
|
||||
trackEvent('ui', 'showKeyboardShortcutsShortcut');
|
||||
} else if (event.keyCode === 27) {
|
||||
} else if (event.keyCode === 27 && event.target.tagName !== 'INPUT') {
|
||||
// We might be listening on keydown for some input inside the app. In that case
|
||||
// we don't want this to trigger which in turn focuses back the last editor.
|
||||
this.closeSavedItemsPane();
|
||||
}
|
||||
});
|
||||
@ -684,21 +738,17 @@ export default class App extends Component {
|
||||
this.setState({ currentItem: item });
|
||||
}
|
||||
onCodeChange(type, code, isUserChange) {
|
||||
this.state.currentItem[type] = code;
|
||||
if (isUserChange) {
|
||||
this.setState({ unsavedEditCount: this.state.unsavedEditCount + 1 });
|
||||
|
||||
if (
|
||||
this.state.unsavedEditCount % UNSAVED_WARNING_COUNT === 0 &&
|
||||
this.state.unsavedEditCount >= UNSAVED_WARNING_COUNT
|
||||
) {
|
||||
window.saveBtn.classList.add('animated');
|
||||
window.saveBtn.classList.add('wobble');
|
||||
window.saveBtn.addEventListener('animationend', () => {
|
||||
window.saveBtn.classList.remove('animated');
|
||||
window.saveBtn.classList.remove('wobble');
|
||||
});
|
||||
if (this.state.currentItem.files) {
|
||||
linearizeFiles(this.state.currentItem.files).map(file => {
|
||||
if (file.name === type.name) {
|
||||
file.content = code;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.state.currentItem[type] = code;
|
||||
}
|
||||
if (isUserChange) {
|
||||
this.incrementUnsavedChanges();
|
||||
}
|
||||
if (this.state.prefs.isJs13kModeOn) {
|
||||
// Throttling codesize calculation
|
||||
@ -1140,6 +1190,10 @@ export default class App extends Component {
|
||||
this.createNewItem();
|
||||
this.setState({ isCreateNewModalOpen: false });
|
||||
}
|
||||
blankFileTemplateSelectHandler() {
|
||||
this.createNewItem(true);
|
||||
this.setState({ isCreateNewModalOpen: false });
|
||||
}
|
||||
|
||||
templateSelectHandler(template) {
|
||||
fetch(`templates/template-${template.id}.json`)
|
||||
@ -1149,10 +1203,97 @@ export default class App extends Component {
|
||||
});
|
||||
this.setState({ isCreateNewModalOpen: false });
|
||||
}
|
||||
addFileHandler(fileName, isFolder) {
|
||||
let newEntry = { name: fileName, content: '' };
|
||||
if (isFolder) {
|
||||
newEntry = {
|
||||
...newEntry,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
isCollapsed: true
|
||||
};
|
||||
}
|
||||
let currentItem = {
|
||||
...this.state.currentItem,
|
||||
files: [...this.state.currentItem.files, newEntry]
|
||||
};
|
||||
assignFilePaths(currentItem.files);
|
||||
|
||||
this.setState({ currentItem });
|
||||
this.incrementUnsavedChanges();
|
||||
}
|
||||
removeFileHandler(filePath) {
|
||||
const currentItem = {
|
||||
...this.state.currentItem,
|
||||
files: [...this.state.currentItem.files]
|
||||
};
|
||||
removeFileAtPath(currentItem.files, filePath);
|
||||
|
||||
this.setState({ currentItem });
|
||||
this.incrementUnsavedChanges();
|
||||
}
|
||||
renameFileHandler(oldFilePath, newFileName) {
|
||||
const currentItem = {
|
||||
...this.state.currentItem,
|
||||
files: [...this.state.currentItem.files]
|
||||
};
|
||||
const { file } = getFileFromPath(currentItem.files, oldFilePath);
|
||||
file.name = newFileName;
|
||||
assignFilePaths(currentItem.files);
|
||||
|
||||
this.setState({ currentItem });
|
||||
this.incrementUnsavedChanges();
|
||||
}
|
||||
fileDropHandler(sourceFilePath, destinationFolder) {
|
||||
let { currentItem } = this.state;
|
||||
const { file } = getFileFromPath(currentItem.files, sourceFilePath);
|
||||
if (doesFileExistInFolder(destinationFolder, file.name)) {
|
||||
alert(
|
||||
`File with name "${
|
||||
file.name
|
||||
}" already exists in the destination folder.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
destinationFolder.children.push(file);
|
||||
removeFileAtPath(currentItem.files, sourceFilePath);
|
||||
currentItem = {
|
||||
...currentItem,
|
||||
files: [...currentItem.files]
|
||||
};
|
||||
assignFilePaths(currentItem.files);
|
||||
|
||||
this.setState({ currentItem });
|
||||
this.incrementUnsavedChanges();
|
||||
}
|
||||
}
|
||||
|
||||
folderSelectHandler(folder) {
|
||||
// Following will make the change in the existing currentItem
|
||||
folder.isCollapsed = !folder.isCollapsed;
|
||||
|
||||
const currentItem = {
|
||||
...this.state.currentItem,
|
||||
files: [...this.state.currentItem.files]
|
||||
};
|
||||
this.setState({
|
||||
currentItem
|
||||
});
|
||||
}
|
||||
|
||||
getRootClasses() {
|
||||
const classes = [];
|
||||
if (this.state.currentItem && this.state.currentItem.files) {
|
||||
classes.push('is-file-mode');
|
||||
}
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div class={this.getRootClasses()}>
|
||||
<div class="main-container">
|
||||
<MainHeader
|
||||
externalLibCount={this.state.externalLibCount}
|
||||
@ -1169,7 +1310,25 @@ export default class App extends Component {
|
||||
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
|
||||
user={this.state.user}
|
||||
unsavedEditCount={this.state.unsavedEditCount}
|
||||
isFileMode={this.state.currentItem && this.state.currentItem.files}
|
||||
/>
|
||||
{this.state.currentItem && this.state.currentItem.files ? (
|
||||
<ContentWrapFiles
|
||||
currentItem={this.state.currentItem}
|
||||
onCodeChange={this.onCodeChange.bind(this)}
|
||||
onCodeSettingsChange={this.onCodeSettingsChange.bind(this)}
|
||||
onCodeModeChange={this.onCodeModeChange.bind(this)}
|
||||
onRef={comp => (this.contentWrap = comp)}
|
||||
prefs={this.state.prefs}
|
||||
onEditorFocus={this.editorFocusHandler.bind(this)}
|
||||
onSplitUpdate={this.splitUpdateHandler.bind(this)}
|
||||
onAddFile={this.addFileHandler.bind(this)}
|
||||
onRemoveFile={this.removeFileHandler.bind(this)}
|
||||
onRenameFile={this.renameFileHandler.bind(this)}
|
||||
onFileDrop={this.fileDropHandler.bind(this)}
|
||||
onFolderSelect={this.folderSelectHandler.bind(this)}
|
||||
/>
|
||||
) : (
|
||||
<ContentWrap
|
||||
currentLayoutMode={this.state.currentLayoutMode}
|
||||
currentItem={this.state.currentItem}
|
||||
@ -1181,6 +1340,7 @@ export default class App extends Component {
|
||||
onEditorFocus={this.editorFocusHandler.bind(this)}
|
||||
onSplitUpdate={this.splitUpdateHandler.bind(this)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Footer
|
||||
prefs={this.state.prefs}
|
||||
@ -1337,6 +1497,9 @@ export default class App extends Component {
|
||||
show={this.state.isCreateNewModalOpen}
|
||||
closeHandler={() => this.setState({ isCreateNewModalOpen: false })}
|
||||
onBlankTemplateSelect={this.blankTemplateSelectHandler.bind(this)}
|
||||
onBlankFileTemplateSelect={this.blankFileTemplateSelectHandler.bind(
|
||||
this
|
||||
)}
|
||||
onTemplateSelect={this.templateSelectHandler.bind(this)}
|
||||
/>
|
||||
|
||||
|
109
src/fileUtils.js
Normal file
109
src/fileUtils.js
Normal file
@ -0,0 +1,109 @@
|
||||
import { deferred } from './deferred';
|
||||
const esprima = require('esprima');
|
||||
|
||||
/**
|
||||
* Returns a linear file list from a nested file strcuture.
|
||||
* It excludes the folders from the returned list.
|
||||
* @param {array} files Nested file structure
|
||||
*/
|
||||
export function linearizeFiles(files) {
|
||||
function reduceToLinearFiles(files) {
|
||||
return files.reduce((list, currentFile) => {
|
||||
if (currentFile.isFolder) {
|
||||
return [...list, ...reduceToLinearFiles(currentFile.children)];
|
||||
} else {
|
||||
return [...list, currentFile];
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
return reduceToLinearFiles(files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterates and assigns the `path` property to the files in passed files
|
||||
* array.
|
||||
* @param {array} files files structure for an item
|
||||
* @param {string} parentPath Parent path to prefix with all processed files
|
||||
*/
|
||||
export function assignFilePaths(files, parentPath = '') {
|
||||
files.forEach(file => {
|
||||
file.path = parentPath ? `${parentPath}/${file.name}` : file.name;
|
||||
if (file.isFolder) {
|
||||
assignFilePaths(
|
||||
file.children,
|
||||
parentPath ? `${parentPath}/${file.name}` : file.name
|
||||
);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file object and it's index that is direct child of passed files array with name as passed fileName.
|
||||
* If not found, returns -1
|
||||
* @param {array} files files structure for an item
|
||||
* @param {string} fileName File/folder name
|
||||
*/
|
||||
export function getChildFileFromName(files, fileName) {
|
||||
const index = files.findIndex(file => file.name === fileName);
|
||||
return { index, file: files[index] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file object and it's index in its parent for the passed path.
|
||||
* If not found, returns {index:-1}
|
||||
* @param {array} files files structure for an item
|
||||
* @param {string} path Path of file to search
|
||||
*/
|
||||
export function getFileFromPath(files, path) {
|
||||
let currentFolder = files;
|
||||
const pathPieces = path.split('/');
|
||||
while (pathPieces.length > 1) {
|
||||
let folderName = pathPieces.shift();
|
||||
currentFolder = getChildFileFromName(currentFolder, folderName).file
|
||||
.children;
|
||||
}
|
||||
// now we should be left with just one value in the pathPieces array - the actual file name
|
||||
return getChildFileFromName(currentFolder, pathPieces[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file object and it's index in its parent for the passed path.
|
||||
* If not found, returns {index:-1}
|
||||
* @param {array} files files structure for an item
|
||||
* @param {string} path Path of file to search
|
||||
*/
|
||||
export function removeFileAtPath(files, path) {
|
||||
let currentFolder = files;
|
||||
const pathPieces = path.split('/');
|
||||
while (pathPieces.length > 1) {
|
||||
let folderName = pathPieces.shift();
|
||||
currentFolder = getChildFileFromName(currentFolder, folderName).file
|
||||
.children;
|
||||
}
|
||||
// now we should be left with just one value in the pathPieces array - the actual file name
|
||||
const { index } = getChildFileFromName(currentFolder, pathPieces[0]);
|
||||
currentFolder.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file with same name exists in the passed folder.
|
||||
* @param {object} folder Folder to search in
|
||||
* @param {string} fileName File name to search for
|
||||
*/
|
||||
export function doesFileExistInFolder(folder, fileName) {
|
||||
const details = getChildFileFromName(folder.children, fileName);
|
||||
return !!details.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parent path of passed path
|
||||
* @param {string} path Path of file to find parent of
|
||||
*/
|
||||
export function getParentPath(path) {
|
||||
const pathPieces = path.split('/');
|
||||
if (pathPieces.length > 1) {
|
||||
return pathPieces.slice(0, -1).join('/');
|
||||
}
|
||||
return '';
|
||||
}
|
@ -19,7 +19,6 @@
|
||||
if (
|
||||
'serviceWorker' in navigator &&
|
||||
location.protocol === 'https:' &&
|
||||
document.cookie.indexOf('wmdebug') === -1 &&
|
||||
location.href.indexOf('chrome-extension://') === -1 &&
|
||||
location.href.indexOf('192.168') === -1
|
||||
) {
|
||||
|
40
src/service-worker.js
Normal file
40
src/service-worker.js
Normal file
@ -0,0 +1,40 @@
|
||||
self.addEventListener('fetch', function(event) {
|
||||
// console.log("fetch event", event.request.url);
|
||||
if (event.request.url.indexOf('/user/') !== -1) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(function(response) {
|
||||
// console.log("responding with ", response);
|
||||
if (response !== undefined) {
|
||||
return response;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getContentType(url) {
|
||||
if (url.match(/\.html$/)) {
|
||||
return 'text/html; charset=UTF-8';
|
||||
} else if (url.match(/\.css$/)) {
|
||||
return 'text/css; charset=UTF-8';
|
||||
}
|
||||
if (url.match(/\.js$/)) {
|
||||
return 'application/javascript; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
self.addEventListener('message', function(e) {
|
||||
// console.log("message aya sw main", e.data);
|
||||
caches.open('v1').then(function(cache) {
|
||||
for (url in e.data) {
|
||||
// console.log('Received data', url, e.data[url])
|
||||
cache.put(
|
||||
url,
|
||||
new Response(e.data[url], {
|
||||
headers: {
|
||||
'Content-Type': getContentType(url)
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
@ -39,8 +39,9 @@ textarea {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.17);
|
||||
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
padding: 5px 10px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[role='button'] {
|
||||
@ -96,6 +97,9 @@ button {
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.jc-sb {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
@ -383,6 +387,10 @@ body:not(.light-version).overlay-visible .main-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.is-file-mode .content-wrap {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* All vertical layout */
|
||||
|
||||
.layout-5 .code-side {
|
||||
@ -447,6 +455,9 @@ body:not(.light-version).overlay-visible .main-container {
|
||||
height: auto;
|
||||
width: 33%;
|
||||
}
|
||||
.is-file-mode .code-wrap {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.code-wrap__header {
|
||||
display: flex;
|
||||
@ -650,6 +661,8 @@ body > #demo-frame {
|
||||
|
||||
.btn--dark > svg {
|
||||
fill: #9297b3;
|
||||
}
|
||||
.btn--dark > svg:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@ -658,9 +671,13 @@ body > #demo-frame {
|
||||
}
|
||||
|
||||
.btn--dark:hover {
|
||||
border-color: rgba(146, 151, 179, 0.5);
|
||||
background: #9297b3;
|
||||
color: #111;
|
||||
/* border-color: rgba(146, 151, 179, 0.5); */
|
||||
}
|
||||
.btn--dark:hover > svg {
|
||||
fill: #111;
|
||||
}
|
||||
|
||||
.main-header__btn-wrap > .is-loading {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
@ -1593,6 +1610,72 @@ body:not(.is-app) .show-when-app {
|
||||
margin-left: -115px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #1e1e2c;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar__file {
|
||||
color: #bbb;
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
padding: 5px 5px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 1st level nesting */
|
||||
.sidebar__folder-wrap .sidebar__file {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* 2nd level nesting */
|
||||
.sidebar__folder-wrap .sidebar__folder-wrap .sidebar__file {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
/* 3rd level nesting */
|
||||
.sidebar__folder-wrap
|
||||
.sidebar__folder-wrap
|
||||
.sidebar__folder-wrap
|
||||
.sidebar__file {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.sidebar__file:hover,
|
||||
.sidebar__file:focus {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.sidebar__file.selected {
|
||||
color: white;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar__file-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.sidebar__file-options {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.sidebar__file:hover .sidebar__file-options,
|
||||
.sidebar__file:focus .sidebar__file-options,
|
||||
.sidebar__file:focus-within .sidebar__file-options {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.sidebar__folder-wrap[data-collapsed='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.is-file-mode .hide-in-file-mode {
|
||||
display: none !important;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
body {
|
||||
font-size: 70%;
|
||||
|
96
src/tests/fileUtils.test.js
Normal file
96
src/tests/fileUtils.test.js
Normal file
@ -0,0 +1,96 @@
|
||||
// See: https://github.com/mzgoddard/preact-render-spy
|
||||
import { shallow, deep } from 'preact-render-spy';
|
||||
import {
|
||||
assignFilePaths,
|
||||
getFileFromPath,
|
||||
removeFileAtPath,
|
||||
getParentPath
|
||||
} from '../fileUtils';
|
||||
|
||||
function getNestedFiles() {
|
||||
return [
|
||||
{ name: 'index.html' },
|
||||
{
|
||||
name: 'styles',
|
||||
isFolder: true,
|
||||
children: [{ name: 'main.css' }, { name: 'style.css' }]
|
||||
},
|
||||
{ name: 'script.js' }
|
||||
];
|
||||
}
|
||||
describe('assignFilePaths', () => {
|
||||
test('should assign path on linear file system', () => {
|
||||
const files = [{ name: 'index.html' }, { name: 'main.css' }];
|
||||
assignFilePaths(files);
|
||||
expect(files[0].path).toBe('index.html');
|
||||
expect(files[1].path).toBe('main.css');
|
||||
});
|
||||
test('should assign path on nested file system', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
expect(files[0].path).toBe('index.html');
|
||||
expect(files[1].children[0].path).toBe('styles/main.css');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileFromPath', () => {
|
||||
test('should return file and correct index', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
const { index, file } = getFileFromPath(files, 'index.html');
|
||||
expect(index).toBe(0);
|
||||
expect(file).toBe(files[index]);
|
||||
});
|
||||
test('should return empty object for non-existent path', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
const { index, file } = getFileFromPath(files, 'style.css');
|
||||
expect(index).toBe(-1);
|
||||
expect(file).toBe(undefined);
|
||||
});
|
||||
test('should return file and correct index for a nested file', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
const { index, file } = getFileFromPath(files, 'styles/style.css');
|
||||
expect(index).toBe(1);
|
||||
expect(file).toBe(files[1].children[index]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFileAtPath', () => {
|
||||
test('should remove direct child file', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
|
||||
expect(files.length).toBe(3);
|
||||
|
||||
removeFileAtPath(files, 'index.html');
|
||||
expect(files.length).toBe(2);
|
||||
expect(files[0].name).toBe('styles');
|
||||
});
|
||||
|
||||
test('should remove a nested file', () => {
|
||||
const files = getNestedFiles();
|
||||
assignFilePaths(files);
|
||||
|
||||
expect(files.length).toBe(3);
|
||||
|
||||
removeFileAtPath(files, 'styles/style.css');
|
||||
expect(files.length).toBe(3);
|
||||
expect(files[1].children.length).toBe(1);
|
||||
expect(files[0].name).toBe('index.html');
|
||||
expect(files[2].name).toBe('script.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParentPath', () => {
|
||||
test('should return correct parent path for root file', () => {
|
||||
expect(getParentPath('style.css')).toBe('');
|
||||
});
|
||||
|
||||
test('should return correct parent path for nested file', () => {
|
||||
expect(getParentPath('styles/style.css')).toBe('styles');
|
||||
expect(getParentPath('js/plugins/main.js')).toBe('js/plugins');
|
||||
expect(getParentPath('js/plugins/readme')).toBe('js/plugins');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user