1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-05-12 21:35:21 +02:00
Arnab Sen c62592bf57
Remove stylesheets from html and include in js
The stylesheets that were linked in the index.html were redundant, as
they were already bundled by the webpack. The style.css was again imported in
the components/app.jsx. Now, it is placed in the index.js in a
order that style.css overrides the other stylesheets.
2022-03-11 20:05:28 +05:30

1839 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) {
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>
);
}
}