mirror of
https://github.com/chinchang/web-maker.git
synced 2025-05-06 18:45:31 +02:00
3616 lines
98 KiB
JavaScript
3616 lines
98 KiB
JavaScript
/**
|
|
* Copyright 2015 Google Inc. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/* eslint-env browser */
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
// Delay registration until after the page has loaded, to ensure that our
|
|
// precaching requests don't degrade the first visit experience.
|
|
// See https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/registration
|
|
window.addEventListener('load', function() {
|
|
// Your service-worker.js *must* be located at the top-level directory relative to your site.
|
|
// It won't be able to control pages unless it's located at the same level or higher than them.
|
|
// *Don't* register service worker file in, e.g., a scripts/ sub-directory!
|
|
// See https://github.com/slightlyoff/ServiceWorker/issues/468
|
|
navigator.serviceWorker
|
|
.register('service-worker.js')
|
|
.then(function(reg) {
|
|
// updatefound is fired if service-worker.js changes.
|
|
reg.onupdatefound = function() {
|
|
// The updatefound event implies that reg.installing is set; see
|
|
// https://w3c.github.io/ServiceWorker/#service-worker-registration-updatefound-event
|
|
var installingWorker = reg.installing;
|
|
|
|
installingWorker.onstatechange = function() {
|
|
switch (installingWorker.state) {
|
|
case 'installed':
|
|
if (navigator.serviceWorker.controller) {
|
|
// At this point, the old content will have been purged and the fresh content will
|
|
// have been added to the cache.
|
|
// It's the perfect time to display a "New content is available; please refresh."
|
|
// message in the page's interface.
|
|
console.log('New or updated content is available.');
|
|
} else {
|
|
// At this point, everything has been precached.
|
|
// It's the perfect time to display a "Content is cached for offline use." message.
|
|
console.log('Content is now available offline!');
|
|
}
|
|
break;
|
|
|
|
case 'redundant':
|
|
console.error(
|
|
'The installing service worker became redundant.'
|
|
);
|
|
break;
|
|
}
|
|
};
|
|
};
|
|
})
|
|
.catch(function(e) {
|
|
console.error('Error during service worker registration:', e);
|
|
});
|
|
});
|
|
}
|
|
|
|
(function() {
|
|
window.DEBUG = document.cookie.indexOf('wmdebug') > -1;
|
|
|
|
window.$ = document.querySelector.bind(document);
|
|
window.$all = selector => [...document.querySelectorAll(selector)];
|
|
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 = [...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 = [...this.parentNode.querySelectorAll(selector)];
|
|
const index = siblings.indexOf(this);
|
|
return siblings[index - 1];
|
|
};
|
|
|
|
// https://github.com/substack/semver-compare/blob/master/index.js
|
|
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;
|
|
}
|
|
|
|
function generateRandomId(len) {
|
|
var length = len || 10;
|
|
var id = '';
|
|
for (var i = length; i--; ) {
|
|
id += alphaNum[~~(Math.random() * alphaNum.length)];
|
|
}
|
|
return id;
|
|
}
|
|
|
|
function onButtonClick(btn, listener) {
|
|
btn.addEventListener('click', function buttonClickListener(e) {
|
|
listener(e);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function log() {
|
|
if (window.DEBUG) {
|
|
console.log(...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.
|
|
*/
|
|
function addInfiniteLoopProtection(code) {
|
|
var loopId = 1;
|
|
var patches = [];
|
|
var varPrefix = '_wmloopvar';
|
|
var varStr = 'var %d = Date.now();\n';
|
|
var checkStr =
|
|
'\nif (Date.now() - %d > 1000) { 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;
|
|
}
|
|
|
|
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
|
|
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);
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
window.utils = {
|
|
semverCompare,
|
|
generateRandomId,
|
|
onButtonClick,
|
|
addInfiniteLoopProtection,
|
|
getHumanDate,
|
|
log,
|
|
once,
|
|
downloadFile
|
|
};
|
|
|
|
window.chrome = window.chrome || {};
|
|
window.chrome.i18n = { getMessage: () => {} };
|
|
|
|
window.IS_EXTENSION = !!window.chrome.extension;
|
|
})();
|
|
|
|
(() => {
|
|
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 */
|
|
}
|
|
};
|
|
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();
|
|
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.
|
|
// ...
|
|
} 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) {
|
|
if (window.IS_EXTENSION) {
|
|
chrome.storage.sync.set(
|
|
{
|
|
lastSeenVersion: version
|
|
},
|
|
function() {}
|
|
);
|
|
return;
|
|
}
|
|
// Settings the lastSeenVersion in localStorage also because next time we need
|
|
// to fetch it irrespective of the user being logged in or out
|
|
local.set({ lastSeenVersion: version });
|
|
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;
|
|
});
|
|
}
|
|
|
|
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
|
|
};
|
|
})();
|
|
|
|
window.logout = function logout() {
|
|
firebase.auth().signOut();
|
|
};
|
|
function login(providerName) {
|
|
var provider;
|
|
if (providerName === 'fb') {
|
|
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(result) {
|
|
return;
|
|
// Save this user in the store
|
|
firebase
|
|
.database()
|
|
.ref('users/' + result.user.uid)
|
|
.update({
|
|
displayName: result.user.displayName,
|
|
email: result.user.email,
|
|
photoURL: result.user.providerData[0].photoURL,
|
|
signedUpOn: Date.now()
|
|
})
|
|
.then(function() {
|
|
// Port items in localstorage to user account
|
|
if (window.localStorage.prototyp) {
|
|
var items = JSON.parse(window.localStorage.prototyp);
|
|
var newItemKey;
|
|
items.forEach(function(localItem) {
|
|
itemService.fetchItem(localItem.id).then(function(item) {
|
|
newItemKey = firebase.database().ref('pens').push().key;
|
|
item.createdBy = result.user.uid;
|
|
delete item.uid;
|
|
firebase.database().ref('pens/' + newItemKey).set(item);
|
|
firebase
|
|
.database()
|
|
.ref('users/' + result.user.uid)
|
|
.child('items')
|
|
.child(newItemKey)
|
|
.set(true);
|
|
});
|
|
});
|
|
delete localStorage.prototyp;
|
|
}
|
|
});
|
|
})
|
|
.catch(function(error) {
|
|
// Handle Errors here.
|
|
var errorCode = error.code;
|
|
var errorMessage = error.message;
|
|
// The email of the user's account used.
|
|
var email = error.email;
|
|
// The firebase.auth.AuthCredential type that was used.
|
|
var credential = error.credential;
|
|
utils.log(error);
|
|
});
|
|
}
|
|
window.login = login;
|
|
|
|
/* global ga */
|
|
// eslint-disable-next-line max-params
|
|
window.trackEvent = function(category, action, label, value) {
|
|
if (window.DEBUG) {
|
|
utils.log('trackevent', category, action, label, value);
|
|
return;
|
|
}
|
|
if (window.ga) {
|
|
ga('send', 'event', category, action, label, value);
|
|
}
|
|
};
|
|
|
|
// if online, load after sometime
|
|
if (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');
|
|
|
|
ga('create', 'UA-87786708-1', {'cookieDomain': 'none'});
|
|
// required for chrome extension protocol
|
|
ga('set', 'checkProtocolTask', function(){ /* nothing */ });
|
|
ga('send', 'pageview');
|
|
}, 100);
|
|
|
|
/* eslint-enable */
|
|
}
|
|
|
|
(function() {
|
|
window.deferred = function() {
|
|
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);
|
|
};
|
|
})();
|
|
|
|
(function(w) {
|
|
window.loadJS = function(src) {
|
|
var d = deferred();
|
|
var ref = w.document.getElementsByTagName('script')[0];
|
|
var script = w.document.createElement('script');
|
|
script.src = src;
|
|
script.async = true;
|
|
ref.parentNode.insertBefore(script, ref);
|
|
script.onload = function() {
|
|
d.resolve();
|
|
};
|
|
return d.promise;
|
|
};
|
|
})(window);
|
|
|
|
(function() {
|
|
const noticationContainerEL = $('#js-alerts-container');
|
|
var hideTimeout;
|
|
|
|
function addNotification(msg) {
|
|
// 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);
|
|
}
|
|
|
|
window.alertsService = {
|
|
add: addNotification
|
|
};
|
|
})();
|
|
|
|
window.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://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/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.4/angular.min.js',
|
|
label: 'Angular',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url:
|
|
'https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/cjs/react.production.min.js',
|
|
label: 'React',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url: 'https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js',
|
|
label: 'React DOM',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url: 'https://unpkg.com/vue@2.5.0/dist/vue.min.js',
|
|
label: 'Vue.js',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url: 'https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js',
|
|
label: 'Three.js',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url: 'https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/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.4/js/uikit.min.js',
|
|
label: 'UIkit 2',
|
|
type: 'js'
|
|
},
|
|
{
|
|
url:
|
|
'https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.31/js/uikit.min.js',
|
|
label: 'UIkit 3',
|
|
type: 'js'
|
|
}
|
|
];
|
|
window.cssLibs = [
|
|
{
|
|
url:
|
|
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
|
|
label: 'Bootstrap 3',
|
|
type: 'css'
|
|
},
|
|
{
|
|
url:
|
|
'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/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.6.0/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://cdnjs.cloudflare.com/ajax/libs/uikit/2.27.4/css/uikit.min.css',
|
|
label: 'UIkit 2',
|
|
type: 'css'
|
|
},
|
|
{
|
|
url:
|
|
'https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.31/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',
|
|
type: 'css'
|
|
}
|
|
];
|
|
|
|
// textarea-autocomplete.js
|
|
(function() {
|
|
class TextareaAutoComplete {
|
|
constructor(textarea, options) {
|
|
this.t = textarea;
|
|
this.filter = options.filter;
|
|
this.selectedCallback = options.selectedCallback;
|
|
var wrap = document.createElement('div');
|
|
wrap.classList.add('btn-group');
|
|
textarea.parentElement.insertBefore(wrap, textarea);
|
|
wrap.insertBefore(textarea, null);
|
|
this.list = document.createElement('ul');
|
|
this.list.classList.add('dropdown__menu');
|
|
this.list.classList.add('autocomplete-dropdown');
|
|
wrap.insertBefore(this.list, null);
|
|
|
|
this.loader = document.createElement('div');
|
|
this.loader.classList.add('loader');
|
|
this.loader.classList.add('autocomplete__loader');
|
|
this.loader.style.display = 'none';
|
|
wrap.insertBefore(this.loader, null);
|
|
|
|
// 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));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
if (this.selectedCallback) {
|
|
this.selectedCallback.call(null, value);
|
|
} else {
|
|
this.replaceCurrentLine(value);
|
|
}
|
|
this.closeSuggestions();
|
|
}
|
|
}
|
|
|
|
window.TextareaAutoComplete = TextareaAutoComplete;
|
|
})();
|
|
|
|
(() => {
|
|
window.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 d = deferred();
|
|
let itemIds = await this.getUserItemIds();
|
|
itemIds = Object.getOwnPropertyNames(itemIds || {});
|
|
utils.log('itemids', itemIds);
|
|
|
|
if (!itemIds.length) {
|
|
d.resolve([]);
|
|
}
|
|
|
|
const items = [];
|
|
for (let i = 0; i < itemIds.length; i++) {
|
|
const id = itemIds[i];
|
|
utils.log('Starting to fetch item ', id);
|
|
this.getItem(id).then(item => {
|
|
items.push(item);
|
|
// Check if we have all items now.
|
|
if (itemIds.length === items.length) {
|
|
d.resolve(items);
|
|
}
|
|
});
|
|
}
|
|
return d.promise;
|
|
},
|
|
|
|
async setUser() {
|
|
const remoteDb = await window.db.getDb();
|
|
return remoteDb.doc(`users/${window.user.uid}`).set({
|
|
items: {}
|
|
});
|
|
},
|
|
|
|
async setItem(id, item) {
|
|
if (!window.user) {
|
|
return new Promise(resolve => resolve());
|
|
}
|
|
var remoteDb = await window.db.getDb();
|
|
utils.log(`Starting to save item ${id}`);
|
|
item.createdBy = window.user.uid;
|
|
return remoteDb
|
|
.collection('items')
|
|
.doc(id)
|
|
.set(item, {
|
|
merge: true
|
|
})
|
|
.then(arg => {
|
|
utils.log('Document written', arg);
|
|
})
|
|
.catch(error => utils.log(error));
|
|
},
|
|
|
|
/**
|
|
* Saves the passed items in the database.
|
|
* @param {Array} items to be saved in DB
|
|
*/
|
|
saveItems(items) {
|
|
var d = deferred();
|
|
if (window.IS_EXTENSION) {
|
|
// 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 out cached user object too
|
|
window.user.items[id] = true;
|
|
}
|
|
batch.commit().then(d.resolve);
|
|
/* eslint-enable guard-for-in */
|
|
});
|
|
}
|
|
return d.promise;
|
|
},
|
|
|
|
async removeItem(id) {
|
|
if (window.IS_EXTENSION) {
|
|
var d = deferred();
|
|
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) {
|
|
if (window.IS_EXTENSION || !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) {
|
|
if (window.IS_EXTENSION) {
|
|
return window.db.local.get(
|
|
{
|
|
items: {}
|
|
},
|
|
function(result) {
|
|
delete result.items[itemId];
|
|
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 => {
|
|
utils.log(`Item ${itemId} unset for user`, arg);
|
|
})
|
|
.catch(error => utils.log(error));
|
|
}
|
|
};
|
|
})();
|
|
|
|
/* global trackEvent */
|
|
/* global layoutBtn1, layoutBtn2, layoutBtn3, helpModal, notificationsModal, addLibraryModal,
|
|
onboardModal, layoutBtn1, layoutBtn2, layoutBtn3, layoutBtn4, onboardModal, onboardModal,
|
|
addLibraryModal, addLibraryModal, notificationsBtn, notificationsModal, notificationsModal,
|
|
notificationsModal, notificationsBtn, codepenBtn, saveHtmlBtn, saveBtn,
|
|
onboardModal, settingsModal, notificationsBtn, onboardShowInTabOptionBtn, editorThemeLinkTag,
|
|
onboardDontShowInTabOptionBtn, TextareaAutoComplete, savedItemCountEl, indentationSizeValueEl,
|
|
runBtn, searchInput, consoleEl, consoleLogEl, logCountEl, fontStyleTag, fontStyleTemplate,
|
|
customEditorFontInput, cssSettingsModal, cssSettingsBtn, acssSettingsTextarea,
|
|
globalConsoleContainerEl, externalLibrarySearchInput, keyboardShortcutsModal, headerAvatarImg,
|
|
loginModal
|
|
*/
|
|
/* eslint-disable no-extra-semi */
|
|
(function(alertsService, itemService) {
|
|
/* eslint-enable no-extra-semi */
|
|
var scope = scope || {};
|
|
var version = '2.9.6';
|
|
|
|
if (window.DEBUG) {
|
|
window.scope = scope;
|
|
}
|
|
|
|
const defaultSettings = {
|
|
preserveLastCode: true,
|
|
replaceNewTab: false,
|
|
htmlMode: 'html',
|
|
jsMode: 'js',
|
|
cssMode: 'css',
|
|
isCodeBlastOn: false,
|
|
indentWith: 'spaces',
|
|
indentSize: 2,
|
|
editorTheme: 'monokai',
|
|
keymap: 'sublime',
|
|
fontSize: 16,
|
|
refreshOnResize: false,
|
|
autoPreview: true,
|
|
editorFont: 'FiraCode',
|
|
editorCustomFont: '',
|
|
autoSave: true,
|
|
autoComplete: true,
|
|
preserveConsoleLogs: true,
|
|
lightVersion: false,
|
|
lineWrap: true
|
|
};
|
|
var HtmlModes = {
|
|
HTML: 'html',
|
|
MARKDOWN: 'markdown',
|
|
JADE: 'jade' // unsafe eval is put in manifest for this file
|
|
};
|
|
var CssModes = {
|
|
CSS: 'css',
|
|
SCSS: 'scss',
|
|
SASS: 'sass',
|
|
LESS: 'less',
|
|
STYLUS: 'stylus',
|
|
ACSS: 'acss'
|
|
};
|
|
var JsModes = {
|
|
JS: 'js',
|
|
ES6: 'es6',
|
|
COFFEESCRIPT: 'coffee',
|
|
TS: 'typescript'
|
|
};
|
|
var 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
|
|
};
|
|
|
|
const AUTO_SAVE_INTERVAL = 15000; // 15 seconds
|
|
const BASE_PATH = chrome.extension ? '/' : '/app';
|
|
|
|
var updateTimer,
|
|
updateDelay = 500,
|
|
autoSaveInterval,
|
|
unsavedEditWarningCount = 15,
|
|
currentLayoutMode,
|
|
hasSeenNotifications = true,
|
|
htmlMode = HtmlModes.HTML,
|
|
jsMode = JsModes.JS,
|
|
cssMode = CssModes.CSS,
|
|
sass,
|
|
currentItem,
|
|
unsavedEditCount,
|
|
savedItems,
|
|
minCodeWrapSize = 33,
|
|
mainSplitInstance,
|
|
codeSplitInstance,
|
|
prefs = {},
|
|
codeInPreview = { html: null, css: null, js: null },
|
|
isSavedItemsPaneOpen = false,
|
|
editorWithFocus,
|
|
logCount = 0,
|
|
isAutoSavingEnabled,
|
|
// DOM nodes
|
|
frame = $('#demo-frame'),
|
|
htmlCode = $('#js-html-code'),
|
|
cssCode = $('#js-css-code'),
|
|
jsCode = $('#js-js-code'),
|
|
codepenForm = $('#js-codepen-form'),
|
|
savedItemsPane = $('#js-saved-items-pane'),
|
|
savedItemsPaneCloseBtn = $('#js-saved-items-pane-close-btn'),
|
|
htmlModelLabel = $('#js-html-mode-label'),
|
|
cssModelLabel = $('#js-css-mode-label'),
|
|
jsModelLabel = $('#js-js-mode-label'),
|
|
titleInput = $('#js-title-input'),
|
|
addLibrarySelect = $('#js-add-library-select'),
|
|
externalJsTextarea = $('#js-external-js'),
|
|
externalCssTextarea = $('#js-external-css');
|
|
|
|
scope.cm = {};
|
|
scope.frame = frame;
|
|
scope.demoFrameDocument =
|
|
frame.contentDocument || frame.contentWindow.document;
|
|
|
|
// Check all the code wrap if they are minimized or maximized
|
|
function updateCodeWrapCollapseStates() {
|
|
// This is debounced!
|
|
clearTimeout(updateCodeWrapCollapseStates.timeout);
|
|
updateCodeWrapCollapseStates.timeout = setTimeout(function() {
|
|
const prop = currentLayoutMode === 2 ? 'width' : 'height';
|
|
[htmlCode, cssCode, jsCode].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);
|
|
}
|
|
|
|
function toggleCodeWrapCollapse(codeWrapEl) {
|
|
if (
|
|
codeWrapEl.classList.contains('is-minimized') ||
|
|
codeWrapEl.classList.contains('is-maximized')
|
|
) {
|
|
codeWrapEl.classList.remove('is-minimized');
|
|
codeWrapEl.classList.remove('is-maximized');
|
|
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)`;
|
|
|
|
codeSplitInstance.setSizes(arr);
|
|
codeWrapEl.classList.add('is-maximized');
|
|
}
|
|
}
|
|
// Returns the sizes of main code & preview panes.
|
|
function getMainSplitSizesToApply() {
|
|
var mainSplitSizes;
|
|
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 = [50, 50];
|
|
}
|
|
return mainSplitSizes;
|
|
}
|
|
|
|
function resetSplitting() {
|
|
if (codeSplitInstance) {
|
|
codeSplitInstance.destroy();
|
|
}
|
|
if (mainSplitInstance) {
|
|
mainSplitInstance.destroy();
|
|
}
|
|
|
|
var options = {
|
|
direction: currentLayoutMode === 2 ? 'horizontal' : 'vertical',
|
|
minSize: minCodeWrapSize,
|
|
gutterSize: 6,
|
|
onDragStart: function() {
|
|
document.body.classList.add('is-dragging');
|
|
},
|
|
onDragEnd: function() {
|
|
updateCodeWrapCollapseStates();
|
|
document.body.classList.remove('is-dragging');
|
|
}
|
|
};
|
|
if (currentItem && currentItem.sizes) {
|
|
options.sizes = currentItem.sizes;
|
|
} else {
|
|
options.sizes = [33.33, 33.33, 33.33];
|
|
}
|
|
|
|
codeSplitInstance = Split(
|
|
['#js-html-code', '#js-css-code', '#js-js-code'],
|
|
options
|
|
);
|
|
mainSplitInstance = Split(['#js-code-side', '#js-demo-side'], {
|
|
direction: currentLayoutMode === 2 ? 'vertical' : 'horizontal',
|
|
minSize: 150,
|
|
gutterSize: 6,
|
|
sizes: getMainSplitSizesToApply(),
|
|
onDragEnd: function() {
|
|
if (prefs.refreshOnResize) {
|
|
// Running preview updation in next call stack, so that error there
|
|
// doesn't affect this dragend listener.
|
|
setTimeout(function() {
|
|
scope.setPreviewContent(true);
|
|
}, 1);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function toggleLayout(mode) {
|
|
if (currentLayoutMode === mode) {
|
|
mainSplitInstance.setSizes(getMainSplitSizesToApply());
|
|
codeSplitInstance.setSizes(currentItem.sizes || [33.33, 33.33, 33.33]);
|
|
currentLayoutMode = mode;
|
|
return;
|
|
}
|
|
currentLayoutMode = mode;
|
|
layoutBtn1.classList.remove('selected');
|
|
layoutBtn2.classList.remove('selected');
|
|
layoutBtn3.classList.remove('selected');
|
|
layoutBtn4.classList.remove('selected');
|
|
$('#layoutBtn' + mode).classList.add('selected');
|
|
document.body.classList.remove('layout-1');
|
|
document.body.classList.remove('layout-2');
|
|
document.body.classList.remove('layout-3');
|
|
document.body.classList.remove('layout-4');
|
|
document.body.classList.add('layout-' + mode);
|
|
|
|
resetSplitting();
|
|
scope.setPreviewContent(true);
|
|
}
|
|
|
|
function onExternalLibChange() {
|
|
utils.log('onExternalLibChange');
|
|
updateExternalLibUi();
|
|
scope.setPreviewContent(true);
|
|
alertsService.add('Libraries updated.');
|
|
}
|
|
|
|
function updateExternalLibUi() {
|
|
// Calculate no. of external libs
|
|
var noOfExternalLibs = 0;
|
|
noOfExternalLibs += externalJsTextarea.value
|
|
.split('\n')
|
|
.filter(lib => !!lib).length;
|
|
noOfExternalLibs += externalCssTextarea.value
|
|
.split('\n')
|
|
.filter(lib => !!lib).length;
|
|
if (noOfExternalLibs) {
|
|
$('#js-external-lib-count').textContent = noOfExternalLibs;
|
|
$('#js-external-lib-count').style.display = 'inline';
|
|
} else {
|
|
$('#js-external-lib-count').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function saveSetting(setting, value) {
|
|
const d = deferred();
|
|
const obj = {
|
|
[setting]: value
|
|
};
|
|
db.local.set(obj, d.resolve);
|
|
return d.promise;
|
|
}
|
|
|
|
// Save current item to storage
|
|
function saveItem() {
|
|
var isNewItem = !currentItem.id;
|
|
currentItem.id = currentItem.id || 'item-' + utils.generateRandomId();
|
|
saveCode().then(() => {
|
|
// If this is the first save, and auto-saving settings is enabled,
|
|
// then start auto-saving from now on.
|
|
// This is done in `saveCode()` completion so that the
|
|
// auto-save notification overrides the `saveCode` function's notification.
|
|
if (!isAutoSavingEnabled && prefs.autoSave) {
|
|
isAutoSavingEnabled = true;
|
|
alertsService.add('Auto-save enabled.');
|
|
}
|
|
});
|
|
// Push into the items hash if its a new item being saved
|
|
if (isNewItem) {
|
|
itemService.setItemForUser(currentItem.id);
|
|
}
|
|
}
|
|
|
|
// Keeps getting called after certain interval to auto-save current creation
|
|
// if it needs to be.
|
|
function autoSaveLoop() {
|
|
if (isAutoSavingEnabled && unsavedEditCount) {
|
|
saveItem();
|
|
}
|
|
}
|
|
|
|
// Calculates the sizes of html, css & js code panes.
|
|
function getCodePaneSizes() {
|
|
var sizes;
|
|
var dimensionProperty = currentLayoutMode === 2 ? 'width' : 'height';
|
|
try {
|
|
sizes = [
|
|
htmlCode.style[dimensionProperty],
|
|
cssCode.style[dimensionProperty],
|
|
jsCode.style[dimensionProperty]
|
|
];
|
|
} catch (e) {
|
|
sizes = [33.33, 33.33, 33.33];
|
|
} finally {
|
|
/* eslint-disable no-unsafe-finally */
|
|
return sizes;
|
|
|
|
/* eslint-enable no-unsafe-finally */
|
|
}
|
|
}
|
|
|
|
// Calculates the current sizes of code & preview panes.
|
|
function getMainPaneSizes() {
|
|
var sizes;
|
|
var dimensionProperty = currentLayoutMode === 2 ? 'height' : 'width';
|
|
try {
|
|
sizes = [
|
|
+$('#js-code-side').style[dimensionProperty].match(/([\d.]+)%/)[1],
|
|
+$('#js-demo-side').style[dimensionProperty].match(/([\d.]+)%/)[1]
|
|
];
|
|
} catch (e) {
|
|
sizes = [50, 50];
|
|
} finally {
|
|
/* eslint-disable no-unsafe-finally */
|
|
return sizes;
|
|
|
|
/* eslint-enable no-unsafe-finally */
|
|
}
|
|
}
|
|
|
|
function saveCode(key) {
|
|
currentItem.title = titleInput.value;
|
|
currentItem.html = scope.cm.html.getValue();
|
|
currentItem.css = scope.cm.css.getValue();
|
|
currentItem.js = scope.cm.js.getValue();
|
|
currentItem.htmlMode = htmlMode;
|
|
currentItem.cssMode = cssMode;
|
|
currentItem.jsMode = jsMode;
|
|
if (modes[cssMode].hasSettings) {
|
|
currentItem.cssSettings = {
|
|
acssConfig: scope.acssSettingsCm.getValue()
|
|
};
|
|
}
|
|
currentItem.updatedOn = Date.now();
|
|
currentItem.layoutMode = currentLayoutMode;
|
|
currentItem.externalLibs = {
|
|
js: externalJsTextarea.value,
|
|
css: externalCssTextarea.value
|
|
};
|
|
|
|
currentItem.sizes = getCodePaneSizes();
|
|
currentItem.mainSizes = getMainPaneSizes();
|
|
|
|
utils.log('saving key', key || currentItem.id, currentItem);
|
|
saveSetting(key || currentItem.id, currentItem);
|
|
// If key is `code`, this is a call on unloadbefore to save the last open thing.
|
|
// Do not presist that on remote.
|
|
if (key === 'code') {
|
|
// No deferred required here as this gets called on unloadbefore
|
|
return false;
|
|
}
|
|
return itemService.setItem(key || currentItem.id, currentItem).then(() => {
|
|
alertsService.add('Item saved.');
|
|
unsavedEditCount = 0;
|
|
saveBtn.classList.remove('is-marked');
|
|
});
|
|
}
|
|
|
|
function populateItemsInSavedPane(items) {
|
|
var html = '';
|
|
if (items.length) {
|
|
// TODO: sort desc. by updation date
|
|
items.sort(function(a, b) {
|
|
return b.updatedOn - a.updatedOn;
|
|
});
|
|
items.forEach(function(item) {
|
|
html +=
|
|
'<div class="js-saved-item-tile saved-item-tile" data-item-id="' +
|
|
item.id +
|
|
'">' +
|
|
'<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)">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">X</a></div>' +
|
|
'<h3 class="saved-item-tile__title">' +
|
|
item.title +
|
|
'</h3><span class="saved-item-tile__meta">Last updated: ' +
|
|
utils.getHumanDate(item.updatedOn) +
|
|
'</span></div>';
|
|
});
|
|
savedItemCountEl.textContent = '(' + items.length + ')';
|
|
savedItemCountEl.style.display = 'inline';
|
|
} else {
|
|
html += '<h2 class="opacity--30">Nothing saved here.</h2>';
|
|
savedItemCountEl.style.display = 'none';
|
|
}
|
|
savedItemsPane.querySelector('#js-saved-items-wrap').innerHTML = html;
|
|
toggleSavedItemsPane();
|
|
// HACK: Set overflow after sometime so that the items can animate without getting cropped.
|
|
// setTimeout(() => $('#js-saved-items-wrap').style.overflowY = 'auto', 1000);
|
|
}
|
|
|
|
function toggleSavedItemsPane(shouldOpen) {
|
|
if (shouldOpen === false) {
|
|
savedItemsPane.classList.remove('is-open');
|
|
} else {
|
|
savedItemsPane.classList.toggle('is-open');
|
|
}
|
|
isSavedItemsPaneOpen = savedItemsPane.classList.contains('is-open');
|
|
if (isSavedItemsPaneOpen) {
|
|
searchInput.focus();
|
|
} else {
|
|
searchInput.value = '';
|
|
// Give last focused editor, focus again
|
|
if (editorWithFocus) {
|
|
editorWithFocus.focus();
|
|
}
|
|
}
|
|
document.body.classList[isSavedItemsPaneOpen ? 'add' : 'remove'](
|
|
'overlay-visible'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fetches all items from storage
|
|
* @param {boolean} shouldSaveGlobally Whether to store the fetched items in global arr for later use.
|
|
* @return {promise} Promise.
|
|
*/
|
|
async function fetchItems(shouldSaveGlobally) {
|
|
var d = deferred();
|
|
savedItems = savedItems || {};
|
|
var items = [];
|
|
if (!window.IS_EXTENSION && window.user) {
|
|
items = await itemService.getAllItems();
|
|
|
|
if (shouldSaveGlobally) {
|
|
items.forEach(item => {
|
|
savedItems[item.id] = item;
|
|
});
|
|
}
|
|
d.resolve(items);
|
|
return d.promise;
|
|
}
|
|
db.local.get('items', function(result) {
|
|
var itemIds = Object.getOwnPropertyNames(result.items || {});
|
|
if (!itemIds.length) {
|
|
d.resolve([]);
|
|
}
|
|
|
|
trackEvent('fn', 'fetchItems', itemIds.length);
|
|
for (let i = 0; i < itemIds.length; i++) {
|
|
/* eslint-disable no-loop-func */
|
|
db.local.get(itemIds[i], function(itemResult) {
|
|
if (shouldSaveGlobally) {
|
|
savedItems[itemIds[i]] = itemResult[itemIds[i]];
|
|
}
|
|
items.push(itemResult[itemIds[i]]);
|
|
// Check if we have all items now.
|
|
if (itemIds.length === items.length) {
|
|
d.resolve(items);
|
|
}
|
|
});
|
|
|
|
/* eslint-enable no-loop-func */
|
|
}
|
|
});
|
|
return d.promise;
|
|
}
|
|
|
|
function openSavedItemsPane() {
|
|
fetchItems(true).then(function(items) {
|
|
populateItemsInSavedPane(items);
|
|
});
|
|
}
|
|
function setCurrentItem(item) {
|
|
currentItem = item;
|
|
utils.log('Current Item set', item);
|
|
|
|
// Reset auto-saving flag
|
|
isAutoSavingEnabled = false;
|
|
// Reset unsaved count, in UI also.
|
|
unsavedEditCount = 0;
|
|
saveBtn.classList.remove('is-marked');
|
|
}
|
|
// Creates a new item with passed item's contents
|
|
function forkItem(sourceItem) {
|
|
if (unsavedEditCount) {
|
|
var shouldDiscard = confirm(
|
|
'You have unsaved changes in your current work. Do you want to discard unsaved changes and continue?'
|
|
);
|
|
if (!shouldDiscard) {
|
|
return;
|
|
}
|
|
}
|
|
const fork = JSON.parse(JSON.stringify(sourceItem));
|
|
delete fork.id;
|
|
fork.title = '(Forked) ' + sourceItem.title;
|
|
fork.updatedOn = Date.now();
|
|
setCurrentItem(fork);
|
|
refreshEditor();
|
|
alertsService.add(`"${sourceItem.title}" was forked`);
|
|
trackEvent('fn', 'itemForked');
|
|
}
|
|
function createNewItem() {
|
|
var d = new Date();
|
|
setCurrentItem({
|
|
title:
|
|
'Untitled ' +
|
|
d.getDate() +
|
|
'-' +
|
|
(d.getMonth() + 1) +
|
|
'-' +
|
|
d.getHours() +
|
|
':' +
|
|
d.getMinutes(),
|
|
html: '',
|
|
css: '',
|
|
js: '',
|
|
externalLibs: { js: '', css: '' },
|
|
layoutMode: currentLayoutMode
|
|
});
|
|
refreshEditor();
|
|
alertsService.add('New item created');
|
|
}
|
|
function openItem(itemId) {
|
|
setCurrentItem(savedItems[itemId]);
|
|
refreshEditor();
|
|
alertsService.add('Saved item loaded');
|
|
}
|
|
function removeItem(itemId) {
|
|
var itemTile = document.querySelector(
|
|
'.js-saved-item-tile[data-item-id="' + itemId + '"]'
|
|
);
|
|
var answer = confirm(
|
|
`Are you sure you want to delete "${savedItems[itemId].title}"?`
|
|
);
|
|
if (!answer) {
|
|
return;
|
|
}
|
|
|
|
itemTile.remove();
|
|
// Remove from items list
|
|
itemService.unsetItemForUser(itemId);
|
|
|
|
// Remove individual item too.
|
|
itemService.removeItem(itemId).then(() => {
|
|
alertsService.add('Item removed.');
|
|
// This item is open in the editor. Lets open a new one.
|
|
if (currentItem.id === itemId) {
|
|
createNewItem();
|
|
}
|
|
});
|
|
|
|
// Remove from cached list
|
|
delete savedItems[itemId];
|
|
|
|
trackEvent('fn', 'itemRemoved');
|
|
}
|
|
|
|
function refreshEditor() {
|
|
titleInput.value = currentItem.title || 'Untitled';
|
|
externalJsTextarea.value =
|
|
(currentItem.externalLibs && currentItem.externalLibs.js) || '';
|
|
externalCssTextarea.value =
|
|
(currentItem.externalLibs && currentItem.externalLibs.css) || '';
|
|
|
|
utils.log('refresh editor');
|
|
// Set the modes manually here, so that the preview refresh triggered by the `setValue`
|
|
// statements below, operate on correct modes.
|
|
htmlMode = currentItem.htmlMode || prefs.htmlMode || HtmlModes.HTML;
|
|
cssMode = currentItem.cssMode || prefs.cssMode || CssModes.CSS;
|
|
jsMode = currentItem.jsMode || prefs.jsMode || JsModes.JS;
|
|
|
|
scope.cm.html.setValue(currentItem.html);
|
|
scope.cm.css.setValue(currentItem.css);
|
|
scope.cm.js.setValue(currentItem.js);
|
|
scope.cm.html.refresh();
|
|
scope.cm.css.refresh();
|
|
scope.cm.js.refresh();
|
|
|
|
scope.acssSettingsCm.setValue(
|
|
currentItem.cssSettings ? currentItem.cssSettings.acssConfig : ''
|
|
);
|
|
scope.acssSettingsCm.refresh();
|
|
|
|
scope.clearConsole();
|
|
|
|
// To have the library count updated
|
|
updateExternalLibUi();
|
|
|
|
// 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([
|
|
updateHtmlMode(htmlMode),
|
|
updateCssMode(cssMode),
|
|
updateJsMode(jsMode)
|
|
]).then(() => scope.setPreviewContent(true));
|
|
|
|
toggleLayout(currentItem.layoutMode || prefs.layoutMode);
|
|
}
|
|
|
|
function closeAllOverlays() {
|
|
helpModal.classList.remove('is-modal-visible');
|
|
notificationsModal.classList.remove('is-modal-visible');
|
|
addLibraryModal.classList.remove('is-modal-visible');
|
|
onboardModal.classList.remove('is-modal-visible');
|
|
settingsModal.classList.remove('is-modal-visible');
|
|
cssSettingsModal.classList.remove('is-modal-visible');
|
|
keyboardShortcutsModal.classList.remove('is-modal-visible');
|
|
loginModal.classList.remove('is-modal-visible');
|
|
toggleSavedItemsPane(false);
|
|
document.dispatchEvent(new Event('overlaysClosed'));
|
|
}
|
|
|
|
/**
|
|
* Loaded the code comiler based on the mode selected
|
|
*/
|
|
function handleModeRequirements(mode) {
|
|
// 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('lib/jade.js').then(setLoadedFlag);
|
|
} else if (mode === HtmlModes.MARKDOWN) {
|
|
loadJS('lib/marked.js').then(setLoadedFlag);
|
|
} else if (mode === CssModes.LESS) {
|
|
loadJS('lib/less.min.js').then(setLoadedFlag);
|
|
} else if (mode === CssModes.SCSS || mode === CssModes.SASS) {
|
|
loadJS('lib/sass.js').then(function() {
|
|
sass = new Sass('lib/sass.worker.js');
|
|
setLoadedFlag();
|
|
});
|
|
} else if (mode === CssModes.STYLUS) {
|
|
loadJS('lib/stylus.min.js').then(setLoadedFlag);
|
|
} else if (mode === CssModes.ACSS) {
|
|
loadJS('lib/atomizer.browser.js').then(setLoadedFlag);
|
|
} else if (mode === JsModes.COFFEESCRIPT) {
|
|
loadJS('lib/coffee-script.js').then(setLoadedFlag);
|
|
} else if (mode === JsModes.ES6) {
|
|
loadJS('lib/babel.min.js').then(setLoadedFlag);
|
|
} else if (mode === JsModes.TS) {
|
|
loadJS('lib/typescript.js').then(setLoadedFlag);
|
|
} else {
|
|
d.resolve();
|
|
}
|
|
|
|
return d.promise;
|
|
}
|
|
|
|
function updateHtmlMode(value) {
|
|
htmlMode = value;
|
|
htmlModelLabel.textContent = modes[value].label;
|
|
// FIXME - use a better selector for the mode selectbox
|
|
htmlModelLabel.parentElement.querySelector('select').value = value;
|
|
scope.cm.html.setOption('mode', modes[value].cmMode);
|
|
CodeMirror.autoLoadMode(
|
|
scope.cm.html,
|
|
modes[value].cmPath || modes[value].cmMode
|
|
);
|
|
return handleModeRequirements(value);
|
|
}
|
|
function updateCssMode(value) {
|
|
cssMode = value;
|
|
cssModelLabel.textContent = modes[value].label;
|
|
// FIXME - use a better selector for the mode selectbox
|
|
cssModelLabel.parentElement.querySelector('select').value = value;
|
|
scope.cm.css.setOption('mode', modes[value].cmMode);
|
|
scope.cm.css.setOption('readOnly', modes[value].cmDisable);
|
|
cssSettingsBtn.classList[modes[value].hasSettings ? 'remove' : 'add'](
|
|
'hide'
|
|
);
|
|
CodeMirror.autoLoadMode(
|
|
scope.cm.css,
|
|
modes[value].cmPath || modes[value].cmMode
|
|
);
|
|
return handleModeRequirements(value);
|
|
}
|
|
function updateJsMode(value) {
|
|
jsMode = value;
|
|
jsModelLabel.textContent = modes[value].label;
|
|
// FIXME - use a better selector for the mode selectbox
|
|
jsModelLabel.parentElement.querySelector('select').value = value;
|
|
scope.cm.js.setOption('mode', modes[value].cmMode);
|
|
CodeMirror.autoLoadMode(
|
|
scope.cm.js,
|
|
modes[value].cmPath || modes[value].cmMode
|
|
);
|
|
return handleModeRequirements(value);
|
|
}
|
|
|
|
// computeHtml, computeCss & computeJs evaluate the final code according
|
|
// to whatever mode is selected and resolve the returned promise with the code.
|
|
function computeHtml() {
|
|
var d = deferred();
|
|
var code = scope.cm.html.getValue();
|
|
if (htmlMode === HtmlModes.HTML) {
|
|
d.resolve(code);
|
|
} else if (htmlMode === HtmlModes.MARKDOWN) {
|
|
d.resolve(marked ? marked(code) : code);
|
|
} else if (htmlMode === HtmlModes.JADE) {
|
|
d.resolve(window.jade ? jade.render(code) : code);
|
|
}
|
|
|
|
return d.promise;
|
|
}
|
|
function computeCss() {
|
|
var d = deferred();
|
|
var code = scope.cm.css.getValue();
|
|
cleanupErrors('css');
|
|
|
|
if (cssMode === CssModes.CSS) {
|
|
d.resolve(code);
|
|
} else if (cssMode === CssModes.SCSS || cssMode === CssModes.SASS) {
|
|
if (sass && code) {
|
|
sass.compile(
|
|
code,
|
|
{ indentedSyntax: cssMode === CssModes.SASS },
|
|
function(result) {
|
|
// Something was wrong
|
|
if (result.line && result.message) {
|
|
showErrors('css', [
|
|
{ lineNumber: result.line - 1, message: result.message }
|
|
]);
|
|
}
|
|
d.resolve(result.text);
|
|
}
|
|
);
|
|
} else {
|
|
d.resolve(code);
|
|
}
|
|
} else if (cssMode === CssModes.LESS) {
|
|
less.render(code).then(
|
|
function(result) {
|
|
d.resolve(result.css);
|
|
},
|
|
function(error) {
|
|
showErrors('css', [
|
|
{ lineNumber: error.line, message: error.message }
|
|
]);
|
|
}
|
|
);
|
|
} else if (cssMode === 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
|
|
showErrors('css', [
|
|
{
|
|
lineNumber: +error.message.match(/stylus:(\d+):/)[1] - 298,
|
|
message: tempArr.pop()
|
|
}
|
|
]);
|
|
}
|
|
d.resolve(result);
|
|
});
|
|
} else if (cssMode === CssModes.ACSS) {
|
|
if (!window.atomizer) {
|
|
d.resolve('');
|
|
} else {
|
|
const html = scope.cm.html.getValue();
|
|
const foundClasses = atomizer.findClassNames(html);
|
|
var finalConfig;
|
|
try {
|
|
finalConfig = atomizer.getConfig(
|
|
foundClasses,
|
|
JSON.parse(scope.acssSettingsCm.getValue())
|
|
);
|
|
} catch (e) {
|
|
finalConfig = atomizer.getConfig(foundClasses, {});
|
|
}
|
|
const acss = atomizer.getCss(finalConfig);
|
|
scope.cm.css.setValue(acss);
|
|
d.resolve(acss);
|
|
}
|
|
}
|
|
|
|
return d.promise;
|
|
}
|
|
function computeJs(shouldPreventInfiniteLoops) {
|
|
var d = deferred();
|
|
var code = scope.cm.js.getValue();
|
|
|
|
cleanupErrors('js');
|
|
if (!code) {
|
|
d.resolve('');
|
|
return d.promise;
|
|
}
|
|
|
|
if (jsMode === JsModes.JS) {
|
|
try {
|
|
esprima.parse(code, {
|
|
tolerant: true
|
|
});
|
|
} catch (e) {
|
|
showErrors('js', [
|
|
{ lineNumber: e.lineNumber - 1, message: e.description }
|
|
]);
|
|
} finally {
|
|
if (shouldPreventInfiniteLoops !== false) {
|
|
code = utils.addInfiniteLoopProtection(code);
|
|
}
|
|
d.resolve(code);
|
|
}
|
|
} else if (jsMode === JsModes.COFFEESCRIPT) {
|
|
var coffeeCode;
|
|
if (!window.CoffeeScript) {
|
|
d.resolve('');
|
|
return d.promise;
|
|
}
|
|
try {
|
|
coffeeCode = CoffeeScript.compile(code, { bare: true });
|
|
} catch (e) {
|
|
showErrors('js', [
|
|
{ lineNumber: e.location.first_line, message: e.message }
|
|
]);
|
|
} finally {
|
|
if (shouldPreventInfiniteLoops !== false) {
|
|
code = utils.addInfiniteLoopProtection(coffeeCode);
|
|
}
|
|
d.resolve(code);
|
|
}
|
|
} else if (jsMode === JsModes.ES6) {
|
|
if (!window.Babel) {
|
|
d.resolve('');
|
|
return d.promise;
|
|
}
|
|
try {
|
|
esprima.parse(code, {
|
|
tolerant: true,
|
|
jsx: true
|
|
});
|
|
} catch (e) {
|
|
showErrors('js', [
|
|
{ lineNumber: e.lineNumber - 1, message: e.description }
|
|
]);
|
|
} finally {
|
|
code = Babel.transform(code, {
|
|
presets: ['latest', 'stage-2', 'react']
|
|
}).code;
|
|
if (shouldPreventInfiniteLoops !== false) {
|
|
code = utils.addInfiniteLoopProtection(code);
|
|
}
|
|
d.resolve(code);
|
|
}
|
|
} else if (jsMode === JsModes.TS) {
|
|
try {
|
|
if (!window.ts) {
|
|
d.resolve('');
|
|
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 */
|
|
throw {
|
|
description: code.diagnostics[0].messageText,
|
|
lineNumber: ts.getLineOfLocalPosition(
|
|
code.diagnostics[0].file,
|
|
code.diagnostics[0].start
|
|
)
|
|
};
|
|
}
|
|
if (shouldPreventInfiniteLoops !== false) {
|
|
code = utils.addInfiniteLoopProtection(code.outputText);
|
|
}
|
|
d.resolve(code);
|
|
} catch (e) {
|
|
showErrors('js', [
|
|
{ lineNumber: e.lineNumber - 1, message: e.description }
|
|
]);
|
|
}
|
|
}
|
|
|
|
return d.promise;
|
|
}
|
|
|
|
window.previewException = function(error) {
|
|
console.error('Possible infinite loop detected.', error.stack);
|
|
window.onMessageFromConsole(
|
|
'Possible infinite loop detected.',
|
|
error.stack
|
|
);
|
|
};
|
|
window.onunload = function() {
|
|
saveCode('code');
|
|
if (scope.detachedWindow) {
|
|
scope.detachedWindow.close();
|
|
}
|
|
};
|
|
|
|
function cleanupErrors(lang) {
|
|
scope.cm[lang].clearGutter('error-gutter');
|
|
}
|
|
function showErrors(lang, errors) {
|
|
var editor = scope.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);
|
|
});
|
|
});
|
|
}
|
|
|
|
/* eslint max-params: ["error", 4] */
|
|
function getCompleteHtml(html, css, js, isForExport) {
|
|
var externalJs = externalJsTextarea.value
|
|
.split('\n')
|
|
.reduce(function(scripts, url) {
|
|
return scripts + (url ? '\n<script src="' + url + '"></script>' : '');
|
|
}, '');
|
|
var externalCss = externalCssTextarea.value
|
|
.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 (jsMode === JsModes.ES6) {
|
|
contents +=
|
|
'<script src="' +
|
|
chrome.extension.getURL('lib/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;
|
|
}
|
|
|
|
function writeFile(name, blob, cb) {
|
|
var fileWritten = false;
|
|
function getErrorHandler(type) {
|
|
return function() {
|
|
utils.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. 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')
|
|
);
|
|
}
|
|
|
|
function createPreviewFile(html, css, js) {
|
|
const shouldInlineJs =
|
|
!window.webkitRequestFileSystem || !window.IS_EXTENSION;
|
|
var contents = getCompleteHtml(html, css, shouldInlineJs ? js : null);
|
|
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) {
|
|
frame.src = frame.src;
|
|
setTimeout(() => {
|
|
frame.contentDocument.open();
|
|
frame.contentDocument.write(contents);
|
|
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;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
scope.setPreviewContent = function(isForced) {
|
|
if (!prefs.preserveConsoleLogs) {
|
|
scope.clearConsole();
|
|
}
|
|
|
|
var currentCode = {
|
|
html: scope.cm.html.getValue(),
|
|
css: scope.cm.css.getValue(),
|
|
js: scope.cm.js.getValue()
|
|
};
|
|
utils.log('🔎 setPreviewContent', isForced);
|
|
const targetFrame = scope.detachedWindow
|
|
? scope.detachedWindow.document.querySelector('iframe')
|
|
: frame;
|
|
|
|
// If just CSS was changed (and everything shudn't be empty),
|
|
// change the styles inside the iframe.
|
|
if (
|
|
!isForced &&
|
|
currentCode.html === codeInPreview.html &&
|
|
currentCode.js === codeInPreview.js
|
|
) {
|
|
computeCss().then(function(css) {
|
|
if (targetFrame.contentDocument.querySelector('#webmakerstyle')) {
|
|
targetFrame.contentDocument.querySelector(
|
|
'#webmakerstyle'
|
|
).textContent = css;
|
|
}
|
|
});
|
|
} else {
|
|
var htmlPromise = computeHtml();
|
|
var cssPromise = computeCss();
|
|
var jsPromise = computeJs();
|
|
Promise.all([htmlPromise, cssPromise, jsPromise]).then(function(result) {
|
|
createPreviewFile(result[0], result[1], result[2]);
|
|
});
|
|
}
|
|
|
|
codeInPreview.html = currentCode.html;
|
|
codeInPreview.css = currentCode.css;
|
|
codeInPreview.js = currentCode.js;
|
|
};
|
|
|
|
function saveFile() {
|
|
var htmlPromise = computeHtml();
|
|
var cssPromise = computeCss();
|
|
var jsPromise = computeJs(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, true);
|
|
|
|
var d = new Date();
|
|
var fileName = [
|
|
'web-maker',
|
|
d.getFullYear(),
|
|
d.getMonth() + 1,
|
|
d.getDate(),
|
|
d.getHours(),
|
|
d.getMinutes(),
|
|
d.getSeconds()
|
|
].join('-');
|
|
fileName += '.html';
|
|
|
|
if (currentItem.title) {
|
|
fileName = currentItem.title;
|
|
}
|
|
|
|
var blob = new Blob([fileContent], { type: 'text/html;charset=UTF-8' });
|
|
utils.downloadFile(fileName, blob);
|
|
|
|
trackEvent('fn', 'saveFileComplete');
|
|
});
|
|
}
|
|
|
|
function initEditor(element, options) {
|
|
var cm = CodeMirror(element, {
|
|
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) {
|
|
var 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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
cm.on('focus', editor => {
|
|
editorWithFocus = editor;
|
|
});
|
|
cm.on('change', function onChange(editor, change) {
|
|
clearTimeout(updateTimer);
|
|
|
|
updateTimer = setTimeout(function() {
|
|
// 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 (prefs.autoPreview !== false) {
|
|
scope.setPreviewContent();
|
|
}
|
|
|
|
saveBtn.classList.add('is-marked');
|
|
unsavedEditCount += 1;
|
|
if (
|
|
unsavedEditCount % unsavedEditWarningCount === 0 &&
|
|
unsavedEditCount >= unsavedEditWarningCount
|
|
) {
|
|
saveBtn.classList.add('animated');
|
|
saveBtn.classList.add('wobble');
|
|
saveBtn.addEventListener('animationend', () => {
|
|
saveBtn.classList.remove('animated');
|
|
saveBtn.classList.remove('wobble');
|
|
});
|
|
}
|
|
|
|
// Track when people actually are working.
|
|
trackEvent.previewCount = (trackEvent.previewCount || 0) + 1;
|
|
if (trackEvent.previewCount === 4) {
|
|
trackEvent('fn', 'usingPreview');
|
|
}
|
|
}
|
|
}, updateDelay);
|
|
});
|
|
cm.addKeyMap({
|
|
'Ctrl-Space': 'autocomplete'
|
|
});
|
|
if (!options.noAutocomplete) {
|
|
cm.on('inputRead', function onChange(editor, input) {
|
|
if (
|
|
!prefs.autoComplete ||
|
|
input.origin !== '+input' ||
|
|
input.text[0] === ';' ||
|
|
input.text[0] === ' '
|
|
) {
|
|
return;
|
|
}
|
|
CodeMirror.commands.autocomplete(cm, null, { completeSingle: false });
|
|
});
|
|
}
|
|
return cm;
|
|
}
|
|
|
|
scope.cm.html = initEditor(htmlCode, {
|
|
mode: 'htmlmixed',
|
|
profile: 'xhtml',
|
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
|
noAutocomplete: true,
|
|
matchTags: { bothTags: true }
|
|
});
|
|
emmetCodeMirror(scope.cm.html);
|
|
scope.cm.css = initEditor(cssCode, {
|
|
mode: 'css',
|
|
gutters: ['error-gutter', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']
|
|
});
|
|
emmetCodeMirror(scope.cm.css);
|
|
Inlet(scope.cm.css);
|
|
scope.cm.js = initEditor(jsCode, {
|
|
mode: 'javascript',
|
|
gutters: ['error-gutter', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']
|
|
});
|
|
Inlet(scope.cm.js);
|
|
|
|
// Initialize codemirror in console
|
|
scope.consoleCm = CodeMirror(consoleLogEl, {
|
|
mode: 'javascript',
|
|
lineWrapping: true,
|
|
theme: 'monokai',
|
|
foldGutter: true,
|
|
readOnly: true,
|
|
gutters: ['CodeMirror-foldgutter']
|
|
});
|
|
|
|
// DEPRECATED
|
|
function openSettings() {
|
|
scope.toggleModal(settingsModal);
|
|
|
|
/* if (chrome.runtime.openOptionsPage) {
|
|
// New way to open options pages, if supported (Chrome 42+).
|
|
// Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=601997
|
|
// Until this bug fixes, use the
|
|
// fallback.
|
|
chrome.runtime.openOptionsPage();
|
|
} else {
|
|
// Fallback.
|
|
chrome.tabs.create({
|
|
url: 'chrome://extensions?options=' + chrome.i18n.getMessage('@@extension_id')
|
|
});
|
|
} */
|
|
}
|
|
|
|
scope.onModalSettingsLinkClick = function onModalSettingsLinkClick() {
|
|
openSettings();
|
|
trackEvent('ui', 'onboardSettingsBtnClick');
|
|
};
|
|
|
|
scope.onShowInTabClicked = function onShowInTabClicked() {
|
|
onboardDontShowInTabOptionBtn.classList.remove('selected');
|
|
onboardShowInTabOptionBtn.classList.add('selected');
|
|
trackEvent('ui', 'onboardShowInTabClick');
|
|
};
|
|
scope.onDontShowInTabClicked = function onDontShowInTabClicked() {
|
|
onboardDontShowInTabOptionBtn.classList.add('selected');
|
|
onboardShowInTabOptionBtn.classList.remove('selected');
|
|
trackEvent('ui', 'onboardDontShowInTabClick');
|
|
};
|
|
|
|
scope.exportItems = function exportItems(e) {
|
|
handleDownloadsPermission().then(() => {
|
|
fetchItems().then(function(items) {
|
|
var d = new Date();
|
|
var fileName = [
|
|
'web-maker-export',
|
|
d.getFullYear(),
|
|
d.getMonth() + 1,
|
|
d.getDate(),
|
|
d.getHours(),
|
|
d.getMinutes(),
|
|
d.getSeconds()
|
|
].join('-');
|
|
fileName += '.json';
|
|
var blob = new Blob([JSON.stringify(items, false, 2)], {
|
|
type: 'application/json;charset=UTF-8'
|
|
});
|
|
|
|
utils.downloadFile(fileName, blob);
|
|
|
|
trackEvent('ui', 'exportBtnClicked');
|
|
});
|
|
});
|
|
e.preventDefault();
|
|
};
|
|
|
|
function mergeImportedItems(items) {
|
|
var existingItemIds = [];
|
|
var toMergeItems = {};
|
|
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 {
|
|
utils.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) {
|
|
utils.log('shouldreplace', shouldReplace);
|
|
items.forEach(item => {
|
|
toMergeItems[item.id] = item;
|
|
});
|
|
mergedItemCount = items.length;
|
|
}
|
|
}
|
|
if (mergedItemCount) {
|
|
itemService.saveItems(toMergeItems).then(() => {
|
|
alertsService.add(
|
|
mergedItemCount + ' creations imported successfully.'
|
|
);
|
|
trackEvent('fn', 'itemsImported', mergedItemCount);
|
|
});
|
|
}
|
|
// FIXME: Move from here
|
|
toggleSavedItemsPane(false);
|
|
}
|
|
|
|
function onImportFileChange(e) {
|
|
var file = e.target.files[0];
|
|
// if (!f.type.match('image.*')) {
|
|
// continue;
|
|
// }
|
|
|
|
var reader = new FileReader();
|
|
reader.onload = function(progressEvent) {
|
|
var items;
|
|
try {
|
|
items = JSON.parse(progressEvent.target.result);
|
|
utils.log(items);
|
|
mergeImportedItems(items);
|
|
} catch (exception) {
|
|
utils.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');
|
|
}
|
|
|
|
scope.onImportBtnClicked = function exportItems(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', onImportFileChange);
|
|
input.click();
|
|
trackEvent('ui', 'importBtnClicked');
|
|
e.preventDefault();
|
|
};
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
scope.takeScreenshot = function(e) {
|
|
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 = frame.getBoundingClientRect();
|
|
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(() => {
|
|
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');
|
|
});
|
|
|
|
e.preventDefault();
|
|
};
|
|
|
|
// Populate the settings in the settings UI
|
|
function updateSettingsInUi() {
|
|
$('[data-setting=preserveLastCode]').checked = prefs.preserveLastCode;
|
|
$('[data-setting=replaceNewTab]').checked = prefs.replaceNewTab;
|
|
$('[data-setting=htmlMode]').value = prefs.htmlMode;
|
|
$('[data-setting=cssMode]').value = prefs.cssMode;
|
|
$('[data-setting=jsMode]').value = prefs.jsMode;
|
|
$('[data-setting=indentSize]').value = prefs.indentSize;
|
|
indentationSizeValueEl.textContent = prefs.indentSize;
|
|
$(
|
|
'[data-setting=indentWith][value=' + (prefs.indentWith || 'spaces') + ']'
|
|
).checked = true;
|
|
$('[data-setting=isCodeBlastOn]').checked = prefs.isCodeBlastOn;
|
|
$('[data-setting=editorTheme]').value = prefs.editorTheme;
|
|
$(
|
|
'[data-setting=keymap][value=' + (prefs.keymap || 'sublime') + ']'
|
|
).checked = true;
|
|
$('[data-setting=fontSize]').value = prefs.fontSize || 16;
|
|
$('[data-setting=refreshOnResize]').checked = prefs.refreshOnResize;
|
|
$('[data-setting=autoPreview]').checked = prefs.autoPreview;
|
|
$('[data-setting=editorFont]').value = prefs.editorFont;
|
|
$('[data-setting=editorCustomFont]').value = prefs.editorCustomFont;
|
|
$('[data-setting=autoSave]').checked = prefs.autoSave;
|
|
$('[data-setting=autoComplete]').checked = prefs.autoComplete;
|
|
$('[data-setting=preserveConsoleLogs]').checked = prefs.preserveConsoleLogs;
|
|
$('[data-setting=lightVersion]').checked = prefs.lightVersion;
|
|
$('[data-setting=lineWrap]').checked = prefs.lineWrap;
|
|
}
|
|
|
|
/**
|
|
* Handles all user triggered preference changes in the UI.
|
|
*/
|
|
scope.updateSetting = function updateSetting(e) {
|
|
// If this was triggered from user interaction, save the setting
|
|
if (e) {
|
|
var settingName = e.target.dataset.setting;
|
|
var obj = {};
|
|
var el = e.target;
|
|
utils.log(settingName, el.type === 'checkbox' ? el.checked : el.value);
|
|
prefs[settingName] = el.type === 'checkbox' ? el.checked : el.value;
|
|
obj[settingName] = prefs[settingName];
|
|
|
|
// In case of !extension, we save in localstorage so that it gets fetched
|
|
// faster on future loads.
|
|
db.sync.set(obj, function() {
|
|
alertsService.add('Setting saved');
|
|
});
|
|
if (!window.IS_EXTENSION) {
|
|
window.db.getDb().then(remoteDb => {
|
|
remoteDb
|
|
.collection('users')
|
|
.doc(window.user.uid)
|
|
.update({
|
|
[`settings.${settingName}`]: prefs[settingName]
|
|
})
|
|
.then(arg => {
|
|
utils.log(`Setting "${settingName}" for user`, arg);
|
|
})
|
|
.catch(error => utils.log(error));
|
|
});
|
|
}
|
|
trackEvent('ui', 'updatePref-' + settingName, prefs[settingName]);
|
|
}
|
|
|
|
// Show/hide RUN button based on autoPreview setting.
|
|
runBtn.classList[prefs.autoPreview ? 'add' : 'remove']('hide');
|
|
|
|
htmlCode.querySelector('.CodeMirror').style.fontSize = prefs.fontSize;
|
|
cssCode.querySelector('.CodeMirror').style.fontSize = prefs.fontSize;
|
|
jsCode.querySelector('.CodeMirror').style.fontSize = prefs.fontSize;
|
|
consoleEl.querySelector('.CodeMirror').style.fontSize = prefs.fontSize;
|
|
|
|
// Update indentation count when slider is updated
|
|
indentationSizeValueEl.textContent = $('[data-setting=indentSize]').value;
|
|
|
|
// Replace correct css file in LINK tags's href
|
|
editorThemeLinkTag.href = `lib/codemirror/theme/${prefs.editorTheme}.css`;
|
|
fontStyleTag.textContent = fontStyleTemplate.textContent.replace(
|
|
/fontname/g,
|
|
(prefs.editorFont === 'other'
|
|
? prefs.editorCustomFont
|
|
: prefs.editorFont) || 'FiraCode'
|
|
);
|
|
customEditorFontInput.classList[
|
|
prefs.editorFont === 'other' ? 'remove' : 'add'
|
|
]('hide');
|
|
|
|
['html', 'js', 'css'].forEach(type => {
|
|
scope.cm[type].setOption(
|
|
'indentWithTabs',
|
|
$('[data-setting=indentWith]:checked').value !== 'spaces'
|
|
);
|
|
scope.cm[type].setOption(
|
|
'blastCode',
|
|
$('[data-setting=isCodeBlastOn]').checked
|
|
? { effect: 2, shake: false }
|
|
: false
|
|
);
|
|
scope.cm[type].setOption(
|
|
'indentUnit',
|
|
+$('[data-setting=indentSize]').value
|
|
);
|
|
scope.cm[type].setOption(
|
|
'tabSize',
|
|
+$('[data-setting=indentSize]').value
|
|
);
|
|
scope.cm[type].setOption('theme', $('[data-setting=editorTheme]').value);
|
|
|
|
scope.cm[type].setOption(
|
|
'keyMap',
|
|
$('[data-setting=keymap]:checked').value
|
|
);
|
|
scope.cm[type].setOption(
|
|
'lineWrapping',
|
|
$('[data-setting=lineWrap]').checked
|
|
);
|
|
scope.cm[type].refresh();
|
|
});
|
|
scope.consoleCm.setOption('theme', $('[data-setting=editorTheme]').value);
|
|
scope.acssSettingsCm.setOption(
|
|
'theme',
|
|
$('[data-setting=editorTheme]').value
|
|
);
|
|
if (prefs.autoSave) {
|
|
if (!autoSaveInterval) {
|
|
autoSaveInterval = setInterval(autoSaveLoop, AUTO_SAVE_INTERVAL);
|
|
}
|
|
} else {
|
|
clearInterval(autoSaveInterval);
|
|
autoSaveInterval = null;
|
|
}
|
|
|
|
document.body.classList[prefs.lightVersion ? 'add' : 'remove'](
|
|
'light-version'
|
|
);
|
|
};
|
|
|
|
scope.onNewBtnClick = function() {
|
|
trackEvent('ui', 'newBtnClick');
|
|
if (unsavedEditCount) {
|
|
var shouldDiscard = confirm(
|
|
'You have unsaved changes. Do you still want to create something new?'
|
|
);
|
|
if (shouldDiscard) {
|
|
createNewItem();
|
|
}
|
|
} else {
|
|
createNewItem();
|
|
}
|
|
};
|
|
scope.onOpenBtnClick = function() {
|
|
trackEvent('ui', 'openBtnClick');
|
|
openSavedItemsPane();
|
|
};
|
|
scope.onSaveBtnClick = function() {
|
|
trackEvent('ui', 'saveBtnClick', currentItem.id ? 'saved' : 'new');
|
|
saveItem();
|
|
};
|
|
|
|
/**
|
|
* Toggles a modal and logs an event.
|
|
* @param {Node} modal modal to be toggled
|
|
*/
|
|
scope.toggleModal = function(modal) {
|
|
modal.classList.toggle('is-modal-visible');
|
|
document.body.classList[
|
|
modal.classList.contains('is-modal-visible') ? 'add' : 'remove'
|
|
]('overlay-visible');
|
|
};
|
|
scope.onSearchInputChange = function(e) {
|
|
const text = e.target.value;
|
|
let el;
|
|
for (const [itemId, item] of Object.entries(savedItems)) {
|
|
el = $(`#js-saved-items-pane [data-item-id=${itemId}]`);
|
|
if (item.title.toLowerCase().indexOf(text) === -1) {
|
|
el.classList.add('hide');
|
|
} else {
|
|
el.classList.remove('hide');
|
|
}
|
|
}
|
|
trackEvent('ui', 'searchInputType');
|
|
};
|
|
|
|
scope.toggleConsole = function() {
|
|
consoleEl.classList.toggle('is-minimized');
|
|
trackEvent('ui', 'consoleToggle');
|
|
};
|
|
// `clearConsole` is on window because it gets called from inside iframe also.
|
|
scope.clearConsole = window.clearConsole = function() {
|
|
scope.consoleCm.setValue('');
|
|
logCount = 0;
|
|
logCountEl.textContent = logCount;
|
|
};
|
|
scope.onClearConsoleBtnClick = function() {
|
|
scope.clearConsole();
|
|
trackEvent('ui', 'consoleClearBtnClick');
|
|
};
|
|
scope.evalConsoleExpr = function(e) {
|
|
// Clear console on CTRL + L
|
|
if ((e.which === 76 || e.which === 12) && e.ctrlKey) {
|
|
scope.clearConsole();
|
|
trackEvent('ui', 'consoleClearKeyboardShortcut');
|
|
} else if (e.which === 13) {
|
|
window.onMessageFromConsole('> ' + e.target.value);
|
|
|
|
/* eslint-disable no-underscore-dangle */
|
|
frame.contentWindow._wmEvaluate(e.target.value);
|
|
|
|
/* eslint-enable no-underscore-dangle */
|
|
|
|
e.target.value = '';
|
|
trackEvent('fn', 'evalConsoleExpr');
|
|
}
|
|
};
|
|
window.onMessageFromConsole = function() {
|
|
/* eslint-disable no-param-reassign */
|
|
[...arguments].forEach(function(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 {
|
|
scope.consoleCm.replaceRange(
|
|
arg +
|
|
' ' +
|
|
((arg + '').match(/\[object \w+]/) ? JSON.stringify(arg) : '') +
|
|
'\n',
|
|
{ line: Infinity }
|
|
);
|
|
} catch (e) {
|
|
scope.consoleCm.replaceRange('🌀\n', { line: Infinity });
|
|
}
|
|
scope.consoleCm.scrollTo(0, Infinity);
|
|
logCount++;
|
|
});
|
|
logCountEl.textContent = logCount;
|
|
|
|
/* eslint-enable no-param-reassign */
|
|
};
|
|
|
|
/**
|
|
* Compiles directives on the given node
|
|
* @param {Node} root The element on which compilation is required
|
|
*/
|
|
function compileNodes(root) {
|
|
if (!(root instanceof Node)) {
|
|
/* eslint-disable no-param-reassign */
|
|
root = document;
|
|
/* eslint-enable no-param-reassign */
|
|
}
|
|
// Create a querySelectorAll function bound to the passed `root` Node
|
|
const query = selector => [...root.querySelectorAll(selector)];
|
|
|
|
function attachListenerForEvent(eventName) {
|
|
const nodes = query(`[d-${eventName}]`);
|
|
nodes.forEach(function(el) {
|
|
el.addEventListener(eventName, function(e) {
|
|
scope[el.getAttribute(`d-${eventName}`)].call(window, e);
|
|
});
|
|
});
|
|
}
|
|
attachListenerForEvent('click');
|
|
attachListenerForEvent('change');
|
|
attachListenerForEvent('input');
|
|
attachListenerForEvent('keyup');
|
|
|
|
// Compile d-open-modal directive
|
|
const modalTriggers = query(`[d-open-modal]`);
|
|
modalTriggers.forEach(function(el) {
|
|
utils.onButtonClick(el, function() {
|
|
scope.toggleModal(window[el.getAttribute('d-open-modal')]);
|
|
trackEvent(
|
|
el.getAttribute('data-event-category'),
|
|
el.getAttribute('data-event-action')
|
|
);
|
|
});
|
|
});
|
|
|
|
// Compile d-html directive
|
|
const dHtmlNodes = query(`[d-html]`);
|
|
dHtmlNodes.forEach(function(el) {
|
|
fetch(el.getAttribute('d-html')).then(response => {
|
|
// Stop further compilation because of future recursion by removing attribute.
|
|
el.removeAttribute('d-html');
|
|
response.text().then(html => {
|
|
requestIdleCallback(() => {
|
|
el.innerHTML = html;
|
|
// Now compile this newly inserted HTML also.
|
|
compileNodes(el);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
scope.openDetachedPreview = function() {
|
|
trackEvent('ui', 'detachPreviewBtnClick');
|
|
|
|
if (scope.detachedWindow) {
|
|
scope.detachedWindow.focus();
|
|
return;
|
|
}
|
|
var iframeBounds = frame.getBoundingClientRect();
|
|
const iframeWidth = iframeBounds.width;
|
|
const iframeHeight = iframeBounds.height;
|
|
document.body.classList.add('is-detached-mode');
|
|
globalConsoleContainerEl.insertBefore(consoleEl, null);
|
|
|
|
scope.detachedWindow = window.open(
|
|
'./preview.html',
|
|
'Web Maker',
|
|
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
|
|
);
|
|
setTimeout(() => {
|
|
scope.detachedWindow.postMessage(frame.src, '*');
|
|
}, 1000);
|
|
function checkWindow() {
|
|
if (scope.detachedWindow && scope.detachedWindow.closed) {
|
|
clearInterval(intervalID);
|
|
document.body.classList.remove('is-detached-mode');
|
|
$('#js-demo-side').insertBefore(consoleEl, null);
|
|
scope.detachedWindow = null;
|
|
// Update main frame preview
|
|
scope.setPreviewContent(true);
|
|
}
|
|
}
|
|
var intervalID = window.setInterval(checkWindow, 500);
|
|
};
|
|
|
|
scope.openCssSettingsModal = function() {
|
|
scope.toggleModal(cssSettingsModal);
|
|
setTimeout(() => {
|
|
// Refresh is required because codemirror gets scaled inside modal and loses alignement.
|
|
scope.acssSettingsCm.refresh();
|
|
scope.acssSettingsCm.focus();
|
|
}, 500);
|
|
trackEvent('ui', 'cssSettingsBtnClick');
|
|
};
|
|
|
|
scope.onModalCloseBtnClick = function(e) {
|
|
closeAllOverlays();
|
|
e.preventDefault();
|
|
};
|
|
|
|
scope.updateProfileUi = () => {
|
|
if (window.user) {
|
|
document.body.classList.add('is-logged-in');
|
|
headerAvatarImg.src = window.user.photoURL;
|
|
} else {
|
|
document.body.classList.remove('is-logged-in');
|
|
headerAvatarImg.src = '';
|
|
}
|
|
};
|
|
|
|
scope.login = e => {
|
|
firebase.auth().signInAnonymously().then().catch(function(error) {
|
|
// Handle Errors here.
|
|
utils.log(error);
|
|
});
|
|
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
scope.login = window.login;
|
|
scope.logout = window.logout;
|
|
|
|
function init() {
|
|
var 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);
|
|
|
|
firebase.auth().onAuthStateChanged(function(user) {
|
|
scope.closeAllOverlays();
|
|
if (user) {
|
|
utils.log('You are -> ', user);
|
|
alertsService.add('You are now logged in!');
|
|
scope.user = window.user = user;
|
|
window.db.getUser(user.uid).then(customUser => {
|
|
if (customUser) {
|
|
Object.assign(prefs, user.settings);
|
|
updateSettingsInUi();
|
|
scope.updateSetting();
|
|
}
|
|
});
|
|
} else {
|
|
delete window.user;
|
|
// User is signed out.
|
|
}
|
|
scope.updateProfileUi();
|
|
});
|
|
|
|
var lastCode;
|
|
|
|
CodeMirror.modeURL = `lib/codemirror/mode/%N/%N.js`;
|
|
|
|
function getToggleLayoutButtonListener(mode) {
|
|
return function() {
|
|
saveSetting('layoutMode', mode);
|
|
trackEvent('ui', 'toggleLayoutClick', mode);
|
|
toggleLayout(mode);
|
|
return false;
|
|
};
|
|
}
|
|
layoutBtn1.addEventListener('click', getToggleLayoutButtonListener(1));
|
|
layoutBtn2.addEventListener('click', getToggleLayoutButtonListener(2));
|
|
layoutBtn3.addEventListener('click', getToggleLayoutButtonListener(3));
|
|
layoutBtn4.addEventListener('click', getToggleLayoutButtonListener(4));
|
|
|
|
notificationsBtn.addEventListener('click', function() {
|
|
scope.toggleModal(notificationsModal);
|
|
|
|
if (
|
|
notificationsModal.classList.contains('is-modal-visible') &&
|
|
!hasSeenNotifications
|
|
) {
|
|
hasSeenNotifications = true;
|
|
notificationsBtn.classList.remove('has-new');
|
|
window.db.setUserLastSeenVersion(version);
|
|
}
|
|
trackEvent('ui', 'notificationButtonClick', version);
|
|
return false;
|
|
});
|
|
|
|
codepenBtn.addEventListener('click', function(e) {
|
|
if (cssMode === CssModes.ACSS) {
|
|
alert("Oops! CodePen doesn't supports Atomic CSS currently.");
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
var json = {
|
|
title: 'A Web Maker experiment',
|
|
html: scope.cm.html.getValue(),
|
|
css: scope.cm.css.getValue(),
|
|
js: scope.cm.js.getValue(),
|
|
|
|
/* eslint-disable camelcase */
|
|
html_pre_processor: modes[htmlMode].codepenVal,
|
|
css_pre_processor: modes[cssMode].codepenVal,
|
|
js_pre_processor: modes[jsMode].codepenVal,
|
|
|
|
css_external: externalCssTextarea.value.split('\n').join(';'),
|
|
js_external: externalJsTextarea.value.split('\n').join(';')
|
|
|
|
/* eslint-enable camelcase */
|
|
};
|
|
if (!currentItem.title.match(/Untitled\s\d\d*-\d/)) {
|
|
json.title = currentItem.title;
|
|
}
|
|
json = JSON.stringify(json);
|
|
codepenForm.querySelector('input').value = json;
|
|
codepenForm.submit();
|
|
trackEvent('ui', 'openInCodepen');
|
|
e.preventDefault();
|
|
});
|
|
|
|
utils.onButtonClick(saveHtmlBtn, function() {
|
|
saveFile();
|
|
trackEvent('ui', 'saveHtmlClick');
|
|
});
|
|
|
|
utils.onButtonClick(savedItemsPaneCloseBtn, toggleSavedItemsPane);
|
|
utils.onButtonClick(savedItemsPane, function(e) {
|
|
// TODO: warn about unsaved changes in current item
|
|
if (e.target.classList.contains('js-saved-item-tile')) {
|
|
setTimeout(function() {
|
|
openItem(e.target.dataset.itemId);
|
|
}, 350);
|
|
toggleSavedItemsPane();
|
|
}
|
|
if (e.target.classList.contains('js-saved-item-tile__remove-btn')) {
|
|
removeItem(e.target.parentElement.parentElement.dataset.itemId);
|
|
} else if (e.target.classList.contains('js-saved-item-tile__fork-btn')) {
|
|
toggleSavedItemsPane();
|
|
setTimeout(function() {
|
|
forkItem(
|
|
savedItems[e.target.parentElement.parentElement.dataset.itemId]
|
|
);
|
|
}, 350);
|
|
}
|
|
});
|
|
|
|
titleInput.addEventListener('blur', function() {
|
|
if (currentItem.id) {
|
|
saveItem();
|
|
trackEvent('ui', 'titleChanged');
|
|
}
|
|
});
|
|
|
|
// Attach listeners on mode change menu items
|
|
$all('.js-mode-select').forEach(selectBox => {
|
|
selectBox.addEventListener('change', function(e) {
|
|
var mode = e.target.value;
|
|
var type = e.target.dataset.type;
|
|
var currentMode =
|
|
type === 'html' ? htmlMode : type === 'css' ? cssMode : jsMode;
|
|
if (currentMode !== mode) {
|
|
if (type === 'html') {
|
|
updateHtmlMode(mode).then(() => scope.setPreviewContent(true));
|
|
} else if (type === 'js') {
|
|
updateJsMode(mode).then(() => scope.setPreviewContent(true));
|
|
} else if (type === 'css') {
|
|
updateCssMode(mode).then(() => scope.setPreviewContent(true));
|
|
}
|
|
trackEvent('ui', 'updateCodeMode', mode);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Collapse btn event listeners
|
|
var collapseBtns = $all('.js-code-collapse-btn');
|
|
collapseBtns.forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
var codeWrapParent =
|
|
e.currentTarget.parentElement.parentElement.parentElement;
|
|
toggleCodeWrapCollapse(codeWrapParent);
|
|
trackEvent('ui', 'paneCollapseBtnClick', codeWrapParent.dataset.type);
|
|
return false;
|
|
});
|
|
});
|
|
|
|
// Update code wrap collapse states whenever any of them transitions due to any
|
|
// reason.
|
|
[htmlCode, cssCode, jsCode].forEach(function(el) {
|
|
el.addEventListener('transitionend', function() {
|
|
updateCodeWrapCollapseStates();
|
|
});
|
|
});
|
|
|
|
// Editor keyboard shortucuts
|
|
window.addEventListener('keydown', function(event) {
|
|
var selectedItemElement;
|
|
// TODO: refactor common listener code
|
|
// Ctrl/⌘ + S
|
|
if ((event.ctrlKey || event.metaKey) && event.keyCode === 83) {
|
|
event.preventDefault();
|
|
saveItem();
|
|
trackEvent('ui', 'saveItemKeyboardShortcut');
|
|
}
|
|
// Ctrl/⌘ + Shift + 5
|
|
if (
|
|
(event.ctrlKey || event.metaKey) &&
|
|
event.shiftKey &&
|
|
event.keyCode === 53
|
|
) {
|
|
event.preventDefault();
|
|
scope.setPreviewContent(true);
|
|
trackEvent('ui', 'previewKeyboardShortcut');
|
|
} else if ((event.ctrlKey || event.metaKey) && event.keyCode === 79) {
|
|
// Ctrl/⌘ + O
|
|
event.preventDefault();
|
|
openSavedItemsPane();
|
|
trackEvent('ui', 'openCreationKeyboardShortcut');
|
|
} else if (
|
|
(event.ctrlKey || event.metaKey) &&
|
|
event.shiftKey &&
|
|
event.keyCode === 191
|
|
) {
|
|
// Ctrl/⌘ + Shift + ?
|
|
event.preventDefault();
|
|
scope.toggleModal(keyboardShortcutsModal);
|
|
trackEvent('ui', 'showKeyboardShortcutsShortcut');
|
|
} else if (event.keyCode === 27) {
|
|
closeAllOverlays();
|
|
}
|
|
if (event.keyCode === 40 && isSavedItemsPaneOpen) {
|
|
// Return if no items present.
|
|
if (!$all('.js-saved-item-tile').length) {
|
|
return;
|
|
}
|
|
selectedItemElement = $('.js-saved-item-tile.selected');
|
|
if (selectedItemElement) {
|
|
selectedItemElement.classList.remove('selected');
|
|
selectedItemElement
|
|
.nextUntil('.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);
|
|
} else if (event.keyCode === 38 && isSavedItemsPaneOpen) {
|
|
if (!$all('.js-saved-item-tile').length) {
|
|
return;
|
|
}
|
|
selectedItemElement = $('.js-saved-item-tile.selected');
|
|
if (selectedItemElement) {
|
|
selectedItemElement.classList.remove('selected');
|
|
selectedItemElement
|
|
.previousUntil('.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);
|
|
} else if (event.keyCode === 13 && isSavedItemsPaneOpen) {
|
|
selectedItemElement = $('.js-saved-item-tile.selected');
|
|
if (!selectedItemElement) {
|
|
return;
|
|
}
|
|
setTimeout(function() {
|
|
openItem(selectedItemElement.dataset.itemId);
|
|
}, 350);
|
|
toggleSavedItemsPane();
|
|
}
|
|
|
|
// Fork shortcut inside saved creations panel with Ctrl/⌘ + F
|
|
if (
|
|
isSavedItemsPaneOpen &&
|
|
(event.ctrlKey || event.metaKey) &&
|
|
event.keyCode === 70
|
|
) {
|
|
event.preventDefault();
|
|
selectedItemElement = $('.js-saved-item-tile.selected');
|
|
setTimeout(function() {
|
|
forkItem(savedItems[selectedItemElement.dataset.itemId]);
|
|
}, 350);
|
|
toggleSavedItemsPane();
|
|
trackEvent('ui', 'forkKeyboardShortcut');
|
|
}
|
|
});
|
|
|
|
window.addEventListener('click', function(e) {
|
|
if (typeof e.target.className !== 'string') {
|
|
return;
|
|
}
|
|
if (e.target.className.indexOf('modal-overlay') !== -1) {
|
|
closeAllOverlays();
|
|
}
|
|
});
|
|
window.addEventListener('dblclick', function(e) {
|
|
var target = e.target;
|
|
if (target.classList.contains('js-console__header')) {
|
|
scope.toggleConsole();
|
|
trackEvent('ui', 'consoleToggleDblClick');
|
|
}
|
|
if (target.classList.contains('js-code-wrap__header')) {
|
|
var codeWrapParent = target.parentElement;
|
|
toggleCodeWrapCollapse(codeWrapParent);
|
|
trackEvent('ui', 'paneHeaderDblClick', codeWrapParent.dataset.type);
|
|
}
|
|
});
|
|
|
|
// Initialize add library select box
|
|
var libOptions = window.jsLibs.reduce(
|
|
(html, lib) =>
|
|
html +
|
|
`<option data-type="${lib.type}" value="${lib.url}">${lib.label}</option>`,
|
|
''
|
|
);
|
|
addLibrarySelect.children[1].innerHTML = libOptions;
|
|
libOptions = window.cssLibs.reduce(
|
|
(html, lib) =>
|
|
html +
|
|
`<option data-type="${lib.type}" value="${lib.url}">${lib.label}</option>`,
|
|
''
|
|
);
|
|
addLibrarySelect.children[2].innerHTML = libOptions;
|
|
addLibrarySelect.addEventListener('change', function onSelectChange(e) {
|
|
var target = e.target;
|
|
if (!target.value) {
|
|
return;
|
|
}
|
|
$('#js-external-' + target.selectedOptions[0].dataset.type).value +=
|
|
'\n' + target.value;
|
|
trackEvent('ui', 'addLibrarySelect', target.selectedOptions[0].label);
|
|
onExternalLibChange();
|
|
// Reset the select to the default value
|
|
target.value = '';
|
|
});
|
|
externalJsTextarea.addEventListener('blur', onExternalLibChange);
|
|
externalCssTextarea.addEventListener('blur', onExternalLibChange);
|
|
|
|
new TextareaAutoComplete(externalJsTextarea, {
|
|
filter: obj => obj.latest.match(/\.js$/)
|
|
});
|
|
new TextareaAutoComplete(externalCssTextarea, {
|
|
filter: obj => obj.latest.match(/\.css$/)
|
|
});
|
|
new TextareaAutoComplete(externalLibrarySearchInput, {
|
|
selectedCallback: value => {
|
|
const textarea = value.match(/\.js$/)
|
|
? externalJsTextarea
|
|
: externalCssTextarea;
|
|
textarea.value = `${textarea.value}\n${value}`;
|
|
externalLibrarySearchInput.value = '';
|
|
}
|
|
});
|
|
|
|
// Console header drag resize logic
|
|
var consoleHeaderDragStartY;
|
|
var consoleInitialHeight;
|
|
function onConsoleHeaderDrag(e) {
|
|
consoleEl.style.height =
|
|
consoleInitialHeight + consoleHeaderDragStartY - e.pageY + 'px';
|
|
}
|
|
$('.js-console__header').addEventListener('mousedown', e => {
|
|
consoleHeaderDragStartY = e.pageY;
|
|
consoleInitialHeight = consoleEl.getBoundingClientRect().height;
|
|
$('#demo-frame').classList.add('pointer-none');
|
|
window.addEventListener('mousemove', onConsoleHeaderDrag);
|
|
});
|
|
$('.js-console__header').addEventListener('mouseup', () => {
|
|
window.removeEventListener('mousemove', onConsoleHeaderDrag);
|
|
$('#demo-frame').classList.remove('pointer-none');
|
|
});
|
|
|
|
db.local.get(
|
|
{
|
|
layoutMode: 1,
|
|
code: ''
|
|
},
|
|
function localGetCallback(result) {
|
|
toggleLayout(result.layoutMode);
|
|
prefs.layoutMode = result.layoutMode;
|
|
if (result.code) {
|
|
lastCode = result.code;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get synced `preserveLastCode` setting to get back last code (or not).
|
|
db.getSettings(defaultSettings).then(result => {
|
|
if (result.preserveLastCode && lastCode) {
|
|
unsavedEditCount = 0;
|
|
if (lastCode.id) {
|
|
// Ignore for remote db
|
|
db.local.get(lastCode.id, function(itemResult) {
|
|
if (itemResult[lastCode.id]) {
|
|
utils.log('Load item ', lastCode.id);
|
|
currentItem = itemResult[lastCode.id];
|
|
refreshEditor();
|
|
}
|
|
});
|
|
} else {
|
|
utils.log('Load last unsaved item', lastCode);
|
|
currentItem = lastCode;
|
|
refreshEditor();
|
|
}
|
|
} else {
|
|
createNewItem();
|
|
}
|
|
Object.assign(prefs, result);
|
|
|
|
updateSettingsInUi();
|
|
scope.updateSetting();
|
|
});
|
|
|
|
// Check for new version notifications
|
|
db.getUserLastSeenVersion().then(lastSeenVersion => {
|
|
// Check if new user
|
|
if (!lastSeenVersion) {
|
|
onboardModal.classList.add('is-modal-visible');
|
|
if (document.cookie.indexOf('onboarded') === -1) {
|
|
trackEvent('ui', 'onboardModalSeen', version);
|
|
document.cookie = 'onboarded=1';
|
|
}
|
|
window.db.setUserLastSeenVersion(version);
|
|
// set some initial preferences on closing the onboard modal
|
|
// Old onboarding.
|
|
// utils.once(document, 'overlaysClosed', function() {});
|
|
}
|
|
if (
|
|
!lastSeenVersion ||
|
|
utils.semverCompare(lastSeenVersion, version) === -1
|
|
) {
|
|
notificationsBtn.classList.add('has-new');
|
|
hasSeenNotifications = false;
|
|
}
|
|
});
|
|
|
|
scope.acssSettingsCm = CodeMirror.fromTextArea(acssSettingsTextarea, {
|
|
mode: 'application/ld+json'
|
|
});
|
|
scope.acssSettingsCm.on('blur', () => {
|
|
scope.setPreviewContent(true);
|
|
});
|
|
|
|
var options = '';
|
|
[
|
|
'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'
|
|
].forEach(theme => {
|
|
options += '<option value="' + theme + '">' + theme + '</option>';
|
|
});
|
|
document.querySelector('[data-setting="editorTheme"]').innerHTML = options;
|
|
|
|
requestAnimationFrame(compileNodes);
|
|
}
|
|
|
|
// Set few stuff on a 'scope' object so that they can be referenced dynamically.
|
|
scope.closeAllOverlays = closeAllOverlays;
|
|
|
|
init();
|
|
})(window.alertsService, window.itemService);
|
|
|
|
// Dropdown.js
|
|
|
|
(function($all) {
|
|
var openDropdown;
|
|
|
|
// Closes all dropdowns except the passed one.
|
|
function closeOpenDropdown(except) {
|
|
if (openDropdown && (!except || except !== openDropdown)) {
|
|
openDropdown.classList.remove('open');
|
|
openDropdown = null;
|
|
}
|
|
}
|
|
function init() {
|
|
var dropdowns = $all('[dropdown]');
|
|
dropdowns.forEach(function(dropdown) {
|
|
dropdown.addEventListener('click', function(e) {
|
|
closeOpenDropdown(e.currentTarget);
|
|
e.currentTarget.classList.toggle('open');
|
|
openDropdown = e.currentTarget;
|
|
e.stopPropagation();
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function() {
|
|
closeOpenDropdown();
|
|
});
|
|
}
|
|
|
|
init();
|
|
})($all);
|