1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-18 04:21:12 +02:00

get preact files into root

This commit is contained in:
Kushagra Gour
2018-06-16 14:56:14 +05:30
parent f5d2b5d1a7
commit dfcd5bc505
57 changed files with 18452 additions and 40 deletions

60
src/CodeMirror.js Normal file
View File

@@ -0,0 +1,60 @@
// Most of the code from this file comes from:
// https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js
import CodeMirror from 'codemirror';
// Make CodeMirror available globally so the modes' can register themselves.
window.CodeMirror = CodeMirror
if (!CodeMirror.modeURL) CodeMirror.modeURL = 'lib/codemirror/mode/%N/%N.js';
var loading = {}
function splitCallback(cont, n) {
var countDown = n
return function () {
if (--countDown === 0) cont()
}
}
function ensureDeps(mode, cont) {
var deps = CodeMirror.modes[mode].dependencies
if (!deps) return cont()
var missing = []
for (var i = 0; i < deps.length; ++i) {
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i])
}
if (!missing.length) return cont()
var split = splitCallback(cont, missing.length)
for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split)
}
CodeMirror.requireMode = function (mode, cont) {
if (typeof mode !== 'string') mode = mode.name
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont)
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont)
var file = CodeMirror.modeURL.replace(/%N/g, mode)
var script = document.createElement('script')
script.src = file
var others = document.getElementsByTagName('script')[0]
var list = loading[mode] = [cont]
CodeMirror.on(script, 'load', function () {
ensureDeps(mode, function () {
for (var i = 0; i < list.length; ++i) list[i]()
})
})
others.parentNode.insertBefore(script, others)
}
CodeMirror.autoLoadMode = function (instance, mode) {
if (CodeMirror.modes.hasOwnProperty(mode)) return
CodeMirror.requireMode(mode, function () {
instance.setOption('mode', instance.getOption('mode'))
})
}
export default CodeMirror

0
src/Inconsolata.ttf Executable file → Normal file
View File

49
src/analytics.js Normal file
View File

@@ -0,0 +1,49 @@
import {
log
} from "./utils";
/* global ga */
// eslint-disable-next-line max-params
export function trackEvent(category, action, label, value) {
if (window.DEBUG) {
log('trackevent', category, action, label, value);
return;
}
if (window.ga) {
ga('send', 'event', category, action, label, value);
}
};
// if online, load after sometime
if (false && navigator.onLine && !window.DEBUG) {
/* eslint-disable */
// prettier-ignore
setTimeout(function () {
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
if (location.href.indexOf('chrome-extension://') === -1) {
ga('create', 'UA-87786708-1');
} else {
ga('create', 'UA-87786708-1', {
'cookieDomain': 'none'
});
// required for chrome extension protocol
ga('set', 'checkProtocolTask', function () { /* nothing */ });
}
ga('send', 'pageview');
}, 100);
/* eslint-enable */
}

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

43
src/auth.js Normal file
View File

@@ -0,0 +1,43 @@
import {
trackEvent
} from './analytics';
import firebase from 'firebase/app'
export const auth = {
logout() {
firebase.auth().signOut();
},
login(providerName) {
var provider;
if (providerName === 'facebook') {
provider = new firebase.auth.FacebookAuthProvider();
} else if (providerName === 'twitter') {
provider = new firebase.auth.TwitterAuthProvider();
} else if (providerName === 'google') {
provider = new firebase.auth.GoogleAuthProvider();
provider.addScope('https://www.googleapis.com/auth/userinfo.profile');
} else {
provider = new firebase.auth.GithubAuthProvider();
}
return firebase
.auth()
.signInWithPopup(provider)
.then(function () {
trackEvent('fn', 'loggedIn', providerName);
// Save to recommend next time
window.db.local.set({
lastAuthProvider: providerName
});
})
.catch(function (error) {
utils.log(error);
if (error.code === 'auth/account-exists-with-different-credential') {
alert(
'You have already signed up with the same email using different social login'
);
}
});
}
}

92
src/codeModes.js Normal file
View File

@@ -0,0 +1,92 @@
export const HtmlModes = {
HTML: 'html',
MARKDOWN: 'markdown',
JADE: 'jade' // unsafe eval is put in manifest for this file
};
export const CssModes = {
CSS: 'css',
SCSS: 'scss',
SASS: 'sass',
LESS: 'less',
STYLUS: 'stylus',
ACSS: 'acss'
};
export const JsModes = {
JS: 'js',
ES6: 'es6',
COFFEESCRIPT: 'coffee',
TS: 'typescript'
};
export const modes = {};
modes[HtmlModes.HTML] = {
label: 'HTML',
cmMode: 'htmlmixed',
codepenVal: 'none'
};
modes[HtmlModes.MARKDOWN] = {
label: 'Markdown',
cmMode: 'markdown',
codepenVal: 'markdown'
};
modes[HtmlModes.JADE] = {
label: 'Pug',
cmMode: 'pug',
codepenVal: 'pug'
};
modes[JsModes.JS] = {
label: 'JS',
cmMode: 'javascript',
codepenVal: 'none'
};
modes[JsModes.COFFEESCRIPT] = {
label: 'CoffeeScript',
cmMode: 'coffeescript',
codepenVal: 'coffeescript'
};
modes[JsModes.ES6] = {
label: 'ES6 (Babel)',
cmMode: 'jsx',
codepenVal: 'babel'
};
modes[JsModes.TS] = {
label: 'TypeScript',
cmPath: 'jsx',
cmMode: 'text/typescript-jsx',
codepenVal: 'typescript'
};
modes[CssModes.CSS] = {
label: 'CSS',
cmPath: 'css',
cmMode: 'css',
codepenVal: 'none'
};
modes[CssModes.SCSS] = {
label: 'SCSS',
cmPath: 'css',
cmMode: 'text/x-scss',
codepenVal: 'scss'
};
modes[CssModes.SASS] = {
label: 'SASS',
cmMode: 'sass',
codepenVal: 'sass'
};
modes[CssModes.LESS] = {
label: 'LESS',
cmPath: 'css',
cmMode: 'text/x-less',
codepenVal: 'less'
};
modes[CssModes.STYLUS] = {
label: 'Stylus',
cmMode: 'stylus',
codepenVal: 'stylus'
};
modes[CssModes.ACSS] = {
label: 'Atomic CSS',
cmPath: 'css',
cmMode: 'css',
codepenVal: 'notsupported',
cmDisable: true,
hasSettings: true
};

View File

@@ -0,0 +1,145 @@
import { h, Component } from 'preact';
import { jsLibs, cssLibs } from '../libraryList';
import { trackEvent } from '../analytics';
import { LibraryAutoSuggest } from './LibraryAutoSuggest';
export default class AddLibrary extends Component {
constructor(props) {
super(props);
this.state = {
css: props.css || '',
js: props.js || ''
};
}
onSelectChange(e) {
const target = e.target;
if (!target.value) {
return;
}
const type = target.selectedOptions[0].dataset.type;
if (type === 'js') {
this.setState({
js: `${this.state.js}\n${target.value}`
});
} else {
this.setState({
css: `${this.state.css}\n${target.value}`
});
}
trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
this.props.onChange({ js: this.state.js, css: this.state.css });
// Reset the select to the default value
target.value = '';
}
textareaBlurHandler(e, textarea) {
const target = e ? e.target : textarea;
const type = target.dataset.lang;
if (type === 'js') {
this.setState({
js: target.value || ''
});
} else {
this.setState({
css: target.value || ''
});
}
// trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
this.props.onChange({ js: this.state.js, css: this.state.css });
}
suggestionSelectHandler(value) {
const textarea = value.match(/\.js$/)
? window.externalJsTextarea
: window.externalCssTextarea;
textarea.value = `${textarea.value}\n${value}`;
window.externalLibrarySearchInput.value = '';
this.textareaBlurHandler(null, textarea);
}
render() {
return (
<div>
<h1>Add Library</h1>
<div class="flex">
<svg style="width: 30px; height: 30px;fill:#999">
<use xlinkHref="#search" />
</svg>
<LibraryAutoSuggest
fullWidth
onSelect={this.suggestionSelectHandler.bind(this)}
>
<input
type="text"
id="externalLibrarySearchInput"
class="full-width"
placeholder="Type here to search libraries"
/>
</LibraryAutoSuggest>
</div>
<div class="tar opacity--70">
<small>Powered by cdnjs</small>
</div>
<div style="margin:20px 0;">
Choose from popular libraries:
<select
name=""
id="js-add-library-select"
onChange={this.onSelectChange.bind(this)}
>
<option value="">-------</option>
<optgroup label="JavaScript Libraries">
{jsLibs.map(lib => (
<option data-type={lib.type} value={lib.url}>
{lib.label}
</option>
))}
</optgroup>
<optgroup label="CSS Libraries">
{cssLibs.map(lib => (
<option data-type={lib.type} value={lib.url}>
{lib.label}
</option>
))}
</optgroup>
</select>
</div>
<h3 class="mb-0">JS</h3>
<p class="mt-0 help-text">Put each library in new line</p>
<p style="font-size: 0.8em;" class="show-when-extension opacity--70">
Note: You can load external scripts from following domains: localhost,
https://ajax.googleapis.com, https://code.jquery.com,
https://cdnjs.cloudflare.com, https://unpkg.com, https://maxcdn.com,
https://cdn77.com, https://maxcdn.bootstrapcdn.com,
https://cdn.jsdelivr.net/, https://rawgit.com, https://wzrd.in
</p>
<textarea
onBlur={this.textareaBlurHandler.bind(this)}
data-lang="js"
class="full-width"
id="externalJsTextarea"
cols="30"
rows="5"
placeholder="Put each library in new line"
value={this.state.js}
/>
<h3 class="mb-0">CSS</h3>
<p class="mt-0 help-text">Put each library in new line</p>
<textarea
onBlur={this.textareaBlurHandler.bind(this)}
data-lang="css"
class="full-width"
id="externalCssTextarea"
cols="30"
rows="5"
placeholder="Put each library in new line"
value={this.state.css}
/>
</div>
);
}
}

12
src/components/Alerts.jsx Normal file
View File

@@ -0,0 +1,12 @@
import { h, Component } from 'preact';
import { log } from '../utils';
export class Alerts extends Component {
shouldComponentUpdate(nextProps, nextState) {
return false;
}
render() {
return <div class="alerts-container" id="js-alerts-container" />;
}
}

View File

@@ -0,0 +1,39 @@
import { h, Component } from 'preact';
import Modal from './Modal';
export default class AskToImportModal extends Component {
render() {
return (
<Modal
extraClasses="ask-to-import-modal"
show={this.props.show}
closeHandler={this.props.closeHandler}
>
<h2>Import your creations in your account</h2>
<div>
<p>
You have <span>{this.props.oldSavedCreationsCount}</span> creations
saved in your local machine. Do you want to import those creations
in your account so they are more secure and accessible anywhere?
</p>
<p>
It's okay if you don't want to. You can simply logout and access
them anytime on this browser.
</p>
<div class="flex flex-h-end">
<button onClick={this.props.dontAskBtnClickHandler} class="btn">
Don't ask me again
</button>
<button
onClick={this.props.importBtnClickHandler}
class="btn btn--primary ml-1"
>
Yes, please import
</button>
</div>
</div>
</Modal>
);
}
}

View File

@@ -0,0 +1,37 @@
import { h, Component } from 'preact';
import CodeMirror from '../CodeMirror';
import 'codemirror/mode/javascript/javascript.js';
export default class CodeMirrorBox extends Component {
componentDidMount() {
this.initEditor();
}
shouldComponentUpdate() {
return false;
}
initEditor() {
const options = this.props.options;
this.cm = CodeMirror.fromTextArea(this.textarea, this.props.options);
if (this.props.onChange) {
this.cm.on('change', this.props.onChange);
}
if (this.props.onBlur) {
this.cm.on('blur', this.props.onBlur);
}
this.props.onCreation(this.cm);
}
render() {
return (
<textarea
ref={el => (this.textarea = el)}
name=""
id=""
cols="30"
rows="10"
/>
);
}
}

View File

@@ -0,0 +1,946 @@
import { h, Component } from 'preact';
import UserCodeMirror from './UserCodeMirror.jsx';
import { computeHtml, computeCss, computeJs } from '../computes';
import { modes, HtmlModes, CssModes, JsModes } from '../codeModes';
import { log, writeFile, loadJS, getCompleteHtml } from '../utils';
import { SplitPane } from './SplitPane.jsx';
import { trackEvent } from '../analytics';
import CodeMirror from '../CodeMirror';
import CodeMirrorBox from './CodeMirrorBox';
import { deferred } from '../deferred';
import CssSettingsModal from './CssSettingsModal';
const BASE_PATH = chrome.extension || window.DEBUG ? '/' : '/app';
const minCodeWrapSize = 33;
export default class ContentWrap extends Component {
constructor(props) {
super(props);
this.state = {
isConsoleOpen: false,
isCssSettingsModalOpen: false
};
this.updateTimer = null;
this.updateDelay = 500;
this.htmlMode = HtmlModes.HTML;
this.jsMode = HtmlModes.HTML;
this.cssMode = CssModes.CSS;
this.jsMode = JsModes.JS;
this.prefs = {};
this.codeInPreview = { html: null, css: null, js: null };
this.cmCodes = { html: props.currentItem.html, css: '', js: '' };
this.cm = {};
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);
}
onHtmlCodeChange(editor, change) {
this.cmCodes.html = editor.getValue();
this.props.onCodeChange(
'html',
this.cmCodes.html,
change.origin !== 'setValue'
);
this.onCodeChange(editor, change);
}
onCssCodeChange(editor, change) {
this.cmCodes.css = editor.getValue();
this.props.onCodeChange(
'css',
this.cmCodes.css,
change.origin !== 'setValue'
);
this.onCodeChange(editor, change);
}
onJsCodeChange(editor, change) {
this.cmCodes.js = editor.getValue();
this.props.onCodeChange(
'js',
this.cmCodes.js,
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) {
const shouldInlineJs =
!window.webkitRequestFileSystem || !window.IS_EXTENSION;
var contents = getCompleteHtml(
html,
css,
shouldInlineJs ? js : null,
this.props.currentItem
);
var blob = new Blob([contents], { type: 'text/plain;charset=UTF-8' });
var blobjs = new Blob([js], { type: 'text/plain;charset=UTF-8' });
// Track if people have written code.
if (!trackEvent.hasTrackedCode && (html || css || js)) {
trackEvent('fn', 'hasCode');
trackEvent.hasTrackedCode = true;
}
if (shouldInlineJs) {
if (this.detachedWindow) {
log('✉️ Sending message to detached window');
this.detachedWindow.postMessage({ contents }, '*');
} else {
this.frame.src = this.frame.src;
setTimeout(() => {
this.frame.contentDocument.open();
this.frame.contentDocument.write(contents);
this.frame.contentDocument.close();
}, 10);
}
} else {
// we need to store user script in external JS file to prevent inline-script
// CSP from affecting it.
writeFile('script.js', blobjs, function() {
writeFile('preview.html', blob, function() {
var origin = chrome.i18n.getMessage()
? `chrome-extension://${chrome.i18n.getMessage('@@extension_id')}`
: `${location.origin}`;
var src = `filesystem:${origin}/temporary/preview.html`;
if (scope.detachedWindow) {
scope.detachedWindow.postMessage(src, '*');
} else {
frame.src = src;
}
});
});
}
}
cleanupErrors(lang) {
this.cm[lang].clearGutter('error-gutter');
}
showErrors(lang, errors) {
var editor = this.cm[lang];
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('html');
this.cleanupErrors('css');
this.cleanupErrors('js');
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;
const cssMode = this.props.currentItem.cssMode;
// If just CSS was changed (and everything shudn't be empty),
// change the styles inside the iframe.
if (
!isForced &&
currentCode.html === this.codeInPreview.html &&
currentCode.js === this.codeInPreview.js
) {
computeCss(
cssMode === CssModes.ACSS ? currentCode.html : currentCode.css,
cssMode,
this.props.currentItem.cssSettings
).then(result => {
if (cssMode === CssModes.ACSS) {
this.cm.css.setValue(result.code || '');
}
if (targetFrame.contentDocument.querySelector('#webmakerstyle')) {
targetFrame.contentDocument.querySelector(
'#webmakerstyle'
).textContent =
result.code || '';
}
});
} else {
var htmlPromise = computeHtml(
currentCode.html,
this.props.currentItem.htmlMode
);
var cssPromise = computeCss(
cssMode === CssModes.ACSS ? currentCode.html : currentCode.css,
cssMode,
this.props.currentItem.cssSettings
);
var jsPromise = computeJs(
currentCode.js,
this.props.currentItem.jsMode,
true,
this.props.prefs.infiniteLoopTimeout
);
Promise.all([htmlPromise, cssPromise, jsPromise]).then(result => {
if (cssMode === CssModes.ACSS) {
this.cm.css.setValue(result[1].code || '');
}
this.createPreviewFile(
result[0].code || '',
result[1].code || '',
result[2].code || ''
);
result.forEach(result => {
if (result.errors) {
this.showErrors(result.errors.lang, result.errors.data);
}
});
});
}
this.codeInPreview.html = currentCode.html;
this.codeInPreview.css = currentCode.css;
this.codeInPreview.js = currentCode.js;
}
isValidItem(item) {
return !!item.title;
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.state.isConsoleOpen !== nextState.isConsoleOpen ||
this.state.isCssSettingsModalOpen !== nextState.isCssSettingsModalOpen ||
this.state.codeSplitSizes != nextState.codeSplitSizes ||
this.state.mainSplitSizes != nextState.mainSplitSizes ||
this.props.currentLayoutMode !== nextProps.currentLayoutMode
);
}
componentDidUpdate() {
// 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);
}
refreshEditor() {
this.cmCodes.html = this.props.currentItem.html;
this.cmCodes.css = this.props.currentItem.css;
this.cmCodes.js = this.props.currentItem.js;
this.cm.html.setValue(this.cmCodes.html || '');
this.cm.css.setValue(this.cmCodes.css || '');
this.cm.js.setValue(this.cmCodes.js || '');
this.cm.html.refresh();
this.cm.css.refresh();
this.cm.js.refresh();
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),
this.updateCssMode(this.props.currentItem.cssMode),
this.updateJsMode(this.props.currentItem.jsMode)
]).then(() => this.setPreviewContent(true));
}
applyCodemirrorSettings(prefs) {
if (!this.cm) {
return;
}
htmlCodeEl.querySelector(
'.CodeMirror'
).style.fontSize = cssCodeEl.querySelector(
'.CodeMirror'
).style.fontSize = jsCodeEl.querySelector(
'.CodeMirror'
).style.fontSize = `${parseInt(prefs.fontSize, 10)}px`;
window.consoleEl.querySelector('.CodeMirror').style.fontSize = `${parseInt(
prefs.fontSize,
10
)}px`;
// Replace correct css file in LINK tags's href
window.editorThemeLinkTag.href = `lib/codemirror/theme/${
prefs.editorTheme
}.css`;
window.fontStyleTag.textContent = window.fontStyleTemplate.textContent.replace(
/fontname/g,
(prefs.editorFont === 'other'
? prefs.editorCustomFont
: prefs.editorFont) || 'FiraCode'
);
// window.customEditorFontInput.classList[
// prefs.editorFont === 'other' ? 'remove' : 'add'
// ]('hide');
this.consoleCm.setOption('theme', prefs.editorTheme);
['html', 'js', 'css'].forEach(type => {
this.cm[type].setOption('indentWithTabs', prefs.indentWith !== 'spaces');
this.cm[type].setOption(
'blastCode',
prefs.isCodeBlastOn ? { effect: 2, shake: false } : false
);
this.cm[type].setOption('indentUnit', +prefs.indentSize);
this.cm[type].setOption('tabSize', +prefs.indentSize);
this.cm[type].setOption('theme', prefs.editorTheme);
this.cm[type].setOption('keyMap', prefs.keymap);
this.cm[type].setOption('lineWrapping', prefs.lineWrap);
this.cm[type].refresh();
});
}
// Check all the code wrap if they are minimized or maximized
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, cssCodeEl, jsCodeEl].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);
}
toggleCodeWrapCollapse(codeWrapEl) {
if (
codeWrapEl.classList.contains('is-minimized') ||
codeWrapEl.classList.contains('is-maximized')
) {
codeWrapEl.classList.remove('is-minimized');
codeWrapEl.classList.remove('is-maximized');
this.codeSplitInstance.setSizes([33.3, 33.3, 33.3]);
} else {
const id = parseInt(codeWrapEl.dataset.codeWrapId, 10);
var arr = [
`${minCodeWrapSize}px`,
`${minCodeWrapSize}px`,
`${minCodeWrapSize}px`
];
arr[id] = `calc(100% - ${minCodeWrapSize * 2}px)`;
this.codeSplitInstance.setSizes(arr);
codeWrapEl.classList.add('is-maximized');
}
}
collapseBtnHandler(e) {
var codeWrapParent =
e.currentTarget.parentElement.parentElement.parentElement;
this.toggleCodeWrapCollapse(codeWrapParent);
trackEvent('ui', 'paneCollapseBtnClick', codeWrapParent.dataset.type);
}
codeWrapHeaderDblClickHandler(e) {
if (!e.target.classList.contains('js-code-wrap__header')) {
return;
}
const codeWrapParent = e.target.parentElement;
this.toggleCodeWrapCollapse(codeWrapParent);
trackEvent('ui', 'paneHeaderDblClick', codeWrapParent.dataset.type);
}
resetSplitting() {
this.setState({
codeSplitSizes: this.getCodeSplitSizes(),
mainSplitSizes: this.getMainSplitSizesToApply()
});
}
// Returns the sizes of main code & preview panes.
getMainSplitSizesToApply() {
var mainSplitSizes;
const { currentItem, currentLayoutMode } = this.props;
if (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 = currentLayoutMode === 5 ? [75, 25] : [50, 50];
}
return mainSplitSizes;
}
getCodeSplitSizes() {
if (this.props.currentItem && this.props.currentItem.sizes) {
return this.props.currentItem.sizes;
} else {
return [33.33, 33.33, 33.33];
}
}
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);
}
}
codeSplitDragStart() {
document.body.classList.add('is-dragging');
}
codeSplitDragEnd() {
this.updateCodeWrapCollapseStates();
document.body.classList.remove('is-dragging');
}
/**
* 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;
const htmlModeLabel = $('#js-html-mode-label');
htmlModeLabel.textContent = modes[value].label;
// FIXME - use a better selector for the mode selectbox
htmlModeLabel.parentElement.querySelector('select').value = value;
this.cm.html.setOption('mode', modes[value].cmMode);
CodeMirror.autoLoadMode(
this.cm.html,
modes[value].cmPath || modes[value].cmMode
);
return this.handleModeRequirements(value);
}
updateCssMode(value) {
this.props.onCodeModeChange('css', value);
this.props.currentItem.cssMode = value;
const cssModeLabel = $('#js-css-mode-label');
cssModeLabel.textContent = modes[value].label;
// FIXME - use a better selector for the mode selectbox
cssModeLabel.parentElement.querySelector('select').value = value;
this.cm.css.setOption('mode', modes[value].cmMode);
this.cm.css.setOption('readOnly', modes[value].cmDisable);
window.cssSettingsBtn.classList[
modes[value].hasSettings ? 'remove' : 'add'
]('hide');
CodeMirror.autoLoadMode(
this.cm.css,
modes[value].cmPath || modes[value].cmMode
);
return this.handleModeRequirements(value);
}
updateJsMode(value) {
this.props.onCodeModeChange('js', value);
this.props.currentItem.jsMode = value;
const jsModeLabel = $('#js-js-mode-label');
jsModeLabel.textContent = modes[value].label;
// FIXME - use a better selector for the mode selectbox
jsModeLabel.parentElement.querySelector('select').value = value;
this.cm.js.setOption('mode', modes[value].cmMode);
CodeMirror.autoLoadMode(
this.cm.js,
modes[value].cmPath || modes[value].cmMode
);
return this.handleModeRequirements(value);
}
codeModeChangeHandler(e) {
var mode = e.target.value;
var type = e.target.dataset.type;
var currentMode = this.props.currentItem[
type === 'html' ? 'htmlMode' : type === 'css' ? 'cssMode' : 'jsMode'
];
if (currentMode !== mode) {
if (type === 'html') {
this.updateHtmlMode(mode).then(() => this.setPreviewContent(true));
} else if (type === 'js') {
this.updateJsMode(mode).then(() => this.setPreviewContent(true));
} else if (type === 'css') {
this.updateCssMode(mode).then(() => this.setPreviewContent(true));
}
trackEvent('ui', 'updateCodeMode', mode);
}
}
detachPreview() {
if (this.detachedWindow) {
this.detachedWindow.focus();
return;
}
const iframeBounds = this.frame.getBoundingClientRect();
const iframeWidth = iframeBounds.width;
const iframeHeight = iframeBounds.height;
document.body.classList.add('is-detached-mode');
window.globalConsoleContainerEl.insertBefore(window.consoleEl, null);
this.detachedWindow = window.open(
'./preview.html',
'Web Maker',
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
);
// Trigger initial render in detached window
setTimeout(() => {
this.setPreviewContent(true);
}, 1500);
function checkWindow() {
if (this.detachedWindow && this.detachedWindow.closed) {
clearInterval(intervalID);
document.body.classList.remove('is-detached-mode');
$('#js-demo-side').insertBefore(window.consoleEl, null);
this.detachedWindow = null;
// Update main frame preview to get latest changes (which were not
// getting reflected while detached window was open)
this.setPreviewContent(true);
}
}
var intervalID = window.setInterval(checkWindow.bind(this), 500);
}
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++;
});
logCountEl.textContent = this.logCount;
/* 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;
window.logCountEl.textContent = this.logCount;
}
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');
}
}
cssSettingsBtnClickHandler() {
this.setState({ isCssSettingsModalOpen: true });
trackEvent('ui', 'cssSettingsBtnClick');
}
cssSettingsChangeHandler(settings) {
this.props.onCodeSettingsChange('css', settings);
this.setPreviewContent(true);
}
getDemoFrame(callback) {
callback(this.frame);
}
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)}
>
<SplitPane
class="code-side"
id="js-code-side"
sizes={this.state.codeSplitSizes}
minSize={minCodeWrapSize}
direction={
this.props.currentLayoutMode === 2 ||
this.props.currentLayoutMode === 5
? 'horizontal'
: 'vertical'
}
onDragStart={this.codeSplitDragStart.bind(this)}
onDragEnd={this.codeSplitDragEnd.bind(this)}
onSplit={splitInstance => (this.codeSplitInstance = splitInstance)}
>
<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"
onDblClick={this.codeWrapHeaderDblClickHandler.bind(this)}
>
<label class="btn-group" dropdow title="Click to change">
<span id="js-html-mode-label" class="code-wrap__header-label">
HTML
</span>
<span class="caret" />
<select
data-type="html"
class="js-mode-select hidden-select"
onChange={this.codeModeChangeHandler.bind(this)}
>
<option value="html">HTML</option>
<option value="markdown">Markdown</option>
<option value="jade">Pug</option>
</select>
</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"
onClick={this.collapseBtnHandler.bind(this)}
/>
</div>
</div>
<UserCodeMirror
options={{
mode: 'htmlmixed',
profile: 'xhtml',
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
noAutocomplete: true,
matchTags: { bothTags: true },
emmet: true
}}
onChange={this.onHtmlCodeChange.bind(this)}
onCreation={el => (this.cm.html = el)}
/>
</div>
<div
data-code-wrap-id="1"
id="cssCodeEl"
data-type="css"
class="code-wrap"
onTransitionEnd={this.updateCodeWrapCollapseStates.bind(this)}
>
<div
class="js-code-wrap__header code-wrap__header"
title="Double click to toggle code pane"
onDblClick={this.codeWrapHeaderDblClickHandler.bind(this)}
>
<label class="btn-group" title="Click to change">
<span id="js-css-mode-label" class="code-wrap__header-label">
CSS
</span>
<span class="caret" />
<select
data-type="css"
class="js-mode-select hidden-select"
onChange={this.codeModeChangeHandler.bind(this)}
>
<option value="css">CSS</option>
<option value="scss">SCSS</option>
<option value="sass">SASS</option>
<option value="less">LESS</option>
<option value="stylus">Stylus</option>
<option value="acss">Atomic CSS</option>
</select>
</label>
<div class="code-wrap__header-right-options">
<a
href="#"
id="cssSettingsBtn"
title="Atomic CSS configuration"
onClick={this.cssSettingsBtnClickHandler.bind(this)}
class="code-wrap__header-btn hide"
>
<svg>
<use xlinkHref="#settings-icon" />
</svg>
</a>
<a
class="js-code-collapse-btn code-wrap__header-btn code-wrap__collapse-btn"
title="Toggle code pane"
onClick={this.collapseBtnHandler.bind(this)}
/>
</div>
</div>
<UserCodeMirror
options={{
mode: 'css',
gutters: [
'error-gutter',
'CodeMirror-linenumbers',
'CodeMirror-foldgutter'
],
emmet: true
}}
onChange={this.onCssCodeChange.bind(this)}
onCreation={el => (this.cm.css = el)}
/>
</div>
<div
data-code-wrap-id="2"
id="jsCodeEl"
data-type="js"
class="code-wrap"
onTransitionEnd={this.updateCodeWrapCollapseStates.bind(this)}
>
<div
class="js-code-wrap__header code-wrap__header"
title="Double click to toggle code pane"
onDblClick={this.codeWrapHeaderDblClickHandler.bind(this)}
>
<label class="btn-group" title="Click to change">
<span id="js-js-mode-label" class="code-wrap__header-label">
JS
</span>
<span class="caret" />
<select
data-type="js"
class="js-mode-select hidden-select"
onChange={this.codeModeChangeHandler.bind(this)}
>
<option value="js">JS</option>
<option value="coffee">CoffeeScript</option>
<option value="es6">ES6 (Babel)</option>
<option value="typescript">TypeScript</option>
</select>
</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"
onClick={this.collapseBtnHandler.bind(this)}
/>
</div>
</div>
<UserCodeMirror
options={{
mode: 'javascript',
gutters: [
'error-gutter',
'CodeMirror-linenumbers',
'CodeMirror-foldgutter'
]
}}
autoComplete={this.props.prefs.autoComplete}
onChange={this.onJsCodeChange.bind(this)}
onCreation={el => (this.cm.js = el)}
/>
{/* Inlet(scope.cm.js); */}
</div>
</SplitPane>
<div class="demo-side" id="js-demo-side" style="">
<iframe
ref={el => (this.frame = el)}
src="about://blank"
frameborder="0"
id="demo-frame"
allowfullscreen
/>
<div
id="consoleEl"
class={`console ${this.state.isConsoleOpen ? '' : 'is-minimized'}`}
>
<div id="consoleLogEl" class="console__log">
<div
class="js-console__header code-wrap__header"
title="Double click to toggle console"
onDblClick={this.toggleConsole.bind(this)}
>
<span class="code-wrap__header-label">
Console (<span id="logCountEl">0</span>)
</span>
<div class="code-wrap__header-right-options">
<a
class="code-wrap__header-btn"
title="Clear console (CTRL + L)"
onClick={this.clearConsoleBtnClickHandler.bind(this)}
>
<svg>
<use xlinkHref="#cancel-icon" />
</svg>
</a>
<a
class="code-wrap__header-btn code-wrap__collapse-btn"
title="Toggle console"
onClick={this.toggleConsole.bind(this)}
/>
</div>
</div>
<CodeMirrorBox
options={{
mode: 'javascript',
lineWrapping: true,
theme: 'monokai',
foldGutter: true,
readOnly: true,
gutters: ['CodeMirror-foldgutter']
}}
onCreation={el => (this.consoleCm = el)}
/>
</div>
<div
id="consolePromptEl"
class="console__prompt flex flex-v-center"
>
<svg width="18" height="18" fill="#346fd2">
<use xlinkHref="#chevron-icon" />
</svg>
<input
onKeyUp={this.evalConsoleExpr.bind(this)}
class="console-exec-input"
/>
</div>
</div>
<CssSettingsModal
show={this.state.isCssSettingsModalOpen}
closeHandler={() =>
this.setState({ isCssSettingsModalOpen: false })
}
onChange={this.cssSettingsChangeHandler.bind(this)}
settings={this.props.currentItem.cssSettings}
editorTheme={this.props.prefs.editorTheme}
/>
</div>
</SplitPane>
);
}
}

View File

@@ -0,0 +1,43 @@
import { h, Component } from 'preact';
import Modal from './Modal';
import CodeMirrorBox from './CodeMirrorBox';
export default class CssSettingsModal extends Component {
componentDidUpdate() {
if (this.props.show) {
setTimeout(() => {
if (this.props.settings) {
this.cm.setValue(this.props.settings.acssConfig);
}
// Refresh is required because codemirror gets scaled inside modal and loses alignement.
this.cm.refresh();
this.cm.focus();
}, 500);
}
}
render() {
return (
<Modal show={this.props.show} closeHandler={this.props.closeHandler}>
<h1>Atomic CSS Settings</h1>
<h3>
Configure Atomizer settings.{' '}
<a href="https://github.com/acss-io/atomizer#api" target="_blank">
Read more
</a>{' '}
about available settings.
</h3>
<div style="height: calc(100vh - 350px);">
<CodeMirrorBox
options={{
mode: 'application/ld+json',
theme: this.props.editorTheme
}}
onCreation={cm => (this.cm = cm)}
onBlur={cm => this.props.onChange(cm.getValue())}
/>
</div>
</Modal>
);
}
}

215
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,215 @@
import { h, Component } from 'preact';
import { A } from './common';
export default class Footer extends Component {
constructor(props) {
super(props);
this.state = {
isKeyboardShortcutsModalOpen: false
};
}
layoutBtnClickhandler(layoutId) {
this.props.layoutBtnClickHandler(layoutId);
}
render() {
return (
<div id="footer" class="footer">
<div class="footer__right fr">
<a
onClick={this.props.saveHtmlBtnClickHandler}
id="saveHtmlBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile"
data-hint="Save as HTML file"
>
<svg viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
</svg>
</a>
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
<symbol id="codepen-logo" viewBox="0 0 120 120">
<path
class="outer-ring"
d="M60.048 0C26.884 0 0 26.9 0 60.048s26.884 60 60 60.047c33.163 0 60.047-26.883 60.047-60.047 S93.211 0 60 0z M60.048 110.233c-27.673 0-50.186-22.514-50.186-50.186S32.375 9.9 60 9.9 c27.672 0 50.2 22.5 50.2 50.186S87.72 110.2 60 110.233z"
/>
<path
class="inner-box"
d="M97.147 48.319c-0.007-0.047-0.019-0.092-0.026-0.139c-0.016-0.09-0.032-0.18-0.056-0.268 c-0.014-0.053-0.033-0.104-0.05-0.154c-0.025-0.078-0.051-0.156-0.082-0.232c-0.021-0.053-0.047-0.105-0.071-0.156 c-0.033-0.072-0.068-0.143-0.108-0.211c-0.029-0.051-0.061-0.1-0.091-0.148c-0.043-0.066-0.087-0.131-0.135-0.193 c-0.035-0.047-0.072-0.094-0.109-0.139c-0.051-0.059-0.104-0.117-0.159-0.172c-0.042-0.043-0.083-0.086-0.127-0.125 c-0.059-0.053-0.119-0.104-0.181-0.152c-0.048-0.037-0.095-0.074-0.145-0.109c-0.019-0.012-0.035-0.027-0.053-0.039L61.817 23.5 c-1.072-0.715-2.468-0.715-3.54 0L24.34 46.081c-0.018 0.012-0.034 0.027-0.053 0.039c-0.05 0.035-0.097 0.072-0.144 0.1 c-0.062 0.049-0.123 0.1-0.181 0.152c-0.045 0.039-0.086 0.082-0.128 0.125c-0.056 0.055-0.108 0.113-0.158 0.2 c-0.038 0.045-0.075 0.092-0.11 0.139c-0.047 0.062-0.092 0.127-0.134 0.193c-0.032 0.049-0.062 0.098-0.092 0.1 c-0.039 0.068-0.074 0.139-0.108 0.211c-0.024 0.051-0.05 0.104-0.071 0.156c-0.031 0.076-0.057 0.154-0.082 0.2 c-0.017 0.051-0.035 0.102-0.05 0.154c-0.023 0.088-0.039 0.178-0.056 0.268c-0.008 0.047-0.02 0.092-0.025 0.1 c-0.019 0.137-0.029 0.275-0.029 0.416V71.36c0 0.1 0 0.3 0 0.418c0.006 0 0 0.1 0 0.1 c0.017 0.1 0 0.2 0.1 0.268c0.015 0.1 0 0.1 0.1 0.154c0.025 0.1 0.1 0.2 0.1 0.2 c0.021 0.1 0 0.1 0.1 0.154c0.034 0.1 0.1 0.1 0.1 0.213c0.029 0 0.1 0.1 0.1 0.1 c0.042 0.1 0.1 0.1 0.1 0.193c0.035 0 0.1 0.1 0.1 0.139c0.05 0.1 0.1 0.1 0.2 0.2 c0.042 0 0.1 0.1 0.1 0.125c0.058 0.1 0.1 0.1 0.2 0.152c0.047 0 0.1 0.1 0.1 0.1 c0.019 0 0 0 0.1 0.039L58.277 96.64c0.536 0.4 1.2 0.5 1.8 0.537c0.616 0 1.233-0.18 1.77-0.537 l33.938-22.625c0.018-0.012 0.034-0.027 0.053-0.039c0.05-0.035 0.097-0.072 0.145-0.109c0.062-0.049 0.122-0.1 0.181-0.152 c0.044-0.039 0.085-0.082 0.127-0.125c0.056-0.055 0.108-0.113 0.159-0.172c0.037-0.045 0.074-0.09 0.109-0.139 c0.048-0.062 0.092-0.127 0.135-0.193c0.03-0.049 0.062-0.098 0.091-0.146c0.04-0.07 0.075-0.141 0.108-0.213 c0.024-0.051 0.05-0.102 0.071-0.154c0.031-0.078 0.057-0.156 0.082-0.234c0.017-0.051 0.036-0.102 0.05-0.154 c0.023-0.088 0.04-0.178 0.056-0.268c0.008-0.045 0.02-0.092 0.026-0.137c0.018-0.139 0.028-0.277 0.028-0.418V48.735 C97.176 48.6 97.2 48.5 97.1 48.319z M63.238 32.073l25.001 16.666L77.072 56.21l-13.834-9.254V32.073z M56.856 32.1 v14.883L43.023 56.21l-11.168-7.471L56.856 32.073z M29.301 54.708l7.983 5.34l-7.983 5.34V54.708z M56.856 88.022L31.855 71.4 l11.168-7.469l13.833 9.252V88.022z M60.048 67.597l-11.286-7.549l11.286-7.549l11.285 7.549L60.048 67.597z M63.238 88.022V73.14 l13.834-9.252l11.167 7.469L63.238 88.022z M90.794 65.388l-7.982-5.34l7.982-5.34V65.388z"
/>
</symbol>
</svg>
<a
href=""
onClick={this.props.codepenBtnClickHandler}
id="codepenBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile"
aria-label="Edit on CodePen"
>
<svg>
<use xlinkHref="#codepen-logo" />
</svg>
</a>
<a
href=""
id="screenshotBtn"
class="mode-btn hint--rounded hint--top-left show-when-extension"
onClick={this.props.screenshotBtnClickHandler}
aria-label="Take screenshot of preview"
>
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
</svg>
</a>
<div class="footer__separator hide-on-mobile" />
<a
onClick={this.layoutBtnClickhandler.bind(this, 1)}
id="layoutBtn1"
class="mode-btn hide-on-mobile"
>
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</a>
<a
onClick={this.layoutBtnClickhandler.bind(this, 2)}
id="layoutBtn2"
class="mode-btn hide-on-mobile"
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#mode-icon" />
</svg>
</a>
<a
onClick={this.layoutBtnClickhandler.bind(this, 3)}
id="layoutBtn3"
class="mode-btn hide-on-mobile"
>
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</a>
<a
onClick={this.layoutBtnClickhandler.bind(this, 5)}
id="layoutBtn5"
class="mode-btn hide-on-mobile"
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#vertical-mode-icon" />
</svg>
</a>
<a
onClick={this.layoutBtnClickhandler.bind(this, 4)}
id="layoutBtn4"
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label="Full Screen"
>
<svg viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" />
</svg>
</a>
<a
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label="Detach Preview"
onClick={this.props.detachedPreviewBtnHandler}
>
<svg viewBox="0 0 24 24">
<path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" />
</svg>
</a>
<div class="footer__separator" />
<a
onClick={this.props.notificationsBtnClickHandler}
id="notificationsBtn"
class={`notifications-btn mode-btn hint--top-left hint--rounded ${
this.props.hasUnseenChangelog ? 'has-new' : ''
}`}
aria-label="Notifications"
>
<svg viewBox="0 0 24 24">
<path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" />
</svg>
<span class="notifications-btn__dot" />
</a>
<A
onClick={this.props.settingsBtnClickHandler}
data-event-category="ui"
data-event-action="settingsBtnClick"
class="mode-btn hint--top-left hint--rounded"
aria-label="Settings"
>
<svg>
<use xlinkHref="#settings-icon" />
</svg>
</A>
</div>
<a href="https://webmakerapp.com/" target="_blank">
<div class="logo" />
</a>
&copy;
<span class="web-maker-with-tag">Web Maker</span> &nbsp;&nbsp;
<A
onClick={this.props.helpBtnClickHandler}
data-event-category="ui"
data-event-action="helpButtonClick"
class="footer__link hint--rounded hint--top-right"
aria-label="Help"
>
<svg
style="width:20px; height:20px; vertical-align:text-bottom"
viewBox="0 0 24 24"
>
<path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
</svg>
</A>
<A
onClick={this.props.keyboardShortcutsBtnClickHandler}
data-event-category="ui"
data-event-action="keyboardShortcutButtonClick"
class="footer__link hint--rounded hint--top-right hide-on-mobile"
aria-label="Keyboard shortcuts"
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#keyboard-icon" />
</svg>
</A>
<a
class="footer__link hint--rounded hint--top-right"
aria-label="Tweet about 'Web Maker'"
href="http://twitter.com/share?url=https://webmakerapp.com/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
target="_blank"
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#twitter-icon" />
</svg>
</a>
<A
onClick={this.props.supportDeveloperBtnClickHandler}
data-event-action="supportDeveloperFooterBtnClick"
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile"
aria-label="Support the developer by pledging some amount"
target="_blank"
>
Support the developer
</A>
</div>
);
}
}

View File

@@ -0,0 +1,158 @@
import { h, Component } from 'preact';
import Modal from './Modal.jsx';
export default class Header extends Component {
render() {
return (
<Modal show={this.props.show} closeHandler={this.props.closeHandler}>
<h1>
<div class="web-maker-with-tag">Web Maker</div>
<small style="font-size:14px;"> v3.2.0</small>
</h1>
<div>
<p>
Made with
<span style="margin-right: 8px;">💖</span> &
<span style="margin-right: 8px;">🙌</span> by
<a href="https://twitter.com/chinchang457" target="_blank">
Kushagra Gour
</a>
</p>
<p>
<a href="/docs" target="_blank">
Read the documentation
</a>.
</p>
<p>
Tweet out your feature requests, comments & suggestions to
<a target="_blank" href="https://twitter.com/webmakerApp">
@webmakerApp
</a>.
</p>
<p>
Like this extension? Please
<a
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
target="_blank"
>
rate it here
</a>.
</p>
<p>
<button
aria-label="Support the developer"
d-click="openSupportDeveloperModal"
data-event-action="supportDeveloperHelpBtnClick"
class="btn btn-icon"
>
<svg>
<use xlinkHref="#gift-icon" />
</svg>Support the developer
</button>
<a
aria-label="Rate Web Maker"
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
target="_blank"
class="btn btn-icon"
>
<svg>
<use xlinkHref="#heart-icon" />
</svg>Share Web Maker
</a>
<a
aria-label="Chat"
href="https://web-maker.slack.com"
target="_blank"
class="btn btn-icon"
>
<svg>
<use xlinkHref="#chat-icon" />
</svg>Chat
</a>
<a
aria-label="Report a Bug"
href="https://github.com/chinchang/web-maker/issues"
target="_blank"
class="btn btn-icon"
>
<svg>
<use xlinkHref="#bug-icon" />
</svg>Report a bug
</a>
</p>
<p>
<h3>Awesome libraries used</h3>
<ul>
<li>
<a target="_blank" href="https://kushagragour.in/lab/hint/">
Hint.css
</a>{' '}
&
<a
target="_blank"
href="https://github.com/chinchang/screenlog.js"
>
Screenlog.js
</a>{' '}
- By me :)
</li>
<li>
<a
target="_blank"
href="https://nathancahill.github.io/Split.js/"
>
Split.js
</a>{' '}
- Nathan Cahill
</li>
<li>
<a target="_blank" href="https://codemirror.net/">
Codemirror
</a>{' '}
- Marijn Haverbeke
</li>
<li>
<a target="_blank" href="https://emmet.io/">
Emmet
</a>{' '}
- Sergey Chikuyonok
</li>
<li>
<a target="_blank" href="http://esprima.org/">
Esprima
</a>{' '}
- Ariya Hidayat
</li>
<li>
<a target="_blank" href="https://github.com/enjalot/Inlet">
Inlet
</a>{' '}
- Ian Johnson
</li>
<li>
<a target="_blank" href="https://webmakerapp.com/">
Web Maker!
</a>{' '}
- whhat!
</li>
</ul>
</p>
<p>
<h3>License</h3>
"Web Maker" is
<a target="_blank" href="https://github.com/chinchang/web-maker">
open-source
</a>{' '}
under the
<a href="https://opensource.org/licenses/MIT" target="_blank">
MIT License
</a>.
</p>
</div>
</Modal>
);
}
}

View File

@@ -0,0 +1,92 @@
import { h, Component } from 'preact';
import Modal from './Modal';
export default class KeyboardShortcutsModal extends Component {
render() {
return (
<Modal show={this.props.show} closeHandler={this.props.closeHandler}>
<h1>Keyboard Shortcuts</h1>
<div class="flex">
<div>
<h2>Global</h2>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + Shift + ?</span>
<span class="kbd-shortcut__details">See keyboard shortcuts</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + Shift + 5</span>
<span class="kbd-shortcut__details">Refresh preview</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + S</span>
<span class="kbd-shortcut__details">Save current creations</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + O</span>
<span class="kbd-shortcut__details">
Open list of saved creations
</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl + L</span>
<span class="kbd-shortcut__details">
Clear console (works when console input is focused)
</span>
</p>
<p>
<span class="kbd-shortcut__keys">Esc</span>
<span class="kbd-shortcut__details">
Close saved creations panel & modals
</span>
</p>
</div>
<div class="ml-2">
<h2>Editor</h2>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + F</span>
<span class="kbd-shortcut__details">Find</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + G</span>
<span class="kbd-shortcut__details">Select next match</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + Shift + G</span>
<span class="kbd-shortcut__details">Select previous match</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + Opt/Alt + F</span>
<span class="kbd-shortcut__details">Find & replace</span>
</p>
<p>
<span class="kbd-shortcut__keys">Shift + Tab</span>
<span class="kbd-shortcut__details">Realign code</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + ]</span>
<span class="kbd-shortcut__details">Indent code right</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + [</span>
<span class="kbd-shortcut__details">Indent code left</span>
</p>
<p>
<span class="kbd-shortcut__keys">Tab</span>
<span class="kbd-shortcut__details">
Emmet code completion{' '}
<a href="https://emmet.io/" target="_blank">
Read more
</a>
</span>
</p>
<p>
<span class="kbd-shortcut__keys">Ctrl/ + /</span>
<span class="kbd-shortcut__details">Single line comment</span>
</p>
</div>
</div>
</Modal>
);
}
}

View File

@@ -0,0 +1,164 @@
import { h, Component } from 'preact';
import { trackEvent } from '../analytics';
export class LibraryAutoSuggest extends Component {
componentDidMount() {
this.t = this.wrap.querySelector('input,textarea');
this.filter = this.props.filter;
this.selectedCallback = this.props.onSelect;
// after list is insrted into the DOM, we put it in the body
// fixed at same position
setTimeout(() => {
requestIdleCallback(() => {
document.body.appendChild(this.list);
this.list.style.position = 'fixed';
});
}, 100);
this.t.addEventListener('input', e => this.onInput(e));
this.t.addEventListener('keydown', e => this.onKeyDown(e));
this.t.addEventListener('blur', e => this.closeSuggestions(e));
this.list.addEventListener('mousedown', e => this.onListMouseDown(e));
}
componentWillUnmount() {
this.t.removeEventListener('input', e => this.onInput(e));
this.t.removeEventListener('keydown', e => this.onKeyDown(e));
this.t.removeEventListener('blur', e => this.closeSuggestions(e));
this.list.removeEventListener('mousedown', e => this.onListMouseDown(e));
}
get currentLineNumber() {
return this.t.value.substr(0, this.t.selectionStart).split('\n').length;
}
get currentLine() {
var line = this.currentLineNumber;
return this.t.value.split('\n')[line - 1];
}
closeSuggestions() {
this.list.classList.remove('is-open');
this.isShowingSuggestions = false;
}
getList(input) {
var url = 'https://api.cdnjs.com/libraries?search=';
return fetch(url + input).then(response => {
return response.json().then(json => json.results);
});
}
replaceCurrentLine(val) {
var lines = this.t.value.split('\n');
lines.splice(this.currentLineNumber - 1, 1, val);
this.t.value = lines.join('\n');
}
onInput() {
var currentLine = this.currentLine;
if (currentLine) {
if (currentLine.indexOf('/') !== -1 || currentLine.match(/https*:\/\//)) {
return;
}
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.loader.style.display = 'block';
this.getList(currentLine).then(arr => {
this.loader.style.display = 'none';
if (!arr.length) {
this.closeSuggestions();
return;
}
this.list.innerHTML = '';
if (this.filter) {
/* eslint-disable no-param-reassign */
arr = arr.filter(this.filter);
}
for (var i = 0; i < Math.min(arr.length, 10); i++) {
this.list.innerHTML += `<li data-url="${arr[i].latest}"><a>${
arr[i].name
}</a></li>`;
}
this.isShowingSuggestions = true;
if (!this.textareaBounds) {
this.textareaBounds = this.t.getBoundingClientRect();
this.list.style.top = this.textareaBounds.bottom + 'px';
this.list.style.left = this.textareaBounds.left + 'px';
this.list.style.width = this.textareaBounds.width + 'px';
}
this.list.classList.add('is-open');
});
}, 500);
}
}
onKeyDown(event) {
var selectedItemElement;
if (!this.isShowingSuggestions) {
return false;
}
if (event.keyCode === 27) {
this.closeSuggestions();
event.stopPropagation();
}
if (event.keyCode === 40 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
if (selectedItemElement) {
selectedItemElement.classList.remove('selected');
selectedItemElement.nextElementSibling.classList.add('selected');
} else {
this.list.querySelector('li:first-child').classList.add('selected');
}
this.list.querySelector('.selected').scrollIntoView(false);
event.preventDefault();
} else if (event.keyCode === 38 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
if (selectedItemElement) {
selectedItemElement.classList.remove('selected');
selectedItemElement.previousElementSibling.classList.add('selected');
} else {
this.list.querySelector('li:first-child').classList.add('selected');
}
this.list.querySelector('.selected').scrollIntoView(false);
event.preventDefault();
} else if (event.keyCode === 13 && this.isShowingSuggestions) {
selectedItemElement = this.list.querySelector('.selected');
this.selectSuggestion(selectedItemElement.dataset.url);
this.closeSuggestions();
}
}
onListMouseDown(event) {
var target = event.target;
if (target.parentElement.dataset.url) {
this.selectSuggestion(target.parentElement.dataset.url);
}
}
selectSuggestion(value) {
// Return back the focus which is getting lost for some reason
this.t.focus();
trackEvent('ui', 'autoSuggestionLibSelected', value);
if (this.selectedCallback) {
this.selectedCallback.call(null, value);
} else {
this.replaceCurrentLine(value);
}
this.closeSuggestions();
}
render() {
return (
<div
class={`btn-group ${this.props.fullWidth ? 'flex-grow' : ''}`}
ref={el => (this.wrap = el)}
>
{this.props.children}
<ul
ref={el => (this.list = el)}
class="dropdown__menu autocomplete-dropdown"
/>
<div
ref={el => (this.loader = el)}
class="loader autocomplete__loader"
style="display:none"
/>
</div>
);
}
}

74
src/components/Login.jsx Normal file
View File

@@ -0,0 +1,74 @@
import { h, Component } from 'preact';
import { jsLibs, cssLibs } from '../libraryList';
import { trackEvent } from '../analytics';
import { auth } from '../auth';
export default class Login extends Component {
login(e) {
const provider = e.target.dataset.authProvider;
trackEvent('ui', 'loginProviderClick', provider);
auth.login(provider);
}
componentDidMount() {
window.db.local.get(
{
lastAuthProvider: ''
},
result => {
if (result.lastAuthProvider) {
document.body.classList.add(`last-login-${result.lastAuthProvider}`);
}
}
);
}
render() {
return (
<div>
<h2>Login / Signup</h2>
<div>
<p>
<button
type="button"
onClick={this.login.bind(this)}
class="social-login-btn social-login-btn--github btn btn-icon btn--big full-width hint--right hint--always"
data-auth-provider="github"
data-hint="You logged in with Github last time"
>
<svg>
<use xlinkHref="#github-icon" />
</svg>Login with Github
</button>
</p>
<p>
<button
type="button"
onClick={this.login.bind(this)}
class="social-login-btn social-login-btn--google btn btn-icon btn--big full-width hint--right hint--always"
data-auth-provider="google"
data-hint="You logged in with Google last time"
>
<svg>
<use xlinkHref="#google-icon" />
</svg>Login with Google
</button>
</p>
<p class="mb-2">
<button
type="button"
onClick={this.login.bind(this)}
class="social-login-btn social-login-btn--facebook btn btn-icon btn--big full-width hint--right hint--always"
data-auth-provider="facebook"
data-hint="You logged in with Facebook last time"
>
<svg>
<use xlinkHref="#fb-icon" />
</svg>Login with Facebook
</button>
</p>
<p>Join a community of 50,000+ Developers</p>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,131 @@
import { h, Component } from 'preact';
import { A } from './common';
const DEFAULT_PROFILE_IMG =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='#ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z' /%3E%3C/svg%3E";
export default class Header extends Component {
render() {
return (
<div class="main-header">
<input
type="text"
id="titleInput"
title="Click to edit"
class="item-title-input"
value={this.props.title}
onBlur={this.props.titleInputBlurHandler}
/>
<div class="main-header__btn-wrap flex flex-v-center">
<a
id="runBtn"
class="hide flex flex-v-center hint--rounded hint--bottom-left"
aria-label="Run preview (Ctrl/⌘ + Shift + 5)"
onClick={this.props.runBtnClickHandler}
>
<svg style="width: 14px; height: 14px;">
<use xlinkHref="#play-icon" />
</svg>Run
</a>
<A
onClick={this.props.addLibraryBtnHandler}
data-event-category="ui"
data-event-action="addLibraryButtonClick"
class="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:${
this.props.externalLibCount ? 'inline' : 'none'
}`}
class="count-label"
>
{this.props.externalLibCount}
</span>
</A>
<a
class="flex flex-v-center hint--rounded hint--bottom-left"
aria-label="Start a new creation"
onClick={this.props.newBtnHandler}
>
<svg
style="vertical-align:middle;width:14px;height:14px"
viewBox="0 0 24 24"
>
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>New
</a>
<a
id="saveBtn"
class={`flex flex-v-center hint--rounded hint--bottom-left ${
this.props.isSaving ? 'is-loading' : ''
} ${this.props.unsavedEditCount ? 'is-marked' : 0}`}
aria-label="Save current creation (Ctrl/⌘ + S)"
onClick={this.props.saveBtnHandler}
>
<svg
style="vertical-align:middle;width:14px;height:14px"
viewBox="0 0 24 24"
>
<path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
</svg>
<svg class="btn-loader" width="15" height="15" stroke="#fff">
<use xlinkHref="#loader-icon" />
</svg>
Save
</a>
<a
id="openItemsBtn"
class={`flex flex-v-center hint--rounded hint--bottom-left ${
this.props.isFetchingItems ? 'is-loading' : ''
}`}
aria-label="Open a saved creation (Ctrl/⌘ + O)"
onClick={this.props.openBtnHandler}
>
<svg
style="width:14px;height:14px;vertical-align:middle;"
viewBox="0 0 24 24"
>
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" />
</svg>
<svg class="btn-loader" width="15" height="15" stroke="#fff">
<use xlinkHref="#loader-icon" />
</svg>
Open
</a>
<A
onClick={this.props.loginBtnHandler}
data-event-category="ui"
data-event-action="loginButtonClick"
class="hide-on-login flex flex-v-center hint--rounded hint--bottom-left"
aria-label="Login/Signup"
>
Login/Signup
</A>
<A
onClick={this.props.profileBtnHandler}
data-event-category="ui"
data-event-action="headerAvatarClick"
aria-label="See profile or Logout"
class="hide-on-logout hint--rounded hint--bottom-left"
>
<img
id="headerAvatarImg"
width="20"
src={
this.props.user
? this.props.user.photoURL || DEFAULT_PROFILE_IMG
: ''
}
class="main-header__avatar-img"
/>
</A>
</div>
</div>
);
}
}

53
src/components/Modal.jsx Normal file
View File

@@ -0,0 +1,53 @@
import { h, Component } from 'preact';
export default class Modal extends Component {
componentDidMount() {
window.addEventListener('keydown', this.onKeyDownHandler.bind(this));
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyDownHandler.bind(this));
}
onKeyDownHandler(e) {
if (e.keyCode === 27) {
this.props.closeHandler();
}
}
onOverlayClick(e) {
if (e.target === this.overlayEl) {
this.props.closeHandler();
}
}
componentDidUpdate(prevProps) {
document.body.classList[this.props.show ? 'add' : 'remove'](
'overlay-visible'
);
if (this.props.show && !prevProps.show) {
this.overlayEl.querySelector('.js-modal__close-btn').focus();
}
}
render() {
if (!this.props.show) return null;
return (
<div
class={`${this.props.extraClasses || ''} modal is-modal-visible`}
ref={el => (this.overlayEl = el)}
onClick={this.onOverlayClick.bind(this)}
>
<div class="modal__content">
<button
type="button"
onClick={this.props.closeHandler}
aria-label="Close modal"
title="Close"
class="js-modal__close-btn modal__close-btn"
>
Close
</button>
{this.props.children}
</div>
</div>
);
}
}

7
src/components/Modals.js Normal file
View File

@@ -0,0 +1,7 @@
import { h, Component } from 'preact';
export default class Header extends Component {
render() {
return <header class={style.header} />;
}
}

View File

@@ -0,0 +1,783 @@
import { h, Component } from 'preact';
export default class Notifications extends Component {
render() {
return (
<div>
<h1>Whats new?</h1>
<div class="notification">
<span class="notification__version">3.1.1</span>
<ul>
<li>
<strong>Bugfix</strong>: Fix the "Run" button not refreshing the
preview after release 3.0.4.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">3.1.0</span>
<ul>
<li>
<strong>Mobile Support (app only).</strong>: Make the Web Maker
app usable on mobile. This is only for web app as Chrome
extensions don't run on mobile.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">3.0.4</span>
<ul>
<li>
<strong>Bugfix</strong>: Guarantee code doesn't execute when "auto
preview" is off.
</li>
<li>
Add link to our new
<a href="https://web-maker.slack.com" target="_blank">
Slack channel
</a>{' '}
🤗.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">3.0.3</span>
<ul>
<li>
<strong>Bugfix (extension)</strong>: "Save as HTML" file saves
with correct extension.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">3.0.1</span>
<ul>
<li>
After months of work, here is Web Maker 3.0.
<a
href="https://medium.com/web-maker/web-maker-3-0-is-here-f158a40eeaee"
target="_blank"
>
Read the blog post about it
</a>.
</li>
<li>
Web Maker is no more just a Chrome extension, it is also available
as web app that runs offline just like the extension! Checkout it
out ->
<a href="https://webmakerapp.com/app/" target="_blank">
https://webmakerapp.com/app/
</a>.
</li>
<li>
Now use Web Maker web app on any modern browser (tested with
Chrome and Firefox).
</li>
<li>
<strong>User Accounts</strong> - The much requested user accounts
are here. Now maintain your account and store all your creations
in the cloud and access them anywhere anytime.
</li>
<li>
<strong>New layout mode</strong> - One more layout mode, that lets
you align all the panes vertically.
</li>
<li>
<strong>No more restriction on scripts (Web app only)</strong> -
If you are using the web app, there is no more a restriction to
load scripts from only specific domains. Load any script!
</li>
<li>
<strong>Inline scripts (Web app only)</strong> - The restriction
of writing JavaScript only in JS pane is also removed.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.7</span>
<ul>
<li>
<a href="https://tailwindcss.com/" target="_blank">
Tailwind CSS
</a>{' '}
added to popular CSS libraries list. Thanks
<a href="https://github.com/diomed" target="_blank">
diomed
</a>.
</li>
<li>
Popular libraries list updated. Thanks
<a href="https://github.com/diomed" target="_blank">
diomed
</a>.
</li>
<li>
<strong>Dev</strong>: Bug fixes and code refactoring to make
things simple. Thanks
<a href="https://github.com/iamandrewluca" target="_blank">
iamandrewluca
</a>.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.6</span>
<ul>
<li>
<strong>Bugfix</strong>: Fix close buttons not working in
notifications and keyboard shortcuts modal.
</li>
<li>
<strong>Bugfix</strong>: Fix keyboard shortcut to see keyboard
shortcuts :) Thanks
<a href="https://github.com/ClassicOldSong" target="_blank">
ClassicOldSong
</a>.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.5</span>
<ul>
<li>
<a
href="https://medium.com/web-maker/release-2-9-5-add-library-search-pane-collapsing-ux-improvements-more-1085216c1301"
target="_blank"
>
Read blog post about this release.
</a>
</li>
<li>
<strong>Keyboard shortcuts panel</strong>: Add a list of all
keyboard shotcuts. Access with
<code> Ctrl/ + Shift + ?</code> or click keyboard button in
footer.
</li>
<li>
<strong>Add external library</strong>: Better UX for searching
third party libraries.
</li>
<li>
<strong>Improvement</strong>: Code panes now go fullscreen when
double-clicked on their headers - which is much more intuitive
behavior based on feedback from lot of developers.
</li>
<li>
<strong>Improvement</strong>: Add
<code>allowfullscreen</code> attribute on iframes. Thanks
<a href="https://github.com/ClassicOldSong" target="_blank">
ClassicOldSong
</a>.
</li>
<li>
<strong>Bugfix</strong> - Stop screenlog.js from showing up in the
exported HTML.
</li>
<li>
Popular external libraries list updated. Thanks
<a href="https://github.com/jlapitan" target="_blank">
jlapitan
</a>.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.4</span>
<ul>
<li>
<strong>Improvement</strong>: Atomic CSS (Atomizer) has been
updated to latest version. Now you can do things like psuedo
elements. Learn More.
</li>
<li>
<strong>Bugfix</strong> - Logging circular objects is now
possible. It won't show in the Web Maker console, but will show
fine in browser's console.
</li>
<li>
<strong>Bugfix</strong> - Console's z-index issue has been fixed.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.3</span>
<ul>
<li>
Choose the save location while exporting your saved creations. Now
easily sync them to your Dropbox or any cloud storage.
</li>
<li>All modals inside the app now have a close button.</li>
<li>
Checkbox that showed on clicking a boolean value is now removed.
Thanks
<a href="https://github.com/gauravmuk" target="_blank">
Gaurav Nanda
</a>.
</li>
<li>
<strong>Bugfix</strong> - Screenshots on retina device are now
correct. Thanks
<a href="https://github.com/AshBardhan" target="_blank">
Ashish Bardhan
</a>.
</li>
<li>
<strong>Bugfix</strong> - Double console log in detached mode
fixed.
</li>
<li>
<strong>Bugfix</strong> - Console.clear now works in detached mode
too.
</li>
<li>
<strong>Bugfix</strong> - DOCTYPE added in preview.
</li>
<li>
<strong>Bugfix</strong> - Typo correction in README. Thanks
<a href="https://github.com/AdilMah" target="_blank">
Adil Mahmood
</a>.
</li>
<li>gstatic.com is available to load external JavaScripts from.</li>
<li>
Popular libraries list updated. Thanks
<a href="https://github.com/diomed" target="_blank">
diomed
</a>.
</li>
<li>
Added
<a
href="https://github.com/chinchang/web-maker/blob/master/CONTRIBUTING.md"
target="_blank"
>
contribution guidelines
</a>{' '}
in the Github repository.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.2</span>
<ul>
<li>Minor bug fixes.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.1</span>
<ul>
<li>
<a
href="https://medium.com/web-maker/v2-9-lots-of-goodies-bd1e939571f6"
target="_blank"
>
Read blog post about last release.
</a>
</li>
<li>
Use Ctrl/Cmd+D to select next occurence of matching selection.
</li>
<li>Improve onboard experience.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.9.0</span>
<ul>
<li>
<a
href="https://medium.com/web-maker/v2-9-lots-of-goodies-bd1e939571f6"
target="_blank"
>
Read blog post about this release.
</a>
</li>
<li>
<strong>Detached Preview</strong> - Yes, you read that correct!
You can now detach your preview and send it to your secondary
monitor.
</li>
<li>
<strong>Find & Replace</strong> - Long awaited, now its there.
Ctrl/Cmd+f to find and add Alt to replace.
</li>
<li>
<strong>Atomic CSS (Atomizer) configurations</strong> - Add custom
config for Atomic CSS.
<a href="https://github.com/acss-io/atomizer#api" target="_blank">
Read more
</a>.
</li>
<li>
<strong>Light mode</strong> - This new setting makes Web Maker
drop some heavy effects like blur etc to gain more performance.
Thanks
<a href="https://github.com/iamandrewluca" target="_blank">
Andrew
</a>.
</li>
<li>
<strong>Preserve logs setting</strong> - Choose whether or not to
preserve logs across preview refreshes. Thanks
<a href="https://github.com/BasitAli" target="_blank">
Basit
</a>.
</li>
<li>
<strong>Line wrap setting</strong> - As the name says.
</li>
<li>Semantic UI added to popular libraries.</li>
<li>
Bootstrap, Vue, UI-Kit and more updated to latest versions in
popular libraries.
</li>
<li>UX improvements in settings UI</li>
<li>
<strong>Bugfix</strong> - Trigger preview refresh anytime with
Ctrl/⌘ + Shift + 5. Even with auto-preview on.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.8.1</span>
<ul>
<li>
Vue.js & UIKit version updated to latest version in 'Add Library'
list.
</li>
<li>
UTF-8 charset added to preview HTML to support universal
characters.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.8.0</span>
<ul>
<li>
<a
href="https://medium.com/web-maker/release-v2-8-is-out-f44e6ea5d9c4"
target="_blank"
>
Read blog post about this release.
</a>
</li>
<li>
<strong>Auto Save</strong> - Your creations now auto-save after
your first manual save. This is configurable from settings.
</li>
<li>
<strong>Base2Tone-Meadow Editor Theme</strong> - First user
contributed theme. Thanks to Diomed.
</li>
<li>
<strong>Use System Fonts</strong> - You can now use any of your
existing system fonts in the editor!
</li>
<li>
<strong>Matching Tag Highlight</strong> - Cursor over any HTML tag
would highlight the matching pair tag.
</li>
<li>
Auto-completion suggestion can now be switched off from settings.
</li>
<li>
<strong>Improvement</strong> - Stop white flicker in editor when
the app opens.
</li>
<li>
<strong>Bugfix</strong> - Add Babel Polyfill to enable use of
next-gen built-ins like Promise or WeakMap.
</li>
<li>Vue.js version updated to 2.4.0 in popular library list.</li>
<li>
Downloads permission is optional. Asked only when you take
screenshot.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.7.2</span>
<ul>
<li>
<strong>External Libraries</strong> - Add Foundation.js and update
UIKit 3 to latest beta.
</li>
<li>
<strong>rawgit.com</strong> &
<strong>wzrd.in</strong> domains are now allowed for loading
external libraries from.
</li>
<li>Minor booting speed improvements</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.7.1</span>
<ul>
<li>
<strong>Framer.js support</strong> - You can now load the latest
framer.js library from
<a href="https://builds.framerjs.com/" target="_blank">
framer builds page
</a>{' '}
and start coding framer prototypes.
</li>
<li>
<strong>Bugfix</strong>: Edit on CodePen is back in action.
</li>
<li>
<strong>Bugfix</strong>: Autocompletion menu doesn't show on cut
and paste now.
</li>
<li>
<strong>Bugfix</strong>: Updated & fixed urls of some common
external libraries to latest versions. UIKit3 & Bootstrap 4α are
now in the list.
</li>
<li>Preprocessor selector are now more accessible.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.7.0</span>
<ul>
<li>
<strong>Fork any creation!</strong>: Now you can fork any existing
creation of yours to start a new work based on it. One big use
case of this feature is "Templates"!
<a
target="_blank"
href="https://kushagragour.in/blog/2017/05/web-maker-fork-templates/?utm_source=webmakerapp&utm_medium=referral"
>
Read more about it
</a>.
</li>
<li>
<strong>Fonts 😍 </strong>: Super-awesome 4 fonts (mostly with
ligature support) now available to choose from. Fira Code is the
default font now.
</li>
<li>Updated most used external libraries to latest versions.</li>
<li>
<strong>Bugfix</strong>: Add missing Bootstrap JS file to most
used external libraries list.
</li>
<li>
Several other minor bugfixes and improvements to make Web Maker
awesome!
</li>
<li>
Great news to share with you - Web Maker has been featured on the
Chrome Webstore homepage! Thanks for all the love :)
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.6.1</span>
<ul>
<li>
<strong>Bugfix</strong>: Emojis vanishing while exporting to
Codepen has been fixed.
</li>
<li>
<strong>Bugfix</strong>:
<code>console.clear()</code> now doesn't error and clears the
inbuilt console.
</li>
<li>
<strong>Bugfix</strong>: External libraries added to the creation
are exported as well to Codepen.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.6.0</span>
<ul>
<li>
<strong>The "Console"</strong>: The most awaited feature is here!
There is now an inbuilt console to see your logs, errors and for
quickly evaluating JavaScript code inside your preview. Enjoy! I
also a
<a
href="https://kushagragour.in/blog/2017/05/web-maker-console-is-here/?utm_source=webmakerapp&utm_medium=referral"
target="_blank"
>
blog post about it
</a>.
</li>
<li>
Number slider which popped on clicking any number in the code has
been removed due to poor user experience.
</li>
<li>Minor usability improvements.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.5.0</span>
<ul>
<li>
<strong>Atomic CSS</strong>: Use can now use Atomic CSS(ACSS) in
your work!
<a href="https://acss.io/" target="_blank">
Read more about it here
</a>.
</li>
<li>
<strong>Search your saved creations</strong>: Easily search
through all your saved creations by title.
</li>
<li>
<strong>Configurable Auto-preview</strong> - You can turn off the
auto preview in settings if you don't want the preview to update
as you type.
</li>
<li>
<strong>Configurable refresh on resize</strong> - You can
configure whether you want the preview to refresh when you resize
the preview panel.
</li>
<li>
<strong>Bugfix</strong> - Fix indentation
<a
href="https://github.com/chinchang/web-maker/issues/104"
target="_blank"
>
issue
</a>{' '}
with custom indentation size.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.4.2</span>
<ul>
<li>
<strong>Improved infinite loop protection</strong>: Infinite loop
protection is now faster and more reliable. And works without the
need of Escodegen. Thanks to Ariya Hidayat!
</li>
<li>
<strong>Bugfix</strong> - Default parameters not working in
JavaScript is fixed.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.4.0</span>
<ul>
<li>
<strong>Import/Export</strong>: Your creations are most important.
Now export all your creations into a single file as a backup that
can be imported anytime & anywhere.
</li>
<li>
<strong>Editor themes</strong>: You have been heard. Now you can
choose from a huge list of wonderful editor themes!
</li>
<li>
<strong>Identation settings</strong>: Not a spaces fan? Switch to
tabs and set your indentation size.
</li>
<li>
<strong>Vim key bindings</strong>: Rejoice Vim lovers!
</li>
<li>
<strong>Code blast</strong>: Why don't you try coding with this
switched on from the settings? Go on...
</li>
<li>
<strong>Important</strong>: Due to security policy changes from
Chrome 57 onwards, Web Maker now allows loading external
JavaScript libraries only from certain whitelisted domains
(localhost, https://ajax.googleapis.com, https://code.jquery.com,
https://cdnjs.cloudflare.com, https://unpkg.com,
https://maxcdn.com, https://cdn77.com,
https://maxcdn.bootstrapcdn.com, https://cdn.jsdelivr.net/)
</li>
<li>Save button now highlights when you have unsaved changes.</li>
<li>Jade is now called Pug. Just a name change.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.3.2</span>
<ul>
<li>Update Babel to support latest and coolest ES6 features.</li>
<li>Improve onboarding experience at first install.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.3.1</span>
<ul>
<li>
<strong>Bugfix</strong> - Splitting of code and preview panes is
remembered by the editor.
</li>
<li>
Title of the creation is used for the file name when saving as
HTML.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.3.0</span>
<ul>
<li>
<strong>Add Library Autocompletion</strong> - Just start typing
the name of library and you'll be shown matching libraries from
cdnjs.
</li>
<li>
<strong>Preview Screenshot Capture</strong> - Want to grab a nice
screenshot of your creation. You have it! Click and capture.
</li>
<li>
<strong>Auto Indent Code</strong> - Select your code and hit
Shift-Tab to auto-indent it!
</li>
<li>
<strong>Keyboard Navigation in Saved List</strong> - Now select
your creation using arrow keys and hit ENTER to open it.
</li>
<li>Highlight active line in code panes.</li>
<li>
<strong>Bugfix</strong> - Fix in generated title of new creation.
</li>
<li>
<strong>Bugfix</strong> - HTML autocompletion is manual triggered
now with Ctrl+Space.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.2.0</span>
<ul>
<li>
<strong>Code Autocompletion</strong> - See code suggestions while
you type!
</li>
<li>
<strong>Full Screen Preview</strong> - Checkout your creation in a
full-screen layout.
</li>
<li>
<strong>SASS</strong> - SASS support added for CSS.
</li>
<li>
<strong>Faster CSS update</strong> - Preview updates instantly
without refresh when just CSS is changed.
</li>
<li>
<strong>Bugfix</strong> - Indentation fixed while going on new
line.
</li>
<li>
<strong>Bugfix</strong> - Works even in Chrome Canary now. Though
libraries can be added only through CDNs.
</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.1.0</span>
<ul>
<li>
<strong>TypeScript</strong> - Now you can code in TypeScript too!
</li>
<li>
<strong>Stylus Preprocessor</strong> - Stylus supported adding for
CSS.
</li>
<li>
<strong>Code Folding</strong> - Collapse large code blocks for
easy editing.
</li>
<li>
<strong>Bugfix</strong> - Support JSX in JavaScript.
</li>
<li>Better onboarding for first time users.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">2.0.0</span>
<ul>
<li>
<strong>Save and Load</strong> - Long pending and super-useful,
now you can save your creations and resume them anytime later.
</li>
<li>
<strong>Insert JS & CSS</strong> - Load popular JavaScript & CSS
libraries in your work without writing any code.
</li>
<li>
<strong>Collapsed Panes</strong> - Collapse/uncollapse code panes
with a single click. Your pane configuration is even saved with
every creation!
</li>
<li>
<strong>Quick color & number change</strong> - Click on any color
or number and experiment with quick values using a slider.
</li>
<li>
<strong>Linting</strong> - See your code errors right where you
are coding.
</li>
<li>No more browser hang due to infinite loops!</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">1.7.0</span>
<ul>
<li>
<strong>Preprocessors!</strong> - Enjoy a whole list of
preprocessors for HTML(Jade & markdown), CSS(SCSS & LESS) and
JavaScript(CoffeeScript & Babel).
</li>
<li>More awesome font for code.</li>
</ul>
</div>
<div class="notification">
<span class="notification__version">1.6.0</span>
<ul>
<li>
You can now configure Web-Maker to not replace new tab page from
the settings. It is always accessible from the icon in the
top-right.
</li>
<li>
Download current code as HTML file with Ctrl/ + S keyboard
shortcut.
</li>
<li>
New notifications panel added so you are always aware of the new
changes in Web-Maker.
</li>
</ul>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,38 @@
import { h, Component } from 'preact';
const DEFAULT_PROFILE_IMG =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='#ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z' /%3E%3C/svg%3E";
export default class Profile extends Component {
render() {
return (
<div class="tac">
<img
height="80"
class="profile-modal__avatar-img"
src={
this.props.user
? this.props.user.photoURL || DEFAULT_PROFILE_IMG
: ''
}
id="profileAvatarImg"
alt="Profile image"
/>
<h3 id="profileUserName" class="mb-2">
{this.props.user && this.props.user.displayName
? this.props.user.displayName
: 'Anonymous Creator'}
</h3>
<p>
<button
class="btn"
aria-label="Logout from your account"
onClick={this.props.logoutBtnHandler}
>
Logout
</button>
</p>
</div>
);
}
}

View File

@@ -0,0 +1,264 @@
import { h, Component } from 'preact';
import { getHumanDate } from '../utils';
import { trackEvent } from '../analytics';
import { itemService } from '../itemService';
import { alertsService } from '../notifications';
export default class SavedItemPane extends Component {
constructor(props) {
super(props);
this.items = [];
this.state = {
filteredItems: []
};
}
componentWillUpdate(nextProps) {
if (this.props.items !== nextProps.items) {
this.items = Object.values(nextProps.items);
this.items.sort(function(a, b) {
return b.updatedOn - a.updatedOn;
});
this.setState({
filteredItems: this.items
});
}
}
onCloseIntent() {
this.props.closeHandler();
}
itemClickHandler(item) {
this.props.itemClickHandler(item);
}
itemRemoveBtnClickHandler(item, e) {
e.stopPropagation();
this.props.itemRemoveBtnClickHandler(item);
}
itemForkBtnClickHandler(item, e) {
e.stopPropagation();
this.props.itemForkBtnClickHandler(item);
}
keyDownHandler(event) {
if (!this.props.isOpen) {
return;
}
const isCtrlOrMetaPressed = event.ctrlKey || event.metaKey;
const isForkKeyPressed = isCtrlOrMetaPressed && event.keyCode === 70;
const isDownKeyPressed = event.keyCode === 40;
const isUpKeyPressed = event.keyCode === 38;
const isEnterKeyPressed = event.keyCode === 13;
const selectedItemElement = $('.js-saved-item-tile.selected');
const havePaneItems = $all('.js-saved-item-tile').length !== 0;
if ((isDownKeyPressed || isUpKeyPressed) && havePaneItems) {
const method = isDownKeyPressed ? 'nextUntil' : 'previousUntil';
if (selectedItemElement) {
selectedItemElement.classList.remove('selected');
selectedItemElement[method](
'.js-saved-item-tile:not(.hide)'
).classList.add('selected');
} else {
$('.js-saved-item-tile:not(.hide)').classList.add('selected');
}
$('.js-saved-item-tile.selected').scrollIntoView(false);
}
if (isEnterKeyPressed && selectedItemElement) {
const item = this.props.items[selectedItemElement.dataset.itemId];
console.log('opening', item);
this.props.itemClickHandler(item);
}
// Fork shortcut inside saved creations panel with Ctrl/⌘ + F
if (isForkKeyPressed) {
event.preventDefault();
const item = this.props.items[selectedItemElement.dataset.itemId];
this.props.itemForkBtnClickHandler(item);
trackEvent('ui', 'forkKeyboardShortcut');
}
}
mergeImportedItems(items) {
var existingItemIds = [];
var toMergeItems = {};
const d = deferred();
const savedItems = {};
this.items.forEach(item => (savedItems[item.id] = item));
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.
if (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();
}
// FIXME: Move from here
// toggleSavedItemsPane(false);
return d.promise;
}
importFileChangeHandler(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.addEventListener('load', progressEvent => {
var items;
try {
items = JSON.parse(progressEvent.target.result);
log(items);
this.mergeImportedItems(items);
} catch (exception) {
log(exception);
alert(
'Oops! Selected file is corrupted. Please select a file that was generated by clicking the "Export" button.'
);
}
});
reader.readAsText(file, 'utf-8');
}
importBtnClickHandler(e) {
var input = document.createElement('input');
input.type = 'file';
input.style.display = 'none';
input.accept = 'accept="application/json';
document.body.appendChild(input);
input.addEventListener('change', this.importFileChangeHandler.bind(this));
input.click();
trackEvent('ui', 'importBtnClicked');
e.preventDefault();
}
searchInputHandler(e) {
const text = e.target.value;
let el;
if (!text) {
this.setState({
filteredItems: this.items
});
} else {
this.setState({
filteredItems: this.items.filter(
item => item.title.toLowerCase().indexOf(text) !== -1
)
});
}
trackEvent('ui', 'searchInputType');
}
render() {
return (
<div
id="js-saved-items-pane"
class={`saved-items-pane ${this.props.isOpen ? 'is-open' : ''}`}
onKeyDown={this.keyDownHandler.bind(this)}
>
<button
onClick={this.onCloseIntent.bind(this)}
class="btn saved-items-pane__close-btn"
id="js-saved-items-pane-close-btn"
>
X
</button>
<div class="flex flex-v-center" style="justify-content: space-between;">
<h3>My Library ({this.items.length})</h3>
<div class="main-header__btn-wrap">
<a
onClick={this.props.exportBtnClickHandler}
href=""
class="btn btn-icon hint--bottom-left hint--rounded hint--medium"
aria-label="Export all your creations into a single importable file."
>
Export
</a>
<a
onClick={this.importBtnClickHandler.bind(this)}
href=""
class="btn btn-icon hint--bottom-left hint--rounded hint--medium"
aria-label="Only the file that you export through the 'Export' button can be imported."
>
Import
</a>
</div>
</div>
<input
type=""
id="searchInput"
class="search-input"
onInput={this.searchInputHandler.bind(this)}
placeholder="Search your creations here..."
/>
<div id="js-saved-items-wrap" class="saved-items-pane__container">
{!this.state.filteredItems.length &&
this.items.length && <div class="mt-1">No match found.</div>}
{this.state.filteredItems.map(item => (
<div
class="js-saved-item-tile saved-item-tile"
data-item-id={item.id}
onClick={this.itemClickHandler.bind(this, item)}
>
<div class="saved-item-tile__btns">
<a
class="js-saved-item-tile__fork-btn saved-item-tile__btn hint--left hint--medium"
aria-label="Creates a duplicate of this creation (Ctrl/⌘ + F)"
onClick={this.itemForkBtnClickHandler.bind(this, item)}
>
Fork<span class="show-when-selected">(Ctrl/ + F)</span>
</a>
<a
class="js-saved-item-tile__remove-btn saved-item-tile__btn hint--left"
aria-label="Remove"
onClick={this.itemRemoveBtnClickHandler.bind(this, item)}
>
X
</a>
</div>
<h3 class="saved-item-tile__title">{item.title}</h3>
<span class="saved-item-tile__meta">
Last updated: {getHumanDate(item.updatedOn)}
</span>
</div>
))}
{!this.items.length && (
<h2 class="opacity--30">Nothing saved here.</h2>
)}
</div>
</div>
);
}
}

317
src/components/Settings.jsx Normal file
View File

@@ -0,0 +1,317 @@
import { h, Component } from 'preact';
import { editorThemes } from '../editorThemes';
function CheckboxSetting({
title,
label,
onChange,
pref,
name,
showWhenExtension
}) {
return (
<label
class={`line ${showWhenExtension ? 'show-when-extension' : ''} `}
title={title}
>
<input
type="checkbox"
checked={pref}
onChange={onChange}
data-setting={name}
/>
{label}
</label>
);
}
export default class Settings extends Component {
updateSetting(e) {
this.props.onChange(e);
}
shouldComponentUpdate() {
// TODO: add check on prefs
return true;
}
render() {
return (
<div>
<h1>Settings</h1>
<h3>Indentation</h3>
<div
class="line"
title="I know this is tough, but you have to decide one!"
>
<label>
<input
type="radio"
name="indentation"
value="spaces"
checked={this.props.prefs.indentation === 'spaces'}
onChange={this.updateSetting.bind(this)}
data-setting="indentWith"
/>{' '}
Spaces
</label>
<label class="ml-1">
<input
type="radio"
name="indentation"
value="tabs"
checked={this.props.prefs.indentation === 'tabs'}
onChange={this.updateSetting.bind(this)}
data-setting="indentWith"
/>{' '}
Tabs
</label>
</div>
<label class="line" title="">
Indentation Size{' '}
<input
type="range"
class="va-m ml-1"
value={this.props.prefs.indentSize}
min="1"
max="7"
list="indentationSizeList"
data-setting="indentSize"
onChange={this.updateSetting.bind(this)}
/>
<span id="indentationSizeValueEl">{this.props.prefs.indentSize}</span>
<datalist id="indentationSizeList">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
</datalist>
</label>
<hr />
<h3>Editor</h3>
<div class="flex block--mobile">
<div>
<label class="line">Default Preprocessors</label>
<div class="flex line">
<select
style="flex:1;margin-left:20px"
data-setting="htmlMode"
value={this.props.prefs.htmlMode}
onChange={this.updateSetting.bind(this)}
>
<option value="html">HTML</option>
<option value="markdown">Markdown</option>
<option value="jade">Pug</option>
</select>
<select
style="flex:1;margin-left:20px"
data-setting="cssMode"
value={this.props.prefs.cssMode}
onChange={this.updateSetting.bind(this)}
>
<option value="css">CSS</option>
<option value="scss">SCSS</option>
<option value="sass">SASS</option>
<option value="less">LESS</option>
<option value="stylus">Stylus</option>
<option value="acss">Atomic CSS</option>
</select>
<select
style="flex:1;margin-left:20px"
data-setting="jsMode"
value={this.props.prefs.jsMode}
onChange={this.updateSetting.bind(this)}
>
<option value="js">JS</option>
<option value="coffee">CoffeeScript</option>
<option value="es6">ES6 (Babel)</option>
<option value="typescript">TypeScript</option>
</select>
</div>
<label class="line">
Theme
<select
style="flex:1;margin:0 20px"
data-setting="editorTheme"
value={this.props.prefs.editorTheme}
onChange={this.updateSetting.bind(this)}
>
{editorThemes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</label>
<label class="line">
Font
<select
style="flex:1;margin:0 20px"
data-setting="editorFont"
value={this.props.prefs.editorFont}
onChange={this.updateSetting.bind(this)}
>
<option value="FiraCode">Fira Code</option>
<option value="Inconsolata">Inconsolata</option>
<option value="Monoid">Monoid</option>
<option value="FixedSys">FixedSys</option>
<option disabled="disabled">----</option>
<option value="other">Other font from system</option>
</select>
{this.props.prefs.editorFont === 'other' && (
<input
id="customEditorFontInput"
type="text"
value={this.props.prefs.editorCustomFont}
placeholder="Custom font name here"
data-setting="editorCustomFont"
onChange={this.updateSetting.bind(this)}
/>
)}
</label>
<label class="line">
Font Size{' '}
<input
style="width:70px"
type="number"
value={this.props.prefs.fontSize}
data-setting="fontSize"
onChange={this.updateSetting.bind(this)}
/>{' '}
px
</label>
<div class="line">
Key bindings
<label class="ml-1">
<input
type="radio"
name="keymap"
value="sublime"
checked={this.props.prefs.keymap === 'sublime'}
data-setting="keymap"
onChange={this.updateSetting.bind(this)}
/>{' '}
Sublime
</label>
<label class="ml-1">
<input
type="radio"
name="keymap"
value="vim"
checked={this.props.prefs.keymap === 'vim'}
data-setting="keymap"
onChange={this.updateSetting.bind(this)}
/>{' '}
Vim
</label>
</div>
</div>
<div class="ml-2 ml-0--mobile">
<CheckboxSetting
name="lineWrap"
title="Toggle wrapping of long sentences onto new line"
label="Line wrap"
pref={this.props.prefs.lineWrap}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="refreshOnResize"
title="Your Preview will refresh when you resize the preview split"
label="Refresh preview on resize"
pref={this.props.prefs.refreshOnResize}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="autoComplete"
title="Turns on the auto-completion suggestions as you type"
label="Auto-complete suggestions"
pref={this.props.prefs.autoComplete}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="autoPreview"
title="Refreshes the preview as you code. Otherwise use the Run button"
label="Auto-preview"
pref={this.props.prefs.autoPreview}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="autoSave"
title="Auto-save keeps saving your code at regular intervals after you hit the first save manually"
label="Auto-save"
pref={this.props.prefs.autoSave}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="preserveLastCode"
title="Loads the last open creation when app starts"
label="Preserve last written code"
pref={this.props.prefs.preserveLastCode}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="replaceNewTab"
title="Turning this on will start showing Web Maker in every new tab you open"
label="Replace new tab page"
pref={this.props.prefs.replaceNewTab}
onChange={this.updateSetting.bind(this)}
showWhenExtension
/>
<CheckboxSetting
name="preserveConsoleLogs"
title="Preserves the console logs across your preview refreshes"
label="Preserve console logs"
pref={this.props.prefs.preserveConsoleLogs}
onChange={this.updateSetting.bind(this)}
/>
<CheckboxSetting
name="lightVersion"
title="Switch to lighter version for better performance. Removes things like blur etc."
label="Fast/light version"
pref={this.props.prefs.lightVersion}
onChange={this.updateSetting.bind(this)}
/>
</div>
</div>
<hr />
<h3>Fun</h3>
<p>
<CheckboxSetting
title="Enjoy wonderful particle blasts while you type"
label="Code blast!"
name="isCodeBlastOn"
pref={this.props.prefs.isCodeBlastOn}
onChange={this.updateSetting.bind(this)}
/>
</p>
<hr />
<h3>Advanced</h3>
<p>
<label
class="line"
title="This timeout is used to indentify a possible infinite loop and prevent it."
>
Maximum time allowed in a loop iteration
<input
type="number"
value={this.props.prefs.infiniteLoopTimeout}
data-setting="infiniteLoopTimeout"
onChange={this.updateSetting.bind(this)}
/>{' '}
ms
</label>
<div class="help-text">
If any loop iteration takes more than the defined time, it is
detected as a potential infinite loop and further iterations are
stopped.
</div>
</p>
</div>
);
}
}

View File

@@ -0,0 +1,50 @@
import { h, Component } from 'preact';
import Split from 'split.js';
import { log } from '../utils';
export class SplitPane extends Component {
// shouldComponentUpdate(nextProps, nextState) {
// return (
// nextProps.direction !== this.props.direction ||
// nextProps.sizes.join('') !== this.props.sizes.join('')
// );
// }
componentDidMount() {
this.updateSplit();
}
componentWillUpdate() {
if (this.splitInstance) {
this.splitInstance.destroy();
}
}
componentDidUpdate() {
this.updateSplit();
}
updateSplit() {
const options = {
direction: this.props.direction,
minSize: this.props.minSize,
gutterSize: 6,
sizes: this.props.sizes
};
if (this.props.onDragEnd) {
options.onDragEnd = this.props.onDragEnd;
}
if (this.props.onDragStart) {
options.onDragStart = this.props.onDragStart;
}
// log('SIZE UPDATTED', ...options);
this.splitInstance = Split(
this.props.children.map(node => '#' + node.attributes.id),
options
);
if (this.props.onSplit) {
this.props.onSplit(this.splitInstance);
}
}
render() {
const { children, ...props } = this.props;
return <div {...props}>{this.props.children}</div>;
}
}

View File

@@ -0,0 +1,58 @@
import { h, Component } from 'preact';
import Modal from './Modal';
export default class SupportDeveloperModal extends Component {
render() {
return (
<Modal
extraClasses="pledge-modal"
show={this.props.show}
closeHandler={this.props.closeHandler}
>
<div class="tac">
<h1>Support the Developer</h1>
<p>
Hi,{' '}
<a href="https://kushagragour.in" target="_blank">
Kushagra
</a>{' '}
here! Web Maker is a free and open-source project. To keep myself
motivated for working on such open-source and free{' '}
<a href="https://kushagragour.in/lab/" target="_blank">
side projects
</a>, I am accepting donations. Your pledge, no matter how small,
will act as an appreciation towards my work and keep me going
forward making Web Maker more awesome🔥. So please consider
donating. 🙏🏼 (could be as small as $1/month).
</p>
<div
class="flex flex-h-center"
id="onboardDontShowInTabOptionBtn"
d-click="onDontShowInTabClicked"
>
<a
class="onboard-selection"
href="https://patreon.com/kushagra"
target="_blank"
aria-label="Make a monthly pledge on Patreon"
>
<img src="patreon.png" height="60" alt="Become a patron image" />
<h3 class="onboard-selection-text">
Make a monthly pledge on Patreon
</h3>
</a>
</div>
<a
href="https://www.paypal.me/kushagragour"
target="_blank"
aria-label="Make a one time donation on Paypal"
>
Or, make a one time donation
</a>
</div>
</Modal>
);
}
}

View File

@@ -0,0 +1,144 @@
import { h, Component } from 'preact';
import CodeMirror from '../CodeMirror';
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/edit/matchtags.js';
import 'codemirror/addon/edit/closebrackets.js';
import 'codemirror/addon/edit/closetag.js';
import 'codemirror/addon/comment/comment.js';
import 'codemirror/addon/fold/foldcode.js';
import 'codemirror/addon/fold/foldgutter.js';
import 'codemirror/addon/fold/xml-fold.js';
import 'codemirror/addon/fold/indent-fold.js';
import 'codemirror/addon/fold/comment-fold.js';
import 'codemirror/addon/fold/brace-fold.js';
// import 'codemirror/addon/mode/loadmode.js';
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/addon/hint/javascript-hint.js';
import 'codemirror/addon/hint/xml-hint.js';
import 'codemirror/addon/hint/html-hint.js';
import 'codemirror/addon/hint/css-hint.js';
import 'codemirror/addon/selection/active-line.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/mode/xml/xml.js';
import 'codemirror/mode/css/css.js';
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/htmlmixed/htmlmixed.js';
import 'codemirror/keymap/sublime.js';
import 'codemirror/keymap/vim.js';
import emmet from '@emmetio/codemirror-plugin';
emmet(CodeMirror);
export default class UserCodeMirror extends Component {
componentDidMount() {
this.initEditor();
}
shouldComponentUpdate() {
return false;
}
initEditor() {
const options = this.props.options;
this.cm = CodeMirror.fromTextArea(this.textarea, {
mode: options.mode,
lineNumbers: true,
lineWrapping: true,
autofocus: options.autofocus || false,
autoCloseBrackets: true,
autoCloseTags: true,
matchBrackets: true,
matchTags: options.matchTags || false,
tabMode: 'indent',
keyMap: 'sublime',
theme: 'monokai',
lint: !!options.lint,
tabSize: 2,
foldGutter: true,
styleActiveLine: true,
gutters: options.gutters || [],
// cursorScrollMargin: '20', has issue with scrolling
profile: options.profile || '',
extraKeys: {
Up: function(editor) {
// Stop up/down keys default behavior when saveditempane is open
// if (isSavedItemsPaneOpen) {
// return;
// }
CodeMirror.commands.goLineUp(editor);
},
Down: function(editor) {
// if (isSavedItemsPaneOpen) {
// return;
// }
CodeMirror.commands.goLineDown(editor);
},
'Shift-Tab': function(editor) {
CodeMirror.commands.indentAuto(editor);
},
Tab: function(editor) {
if (options.emmet) {
const didEmmetWork = editor.execCommand('emmetExpandAbbreviation');
if (didEmmetWork === true) {
return;
}
}
const input = $('[data-setting=indentWith]:checked');
if (
!editor.somethingSelected() &&
(!input || input.value === 'spaces')
) {
// softtabs adds spaces. This is required because by default tab key will put tab, but we want
// to indent with spaces if `spaces` is preferred mode of indentation.
// `somethingSelected` needs to be checked otherwise, all selected code is replaced with softtab.
CodeMirror.commands.insertSoftTab(editor);
} else {
CodeMirror.commands.defaultTab(editor);
}
},
Enter: 'emmetInsertLineBreak'
}
});
this.cm.on('focus', editor => {
// editorWithFocus = editor;
});
this.cm.on('change', this.props.onChange);
this.cm.addKeyMap({
'Ctrl-Space': 'autocomplete'
});
if (!options.noAutocomplete) {
this.cm.on('inputRead', (editor, input) => {
if (
!this.props.autoComplete ||
input.origin !== '+input' ||
input.text[0] === ';' ||
input.text[0] === ',' ||
input.text[0] === ' '
) {
return;
}
CodeMirror.commands.autocomplete(this.cm, null, {
completeSingle: false
});
});
}
this.props.onCreation(this.cm);
}
render() {
return (
<textarea
ref={el => (this.textarea = el)}
name=""
id=""
cols="30"
rows="10"
/>
);
}
}

1264
src/components/app.jsx Normal file

File diff suppressed because it is too large Load Diff

26
src/components/common.jsx Normal file
View File

@@ -0,0 +1,26 @@
import { h, Component } from 'preact';
import { log } from '../utils';
import { trackEvent } from '../analytics';
class Clickable extends Component {
handleClick(e) {
const el = e.currentTarget;
trackEvent(
el.getAttribute('data-event-category'),
el.getAttribute('data-event-action')
);
this.props.onClick();
}
render() {
const { onClick, Tag, ...props } = this.props;
return <Tag onClick={this.handleClick.bind(this)} {...props} />;
}
}
export function A(props) {
return <Clickable Tag={'a'} {...props} />;
}
export function Button(props) {
return <Clickable Tag={'button'} {...props} />;
}

284
src/computes.js Normal file
View File

@@ -0,0 +1,284 @@
import {
deferred
} from './deferred';
import {
addInfiniteLoopProtection
} from './utils';
import {
HtmlModes,
CssModes,
JsModes
} from './codeModes';
const esprima = require('esprima');
// computeHtml, computeCss & computeJs evaluate the final code according
// to whatever mode is selected and resolve the returned promise with the code.
export function computeHtml(code, mode) {
var d = deferred();
if (mode === HtmlModes.HTML) {
d.resolve({
code
});
} else if (mode === HtmlModes.MARKDOWN) {
d.resolve(window.marked ? {
code: marked(code)
} : {
code
});
} else if (mode === HtmlModes.JADE) {
d.resolve(window.jade ? {
code: jade.render(code)
} : {
code
});
}
return d.promise;
}
export function computeCss(code, mode, settings) {
var d = deferred();
var errors;
if (mode === CssModes.CSS) {
d.resolve({
code
});
} else if (mode === CssModes.SCSS || mode === CssModes.SASS) {
if (window.sass && code) {
window.sass.compile(
code, {
indentedSyntax: mode === CssModes.SASS
},
function (result) {
// Something was wrong
if (result.line && result.message) {
errors = {
lang: 'css',
data: [{
lineNumber: result.line - 1,
message: result.message
}]
};
}
d.resolve({
code: result.text,
errors
});
}
);
} else {
d.resolve({
code
});
}
} else if (mode === CssModes.LESS) {
less.render(code).then(
function (result) {
d.resolve({
code: result.css
});
},
function (error) {
errors = {
lang: 'css',
data: [{
lineNumber: error.line,
message: error.message
}]
};
d.resolve({
code: '',
errors
})
}
);
} else if (mode === CssModes.STYLUS) {
stylus(code).render(function (error, result) {
if (error) {
window.err = error;
// Last line of message is the actual message
var tempArr = error.message.split('\n');
tempArr.pop(); // This is empty string in the end
errors = {
lang: 'css',
data: [{
lineNumber: +error.message.match(/stylus:(\d+):/)[1] - 298,
message: tempArr.pop()
}]
};
}
d.resolve({
code: result,
errors
});
});
} else if (mode === CssModes.ACSS) {
if (!window.atomizer) {
d.resolve({
code: ''
});
} else {
const html = code;
const foundClasses = atomizer.findClassNames(html);
var finalConfig;
try {
finalConfig = atomizer.getConfig(
foundClasses,
JSON.parse(settings.acssConfig)
);
} catch (e) {
finalConfig = atomizer.getConfig(foundClasses, {});
}
const acss = atomizer.getCss(finalConfig);
d.resolve({
code: acss
});
}
}
return d.promise;
}
export function computeJs(code, mode, shouldPreventInfiniteLoops, infiniteLoopTimeout) {
var d = deferred();
var errors;
if (!code) {
d.resolve('');
return d.promise;
}
if (mode === JsModes.JS) {
try {
esprima.parse(code, {
tolerant: true
});
} catch (e) {
errors = {
lang: 'js',
data: [{
lineNumber: e.lineNumber - 1,
message: e.description
}]
};
} finally {
if (shouldPreventInfiniteLoops !== false) {
// If errors are found in last parse, we don't run infinite loop
// protection otherwise it will again throw error.
code = errors ? code : addInfiniteLoopProtection(code, {
timeout: infiniteLoopTimeout
});
}
d.resolve({
code,
errors
});
}
} else if (mode === JsModes.COFFEESCRIPT) {
if (!window.CoffeeScript) {
d.resolve('');
return d.promise;
}
try {
code = CoffeeScript.compile(code, {
bare: true
});
} catch (e) {
errors = {
lang: 'js',
data: [{
lineNumber: e.location.first_line,
message: e.message
}]
};
} finally {
if (shouldPreventInfiniteLoops !== false) {
code = errors ? code : addInfiniteLoopProtection(code, {
timeout: infiniteLoopTimeout
});
}
d.resolve({
code,
errors
});
}
} else if (mode === JsModes.ES6) {
if (!window.Babel) {
d.resolve('');
return d.promise;
}
try {
esprima.parse(code, {
tolerant: true,
jsx: true
});
} catch (e) {
errors = {
lang: 'js',
data: [{
lineNumber: e.lineNumber - 1,
message: e.description
}]
};
} finally {
code = Babel.transform(code, {
presets: ['latest', 'stage-2', 'react']
}).code;
if (shouldPreventInfiniteLoops !== false) {
code = errors ? code : addInfiniteLoopProtection(code, {
timeout: infiniteLoopTimeout
});
}
d.resolve({
code,
errors
});
}
} else if (mode === JsModes.TS) {
try {
if (!window.ts) {
d.resolve({
code: ''
});
return d.promise;
}
code = ts.transpileModule(code, {
reportDiagnostics: true,
compilerOptions: {
noEmitOnError: true,
diagnostics: true,
module: ts.ModuleKind.ES2015
}
});
if (code.diagnostics.length) {
/* eslint-disable no-throw-literal */
errors = {
lang: 'js',
data: [{
message: code.diagnostics[0].messageText,
lineNumber: ts.getLineOfLocalPosition(
code.diagnostics[0].file,
code.diagnostics[0].start
) - 1
}]
};
}
code = code.outputText;
if (shouldPreventInfiniteLoops !== false && !errors) {
code = addInfiniteLoopProtection(code, {
timeout: infiniteLoopTimeout
});
}
d.resolve({
code,
errors
});
} catch (e) {
}
}
return d.promise;
}

167
src/db.js Normal file
View File

@@ -0,0 +1,167 @@
import './firebaseInit';
import firebase from 'firebase/app';
import 'firebase/firestore';
import {
deferred
} from './deferred';
import {
trackEvent
} from './analytics';
(() => {
const FAUX_DELAY = 1;
var db;
var dbPromise;
var local = {
get: (obj, cb) => {
const retVal = {};
if (typeof obj === 'string') {
retVal[obj] = JSON.parse(window.localStorage.getItem(obj));
setTimeout(() => cb(retVal), FAUX_DELAY);
} else {
Object.keys(obj).forEach(key => {
const val = window.localStorage.getItem(key);
retVal[key] =
val === undefined || val === null ? obj[key] : JSON.parse(val);
});
setTimeout(() => cb(retVal), FAUX_DELAY);
}
},
set: (obj, cb) => {
Object.keys(obj).forEach(key => {
window.localStorage.setItem(key, JSON.stringify(obj[key]));
});
/* eslint-disable consistent-return */
setTimeout(() => {
if (cb) {
return cb();
}
}, FAUX_DELAY);
/* eslint-enable consistent-return */
},
remove: (key, cb) => {
window.localStorage.removeItem(key);
setTimeout(() => cb(), FAUX_DELAY);
}
};
const dbLocalAlias = chrome && chrome.storage ? chrome.storage.local : local;
const dbSyncAlias = chrome && chrome.storage ? chrome.storage.sync : local;
async function getDb() {
if (dbPromise) {
return dbPromise;
}
utils.log('Initializing firestore');
dbPromise = new Promise((resolve, reject) => {
if (db) {
return resolve(db);
}
return firebase
.firestore()
.enablePersistence()
.then(function () {
// Initialize Cloud Firestore through firebase
db = firebase.firestore();
// const settings = {
// timestampsInSnapshots: true
// };
// db.settings(settings);
utils.log('firebase db ready', db);
resolve(db);
})
.catch(function (err) {
reject(err.code);
if (err.code === 'failed-precondition') {
// Multiple tabs open, persistence can only be enabled
// in one tab at a a time.
alert(
"Opening Web Maker web app in multiple tabs isn't supported at present and it seems like you already have it opened in another tab. Please use in one tab."
);
trackEvent('fn', 'multiTabError');
} else if (err.code === 'unimplemented') {
// The current browser does not support all of the
// features required to enable persistence
// ...
}
});
});
return dbPromise;
}
async function getUserLastSeenVersion() {
const d = deferred();
// Will be chrome.storage.sync in extension environment,
// otherwise will fallback to localstorage
dbSyncAlias.get({
lastSeenVersion: ''
},
result => {
d.resolve(result.lastSeenVersion);
}
);
return d.promise;
// Might consider getting actual value from remote db.
// Not critical right now.
}
async function setUserLastSeenVersion(version) {
// Setting the `lastSeenVersion` in localStorage(sync for extension) always
// because next time we need to fetch it irrespective of the user being
// logged in or out quickly from local storage.
dbSyncAlias.set({
lastSeenVersion: version
},
function () {}
);
if (window.user) {
const remoteDb = await getDb();
remoteDb
.doc(`users/${window.user.uid}`)
.update({
lastSeenVersion: version
});
}
}
async function getUser(userId) {
const remoteDb = await getDb();
return remoteDb
.doc(`users/${userId}`)
.get()
.then(doc => {
if (!doc.exists)
return remoteDb.doc(`users/${userId}`).set({}, {
merge: true
});
const user = doc.data();
Object.assign(window.user, user);
return user;
});
}
// Fetch user settings.
// This isn't hitting the remote db because remote settings
// get fetch asynchronously (in user/) and update the envioronment.
function getSettings(defaultSettings) {
const d = deferred();
// Will be chrome.storage.sync in extension environment,
// otherwise will fallback to localstorage
dbSyncAlias.get(defaultSettings, result => {
d.resolve(result);
});
return d.promise;
}
window.db = {
getDb,
getUser,
getUserLastSeenVersion,
setUserLastSeenVersion,
getSettings,
local: dbLocalAlias,
sync: dbSyncAlias
};
})();

12
src/deferred.js Normal file
View File

@@ -0,0 +1,12 @@
export function deferred() {
var d = {};
var promise = new Promise(function (resolve, reject) {
d.resolve = resolve;
d.reject = reject;
});
// Add the native promise as a key on deferred object.
d.promise = promise;
// Also move all props/methods of native promise on the deferred obj.
return Object.assign(d, promise);
}

51
src/editorThemes.js Normal file
View File

@@ -0,0 +1,51 @@
export const editorThemes = [
'3024-day',
'3024-night',
'abcdef',
'ambiance',
'base2tone-meadow-dark',
'base16-dark',
'base16-light',
'bespin',
'blackboard',
'cobalt',
'colorforth',
'dracula',
'duotone-dark',
'duotone-light',
'eclipse',
'elegant',
'erlang-dark',
'hopscotch',
'icecoder',
'isotope',
'lesser-dark',
'liquibyte',
'material',
'mbo',
'mdn-like',
'midnight',
'monokai',
'neat',
'neo',
'night',
'panda-syntax',
'paraiso-dark',
'paraiso-light',
'pastel-on-dark',
'railscasts',
'rubyblue',
'seti',
'solarized dark',
'solarized light',
'the-matrix',
'tomorrow-night-bright',
'tomorrow-night-eighties',
'ttcn',
'twilight',
'vibrant-ink',
'xq-dark',
'xq-light',
'yeti',
'zenburn'
];

11
src/firebaseInit.js Normal file
View File

@@ -0,0 +1,11 @@
import firebase from 'firebase/app';
// import 'firebase/firestore';
const config = {
apiKey: 'AIzaSyBl8Dz7ZOE7aP75mipYl2zKdLSRzBU2fFc',
authDomain: 'web-maker-app.firebaseapp.com',
databaseURL: 'https://web-maker-app.firebaseio.com',
projectId: 'web-maker-app',
storageBucket: 'web-maker-app.appspot.com',
messagingSenderId: '560473480645'
};
firebase.initializeApp(config);

99
src/index.html Normal file
View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Web Maker</title>
<link rel="icon" href="icon-128.png">
<meta name=viewport content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json">
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.manifest.theme_color %>">
<% } %>
<% for (var chunk of webpack.chunks) { %>
<% if (chunk.names.length === 1 && chunk.names[0] === 'polyfills') continue; %>
<% for (var file of chunk.files) { %>
<% if (htmlWebpackPlugin.options.preload && file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="<%= htmlWebpackPlugin.files.publicPath + file %>" as="<%= file.match(/\.css$/)?'style':'script' %>">
<% } else if (file.match(/manifest\.json$/)) { %>
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath + file %>">
<% } %>
<% } %>
<% } %>
<style>
/* Critically acclaimed CSS */
.saved-items-pane {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 450px;
transform: translateX(100%);
}
.modal {
visibility: hidden;
}
</style>
<!-- build:css vendor.css -->
<link rel="stylesheet" href="lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="lib/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="lib/codemirror/addon/fold/foldgutter.css">
<link rel="stylesheet" href="lib/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="lib/hint.min.css">
<link rel="stylesheet" href="lib/inlet.css">
<!-- endbuild -->
<link rel="stylesheet" id="editorThemeLinkTag" href="lib/codemirror/theme/monokai.css"></link>
<!-- build:css style.css -->
<link rel="stylesheet" href="style.css">
<!-- endbuild -->
<style id="fontStyleTemplate" type="template">
@font-face { font-family: 'fontname'; font-style: normal; font-weight: 400; src: url(fontname.ttf) format('truetype'); unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF,
U+EFFD, U+F000; } .CodeMirror pre { font-family: 'fontname', monospace; }
</style>
<style type="text/css" id="fontStyleTag">
@font-face {
font-family: 'FiraCode';
font-style: normal;
font-weight: 400;
src: url(FiraCode.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
.CodeMirror pre {
font-family: 'FiraCode', monospace;
}
</style>
</head>
<body>
<%= htmlWebpackPlugin.options.ssr({
url: '/'
}) %>
<script defer src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
<script>
window.fetch || document.write('<script src="<%= htmlWebpackPlugin.files.chunks["polyfills"].entry %>"><\/script>')
</script>
</body>
</html>

3
src/index.js Normal file
View File

@@ -0,0 +1,3 @@
import App from './components/app.jsx';
export default App;

233
src/itemService.js Normal file
View File

@@ -0,0 +1,233 @@
import {
deferred
} from './deferred';
export const itemService = {
async getItem(id) {
var remoteDb = await window.db.getDb();
return remoteDb
.doc(`items/${id}`)
.get()
.then(doc => {
return doc.data();
});
},
async getUserItemIds() {
if (window.user) {
return new Promise(resolve => {
resolve(window.user.items || {});
});
}
var remoteDb = await window.db.getDb();
return remoteDb
.doc(`users/${window.user.uid}`)
.get()
.then(doc => {
if (!doc.exists) {
return {};
}
return doc.data().items;
});
},
async getAllItems() {
var t = Date.now()
var d = deferred();
let itemIds = await this.getUserItemIds();
itemIds = Object.getOwnPropertyNames(itemIds || {});
utils.log('itemids', itemIds);
if (!itemIds.length) {
d.resolve([]);
}
var remoteDb = await window.db.getDb();
const items = [];
remoteDb
.collection('items')
.where('createdBy', '==', window.user.uid)
.onSnapshot(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
items.push(doc.data());
});
utils.log('Items fetched in ', Date.now() - t, 'ms')
d.resolve(items);
}, function () {
d.resolve([])
});
return d.promise;
},
async setUser() {
const remoteDb = await window.db.getDb();
return remoteDb.doc(`users/${window.user.uid}`).set({
items: {}
});
},
async setItem(id, item) {
const d = deferred();
var remotePromise;
// TODO: check why we need to save locally always?
const obj = {
[id]: item
};
db.local.set(obj, () => {
// Is extension OR is app but logged out OR is logged in but offline
// If logged in but offline, resolve immediately so
// that you see the feedback msg immediately and not wait for
// later sync.
if (window.IS_EXTENSION || !window.user || !navigator.onLine) {
d.resolve();
}
});
// If `id` is `code`, this is a call on unloadbefore to save the last open thing.
// Do not presist that on remote.
if (id === 'code') {
// No deferred required here as this gets called on unloadbefore
return false;
}
if (window.user) {
var remoteDb = await window.db.getDb();
utils.log(`Starting to save item ${id}`);
item.createdBy = window.user.uid;
remotePromise = remoteDb
.collection('items')
.doc(id)
.set(item, {
merge: true
})
.then(arg => {
utils.log('Document written', arg);
d.resolve();
})
.catch(d.reject);
}
return window.user && navigator.onLine ? remotePromise : d.promise;
},
/**
* Saves the passed items in the database.
* @param {Array} items to be saved in DB
*/
saveItems(items) {
var d = deferred();
// When not logged in
if (!window.user) {
// save new items
window.db.local.set(items, d.resolve);
// Push in new item IDs
window.db.local.get({
items: {}
},
function (result) {
/* eslint-disable guard-for-in */
for (var id in items) {
result.items[id] = true;
}
window.db.local.set({
items: result.items
});
/* eslint-enable guard-for-in */
}
);
} else {
window.db.getDb().then(remoteDb => {
const batch = remoteDb.batch();
/* eslint-disable guard-for-in */
for (var id in items) {
items[id].createdBy = window.user.uid;
batch.set(remoteDb.doc(`items/${id}`), items[id]);
batch.update(remoteDb.doc(`users/${window.user.uid}`), {
[`items.${id}`]: true
});
// Set these items on our cached user object too
window.user.items = window.user.items || {};
window.user.items[id] = true;
}
batch.commit().then(d.resolve);
/* eslint-enable guard-for-in */
});
}
return d.promise;
},
async removeItem(id) {
// When not logged in
if (!window.user) {
var d = deferred();
window.db.local.remove(id, d.resolve);
return d.promise;
}
const remoteDb = await window.db.getDb();
utils.log(`Starting to save item ${id}`);
return remoteDb
.collection('items')
.doc(id)
.delete()
.then(arg => {
utils.log('Document removed', arg);
})
.catch(error => utils.log(error));
},
async setItemForUser(itemId) {
// When not logged in
if (!window.user) {
return window.db.local.get({
items: {}
},
function (result) {
result.items[itemId] = true;
window.db.local.set({
items: result.items
});
}
);
}
const remoteDb = await window.db.getDb();
return remoteDb
.collection('users')
.doc(window.user.uid)
.update({
[`items.${itemId}`]: true
})
.then(arg => {
utils.log(`Item ${itemId} set for user`, arg);
window.user.items = window.user.items || {};
window.user.items[itemId] = true;
})
.catch(error => utils.log(error));
},
async unsetItemForUser(itemId) {
// When not logged in
if (!window.user) {
return window.db.local.get({
items: {}
},
function (result) {
delete result.items[itemId];
window.db.local.set({
items: result.items
});
}
);
}
const remoteDb = await window.db.getDb();
return remoteDb
.collection('users')
.doc(window.user.uid)
.update({
[`items.${itemId}`]: firebase.firestore.FieldValue.delete()
})
.then(arg => {
delete window.user.items[itemId];
utils.log(`Item ${itemId} unset for user`, arg);
})
.catch(error => utils.log(error));
}
}

153
src/libraryList.js Normal file
View File

@@ -0,0 +1,153 @@
export const jsLibs = [
{
url: 'https://code.jquery.com/jquery-3.2.1.min.js',
label: 'jQuery',
type: 'js'
},
{
url: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js',
label: 'Bootstrap 3',
type: 'js'
},
{
url:
'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js',
label: 'Bootstrap 4',
type: 'js'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/js/foundation.min.js',
label: 'Foundation',
type: 'js'
},
{
url: 'https://semantic-ui.com/dist/semantic.min.js',
label: 'Semantic UI',
type: 'js'
},
{
url: 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.min.js',
label: 'Angular',
type: 'js'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js',
label: 'React',
type: 'js'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js',
label: 'React DOM',
type: 'js'
},
{
url: 'https://unpkg.com/vue/dist/vue.min.js',
label: 'Vue.js',
type: 'js'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js',
label: 'Three.js',
type: 'js'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js',
label: 'D3',
type: 'js'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js',
label: 'Underscore',
type: 'js'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js',
label: 'Greensock TweenMax',
type: 'js'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/uikit/2.27.5/js/uikit.min.js',
label: 'UIkit 2',
type: 'js'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.42/js/uikit.min.js',
label: 'UIkit 3',
type: 'js'
}
];
export const cssLibs = [
{
url:
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
label: 'Bootstrap 3',
type: 'css'
},
{
url:
'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css',
label: 'Bootstrap 4',
type: 'css'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/css/foundation.min.css',
label: 'Foundation',
type: 'css'
},
{
url: 'https://semantic-ui.com/dist/semantic.min.css',
label: 'Semantic UI',
type: 'css'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css',
label: 'Bulma',
type: 'css'
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/hint.css/2.5.0/hint.min.css',
label: 'Hint.css',
type: 'css'
},
{
url: 'https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css',
label: 'Tailwind.css',
type: 'css'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/uikit/2.27.5/css/uikit.min.css',
label: 'UIkit 2',
type: 'css'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.42/css/uikit.min.css',
label: 'UIkit 3',
type: 'css'
},
{
url:
'https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css',
label: 'Animate.css',
type: 'css'
},
{
url:
'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
label: 'FontAwesome 4',
type: 'css'
},
{
url: 'https://use.fontawesome.com/releases/v5.0.10/css/all.css',
label: 'FontAwesome 5',
type: 'css'
}
];

View File

@@ -1,27 +1,21 @@
{
"name": "Web Maker",
"version": "3.2.0",
"manifest_version": 2,
"description": "Blazing fast & offline playground for your web experiments",
"homepage_url": "https://webmakerapp.com",
"permissions": ["storage", "tabs", "<all_urls>"],
"optional_permissions": ["downloads"],
"content_security_policy":
"script-src 'self' filesystem: http://localhost:* https://localhost:* https://apis.google.com https://ajax.googleapis.com https://code.jquery.com https://cdnjs.cloudflare.com https://unpkg.com https://maxcdn.com https://cdn77.com https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net/ https://*.stripe.com/ https://builds.framerjs.com/ https://rawgit.com https://wzrd.in https://www.gstatic.com https://semantic-ui.com https://www.google-analytics.com 'unsafe-eval'; object-src 'self'",
"options_ui": {
"page": "options.html",
"chrome_style": true
},
"browser_action": {
"default_title": "Start Web Maker",
"default_icon": "icon-16.png"
},
"background": {
"scripts": ["eventPage.js"],
"persistent": false
},
"icons": {
"16": "icon-16.png",
"48": "icon-48.png"
}
}
"name": "webmaker",
"short_name": "webmaker",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#fff",
"theme_color": "#673ab8",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/assets/icons/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

20
src/notifications.js Normal file
View File

@@ -0,0 +1,20 @@
var hideTimeout;
function addNotification(msg) {
const noticationContainerEL = $('#js-alerts-container');
// var n = document.createElement('div');
// div.textContent = msg;
// noticationContainerEL.appendChild(n);
noticationContainerEL.textContent = msg;
noticationContainerEL.classList.add('is-active');
clearTimeout(hideTimeout);
hideTimeout = setTimeout(function () {
noticationContainerEL.classList.remove('is-active');
}, 2000);
}
export const alertsService = {
add: addNotification
};

BIN
src/patreon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -2,8 +2,7 @@
<body>
<iframe src="about://blank" frameborder="0" id="demo-frame" allowfullscreen></iframe>
<iframe src="about://blank" frameborder="0" id="demo-frame" allowfullscreen></iframe>
<script src="detached-window.js"></script>
<script src="detached-window.js"></script>
</body>

1596
src/style.css Normal file

File diff suppressed because it is too large Load Diff

138
src/takeScreenshot.js Normal file
View File

@@ -0,0 +1,138 @@
import {
handleDownloadsPermission
} from "./utils";
function saveScreenshot(dataURI) {
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs
var byteString = atob(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI
.split(',')[0]
.split(':')[1]
.split(';')[0];
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// create a blob for writing to a file
var blob = new Blob([ab], {
type: mimeString
});
var size = blob.size + 1024 / 2;
var d = new Date();
var fileName = [
'web-maker-screenshot',
d.getFullYear(),
d.getMonth() + 1,
d.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds()
].join('-');
fileName += '.png';
function onWriteEnd() {
var filePath =
'filesystem:chrome-extension://' +
chrome.i18n.getMessage('@@extension_id') +
'/temporary/' +
fileName;
chrome.downloads.download({
url: filePath
},
function () {
// If there was an error, just open the screenshot in a tab.
// This happens in incognito mode where extension cannot access filesystem.
if (chrome.runtime.lastError) {
window.open(filePath);
}
}
);
}
function errorHandler(e) {
utils.log(e);
}
// create a blob for writing to a file
window.webkitRequestFileSystem(
window.TEMPORARY,
size,
fs => {
fs.root.getFile(
fileName, {
create: true
},
fileEntry => {
fileEntry.createWriter(fileWriter => {
fileWriter.onwriteend = onWriteEnd;
fileWriter.write(blob);
}, errorHandler);
},
errorHandler
);
},
errorHandler
);
}
export function takeScreenshot(boundRect) {
handleDownloadsPermission().then(() => {
// Hide tooltips so that they don't show in the screenshot
var s = document.createElement('style');
s.textContent =
'[class*="hint"]:after, [class*="hint"]:before { display: none!important; }';
document.body.appendChild(s);
function onImgLoad(image) {
var c = document.createElement('canvas');
var iframeBounds = boundRect;
c.width = iframeBounds.width;
c.height = iframeBounds.height;
var ctx = c.getContext('2d');
var devicePixelRatio = window.devicePixelRatio || 1;
ctx.drawImage(
image,
iframeBounds.left * devicePixelRatio,
iframeBounds.top * devicePixelRatio,
iframeBounds.width * devicePixelRatio,
iframeBounds.height * devicePixelRatio,
0,
0,
iframeBounds.width,
iframeBounds.height
);
image.removeEventListener('load', onImgLoad);
saveScreenshot(c.toDataURL());
}
setTimeout(() => {
debugger
chrome.tabs.captureVisibleTab(
null, {
format: 'png',
quality: 100
},
function (dataURI) {
s.remove();
if (dataURI) {
var image = new Image();
image.src = dataURI;
image.addEventListener('load', () => onImgLoad(image, dataURI));
}
}
);
}, 50);
trackEvent('ui', 'takeScreenshotBtnClick');
});
};

View File

@@ -0,0 +1,21 @@
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
* An example how to mock localStorage is given below 👇
*/
/*
// Mocks localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: (key) => store[key] || null,
setItem: (key, value) => store[key] = value.toString(),
clear: () => store = {}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}); */

View File

@@ -0,0 +1,3 @@
// This fixed an error related to the CSS and loading gif breaking my Jest test
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
module.exports = 'test-file-stub';

14
src/tests/header.test.js Normal file
View File

@@ -0,0 +1,14 @@
import { h, Component } from 'preact';
import Header from '../components/header';
import { Link } from 'preact-router/match';
// See: https://github.com/mzgoddard/preact-render-spy
import { shallow, deep } from 'preact-render-spy';
describe('Initial Test of the Header', () => {
test('Header renders 3 nav items', () => {
const context = shallow(<Header />);
expect(context.find('h1').text()).toBe('Preact App');
expect(context.find(<Link />).length).toBe(3);
});
});

448
src/utils.js Normal file
View File

@@ -0,0 +1,448 @@
import {
trackEvent
} from './analytics';
import {
computeHtml,
computeCss,
computeJs
} from './computes';
import {
JsModes
} from './codeModes';
import {
deferred
} from './deferred';
const esprima = require('esprima');
window.DEBUG = document.cookie.indexOf('wmdebug') > -1;
window.$ = document.querySelector.bind(document);
window.$all = selector => [...document.querySelectorAll(selector)];
const BASE_PATH = chrome.extension || window.DEBUG ? '/' : '/app';
var alphaNum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
/**
* The following 2 functions are supposed to find the next/previous sibling until the
* passed `selector` is matched. But for now it actually finds the next/previous
* element of `this` element in the list of `selector` matched element inside `this`'s
* parent.
* @param Selector that should match for next siblings
* @return element Next element that mathes `selector`
*/
Node.prototype.nextUntil = function (selector) {
const siblings = Array.from(this.parentNode.querySelectorAll(selector));
const index = siblings.indexOf(this);
return siblings[index + 1];
};
/*
* @param Selector that should match for next siblings
* @return element Next element that mathes `selector`
*/
Node.prototype.previousUntil = function (selector) {
const siblings = Array.from(this.parentNode.querySelectorAll(selector));
const index = siblings.indexOf(this);
return siblings[index - 1];
};
// Safari doesn't have this!
window.requestIdleCallback =
window.requestIdleCallback ||
function (fn) {
setTimeout(fn, 10);
};
// https://github.com/substack/semver-compare/blob/master/index.js
export function semverCompare(a, b) {
var pa = a.split('.');
var pb = b.split('.');
for (var i = 0; i < 3; i++) {
var na = Number(pa[i]);
var nb = Number(pb[i]);
if (na > nb) {
return 1;
}
if (nb > na) {
return -1;
}
if (!isNaN(na) && isNaN(nb)) {
return 1;
}
if (isNaN(na) && !isNaN(nb)) {
return -1;
}
}
return 0;
}
export function generateRandomId(len) {
var length = len || 10;
var id = '';
for (var i = length; i--;) {
id += alphaNum[~~(Math.random() * alphaNum.length)];
}
return id;
}
export function onButtonClick(btn, listener) {
btn.addEventListener('click', function buttonClickListener(e) {
listener(e);
return false;
});
}
export function log() {
if (window.DEBUG) {
console.log(Date.now(), ...arguments);
}
}
/**
* Adds timed limit on the loops found in the passed code.
* Contributed by Ariya Hidayat!
* @param code {string} Code to be protected from infinite loops.
*/
export function addInfiniteLoopProtection(code, {
timeout
}) {
var loopId = 1;
var patches = [];
var varPrefix = '_wmloopvar';
var varStr = 'var %d = Date.now();\n';
var checkStr = `\nif (Date.now() - %d > ${timeout}) { window.top.previewException(new Error("Infinite loop")); break;}\n`;
esprima.parse(
code, {
tolerant: true,
range: true,
jsx: true
},
function (node) {
switch (node.type) {
case 'DoWhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WhileStatement':
var start = 1 + node.body.range[0];
var end = node.body.range[1];
var prolog = checkStr.replace('%d', varPrefix + loopId);
var epilog = '';
if (node.body.type !== 'BlockStatement') {
// `while(1) doThat()` becomes `while(1) {doThat()}`
prolog = '{' + prolog;
epilog = '}';
--start;
}
patches.push({
pos: start,
str: prolog
});
patches.push({
pos: end,
str: epilog
});
patches.push({
pos: node.range[0],
str: varStr.replace('%d', varPrefix + loopId)
});
++loopId;
break;
default:
break;
}
}
);
/* eslint-disable no-param-reassign */
patches
.sort(function (a, b) {
return b.pos - a.pos;
})
.forEach(function (patch) {
code = code.slice(0, patch.pos) + patch.str + code.slice(patch.pos);
});
/* eslint-disable no-param-reassign */
return code;
}
export function getHumanDate(timestamp) {
var d = new Date(timestamp);
var retVal =
d.getDate() +
' ' + [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
][d.getMonth()] +
' ' +
d.getFullYear();
return retVal;
}
// create a one-time event
export function once(node, type, callback) {
// create event
node.addEventListener(type, function (e) {
// remove event
e.target.removeEventListener(type, arguments.callee);
// call handler
return callback(e);
});
}
export function downloadFile(fileName, blob) {
function downloadWithAnchor() {
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
}
if (window.IS_EXTENSION) {
chrome.downloads.download({
url: window.URL.createObjectURL(blob),
filename: fileName,
saveAs: true
},
() => {
// If there was an error, just download the file using ANCHOR method.
if (chrome.runtime.lastError) {
downloadWithAnchor();
}
}
);
} else {
downloadWithAnchor();
}
}
export function writeFile(name, blob, cb) {
var fileWritten = false;
function getErrorHandler(type) {
return function () {
log(arguments);
trackEvent('fn', 'error', type);
// When there are too many write errors, show a message.
writeFile.errorCount = (writeFile.errorCount || 0) + 1;
if (writeFile.errorCount === 4) {
setTimeout(function () {
alert(
"Oops! Seems like your preview isn't updating. It's recommended to switch to the web app: https://webmakerapp.com/app/.\n\n If you still want to get the extension working, please try the following steps until it fixes:\n - Refresh Web Maker\n - Restart browser\n - Update browser\n - Reinstall Web Maker (don't forget to export all your creations from saved items pane (click the OPEN button) before reinstalling)\n\nIf nothing works, please tweet out to @webmakerApp."
);
trackEvent('ui', 'writeFileMessageSeen');
}, 1000);
}
};
}
// utils.log('writing file ', name);
window.webkitRequestFileSystem(
window.TEMPORARY,
1024 * 1024 * 5,
function (fs) {
fs.root.getFile(
name, {
create: true
},
function (fileEntry) {
fileEntry.createWriter(fileWriter => {
function onWriteComplete() {
if (fileWritten) {
// utils.log('file written ', name);
return cb();
}
fileWritten = true;
// Set the write pointer to starting of file
fileWriter.seek(0);
fileWriter.write(blob);
return false;
}
fileWriter.onwriteend = onWriteComplete;
// Empty the file contents
fileWriter.truncate(0);
// utils.log('truncating file ', name);
}, getErrorHandler('createWriterFail'));
},
getErrorHandler('getFileFail')
);
},
getErrorHandler('webkitRequestFileSystemFail')
);
}
export function loadJS(src) {
var d = deferred();
var ref = window.document.getElementsByTagName('script')[0];
var script = window.document.createElement('script');
script.src = src;
script.async = true;
ref.parentNode.insertBefore(script, ref);
script.onload = function () {
d.resolve();
};
return d.promise;
}
export function getCompleteHtml(html, css, js, item, isForExport) {
if (!item) {
return '';
}
var externalJs = item.externalLibs.js
.split('\n')
.reduce(function (scripts, url) {
return scripts + (url ? '\n<script src="' + url + '"></script>' : '');
}, '');
var externalCss = item.externalLibs.css
.split('\n')
.reduce(function (links, url) {
return (
links +
(url ? '\n<link rel="stylesheet" href="' + url + '"></link>' : '')
);
}, '');
var contents =
'<!DOCTYPE html>\n' +
'<html>\n<head>\n' +
'<meta charset="UTF-8" />\n' +
externalCss +
'\n' +
'<style id="webmakerstyle">\n' +
css +
'\n</style>\n' +
'</head>\n' +
'<body>\n' +
html +
'\n' +
externalJs +
'\n';
if (!isForExport) {
contents +=
'<script src="' +
(chrome.extension ?
chrome.extension.getURL('lib/screenlog.js') :
`${location.origin}${BASE_PATH}/lib/screenlog.js`) +
'"></script>';
}
if (item.jsMode === JsModes.ES6) {
contents +=
'<script src="' +
(chrome.extension ?
chrome.extension.getURL('lib/transpilers/babel-polyfill.min.js') :
`${
location.origin
}${BASE_PATH}/lib/transpilers/babel-polyfill.min.js`) +
'"></script>';
}
if (typeof js === 'string') {
contents += '<script>\n' + js + '\n//# sourceURL=userscript.js';
} else {
var origin = chrome.i18n.getMessage() ?
`chrome-extension://${chrome.i18n.getMessage('@@extension_id')}` :
`${location.origin}`;
contents +=
'<script src="' + `filesystem:${origin}/temporary/script.js` + '">';
}
contents += '\n</script>\n</body>\n</html>';
return contents;
}
export function saveAsHtml(item) {
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]).then(function (result) {
var html = result[0],
css = result[1],
js = result[2];
var fileContent = getCompleteHtml(html, css, js, item, true);
var d = new Date();
var fileName = [
'web-maker',
d.getFullYear(),
d.getMonth() + 1,
d.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds()
].join('-');
if (item.title) {
fileName = item.title;
}
fileName += '.html';
var blob = new Blob([fileContent], {
type: 'text/html;charset=UTF-8'
});
downloadFile(fileName, blob);
trackEvent('fn', 'saveFileComplete');
});
}
export function handleDownloadsPermission() {
var d = deferred();
if (!window.IS_EXTENSION) {
d.resolve();
return d.promise;
}
chrome.permissions.contains({
permissions: ['downloads']
},
function (result) {
if (result) {
d.resolve();
} else {
chrome.permissions.request({
permissions: ['downloads']
},
function (granted) {
if (granted) {
trackEvent('fn', 'downloadsPermGiven');
d.resolve();
} else {
d.reject();
}
}
);
}
}
);
return d.promise;
}
window.chrome = window.chrome || {};
window.chrome.i18n = {
getMessage: () => {}
};
window.IS_EXTENSION = !!window.chrome.extension;
if (window.IS_EXTENSION) {
document.body.classList.add('is-extension');
} else {
document.body.classList.add('is-app');
}