1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-06-02 15:44:50 +02:00

1842 lines
50 KiB
JavaScript

/* global htmlCodeEl, cssCodeEl, jsCodeEl
*/
import { h, Component } from 'preact';
// import '../service-worker-registration';
import { MainHeader } from './MainHeader.jsx';
import ContentWrap from './ContentWrap.jsx';
import ContentWrapFiles from './ContentWrapFiles.jsx';
import Footer from './Footer.jsx';
import SavedItemPane from './SavedItemPane.jsx';
import AddLibrary from './AddLibrary.jsx';
import Modal from './Modal.jsx';
import Login from './Login.jsx';
import { computeHtml, computeCss, computeJs } from '../computes';
import {
log,
generateRandomId,
semverCompare,
saveAsHtml,
handleDownloadsPermission,
downloadFile,
getCompleteHtml,
getFilenameFromUrl,
prettify,
sanitizeSplitSizes
} from '../utils';
import {
linearizeFiles,
assignFilePaths,
getFileFromPath,
removeFileAtPath,
doesFileExistInFolder,
importGithubRepo
} from '../fileUtils';
import { itemService } from '../itemService';
import '../db';
import { Notifications } from './Notifications';
import Settings from './Settings.jsx';
import { modes, HtmlModes, CssModes, JsModes } from '../codeModes';
import { trackEvent } from '../analytics';
import { deferred } from '../deferred';
import { alertsService } from '../notifications';
import firebase from 'firebase/app';
import 'firebase/auth';
import { Profile } from './Profile';
import { auth } from '../auth';
import { SupportDeveloperModal } from './SupportDeveloperModal';
import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
import { takeScreenshot } from '../takeScreenshot';
import { AskToImportModal } from './AskToImportModal';
import { Alerts } from './Alerts';
import Portal from './Portal';
import { HelpModal } from './HelpModal';
import { OnboardingModal } from './OnboardingModal';
import { Js13KModal } from './Js13KModal';
import { CreateNewModal } from './CreateNewModal';
import { Icons } from './Icons';
import JSZip from 'jszip';
import { CommandPalette } from './CommandPalette';
import {
OPEN_SAVED_CREATIONS_EVENT,
SAVE_EVENT,
OPEN_SETTINGS_EVENT,
NEW_CREATION_EVENT,
SHOW_KEYBOARD_SHORTCUTS_EVENT
} from '../commands';
import { commandPaletteService } from '../commandPaletteService';
import { I18nProvider } from '@lingui/react';
if (module.hot) {
require('preact/debug');
}
const LocalStorageKeys = {
LOGIN_AND_SAVE_MESSAGE_SEEN: 'loginAndsaveMessageSeen',
ASKED_TO_IMPORT_CREATIONS: 'askedToImportCreations'
};
const UNSAVED_WARNING_COUNT = 15;
const version = '4.2.0';
// Read forced settings as query parameters
window.forcedSettings = {};
if (location.search) {
let match = location.search.replace(/^\?/, '').match(/settings=([^=]*)/);
if (match) {
match = match[1];
match.split(',').map(pair => {
pair = pair.split(':');
if (pair[1] === 'true') pair[1] = true;
else if (pair[1] === 'false') pair[1] = false;
window.forcedSettings[pair[0]] = pair[1];
});
}
}
export default class App extends Component {
constructor() {
super();
this.AUTO_SAVE_INTERVAL = 15000; // 15 seconds
this.modalDefaultStates = {
isModalOpen: false,
isAddLibraryModalOpen: false,
isSettingsModalOpen: false,
isHelpModalOpen: false,
isNotificationsModalOpen: false,
isLoginModalOpen: false,
isProfileModalOpen: false,
isSupportDeveloperModalOpen: false,
isKeyboardShortcutsModalOpen: false,
isAskToImportModalOpen: false,
isOnboardModalOpen: false,
isJs13KModalOpen: false,
isCreateNewModalOpen: false,
isCommandPaletteOpen: false
};
this.state = {
isSavedItemPaneOpen: false,
...this.modalDefaultStates,
prefs: {},
currentItem: {
title: '',
externalLibs: { js: '', css: '' }
},
catalogs: {}
};
this.defaultSettings = {
preserveLastCode: true,
replaceNewTab: false,
htmlMode: 'html',
jsMode: 'js',
cssMode: 'css',
isCodeBlastOn: false,
indentWith: 'spaces',
indentSize: 2,
editorTheme: 'monokai',
keymap: 'sublime',
fontSize: 16,
refreshOnResize: false,
autoPreview: true,
editorFont: 'FiraCode',
editorCustomFont: '',
autoSave: true,
autoComplete: true,
preserveConsoleLogs: true,
lightVersion: false,
lineWrap: true,
infiniteLoopTimeout: 1000,
layoutMode: 2,
isJs13kModeOn: false,
autoCloseTags: true,
lang: 'en',
isMonacoEditorOn: false,
previewDelay: 500
};
this.prefs = {};
firebase.auth().onAuthStateChanged(user => {
this.setState({ isLoginModalOpen: false });
if (user) {
log('You are -> ', user);
alertsService.add('You are now logged in!');
this.setState({ user });
window.user = user;
if (!window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS]) {
this.fetchItems(false, true).then(items => {
if (!items.length) {
return;
}
this.oldSavedItems = items;
this.oldSavedCreationsCount = items.length;
this.setState({
isAskToImportModalOpen: true
});
trackEvent('ui', 'askToImportModalSeen');
});
}
window.db.getUser(user.uid).then(customUser => {
if (customUser) {
const prefs = { ...this.state.prefs };
Object.assign(prefs, user.settings);
this.setState({ prefs }, this.updateSetting);
}
});
} else {
// User is signed out.
this.setState({ user: undefined });
delete window.user;
}
this.updateProfileUi();
});
}
componentWillMount() {
var lastCode;
window.onunload = () => {
if (this.detachedWindow) {
this.detachedWindow.close();
}
};
window.onbeforeunload = event => {
if (this.state.unsavedEditCount) {
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
}
};
db.local.get(
{
layoutMode: 1,
code: ''
},
result => {
this.toggleLayout(result.layoutMode);
this.state.prefs.layoutMode = result.layoutMode;
if (result.code) {
lastCode = result.code;
}
}
);
// Get synced `preserveLastCode` setting to get back last code (or not).
db.getSettings(this.defaultSettings).then(result => {
if (result.preserveLastCode && lastCode) {
this.setState({ unsavedEditCount: 0 });
log('Load last unsaved item', lastCode);
this.setCurrentItem(lastCode).then(() => this.refreshEditor());
} else {
this.createNewItem();
}
Object.assign(this.state.prefs, result);
this.setState({ prefs: { ...this.state.prefs } }, this.updateSetting);
});
// Check for new version notifications
db.getUserLastSeenVersion().then(lastSeenVersion => {
// Check if new user
if (!lastSeenVersion) {
this.setState({
isOnboardModalOpen: true
});
if (document.cookie.indexOf('onboarded') === -1) {
trackEvent('ui', 'onboardModalSeen', version);
document.cookie = 'onboarded=1';
}
window.db.setUserLastSeenVersion(version);
// set some initial preferences on closing the onboard modal
// Old onboarding.
//once(document, 'overlaysClosed', function() {});
}
// If its an upgrade
if (
lastSeenVersion &&
semverCompare(lastSeenVersion, version) === -1 &&
!window.localStorage.pledgeModalSeen
) {
this.openSupportDeveloperModal();
window.localStorage.pledgeModalSeen = true;
}
if (!lastSeenVersion || semverCompare(lastSeenVersion, version) === -1) {
this.setState({ hasUnseenChangelog: true });
this.hasSeenNotifications = false;
}
});
}
async loadLanguage(lang) {
log('🇯🇲 fetching defninition');
const catalog = await import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */ `../locales/${lang}/messages.js`
);
this.setState(state => ({
catalogs: {
...state.catalogs,
[lang]: catalog.default
}
}));
}
incrementUnsavedChanges() {
this.setState(prevState => {
const newCount = prevState.unsavedEditCount + 1;
if (
newCount % UNSAVED_WARNING_COUNT === 0 &&
newCount >= 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');
});
}
return { unsavedEditCount: newCount };
});
}
updateProfileUi() {
this.setState(prevState => {
if (prevState.user) {
document.body.classList.add('is-logged-in');
} else {
document.body.classList.remove('is-logged-in');
}
return null;
});
}
refreshEditor() {
this.toggleLayout(
this.state.currentItem.layoutMode || this.state.prefs.layoutMode
);
this.updateExternalLibCount();
this.contentWrap.refreshEditor();
}
askForUnsavedChanges() {
return confirm(
'You have unsaved changes in your current work. Do you want to discard unsaved changes and continue?'
);
}
// Creates a new item with passed item's contents
forkItem(sourceItem) {
if (this.state.unsavedEditCount) {
var shouldDiscard = this.askForUnsavedChanges();
if (!shouldDiscard) {
return;
}
}
const fork = JSON.parse(JSON.stringify(sourceItem));
delete fork.id;
fork.title = '(Forked) ' + sourceItem.title;
fork.updatedOn = Date.now();
this.setCurrentItem(fork).then(() => this.refreshEditor());
alertsService.add(`"${sourceItem.title}" was forked`);
trackEvent('fn', 'itemForked');
}
createNewItem(isFileMode = false, files) {
const d = new Date();
let item = {
title:
'Untitled ' +
d.getDate() +
'-' +
(d.getMonth() + 1) +
'-' +
d.getHours() +
':' +
d.getMinutes(),
createdOn: +d,
content: ''
};
if (isFileMode) {
item = {
...item,
files: assignFilePaths(
files || [
{
name: 'index.html',
content: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Webmaker untitled 1</title>
<link rel="stylesheet" href="styles/style.css" />
</head>
<body>
Hello World
<script src="script.js"></script>
</body>
</html>`
},
{
name: 'styles',
isFolder: true,
children: [{ name: 'style.css', content: '' }]
},
{ name: 'script.js', 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');
}
openItem(item) {
this.setCurrentItem(item).then(() => this.refreshEditor());
alertsService.add('Saved item loaded');
}
removeItem(item) {
var answer = confirm(`Are you sure you want to delete "${item.title}"?`);
if (!answer) {
return;
}
// Remove from items list
itemService.unsetItemForUser(item.id);
// Remove individual item too.
itemService.removeItem(item.id).then(() => {
alertsService.add('Item removed.', item);
// This item is open in the editor. Lets open a new one.
if (this.state.currentItem.id === item.id) {
this.createNewItem();
}
});
// Remove from cached list
delete this.state.savedItems[item.id];
this.setState({
savedItems: { ...this.state.savedItems }
});
trackEvent('fn', 'itemRemoved');
}
setCurrentItem(item) {
const d = deferred();
// TODO: remove later
if (!item.files) {
item.htmlMode =
item.htmlMode || this.state.prefs.htmlMode || HtmlModes.HTML;
item.cssMode = item.cssMode || this.state.prefs.cssMode || CssModes.CSS;
item.jsMode = item.jsMode || this.state.prefs.jsMode || JsModes.JS;
}
this.setState({ currentItem: item }, () => {
d.resolve();
// savecode will try to get split sizes from DOM, and DOM
// will update after sometime. so delay the saving
setTimeout(() => {
// Save locally so that something which is simply opened (or created newly) and closed without save can opened the next time
this.saveCode('code');
}, 2000);
});
// Reset auto-saving flag
this.isAutoSavingEnabled = false;
// Reset unsaved count, in UI also.
this.setState({ unsavedEditCount: 0 });
return d.promise;
}
saveBtnClickHandler() {
trackEvent(
'ui',
'saveBtnClick',
this.state.currentItem.id ? 'saved' : 'new'
);
this.saveItem();
}
populateItemsInSavedPane(items) {
// TODO: sort desc. by updation date
// this.setState({
// savedItems: { ...this.state.savedItems }
// });
this.toggleSavedItemsPane();
// HACK: Set overflow after sometime so that the items can animate without getting cropped.
// setTimeout(() => $('#js-saved-items-wrap').style.overflowY = 'auto', 1000);
}
toggleSavedItemsPane(shouldOpen) {
this.setState(prevState => {
const isSavedItemPaneOpen =
shouldOpen === undefined ? !prevState.isSavedItemPaneOpen : shouldOpen;
document.body.classList[isSavedItemPaneOpen ? 'add' : 'remove'](
'overlay-visible'
);
return {
isSavedItemPaneOpen
};
});
}
/**
* Fetches all items from storage
* @param {boolean} shouldSaveGlobally Whether to store the fetched items in global arr for later use.
* @param {boolean} shouldFetchLocally Intentionally get local items. Used when importing local items to account.
* @return {promise} Promise.
*/
async fetchItems(shouldSaveGlobally, shouldFetchLocally) {
var items = [];
const savedItems = {};
items = await itemService.getAllItems(shouldFetchLocally);
trackEvent('fn', 'fetchItems', items.length);
if (shouldSaveGlobally) {
items.forEach(item => {
savedItems[item.id] = item;
});
this.setState({
savedItems
});
}
return items;
}
openSavedItemsPane() {
this.setState({
isFetchingItems: true
});
this.fetchItems(true).then(items => {
this.setState({
isFetchingItems: false
});
this.populateItemsInSavedPane(items);
});
}
openAddLibrary() {
this.setState({ isAddLibraryModalOpen: true });
}
closeSavedItemsPane() {
this.setState({
isSavedItemPaneOpen: false
});
document.body.classList.remove('overlay-visible');
if (this.editorWithFocus) {
this.editorWithFocus.focus();
}
}
openSettings() {
this.setState({ isSettingsModalOpen: true });
}
openKeyboardShortcuts() {
this.setState({ isKeyboardShortcutsModalOpen: true });
}
componentDidMount() {
function setBodySize() {
document.body.style.height = `${window.innerHeight}px`;
}
window.addEventListener('resize', () => {
setBodySize();
});
// Editor keyboard shortucuts
window.addEventListener('keydown', event => {
// TODO: refactor common listener code
// Ctrl/⌘ + S
if ((event.ctrlKey || event.metaKey) && event.keyCode === 83) {
event.preventDefault();
this.saveItem();
trackEvent('ui', 'saveItemKeyboardShortcut');
}
// Ctrl/⌘ + Shift + 5
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.keyCode === 53
) {
event.preventDefault();
this.contentWrap.setPreviewContent(true, true);
trackEvent('ui', 'previewKeyboardShortcut');
} else if ((event.ctrlKey || event.metaKey) && event.keyCode === 79) {
// Ctrl/⌘ + O
event.preventDefault();
this.openSavedItemsPane();
trackEvent('ui', 'openCreationKeyboardShortcut');
} else if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.keyCode === 191
) {
// Ctrl/⌘ + Shift + ?
event.preventDefault();
this.setState({
isKeyboardShortcutsModalOpen: !this.state.isKeyboardShortcutsModalOpen
});
trackEvent('ui', 'showKeyboardShortcutsShortcut');
} else if (
event.keyCode === 27 &&
(event.target.tagName !== 'INPUT' || event.target.id === 'searchInput')
) {
// ESCAPE
// TODO: whats written next doesn't make sense. Review it.
// We might be listening on keydown for some input inside the app, UNLESS its
// the search input in saved items pane. In that case
// we don't want this to trigger which in turn focuses back the last editor.
this.closeSavedItemsPane();
} else if ((event.ctrlKey || event.metaKey) && event.keyCode === 80) {
// cmd+shift+P and this is Firefox, do nothing
if (event.shiftKey && navigator.userAgent.match(/Firefox/)) {
return true;
}
this.setState({
isCommandPaletteOpen: true,
isCommandPaletteInCommandMode: !!event.shiftKey
});
trackEvent(
'ui',
'openCommandPaletteKeyboardShortcut',
!!event.shiftKey ? 'command' : 'files'
);
event.preventDefault();
} else if (event.key === 'F1' && navigator.userAgent.match(/Firefox/)) {
// On firefox, open command palette with F1 because Cmd+Shift+P
// is for opening private window
this.setState({
isCommandPaletteOpen: true,
isCommandPaletteInCommandMode: true
});
trackEvent('ui', 'openCommandPaletteKeyboardShortcut', 'command');
event.preventDefault();
}
});
// Basic Focus trapping
window.addEventListener('focusin', e => {
if (document.body.classList.contains('overlay-visible')) {
const modal = $('.is-modal-visible');
if (!modal) {
return;
}
if (!modal.contains(e.target)) {
e.preventDefault();
modal.querySelector('.js-modal__close-btn').focus();
}
}
});
const commandPalleteHooks = {
[NEW_CREATION_EVENT]: () => {
this.openNewCreationModal();
},
[OPEN_SAVED_CREATIONS_EVENT]: () => {
this.openSavedItemsPane();
},
[SAVE_EVENT]: () => {
this.saveItem();
},
[OPEN_SETTINGS_EVENT]: () => {
this.openSettings();
},
[SHOW_KEYBOARD_SHORTCUTS_EVENT]: () => {
this.openKeyboardShortcuts();
}
};
for (let eventName in commandPalleteHooks) {
commandPaletteService.subscribe(
eventName,
commandPalleteHooks[eventName]
);
}
}
shouldComponentUpdate(nextProps, nextState) {
const { catalogs } = nextState;
const { lang } = nextState.prefs;
if (lang && lang !== 'en' && !catalogs[lang]) {
this.loadLanguage(lang);
}
return true;
}
closeAllOverlays() {
if (this.state.isSavedItemPaneOpen) {
this.closeSavedItemsPane();
}
this.setState({
...this.modalDefaultStates
});
}
onExternalLibChange(newValues) {
log('onExternalLibChange');
this.state.currentItem.externalLibs = {
js: newValues.js,
css: newValues.css
};
this.updateExternalLibCount();
this.setState({
currentItem: { ...this.state.currentItem }
});
this.contentWrap.setPreviewContent(true);
alertsService.add('Libraries updated.');
}
updateExternalLibCount() {
// Calculate no. of external libs
var noOfExternalLibs = 0;
if (!this.state.currentItem.externalLibs) {
return;
}
noOfExternalLibs += this.state.currentItem.externalLibs.js
.split('\n')
.filter(lib => !!lib).length;
noOfExternalLibs += this.state.currentItem.externalLibs.css
.split('\n')
.filter(lib => !!lib).length;
this.setState({
externalLibCount: noOfExternalLibs
});
}
toggleLayout(mode) {
/* eslint-disable no-param-reassign */
mode = window.innerWidth < 600 ? 2 : mode;
if (this.state.currentLayoutMode === mode) {
this.contentWrap.resetSplitting();
// mainSplitInstance.setSizes(getMainSplitSizesToApply());
// codeSplitInstance.setSizes(currentItem.sizes || [33.33, 33.33, 33.33]);
this.setState({ currentLayoutMode: mode });
return;
}
// Remove all layout classes
[1, 2, 3, 4, 5].forEach(layoutNumber => {
window[`layoutBtn${layoutNumber}`].classList.remove('selected');
document.body.classList.remove(`layout-${layoutNumber}`);
});
$('#layoutBtn' + mode).classList.add('selected');
document.body.classList.add('layout-' + mode);
this.setState({ currentLayoutMode: mode }, () => {
this.contentWrap.resetSplitting();
this.contentWrap.setPreviewContent(true);
});
}
layoutBtnClickHandler(layoutId) {
if (layoutId === this.state.currentLayoutMode) {
layoutId = 2;
}
this.saveSetting('layoutMode', layoutId);
trackEvent('ui', 'toggleLayoutClick', layoutId);
this.toggleLayout(layoutId);
}
// Calculates the sizes of html, css & js code panes.
getCodePaneSizes() {
var sizes;
const currentLayoutMode = this.state.currentLayoutMode;
var dimensionProperty =
currentLayoutMode === 2 || currentLayoutMode === 5 ? 'width' : 'height';
try {
sizes = [
htmlCodeEl.style[dimensionProperty],
cssCodeEl.style[dimensionProperty],
jsCodeEl.style[dimensionProperty]
];
} catch (e) {
sizes = [33.33, 33.33, 33.33];
} finally {
/* eslint-disable no-unsafe-finally */
return sanitizeSplitSizes(sizes);
/* eslint-enable no-unsafe-finally */
}
}
// Calculates the current sizes of code & preview panes.
getMainPaneSizes() {
let sizes;
function getPercentFromDimension(el, dimension = 'width') {
const match = el.style[dimension].match(/[\d.]+(%|px)/);
if (match) {
return match[0];
}
return null;
}
// File mode
if (this.state.currentItem && this.state.currentItem.files) {
const sidebarWidth = 200;
sizes = [
getPercentFromDimension($('#js-sidebar')),
getPercentFromDimension($('#js-code-side')),
getPercentFromDimension($('#js-demo-side'))
];
// Check if anything was returned falsy, reset in that case
if (sizes.filter(s => s).length !== 3) {
sizes = [
`${sidebarWidth}px`,
`calc(50% - ${sidebarWidth / 2}px)`,
`calc(50% - ${sidebarWidth / 2}px)`
];
}
return sanitizeSplitSizes(sizes);
}
const currentLayoutMode = this.state.currentLayoutMode;
var dimensionProperty = currentLayoutMode === 2 ? 'height' : 'width';
try {
sizes = [
getPercentFromDimension($('#js-code-side'), dimensionProperty),
getPercentFromDimension($('#js-demo-side'), dimensionProperty)
];
if (sizes.filter(s => s).length !== 2) {
sizes = [50, 50];
}
} catch (e) {
sizes = [50, 50];
} finally {
/* eslint-disable no-unsafe-finally */
return sanitizeSplitSizes(sizes);
/* eslint-enable no-unsafe-finally */
}
}
saveSetting(setting, value) {
const d = deferred();
const obj = {
[setting]: value
};
db.local.set(obj, d.resolve);
return d.promise;
}
saveCode(key) {
const { currentItem } = this.state;
currentItem.updatedOn = Date.now();
currentItem.layoutMode = this.state.currentLayoutMode;
currentItem.mainSizes = this.getMainPaneSizes();
if (!currentItem.files) {
currentItem.sizes = this.getCodePaneSizes();
}
log('saving key', key || currentItem.id, currentItem);
function onSaveComplete() {
// No feedback on saving `code` key. Its just to silently preserve
// last written code.
if (key === 'code') {
return;
}
if (window.user && !navigator.onLine) {
alertsService.add(
'Item saved locally. Will save to account when you are online.'
);
} else {
alertsService.add('Item saved.');
}
this.setState({ unsavedEditCount: 0 });
}
return itemService
.setItem(key || currentItem.id, currentItem)
.then(onSaveComplete.bind(this));
}
// Save current item to storage
saveItem() {
if (
!window.user &&
!window.localStorage[LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN]
) {
const answer = confirm(
'Saving without signing in will save your work only on this machine and this browser. If you want it to be secure & available anywhere, please login in your account and then save.\n\nDo you still want to continue saving locally?'
);
window.localStorage[LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN] = true;
if (!answer) {
trackEvent('ui', LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN, 'login');
this.closeAllOverlays();
this.setState({ isLoginModalOpen: true });
return;
}
trackEvent('ui', LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN, 'local');
}
var isNewItem = !this.state.currentItem.id;
this.state.currentItem.id =
this.state.currentItem.id || 'item-' + generateRandomId();
this.setState({
isSaving: true
});
this.saveCode().then(() => {
this.setState({
isSaving: false
});
// TODO: May be setState with currentItem
// If this is the first save, and auto-saving settings is enabled,
// then start auto-saving from now on.
// This is done in `saveCode()` completion so that the
// auto-save notification overrides the `saveCode` function's notification.
if (!this.isAutoSavingEnabled && this.state.prefs.autoSave) {
this.isAutoSavingEnabled = true;
alertsService.add('Auto-save enabled.');
}
});
// Push into the items hash if its a new item being saved
if (isNewItem) {
itemService.setItemForUser(this.state.currentItem.id);
}
}
onCodeModeChange(ofWhat, mode) {
const item = { ...this.state.currentItem };
item[`${ofWhat}Mode`] = mode;
this.setState({ currentItem: item });
}
onCodeChange(type, code, isUserChange) {
if (this.state.currentItem.files) {
linearizeFiles(this.state.currentItem.files).map(file => {
if (file.path === type.path) {
file.content = code;
}
});
} else {
this.state.currentItem[type] = code;
}
if (isUserChange) {
this.incrementUnsavedChanges();
}
if (this.state.prefs.isJs13kModeOn) {
// Throttling codesize calculation
if (this.codeSizeCalculationTimeout) {
clearTimeout(this.codeSizeCalculationTimeout);
}
this.codeSizeCalculationTimeout = setTimeout(() => {
this.calculateCodeSize();
this.codeSizeCalculationTimeout = null;
}, 1000);
}
}
onCodeSettingsChange(type, settings) {
this.state.currentItem[`${type}Settings`] = {
acssConfig: settings
};
}
titleInputBlurHandler(e) {
this.setState({
currentItem: { ...this.state.currentItem, title: e.target.value }
});
if (this.state.currentItem.id) {
this.saveItem();
trackEvent('ui', 'titleChanged');
}
}
/**
* Handles all user triggered preference changes in the UI.
*/
updateSetting(settingName, value) {
const prefs = { ...this.state.prefs };
// If this was triggered from user interaction, save the setting
if (settingName) {
// var settingName = e.target.dataset.setting;
var obj = {};
log(settingName, value);
// const prefs = { ...this.state.prefs };
prefs[settingName] = value;
obj[settingName] = prefs[settingName];
this.setState({ prefs });
// We always save locally so that it gets fetched
// faster on future loads.
db.sync.set(obj, function () {
alertsService.add('Setting saved');
});
if (window.user) {
window.db.getDb().then(remoteDb => {
remoteDb
.collection('users')
.doc(window.user.uid)
.update({
[`settings.${settingName}`]: this.state.prefs[settingName]
})
.then(arg => {
log(`Setting "${settingName}" for user`, arg);
})
.catch(error => log(error));
});
}
trackEvent('ui', 'updatePref-' + settingName, prefs[settingName]);
}
this.contentWrap.applyCodemirrorSettings(prefs);
if (prefs.autoSave) {
if (!this.autoSaveInterval) {
this.autoSaveInterval = setInterval(() => {
this.autoSaveLoop();
}, this.AUTO_SAVE_INTERVAL);
}
} else {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
document.body.classList[prefs.lightVersion ? 'add' : 'remove'](
'light-version'
);
}
// Keeps getting called after certain interval to auto-save current creation
// if it needs to be.
autoSaveLoop() {
if (this.isAutoSavingEnabled && this.state.unsavedEditCount) {
this.saveItem();
}
}
loginBtnClickHandler() {
this.setState({ isLoginModalOpen: true });
}
profileBtnClickHandler() {
this.setState({ isProfileModalOpen: true });
}
logout() {
if (this.state.unsavedEditCount) {
var shouldDiscard = confirm(
'You have unsaved changes. Do you still want to logout?'
);
if (!shouldDiscard) {
return;
}
}
trackEvent('fn', 'loggedOut');
auth.logout();
this.setState({ isProfileModalOpen: false });
alertsService.add('Log out successfull');
}
itemClickHandler(item) {
setTimeout(() => {
this.openItem(item);
}, 350);
this.toggleSavedItemsPane();
}
itemRemoveBtnClickHandler(item) {
this.removeItem(item);
}
itemForkBtnClickHandler(item) {
this.toggleSavedItemsPane();
setTimeout(() => {
this.forkItem(item);
}, 350);
}
openNewCreationModal() {
if (this.state.unsavedEditCount) {
var shouldDiscard = confirm(
'You have unsaved changes. Do you still want to create something new?'
);
if (shouldDiscard) {
this.setState({
isCreateNewModalOpen: true
});
}
} else {
this.setState({
isCreateNewModalOpen: true
});
}
}
newBtnClickHandler() {
trackEvent('ui', 'newBtnClick');
this.openNewCreationModal();
}
openBtnClickHandler() {
trackEvent('ui', 'openBtnClick');
this.openSavedItemsPane();
}
detachedPreviewBtnHandler() {
trackEvent('ui', 'detachPreviewBtnClick');
this.contentWrap.detachPreview();
}
notificationsBtnClickHandler() {
this.setState({ isNotificationsModalOpen: true });
if (this.state.isNotificationsModalOpen && !this.hasSeenNotifications) {
this.hasSeenNotifications = true;
this.setState({ hasUnseenChangelog: false });
window.db.setUserLastSeenVersion(version);
}
trackEvent('ui', 'notificationButtonClick', version);
return false;
}
codepenBtnClickHandler(e) {
if (this.state.currentItem.cssMode === CssModes.ACSS) {
alert(
"Oops! CodePen doesn't supports Atomic CSS currently. \nHere is something you can still do -> https://medium.com/web-maker/sharing-your-atomic-css-work-on-codepen-a402001b26ab"
);
e.preventDefault();
return;
}
var json = {
title: 'A Web Maker experiment',
html: this.state.currentItem.html,
css: this.state.currentItem.css,
js: this.state.currentItem.js,
/* eslint-disable camelcase */
html_pre_processor: modes[this.state.currentItem.htmlMode].codepenVal,
css_pre_processor: modes[this.state.currentItem.cssMode].codepenVal,
js_pre_processor: modes[this.state.currentItem.jsMode].codepenVal,
css_external: this.state.currentItem.externalLibs.css
.split('\n')
.join(';'),
js_external: this.state.currentItem.externalLibs.js.split('\n').join(';')
/* eslint-enable camelcase */
};
if (!this.state.currentItem.title.match(/Untitled\s\d\d*-\d/)) {
json.title = this.state.currentItem.title;
}
json = JSON.stringify(json);
window.codepenForm.querySelector('input').value = json;
window.codepenForm.submit();
trackEvent('ui', 'openInCodepen');
e.preventDefault();
}
saveHtmlBtnClickHandler(e) {
saveAsHtml(this.state.currentItem);
trackEvent('ui', 'saveHtmlClick');
e.preventDefault();
}
runBtnClickHandler() {
this.contentWrap.setPreviewContent(true, true);
trackEvent('ui', 'runBtnClick');
}
exportItems() {
handleDownloadsPermission().then(() => {
this.fetchItems().then(items => {
var d = new Date();
var fileName = [
'web-maker-export',
d.getFullYear(),
d.getMonth() + 1,
d.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds()
].join('-');
fileName += '.json';
var blob = new Blob([JSON.stringify(items, false, 2)], {
type: 'application/json;charset=UTF-8'
});
downloadFile(fileName, blob);
trackEvent('fn', 'exportItems');
});
});
}
exportBtnClickHandler(e) {
this.exportItems();
e.preventDefault();
trackEvent('ui', 'exportBtnClicked');
}
screenshotBtnClickHandler(e) {
this.contentWrap.getDemoFrame(frame => {
takeScreenshot(frame.getBoundingClientRect());
});
e.preventDefault();
}
openSupportDeveloperModal() {
this.closeAllOverlays();
this.setState({
isSupportDeveloperModalOpen: true
});
}
supportDeveloperBtnClickHandler(e) {
this.openSupportDeveloperModal(e);
}
/**
* Called from inside ask-to-import-modal
*/
dontAskToImportAnymore(e) {
this.setState({ isAskToImportModalOpen: false });
window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS] = true;
if (e) {
trackEvent('ui', 'dontAskToImportBtnClick');
}
}
mergeImportedItems(items, isMergingToCloud = false) {
var existingItemIds = [];
var toMergeItems = {};
const d = deferred();
const { savedItems } = this.state;
items.forEach(item => {
// We can access `savedItems` here because this gets set when user
// opens the saved creations panel. And import option is available
// inside the saved items panel.
// When we are merging to cloud, savedItems contains the local items. And if we start matching,
// all items will match with themselves and nothing would import :P
if (!isMergingToCloud && savedItems[item.id]) {
// Item already exists
existingItemIds.push(item.id);
} else {
// log('merging', item.id);
toMergeItems[item.id] = item;
}
});
var mergedItemCount = items.length - existingItemIds.length;
if (existingItemIds.length) {
var shouldReplace = confirm(
existingItemIds.length +
' creations already exist. Do you want to replace them?'
);
if (shouldReplace) {
log('shouldreplace', shouldReplace);
items.forEach(item => {
toMergeItems[item.id] = item;
});
mergedItemCount = items.length;
}
}
if (mergedItemCount) {
itemService.saveItems(toMergeItems).then(() => {
d.resolve();
alertsService.add(
mergedItemCount + ' creations imported successfully.'
);
trackEvent('fn', 'itemsImported', mergedItemCount);
});
} else {
d.resolve();
}
this.closeSavedItemsPane();
return d.promise;
}
/**
* Called from inside ask-to-import-modal
*/
importCreationsAndSettingsIntoApp() {
this.mergeImportedItems(this.oldSavedItems, true).then(() => {
trackEvent('fn', 'oldItemsImported');
this.dontAskToImportAnymore();
});
}
editorFocusHandler(editor) {
this.editorWithFocus = editor;
}
modalOverlayClickHandler() {
this.closeAllOverlays();
}
splitUpdateHandler(sizes) {
const { currentItem } = this.state;
if (!sizes) {
currentItem.mainSizes = this.getMainPaneSizes();
currentItem.sizes = this.getCodePaneSizes();
return;
}
// Not using setState to avoid re-render
if (sizes.length === 3) {
if (currentItem.files) {
currentItem.mainSizes = sizes;
} else {
currentItem.sizes = sizes;
}
} else {
currentItem.mainSizes = sizes;
}
}
/**
* Calculate byte size of a text snippet
* @author Lea Verou
* MIT License
*/
calculateTextSize(text) {
if (!text) {
return 0;
}
var crlf = /(\r?\n|\r)/g,
whitespace = /(\r?\n|\r|\s+)/g;
const ByteSize = {
count: function (text, options) {
// Set option defaults
options = options || {};
options.lineBreaks = options.lineBreaks || 1;
options.ignoreWhitespace = options.ignoreWhitespace || false;
var length = text.length,
nonAscii = length - text.replace(/[\u0100-\uFFFF]/g, '').length,
lineBreaks = length - text.replace(crlf, '').length;
if (options.ignoreWhitespace) {
// Strip whitespace
text = text.replace(whitespace, '');
return text.length + nonAscii;
} else {
return (
length +
nonAscii +
Math.max(0, options.lineBreaks * (lineBreaks - 1))
);
}
},
format: function (count, plainText) {
var level = 0;
while (count > 1024) {
count /= 1024;
level++;
}
// Round to 2 decimals
count = Math.round(count * 100) / 100;
level = ['', 'K', 'M', 'G', 'T'][level];
return (
(plainText ? count : '<strong>' + count + '</strong>') +
' ' +
level +
'B'
);
}
};
return ByteSize.count(text);
}
getExternalLibCode() {
const item = this.state.currentItem;
var libs = (item.externalLibs && item.externalLibs.js) || '';
libs += ('\n' + item.externalLibs && item.externalLibs.css) || '';
libs = libs.split('\n').filter(lib => lib);
return libs.map(lib =>
fetch(lib)
.then(res => res.text())
.then(data => {
return {
code: data,
fileName: getFilenameFromUrl(lib)
};
})
);
}
calculateCodeSize() {
const item = this.state.currentItem;
var htmlPromise = computeHtml(item.html, item.htmlMode);
var cssPromise = computeCss(item.css, item.cssMode);
var jsPromise = computeJs(item.js, item.jsMode, false);
Promise.all([
htmlPromise,
cssPromise,
jsPromise,
...this.getExternalLibCode()
]).then(result => {
var html = result[0].code || '',
css = result[1].code || '',
js = result[2].code || '';
var fileContent = getCompleteHtml(html, css, js, item, true);
// Replace external lib urls with local relative urls (picked from zip)
fileContent = fileContent.replace(
/<script src="(.*\/)([^/<]*?)"/g,
'<script src="$2"'
);
var zip = new JSZip();
zip.file('index.html', fileContent);
for (let i = 3; i < result.length; i++) {
const externalLib = result[i];
zip.file(externalLib.fileName, externalLib.code);
}
var promise = null;
if (0 && JSZip.support.uint8array) {
promise = zip.generateAsync({ type: 'uint8array' });
} else {
promise = zip.generateAsync({
type: 'base64',
compression: 'DEFLATE',
compressionOptions: {
level: 9
}
});
}
promise.then(data => {
const zipContent = data;
const size = this.calculateTextSize(atob(data));
this.setState({
codeSize: size
});
this.currentItemZipBase64Data = data;
});
});
}
js13KHelpBtnClickHandler() {
this.setState({
isJs13KModalOpen: true
});
}
js13KDownloadBtnClickHandler() {
const a = document.createElement('a');
a.setAttribute('download', this.state.currentItem.title);
a.href = 'data:application/zip;base64,' + this.currentItemZipBase64Data;
document.body.appendChild(a);
a.click();
a.remove();
}
blankTemplateSelectHandler() {
this.createNewItem();
this.setState({ isCreateNewModalOpen: false });
}
blankFileTemplateSelectHandler() {
itemService.getCountOfFileModeItems().then(count => {
if (count < 2) {
this.createNewItem(true);
this.setState({ isCreateNewModalOpen: false });
} else {
trackEvent('ui', 'FileModeCreationLimitMessageSeen');
return alert(
'"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
);
}
});
}
templateSelectHandler(template, isFileMode) {
if (isFileMode) {
itemService.getCountOfFileModeItems().then(count => {
if (count < 2) {
fetch(
`templates/template-${isFileMode ? 'files-' : ''}${
template.id
}.json`
)
.then(res => res.json())
.then(json => {
this.forkItem(json);
});
this.setState({ isCreateNewModalOpen: false });
} else {
trackEvent('ui', 'FileModeCreationLimitMessageSeen');
return alert(
'"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
);
}
});
} else {
fetch(
`templates/template-${isFileMode ? 'files-' : ''}${template.id}.json`
)
.then(res => res.json())
.then(json => {
this.forkItem(json);
});
this.setState({ isCreateNewModalOpen: false });
}
}
importGithubRepoSelectHandler(repoUrl) {
importGithubRepo(repoUrl).then(files => {
this.createNewItem(true, files);
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(' ');
}
prettifyHandler(what) {
// 3 pane mode
if (typeof what === 'string') {
prettify({
content: this.state.currentItem[what],
type: { html: 'html', js: 'js', css: 'css' }[what]
}).then(formattedContent => {
if (this.state.currentItem[what] === formattedContent) {
return;
}
this.state.currentItem[what] = formattedContent;
this.setState({ currentItem: { ...this.state.currentItem } }, () => {
// TODO: This is not right way. Editors should refresh automatically
// on state change.
this.contentWrap.refreshEditor();
});
this.incrementUnsavedChanges();
});
return;
}
const selectedFile = what;
const currentItem = {
...this.state.currentItem,
files: [...this.state.currentItem.files]
};
prettify({ file: selectedFile }).then(formattedContent => {
if (formattedContent !== selectedFile.content) {
selectedFile.content = formattedContent;
this.incrementUnsavedChanges();
this.setState({ currentItem });
}
});
}
render(props, { catalogs = {}, prefs = {} }) {
return (
<I18nProvider language={this.state.prefs.lang} catalogs={catalogs}>
<div class={this.getRootClasses()}>
<div class="main-container">
<MainHeader
externalLibCount={this.state.externalLibCount}
openBtnHandler={this.openBtnClickHandler.bind(this)}
newBtnHandler={this.newBtnClickHandler.bind(this)}
saveBtnHandler={this.saveBtnClickHandler.bind(this)}
loginBtnHandler={this.loginBtnClickHandler.bind(this)}
profileBtnHandler={this.profileBtnClickHandler.bind(this)}
addLibraryBtnHandler={this.openAddLibrary.bind(this)}
runBtnClickHandler={this.runBtnClickHandler.bind(this)}
isFetchingItems={this.state.isFetchingItems}
isSaving={this.state.isSaving}
title={this.state.currentItem.title}
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
user={this.state.user}
isAutoPreviewOn={this.state.prefs.autoPreview}
unsavedEditCount={this.state.unsavedEditCount}
isFileMode={
this.state.currentItem && this.state.currentItem.files
}
/>
{this.state.currentItem && this.state.currentItem.files ? (
<ContentWrapFiles
currentItem={this.state.currentItem}
onCodeChange={this.onCodeChange.bind(this)}
onCodeSettingsChange={this.onCodeSettingsChange.bind(this)}
onCodeModeChange={this.onCodeModeChange.bind(this)}
onRef={comp => (this.contentWrap = comp)}
prefs={this.state.prefs}
onEditorFocus={this.editorFocusHandler.bind(this)}
onSplitUpdate={this.splitUpdateHandler.bind(this)}
onAddFile={this.addFileHandler.bind(this)}
onRemoveFile={this.removeFileHandler.bind(this)}
onRenameFile={this.renameFileHandler.bind(this)}
onFileDrop={this.fileDropHandler.bind(this)}
onFolderSelect={this.folderSelectHandler.bind(this)}
onPrettifyBtnClick={this.prettifyHandler.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)}
onPrettifyBtnClick={this.prettifyHandler.bind(this)}
/>
)}
<Footer
prefs={this.state.prefs}
layoutBtnClickHandler={this.layoutBtnClickHandler.bind(this)}
helpBtnClickHandler={() =>
this.setState({ isHelpModalOpen: true })
}
settingsBtnClickHandler={this.openSettings.bind(this)}
notificationsBtnClickHandler={this.notificationsBtnClickHandler.bind(
this
)}
supportDeveloperBtnClickHandler={this.supportDeveloperBtnClickHandler.bind(
this
)}
detachedPreviewBtnHandler={this.detachedPreviewBtnHandler.bind(
this
)}
codepenBtnClickHandler={this.codepenBtnClickHandler.bind(this)}
saveHtmlBtnClickHandler={this.saveHtmlBtnClickHandler.bind(this)}
keyboardShortcutsBtnClickHandler={this.openKeyboardShortcuts.bind(
this
)}
screenshotBtnClickHandler={this.screenshotBtnClickHandler.bind(
this
)}
onJs13KHelpBtnClick={this.js13KHelpBtnClickHandler.bind(this)}
onJs13KDownloadBtnClick={this.js13KDownloadBtnClickHandler.bind(
this
)}
hasUnseenChangelog={this.state.hasUnseenChangelog}
codeSize={this.state.codeSize}
/>
</div>
<SavedItemPane
itemsMap={this.state.savedItems}
isOpen={this.state.isSavedItemPaneOpen}
closeHandler={this.closeSavedItemsPane.bind(this)}
onItemSelect={this.itemClickHandler.bind(this)}
onItemRemove={this.itemRemoveBtnClickHandler.bind(this)}
onItemFork={this.itemForkBtnClickHandler.bind(this)}
onExport={this.exportBtnClickHandler.bind(this)}
mergeImportedItems={this.mergeImportedItems.bind(this)}
/>
<Alerts />
<Modal
show={this.state.isAddLibraryModalOpen}
closeHandler={() => this.setState({ isAddLibraryModalOpen: false })}
>
<AddLibrary
js={
this.state.currentItem.externalLibs
? this.state.currentItem.externalLibs.js
: ''
}
css={
this.state.currentItem.externalLibs
? this.state.currentItem.externalLibs.css
: ''
}
onChange={this.onExternalLibChange.bind(this)}
/>
</Modal>
<Modal
show={this.state.isNotificationsModalOpen}
closeHandler={() =>
this.setState({ isNotificationsModalOpen: false })
}
>
<Notifications
onSupportBtnClick={this.openSupportDeveloperModal.bind(this)}
/>
</Modal>
<Modal
extraClasses="modal--settings"
show={this.state.isSettingsModalOpen}
closeHandler={() => this.setState({ isSettingsModalOpen: false })}
>
<Settings
prefs={this.state.prefs}
onChange={this.updateSetting.bind(this)}
/>
</Modal>
<Modal
extraClasses="login-modal"
show={this.state.isLoginModalOpen}
closeHandler={() => this.setState({ isLoginModalOpen: false })}
>
<Login />
</Modal>
<Modal
show={this.state.isProfileModalOpen}
closeHandler={() => this.setState({ isProfileModalOpen: false })}
>
<Profile
user={this.state.user}
logoutBtnHandler={this.logout.bind(this)}
/>
</Modal>
<HelpModal
show={this.state.isHelpModalOpen}
closeHandler={() => this.setState({ isHelpModalOpen: false })}
onSupportBtnClick={this.openSupportDeveloperModal.bind(this)}
version={version}
/>
<SupportDeveloperModal
show={this.state.isSupportDeveloperModalOpen}
closeHandler={() =>
this.setState({ isSupportDeveloperModalOpen: false })
}
/>
<KeyboardShortcutsModal
show={this.state.isKeyboardShortcutsModalOpen}
closeHandler={() =>
this.setState({ isKeyboardShortcutsModalOpen: false })
}
/>
<AskToImportModal
show={this.state.isAskToImportModalOpen}
closeHandler={() =>
this.setState({ isAskToImportModalOpen: false })
}
oldSavedCreationsCount={this.oldSavedCreationsCount}
importBtnClickHandler={this.importCreationsAndSettingsIntoApp.bind(
this
)}
dontAskBtnClickHandler={this.dontAskToImportAnymore.bind(this)}
/>
<OnboardingModal
show={this.state.isOnboardModalOpen}
closeHandler={() => this.setState({ isOnboardModalOpen: false })}
/>
<Js13KModal
show={this.state.isJs13KModalOpen}
closeHandler={() => this.setState({ isJs13KModalOpen: false })}
/>
<CreateNewModal
show={this.state.isCreateNewModalOpen}
closeHandler={() => this.setState({ isCreateNewModalOpen: false })}
onBlankTemplateSelect={this.blankTemplateSelectHandler.bind(this)}
onBlankFileTemplateSelect={this.blankFileTemplateSelectHandler.bind(
this
)}
onTemplateSelect={this.templateSelectHandler.bind(this)}
onImportGithubRepoSelect={this.importGithubRepoSelectHandler.bind(
this
)}
/>
<CommandPalette
show={this.state.isCommandPaletteOpen}
files={linearizeFiles(this.state.currentItem.files || [])}
isCommandMode={this.state.isCommandPaletteInCommandMode}
closeHandler={() => this.setState({ isCommandPaletteOpen: false })}
/>
<Portal into="#portal">
<div
class="modal-overlay"
onClick={this.modalOverlayClickHandler.bind(this)}
/>
</Portal>
<Icons />
<form
style="display:none;"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
id="codepenForm"
>
<input
type="hidden"
name="data"
value='{"title": "New Pen!", "html": "<div>Hello, World!</div>"}'
/>
</form>
</div>
</I18nProvider>
);
}
}