mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-12 01:26:23 +02:00
@ -34,7 +34,6 @@ export default class ContentWrap extends Component {
|
|||||||
this.logCount = 0;
|
this.logCount = 0;
|
||||||
|
|
||||||
window.onMessageFromConsole = this.onMessageFromConsole.bind(this);
|
window.onMessageFromConsole = this.onMessageFromConsole.bind(this);
|
||||||
|
|
||||||
window.previewException = this.previewException.bind(this);
|
window.previewException = this.previewException.bind(this);
|
||||||
// `clearConsole` is on window because it gets called from inside iframe also.
|
// `clearConsole` is on window because it gets called from inside iframe also.
|
||||||
window.clearConsole = this.clearConsole.bind(this);
|
window.clearConsole = this.clearConsole.bind(this);
|
||||||
@ -289,50 +288,25 @@ export default class ContentWrap extends Component {
|
|||||||
]).then(() => this.setPreviewContent(true));
|
]).then(() => this.setPreviewContent(true));
|
||||||
}
|
}
|
||||||
applyCodemirrorSettings(prefs) {
|
applyCodemirrorSettings(prefs) {
|
||||||
if (!this.cm) {
|
if (window.consoleEl) {
|
||||||
return;
|
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
|
// Replace correct css file in LINK tags's href
|
||||||
window.editorThemeLinkTag.href = `lib/codemirror/theme/${
|
if (prefs.editorTheme) {
|
||||||
prefs.editorTheme
|
window.editorThemeLinkTag.href = `lib/codemirror/theme/${
|
||||||
}.css`;
|
prefs.editorTheme
|
||||||
|
}.css`;
|
||||||
|
}
|
||||||
|
|
||||||
window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
|
window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
|
||||||
/fontname/g,
|
/fontname/g,
|
||||||
(prefs.editorFont === 'other'
|
(prefs.editorFont === 'other'
|
||||||
? prefs.editorCustomFont
|
? prefs.editorCustomFont
|
||||||
: prefs.editorFont) || 'FiraCode'
|
: 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
|
// 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,
|
show,
|
||||||
closeHandler,
|
closeHandler,
|
||||||
onBlankTemplateSelect,
|
onBlankTemplateSelect,
|
||||||
|
onBlankFileTemplateSelect,
|
||||||
onTemplateSelect
|
onTemplateSelect
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -15,6 +16,9 @@ export function CreateNewModal({
|
|||||||
<button className="btn" onClick={onBlankTemplateSelect}>
|
<button className="btn" onClick={onBlankTemplateSelect}>
|
||||||
Start a blank creation
|
Start a blank creation
|
||||||
</button>
|
</button>
|
||||||
|
<button className="btn" onClick={onBlankFileTemplateSelect}>
|
||||||
|
Start a blank files creation
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
Or choose from a template:
|
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
|
<button
|
||||||
onClick={this.props.saveHtmlBtnClickHandler}
|
onClick={this.props.saveHtmlBtnClickHandler}
|
||||||
id="saveHtmlBtn"
|
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"
|
aria-label="Save as HTML file"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24">
|
<svg viewBox="0 0 24 24">
|
||||||
@ -219,7 +219,7 @@ export default class Footer extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.props.codepenBtnClickHandler}
|
onClick={this.props.codepenBtnClickHandler}
|
||||||
id="codepenBtn"
|
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"
|
aria-label="Edit on CodePen"
|
||||||
>
|
>
|
||||||
<svg>
|
<svg>
|
||||||
@ -243,7 +243,7 @@ export default class Footer extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.layoutBtnClickhandler.bind(this, 1)}
|
onClick={this.layoutBtnClickhandler.bind(this, 1)}
|
||||||
id="layoutBtn1"
|
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"
|
aria-label="Switch to layout with preview on right"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
|
||||||
@ -253,7 +253,7 @@ export default class Footer extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.layoutBtnClickhandler.bind(this, 2)}
|
onClick={this.layoutBtnClickhandler.bind(this, 2)}
|
||||||
id="layoutBtn2"
|
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"
|
aria-label="Switch to layout with preview on bottom"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 100 100">
|
<svg viewBox="0 0 100 100">
|
||||||
@ -263,7 +263,7 @@ export default class Footer extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.layoutBtnClickhandler.bind(this, 3)}
|
onClick={this.layoutBtnClickhandler.bind(this, 3)}
|
||||||
id="layoutBtn3"
|
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"
|
aria-label="Switch to layout with preview on left"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
|
||||||
@ -273,7 +273,7 @@ export default class Footer extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.layoutBtnClickhandler.bind(this, 5)}
|
onClick={this.layoutBtnClickhandler.bind(this, 5)}
|
||||||
id="layoutBtn5"
|
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"
|
aria-label="Switch to layout with all vertical panes"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 100 100">
|
<svg viewBox="0 0 100 100">
|
||||||
|
@ -49,6 +49,14 @@ export function ItemTile({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<h3 class="saved-item-tile__title">{item.title}</h3>
|
<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>
|
</div>
|
||||||
{item.updatedOn ? (
|
{item.updatedOn ? (
|
||||||
<div class="saved-item-tile__meta">
|
<div class="saved-item-tile__meta">
|
||||||
|
@ -27,22 +27,24 @@ export function MainHeader(props) {
|
|||||||
</svg>Run
|
</svg>Run
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button
|
{!this.props.isFileMode && (
|
||||||
onClick={props.addLibraryBtnHandler}
|
<Button
|
||||||
data-event-category="ui"
|
onClick={props.addLibraryBtnHandler}
|
||||||
data-event-action="addLibraryButtonClick"
|
data-event-category="ui"
|
||||||
class="btn--dark flex-v-center hint--rounded hint--bottom-left"
|
data-event-action="addLibraryButtonClick"
|
||||||
aria-label="Add a JS/CSS library"
|
class="btn--dark flex-v-center hint--rounded hint--bottom-left"
|
||||||
>
|
aria-label="Add a JS/CSS library"
|
||||||
Add library{' '}
|
|
||||||
<span
|
|
||||||
id="js-external-lib-count"
|
|
||||||
style={`display:${props.externalLibCount ? 'inline' : 'none'}`}
|
|
||||||
class="count-label"
|
|
||||||
>
|
>
|
||||||
{props.externalLibCount}
|
Add library{' '}
|
||||||
</span>
|
<span
|
||||||
</Button>
|
id="js-external-lib-count"
|
||||||
|
style={`display:${props.externalLibCount ? 'inline' : 'none'}`}
|
||||||
|
class="count-label"
|
||||||
|
>
|
||||||
|
{props.externalLibCount}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn--dark flex flex-v-center hint--rounded hint--bottom-left"
|
class="btn--dark flex flex-v-center hint--rounded hint--bottom-left"
|
||||||
|
@ -33,6 +33,7 @@ export default class Settings extends Component {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
|
const { prefs } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
@ -47,7 +48,7 @@ export default class Settings extends Component {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="indentation"
|
name="indentation"
|
||||||
value="spaces"
|
value="spaces"
|
||||||
checked={this.props.prefs.indentation === 'spaces'}
|
checked={prefs.indentWith === 'spaces'}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
data-setting="indentWith"
|
data-setting="indentWith"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@ -58,7 +59,7 @@ export default class Settings extends Component {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="indentation"
|
name="indentation"
|
||||||
value="tabs"
|
value="tabs"
|
||||||
checked={this.props.prefs.indentation === 'tabs'}
|
checked={prefs.indentWith === 'tabs'}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
data-setting="indentWith"
|
data-setting="indentWith"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@ -70,14 +71,14 @@ export default class Settings extends Component {
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
class="va-m ml-1"
|
class="va-m ml-1"
|
||||||
value={this.props.prefs.indentSize}
|
value={prefs.indentSize}
|
||||||
min="1"
|
min="1"
|
||||||
max="7"
|
max="7"
|
||||||
list="indentationSizeList"
|
list="indentationSizeList"
|
||||||
data-setting="indentSize"
|
data-setting="indentSize"
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<span id="indentationSizeValueEl">{this.props.prefs.indentSize}</span>
|
<span id="indentationSizeValueEl">{prefs.indentSize}</span>
|
||||||
<datalist id="indentationSizeList">
|
<datalist id="indentationSizeList">
|
||||||
<option>1</option>
|
<option>1</option>
|
||||||
<option>2</option>
|
<option>2</option>
|
||||||
@ -99,7 +100,7 @@ export default class Settings extends Component {
|
|||||||
<select
|
<select
|
||||||
style="flex:1;margin-left:20px"
|
style="flex:1;margin-left:20px"
|
||||||
data-setting="htmlMode"
|
data-setting="htmlMode"
|
||||||
value={this.props.prefs.htmlMode}
|
value={prefs.htmlMode}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
>
|
>
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
@ -109,7 +110,7 @@ export default class Settings extends Component {
|
|||||||
<select
|
<select
|
||||||
style="flex:1;margin-left:20px"
|
style="flex:1;margin-left:20px"
|
||||||
data-setting="cssMode"
|
data-setting="cssMode"
|
||||||
value={this.props.prefs.cssMode}
|
value={prefs.cssMode}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
>
|
>
|
||||||
<option value="css">CSS</option>
|
<option value="css">CSS</option>
|
||||||
@ -122,7 +123,7 @@ export default class Settings extends Component {
|
|||||||
<select
|
<select
|
||||||
style="flex:1;margin-left:20px"
|
style="flex:1;margin-left:20px"
|
||||||
data-setting="jsMode"
|
data-setting="jsMode"
|
||||||
value={this.props.prefs.jsMode}
|
value={prefs.jsMode}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
>
|
>
|
||||||
<option value="js">JS</option>
|
<option value="js">JS</option>
|
||||||
@ -136,7 +137,7 @@ export default class Settings extends Component {
|
|||||||
<select
|
<select
|
||||||
style="flex:1;margin:0 20px"
|
style="flex:1;margin:0 20px"
|
||||||
data-setting="editorTheme"
|
data-setting="editorTheme"
|
||||||
value={this.props.prefs.editorTheme}
|
value={prefs.editorTheme}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
>
|
>
|
||||||
{editorThemes.map(theme => (
|
{editorThemes.map(theme => (
|
||||||
@ -149,7 +150,7 @@ export default class Settings extends Component {
|
|||||||
<select
|
<select
|
||||||
style="flex:1;margin:0 20px"
|
style="flex:1;margin:0 20px"
|
||||||
data-setting="editorFont"
|
data-setting="editorFont"
|
||||||
value={this.props.prefs.editorFont}
|
value={prefs.editorFont}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
>
|
>
|
||||||
<option value="FiraCode">Fira Code</option>
|
<option value="FiraCode">Fira Code</option>
|
||||||
@ -159,11 +160,11 @@ export default class Settings extends Component {
|
|||||||
<option disabled="disabled">----</option>
|
<option disabled="disabled">----</option>
|
||||||
<option value="other">Other font from system</option>
|
<option value="other">Other font from system</option>
|
||||||
</select>
|
</select>
|
||||||
{this.props.prefs.editorFont === 'other' && (
|
{prefs.editorFont === 'other' && (
|
||||||
<input
|
<input
|
||||||
id="customEditorFontInput"
|
id="customEditorFontInput"
|
||||||
type="text"
|
type="text"
|
||||||
value={this.props.prefs.editorCustomFont}
|
value={prefs.editorCustomFont}
|
||||||
placeholder="Custom font name here"
|
placeholder="Custom font name here"
|
||||||
data-setting="editorCustomFont"
|
data-setting="editorCustomFont"
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
@ -175,7 +176,7 @@ export default class Settings extends Component {
|
|||||||
<input
|
<input
|
||||||
style="width:70px"
|
style="width:70px"
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.prefs.fontSize}
|
value={prefs.fontSize}
|
||||||
data-setting="fontSize"
|
data-setting="fontSize"
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@ -188,7 +189,7 @@ export default class Settings extends Component {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="keymap"
|
name="keymap"
|
||||||
value="sublime"
|
value="sublime"
|
||||||
checked={this.props.prefs.keymap === 'sublime'}
|
checked={prefs.keymap === 'sublime'}
|
||||||
data-setting="keymap"
|
data-setting="keymap"
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@ -199,7 +200,7 @@ export default class Settings extends Component {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="keymap"
|
name="keymap"
|
||||||
value="vim"
|
value="vim"
|
||||||
checked={this.props.prefs.keymap === 'vim'}
|
checked={prefs.keymap === 'vim'}
|
||||||
data-setting="keymap"
|
data-setting="keymap"
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@ -212,49 +213,49 @@ export default class Settings extends Component {
|
|||||||
name="lineWrap"
|
name="lineWrap"
|
||||||
title="Toggle wrapping of long sentences onto new line"
|
title="Toggle wrapping of long sentences onto new line"
|
||||||
label="Line wrap"
|
label="Line wrap"
|
||||||
pref={this.props.prefs.lineWrap}
|
pref={prefs.lineWrap}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="refreshOnResize"
|
name="refreshOnResize"
|
||||||
title="Your Preview will refresh when you resize the preview split"
|
title="Your Preview will refresh when you resize the preview split"
|
||||||
label="Refresh preview on resize"
|
label="Refresh preview on resize"
|
||||||
pref={this.props.prefs.refreshOnResize}
|
pref={prefs.refreshOnResize}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="autoComplete"
|
name="autoComplete"
|
||||||
title="Turns on the auto-completion suggestions as you type"
|
title="Turns on the auto-completion suggestions as you type"
|
||||||
label="Auto-complete suggestions"
|
label="Auto-complete suggestions"
|
||||||
pref={this.props.prefs.autoComplete}
|
pref={prefs.autoComplete}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="autoPreview"
|
name="autoPreview"
|
||||||
title="Refreshes the preview as you code. Otherwise use the Run button"
|
title="Refreshes the preview as you code. Otherwise use the Run button"
|
||||||
label="Auto-preview"
|
label="Auto-preview"
|
||||||
pref={this.props.prefs.autoPreview}
|
pref={prefs.autoPreview}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="autoSave"
|
name="autoSave"
|
||||||
title="Auto-save keeps saving your code at regular intervals after you hit the first save manually"
|
title="Auto-save keeps saving your code at regular intervals after you hit the first save manually"
|
||||||
label="Auto-save"
|
label="Auto-save"
|
||||||
pref={this.props.prefs.autoSave}
|
pref={prefs.autoSave}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="preserveLastCode"
|
name="preserveLastCode"
|
||||||
title="Loads the last open creation when app starts"
|
title="Loads the last open creation when app starts"
|
||||||
label="Preserve last written code"
|
label="Preserve last written code"
|
||||||
pref={this.props.prefs.preserveLastCode}
|
pref={prefs.preserveLastCode}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="replaceNewTab"
|
name="replaceNewTab"
|
||||||
title="Turning this on will start showing Web Maker in every new tab you open"
|
title="Turning this on will start showing Web Maker in every new tab you open"
|
||||||
label="Replace new tab page"
|
label="Replace new tab page"
|
||||||
pref={this.props.prefs.replaceNewTab}
|
pref={prefs.replaceNewTab}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
showWhenExtension
|
showWhenExtension
|
||||||
/>
|
/>
|
||||||
@ -262,14 +263,14 @@ export default class Settings extends Component {
|
|||||||
name="preserveConsoleLogs"
|
name="preserveConsoleLogs"
|
||||||
title="Preserves the console logs across your preview refreshes"
|
title="Preserves the console logs across your preview refreshes"
|
||||||
label="Preserve console logs"
|
label="Preserve console logs"
|
||||||
pref={this.props.prefs.preserveConsoleLogs}
|
pref={prefs.preserveConsoleLogs}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
<CheckboxSetting
|
<CheckboxSetting
|
||||||
name="lightVersion"
|
name="lightVersion"
|
||||||
title="Switch to lighter version for better performance. Removes things like blur etc."
|
title="Switch to lighter version for better performance. Removes things like blur etc."
|
||||||
label="Fast/light version"
|
label="Fast/light version"
|
||||||
pref={this.props.prefs.lightVersion}
|
pref={prefs.lightVersion}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -283,7 +284,7 @@ export default class Settings extends Component {
|
|||||||
title="Enjoy wonderful particle blasts while you type"
|
title="Enjoy wonderful particle blasts while you type"
|
||||||
label="Code blast!"
|
label="Code blast!"
|
||||||
name="isCodeBlastOn"
|
name="isCodeBlastOn"
|
||||||
pref={this.props.prefs.isCodeBlastOn}
|
pref={prefs.isCodeBlastOn}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -291,7 +292,7 @@ export default class Settings extends Component {
|
|||||||
title="Get ready to build some games at JS13KGames"
|
title="Get ready to build some games at JS13KGames"
|
||||||
label="Js13kGames Mode"
|
label="Js13kGames Mode"
|
||||||
name="isJs13kModeOn"
|
name="isJs13kModeOn"
|
||||||
pref={this.props.prefs.isJs13kModeOn}
|
pref={prefs.isJs13kModeOn}
|
||||||
onChange={this.updateSetting.bind(this)}
|
onChange={this.updateSetting.bind(this)}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
@ -307,7 +308,7 @@ export default class Settings extends Component {
|
|||||||
Maximum time allowed in a loop iteration{' '}
|
Maximum time allowed in a loop iteration{' '}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.prefs.infiniteLoopTimeout}
|
value={prefs.infiniteLoopTimeout}
|
||||||
data-setting="infiniteLoopTimeout"
|
data-setting="infiniteLoopTimeout"
|
||||||
onChange={this.updateSetting.bind(this)}
|
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 {
|
export default class UserCodeMirror extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.initEditor();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initEditor() {
|
initEditor() {
|
||||||
const options = this.props.options;
|
const { options, prefs } = this.props;
|
||||||
this.cm = CodeMirror.fromTextArea(this.textarea, {
|
this.cm = CodeMirror.fromTextArea(this.textarea, {
|
||||||
mode: options.mode,
|
mode: options.mode,
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
lineWrapping: true,
|
lineWrapping: !!prefs.lineWrap,
|
||||||
autofocus: options.autofocus || false,
|
autofocus: options.autofocus || false,
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
autoCloseTags: true,
|
autoCloseTags: true,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
matchTags: options.matchTags || false,
|
matchTags: options.matchTags || false,
|
||||||
tabMode: 'indent',
|
tabMode: 'indent',
|
||||||
keyMap: 'sublime',
|
keyMap: prefs.keyMap || 'sublime',
|
||||||
theme: 'monokai',
|
theme: prefs.editorTheme || 'monokai',
|
||||||
lint: !!options.lint,
|
lint: !!options.lint,
|
||||||
tabSize: 2,
|
tabSize: +prefs.indentSize || 2,
|
||||||
|
indentWithTabs: prefs.indentWith !== 'spaces',
|
||||||
|
indentUnit: +prefs.indentSize,
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
gutters: options.gutters || [],
|
gutters: options.gutters || [],
|
||||||
@ -91,7 +122,7 @@ export default class UserCodeMirror extends Component {
|
|||||||
const input = $('[data-setting=indentWith]:checked');
|
const input = $('[data-setting=indentWith]:checked');
|
||||||
if (
|
if (
|
||||||
!editor.somethingSelected() &&
|
!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
|
// 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.
|
// to indent with spaces if `spaces` is preferred mode of indentation.
|
||||||
@ -111,10 +142,11 @@ export default class UserCodeMirror extends Component {
|
|||||||
this.cm.addKeyMap({
|
this.cm.addKeyMap({
|
||||||
'Ctrl-Space': 'autocomplete'
|
'Ctrl-Space': 'autocomplete'
|
||||||
});
|
});
|
||||||
if (!options.noAutocomplete) {
|
this.cm.on('inputRead', (editor, input) => {
|
||||||
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 (
|
if (
|
||||||
!this.props.prefs.autoComplete ||
|
|
||||||
input.origin !== '+input' ||
|
input.origin !== '+input' ||
|
||||||
input.text[0] === ';' ||
|
input.text[0] === ';' ||
|
||||||
input.text[0] === ',' ||
|
input.text[0] === ',' ||
|
||||||
@ -125,8 +157,8 @@ export default class UserCodeMirror extends Component {
|
|||||||
CodeMirror.commands.autocomplete(this.cm, null, {
|
CodeMirror.commands.autocomplete(this.cm, null, {
|
||||||
completeSingle: false
|
completeSingle: false
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
this.props.onCreation(this.cm);
|
this.props.onCreation(this.cm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
|
import '../service-worker-registration';
|
||||||
import { MainHeader } from './MainHeader.jsx';
|
import { MainHeader } from './MainHeader.jsx';
|
||||||
import ContentWrap from './ContentWrap.jsx';
|
import ContentWrap from './ContentWrap.jsx';
|
||||||
|
import ContentWrapFiles from './ContentWrapFiles.jsx';
|
||||||
import Footer from './Footer.jsx';
|
import Footer from './Footer.jsx';
|
||||||
import SavedItemPane from './SavedItemPane.jsx';
|
import SavedItemPane from './SavedItemPane.jsx';
|
||||||
import AddLibrary from './AddLibrary.jsx';
|
import AddLibrary from './AddLibrary.jsx';
|
||||||
@ -21,6 +22,13 @@ import {
|
|||||||
getCompleteHtml,
|
getCompleteHtml,
|
||||||
getFilenameFromUrl
|
getFilenameFromUrl
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
import {
|
||||||
|
linearizeFiles,
|
||||||
|
assignFilePaths,
|
||||||
|
getFileFromPath,
|
||||||
|
removeFileAtPath,
|
||||||
|
doesFileExistInFolder
|
||||||
|
} from '../fileUtils';
|
||||||
import { itemService } from '../itemService';
|
import { itemService } from '../itemService';
|
||||||
import '../db';
|
import '../db';
|
||||||
import { Notifications } from './Notifications';
|
import { Notifications } from './Notifications';
|
||||||
@ -194,7 +202,7 @@ export default class App extends Component {
|
|||||||
this.createNewItem();
|
this.createNewItem();
|
||||||
}
|
}
|
||||||
Object.assign(this.state.prefs, result);
|
Object.assign(this.state.prefs, result);
|
||||||
this.setState({ prefs: this.state.prefs });
|
this.setState({ prefs: { ...this.state.prefs } });
|
||||||
this.updateSetting();
|
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() {
|
updateProfileUi() {
|
||||||
if (this.state.user) {
|
if (this.state.user) {
|
||||||
document.body.classList.add('is-logged-in');
|
document.body.classList.add('is-logged-in');
|
||||||
@ -264,9 +288,9 @@ export default class App extends Component {
|
|||||||
alertsService.add(`"${sourceItem.title}" was forked`);
|
alertsService.add(`"${sourceItem.title}" was forked`);
|
||||||
trackEvent('fn', 'itemForked');
|
trackEvent('fn', 'itemForked');
|
||||||
}
|
}
|
||||||
createNewItem() {
|
createNewItem(isFileMode = false) {
|
||||||
var d = new Date();
|
const d = new Date();
|
||||||
this.setCurrentItem({
|
let item = {
|
||||||
title:
|
title:
|
||||||
'Untitled ' +
|
'Untitled ' +
|
||||||
d.getDate() +
|
d.getDate() +
|
||||||
@ -276,12 +300,38 @@ export default class App extends Component {
|
|||||||
d.getHours() +
|
d.getHours() +
|
||||||
':' +
|
':' +
|
||||||
d.getMinutes(),
|
d.getMinutes(),
|
||||||
html: '',
|
createdOn: +d,
|
||||||
css: '',
|
content: ''
|
||||||
js: '',
|
};
|
||||||
externalLibs: { js: '', css: '' },
|
if (isFileMode) {
|
||||||
layoutMode: this.state.currentLayoutMode
|
item = {
|
||||||
}).then(() => this.refreshEditor());
|
...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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.setCurrentItem(item).then(() => this.refreshEditor());
|
||||||
alertsService.add('New item created');
|
alertsService.add('New item created');
|
||||||
}
|
}
|
||||||
openItem(item) {
|
openItem(item) {
|
||||||
@ -317,10 +367,12 @@ export default class App extends Component {
|
|||||||
setCurrentItem(item) {
|
setCurrentItem(item) {
|
||||||
const d = deferred();
|
const d = deferred();
|
||||||
// TODO: remove later
|
// TODO: remove later
|
||||||
item.htmlMode =
|
if (!item.files) {
|
||||||
item.htmlMode || this.state.prefs.htmlMode || HtmlModes.HTML;
|
item.htmlMode =
|
||||||
item.cssMode = item.cssMode || this.state.prefs.cssMode || CssModes.CSS;
|
item.htmlMode || this.state.prefs.htmlMode || HtmlModes.HTML;
|
||||||
item.jsMode = item.jsMode || this.state.prefs.jsMode || JsModes.JS;
|
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);
|
this.setState({ currentItem: item }, d.resolve);
|
||||||
|
|
||||||
@ -470,7 +522,9 @@ export default class App extends Component {
|
|||||||
isKeyboardShortcutsModalOpen: !this.state.isKeyboardShortcutsModalOpen
|
isKeyboardShortcutsModalOpen: !this.state.isKeyboardShortcutsModalOpen
|
||||||
});
|
});
|
||||||
trackEvent('ui', 'showKeyboardShortcutsShortcut');
|
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();
|
this.closeSavedItemsPane();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -684,21 +738,17 @@ export default class App extends Component {
|
|||||||
this.setState({ currentItem: item });
|
this.setState({ currentItem: item });
|
||||||
}
|
}
|
||||||
onCodeChange(type, code, isUserChange) {
|
onCodeChange(type, code, isUserChange) {
|
||||||
this.state.currentItem[type] = code;
|
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) {
|
if (isUserChange) {
|
||||||
this.setState({ unsavedEditCount: this.state.unsavedEditCount + 1 });
|
this.incrementUnsavedChanges();
|
||||||
|
|
||||||
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.prefs.isJs13kModeOn) {
|
if (this.state.prefs.isJs13kModeOn) {
|
||||||
// Throttling codesize calculation
|
// Throttling codesize calculation
|
||||||
@ -1140,6 +1190,10 @@ export default class App extends Component {
|
|||||||
this.createNewItem();
|
this.createNewItem();
|
||||||
this.setState({ isCreateNewModalOpen: false });
|
this.setState({ isCreateNewModalOpen: false });
|
||||||
}
|
}
|
||||||
|
blankFileTemplateSelectHandler() {
|
||||||
|
this.createNewItem(true);
|
||||||
|
this.setState({ isCreateNewModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
templateSelectHandler(template) {
|
templateSelectHandler(template) {
|
||||||
fetch(`templates/template-${template.id}.json`)
|
fetch(`templates/template-${template.id}.json`)
|
||||||
@ -1149,10 +1203,97 @@ export default class App extends Component {
|
|||||||
});
|
});
|
||||||
this.setState({ isCreateNewModalOpen: false });
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class={this.getRootClasses()}>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<MainHeader
|
<MainHeader
|
||||||
externalLibCount={this.state.externalLibCount}
|
externalLibCount={this.state.externalLibCount}
|
||||||
@ -1169,18 +1310,37 @@ export default class App extends Component {
|
|||||||
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
|
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
|
||||||
user={this.state.user}
|
user={this.state.user}
|
||||||
unsavedEditCount={this.state.unsavedEditCount}
|
unsavedEditCount={this.state.unsavedEditCount}
|
||||||
|
isFileMode={this.state.currentItem && this.state.currentItem.files}
|
||||||
/>
|
/>
|
||||||
<ContentWrap
|
{this.state.currentItem && this.state.currentItem.files ? (
|
||||||
currentLayoutMode={this.state.currentLayoutMode}
|
<ContentWrapFiles
|
||||||
currentItem={this.state.currentItem}
|
currentItem={this.state.currentItem}
|
||||||
onCodeChange={this.onCodeChange.bind(this)}
|
onCodeChange={this.onCodeChange.bind(this)}
|
||||||
onCodeSettingsChange={this.onCodeSettingsChange.bind(this)}
|
onCodeSettingsChange={this.onCodeSettingsChange.bind(this)}
|
||||||
onCodeModeChange={this.onCodeModeChange.bind(this)}
|
onCodeModeChange={this.onCodeModeChange.bind(this)}
|
||||||
onRef={comp => (this.contentWrap = comp)}
|
onRef={comp => (this.contentWrap = comp)}
|
||||||
prefs={this.state.prefs}
|
prefs={this.state.prefs}
|
||||||
onEditorFocus={this.editorFocusHandler.bind(this)}
|
onEditorFocus={this.editorFocusHandler.bind(this)}
|
||||||
onSplitUpdate={this.splitUpdateHandler.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}
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
prefs={this.state.prefs}
|
prefs={this.state.prefs}
|
||||||
@ -1337,6 +1497,9 @@ export default class App extends Component {
|
|||||||
show={this.state.isCreateNewModalOpen}
|
show={this.state.isCreateNewModalOpen}
|
||||||
closeHandler={() => this.setState({ isCreateNewModalOpen: false })}
|
closeHandler={() => this.setState({ isCreateNewModalOpen: false })}
|
||||||
onBlankTemplateSelect={this.blankTemplateSelectHandler.bind(this)}
|
onBlankTemplateSelect={this.blankTemplateSelectHandler.bind(this)}
|
||||||
|
onBlankFileTemplateSelect={this.blankFileTemplateSelectHandler.bind(
|
||||||
|
this
|
||||||
|
)}
|
||||||
onTemplateSelect={this.templateSelectHandler.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 '';
|
||||||
|
}
|
@ -67,9 +67,9 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- SCRIPT-TAGS -->
|
<!-- SCRIPT-TAGS -->
|
||||||
<%= htmlWebpackPlugin.options.ssr({
|
<%= htmlWebpackPlugin.options.ssr({
|
||||||
url: '/'
|
url: '/'
|
||||||
}) %>
|
}) %>
|
||||||
<script defer src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
|
<script defer src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
if (
|
if (
|
||||||
'serviceWorker' in navigator &&
|
'serviceWorker' in navigator &&
|
||||||
location.protocol === 'https:' &&
|
location.protocol === 'https:' &&
|
||||||
document.cookie.indexOf('wmdebug') === -1 &&
|
|
||||||
location.href.indexOf('chrome-extension://') === -1 &&
|
location.href.indexOf('chrome-extension://') === -1 &&
|
||||||
location.href.indexOf('192.168') === -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);
|
border-bottom-color: rgba(255, 255, 255, 0.17);
|
||||||
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
|
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 10px;
|
padding: 5px 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[role='button'] {
|
[role='button'] {
|
||||||
@ -96,6 +97,9 @@ button {
|
|||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.jc-sb {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.fr {
|
.fr {
|
||||||
float: right;
|
float: right;
|
||||||
@ -383,6 +387,10 @@ body:not(.light-version).overlay-visible .main-container {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-file-mode .content-wrap {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
/* All vertical layout */
|
/* All vertical layout */
|
||||||
|
|
||||||
.layout-5 .code-side {
|
.layout-5 .code-side {
|
||||||
@ -447,6 +455,9 @@ body:not(.light-version).overlay-visible .main-container {
|
|||||||
height: auto;
|
height: auto;
|
||||||
width: 33%;
|
width: 33%;
|
||||||
}
|
}
|
||||||
|
.is-file-mode .code-wrap {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.code-wrap__header {
|
.code-wrap__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -650,6 +661,8 @@ body > #demo-frame {
|
|||||||
|
|
||||||
.btn--dark > svg {
|
.btn--dark > svg {
|
||||||
fill: #9297b3;
|
fill: #9297b3;
|
||||||
|
}
|
||||||
|
.btn--dark > svg:not(:last-child) {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,9 +671,13 @@ body > #demo-frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn--dark:hover {
|
.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 {
|
.main-header__btn-wrap > .is-loading {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
@ -1593,6 +1610,72 @@ body:not(.is-app) .show-when-app {
|
|||||||
margin-left: -115px;
|
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) {
|
@media screen and (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
font-size: 70%;
|
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