🌀 Eddy: Static Websites For The Rest of Us

6 min read Original article ↗
//- polyfills 'use strict'; (function (p) { if (!p.matches) p.matches = p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector; })(Element.prototype); if (typeof window.CustomEvent !== 'function') { window.CustomEvent = function (name, p) { p = p || {}; var e = document.createEvent('CustomEvent'); e.initCustomEvent(name, p.bubbles || false, p.cancelable || false, p.detail); return e; }; window.CustomEvent.prototype = window.Event.prototype; } //- Eddy The Editor by //github.com/Fedia // License: MIT (function (ui_html) { var api = window._ed || {}; if (!api.client_id) { console.error('client_id is missing'); return; } if (!/^\?v\d+/.test(location.search)) { refresh(); return; } document.body.insertAdjacentHTML('beforeend', ui_html); if (api.plugins) { api.plugins.forEach(load); } load('//apis.google.com/js/client:platform.js', init); function load(url, cb) { var s = document.createElement('script'); s.src = url; s.onload = typeof cb === 'function' ? cb : null; document.head.appendChild(s); } function init() { auth(function () { gapi.client.load('storage').then(editPage); document.querySelector('#ed-gauth').style.display = 'none'; }); } function auth(done) { gapi.load('auth2', function () { gapi.auth2.init({ client_id: api.client_id }).then(function (auth) { if (auth.isSignedIn.get()) { done(); } else { gapi.signin2.render('ed-gauth', { 'scope': 'https://www.googleapis.com/auth/devstorage.read_write', 'width': 220, 'height': 48, 'longtitle': true, 'theme': 'dark', 'onsuccess': done }); } }); }); } function refresh() { location.search = '?v' + Date.now(); } function getContainer(root) { if (!api.selector) { api.selector = '.ed-container'; } return (root || document).querySelector(api.selector); } function Eddy(el, opts) { this.el = el; var conf = this.conf = { schema: el.tagName + ' > p, p > br, a, b, i', paragraph: 'p' }; if (opts) for (var k in opts) conf[k] = opts[k]; var each = Function.prototype.call.bind(Array.prototype.forEach); var sanitize = this.applySchema.bind(this); this.onMutation(el, function (mutations) { if (el.isContentEditable) { each(mutations, function (m) { each(m.addedNodes, sanitize); }); el.dispatchEvent(new CustomEvent('change', { bubbles: true })); } }); el.addEventListener('focus', function () { setTimeout(function () { // walkaround for Chrome warnings var doc = el.ownerDocument; doc.execCommand('enableObjectResizing', false, false); doc.execCommand('defaultParagraphSeparator', false, conf.paragraph); }, 1); }); } Eddy.prototype.onMutation = function (el, cb) { var o = new MutationObserver(cb); o.observe(el, { subtree: true, childList: true, characterData: true }); return o; }; Eddy.prototype.applySchema = function (el) { if (el.nodeType === el.ELEMENT_NODE && el.parentNode) { var schema = this.conf.schema; var doc = el.ownerDocument; var iter = doc.createNodeIterator(el, NodeFilter.SHOW_ELEMENT, function (n) { return n.matches(schema) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }, false); var inv, parent, next; while (inv = iter.nextNode()) { parent = inv.parentNode; next = inv.nextSibling; while (inv.firstChild) { parent.insertBefore(inv.firstChild, next); } parent.removeChild(inv); } } }; function editPage() { var toolbar = document.querySelector('.ed-panel'); toolbar.style.display = ''; var editable = getContainer(); api.editor = new Eddy(editable); editable.addEventListener('change', function () { clearAttribute(editable, 'style'); clearAttribute(editable, 'class'); clearAttribute(editable, 'id'); if (editable.children.length === 0) { document.execCommand('formatBlock', false, 'p'); } }); editable.addEventListener('click', function (e) { var el = e.target; if (editable.isContentEditable && el.matches('a[href]')) { e.preventDefault(); var url = prompt('Link', el.getAttribute('href')); if (url !== null) { el.setAttribute('href', url); } } }); editable.dispatchEvent(new CustomEvent('beforeedit', { bubbles: true })); editable.contentEditable = true; editable.dispatchEvent(new CustomEvent('edit', { bubbles: true })); } function clearAttribute(root, attr) { var nodes = root.querySelectorAll('[' + attr + ']'); for (var i = 0; i < nodes.length; i++) { nodes[i].removeAttribute(attr); } } function getBucketName() { return location.hostname; } function getObjectName() { return location.pathname.replace(/^\/|\/$/g, '') || 'index.html'; } function getPageHTML(name, cb) { return gapi.client.storage.objects.get({ bucket: getBucketName(), object: name || getObjectName(), alt: 'media' }).then(function (res) { if (cb) cb(res.body); return res.body; }, function (e) { alert(e.result.error.message); }); } api.uploadFile = uploadFile; function uploadFile(name, data, headers) { return gapi.client.request({ path: '/upload/storage/v1/b/' + getBucketName() + '/o', method: 'POST', params: { name: name, uploadType: 'media' }, headers: headers || {}, body: data }); } api.deleteFile = deleteFile; function deleteFile(name) { return gapi.client.storage.objects['delete']({ bucket: getBucketName(), object: name }).then(function (res) { return res; }, function (e) { alert(e.result.error.message); }); } function parseHTML(html) { // return (new DOMParser()).parseFromString(html, 'text/html'); var dom = document.implementation.createHTMLDocument(); dom.documentElement.innerHTML = html; return dom; } function toHTML(dom) { var garbage = ' xmlns="http://www.w3.org/1999/xhtml"'; return new XMLSerializer().serializeToString(dom).replace(garbage, ''); } api.save = savePage; function savePage() { var saveAs = ''; while (!saveAs.length || /[^\w\d\.\-]/.test(saveAs)) { saveAs = window.prompt('URL', saveAs || getObjectName()); if (saveAs === null) return; } if (saveAs.substr(-5) !== '.html') { saveAs += '.html'; } var container = getContainer(); //.cloneNode(true); container.dispatchEvent(new CustomEvent('beforesave', { bubbles: true })); var template = api.template || null; return getPageHTML(template).then(function (html) { var doc = parseHTML(html); getContainer(doc).innerHTML = container.innerHTML; container.dispatchEvent(new CustomEvent('save', { bubbles: true, detail: { document: doc } })); return toHTML(doc); }).then(function (html) { return uploadFile(saveAs, html, { 'Content-Type': 'text/html' }); }).then(function () { location.href = '/' + saveAs; }); } if ('index.html' === getObjectName()) { //document.querySelector('.ed-btn-deletepage').style.display = 'none'; document.querySelector('.ed-actions .ed-btn:last-of-type').style.display = 'none'; } api['delete'] = deletePage; function deletePage() { var page = getObjectName(); if (page === 'index.html') { return; } if (confirm('Delete ' + page + '?')) { return deleteFile(page); } } document.addEventListener('save', function (e) { var doc = e.detail.document; var page_title = getContainer(doc).querySelector('h1,h2'); if (page_title) { var title = doc.querySelector('title'); title.textContent = page_title.textContent; } }); api.cmd_format = function (cmd) { document.execCommand(cmd, false, null); }; document.addEventListener('beforeedit', function () { api.editor.conf.schema += ', ' + api.selector + '> h1, ' + api.selector + '> h2'; }); var cmd_block_edgevalues = {}; api.cmd_block = function (tag) { var cmd = 'formatBlock'; var val = document.queryCommandValue(cmd); if (val === tag || val === cmd_block_edgevalues[tag]) { tag = 'p'; } document.execCommand(cmd, false, tag); cmd_block_edgevalues[tag] = document.queryCommandValue(cmd); }; api.cmd_clear = function () { document.execCommand('unlink', false, null); document.execCommand('removeFormat', false, null); }; api.cmd_link = function (val) { var url = prompt('Link', val || 'https://'); if (url) { document.execCommand('createLink', false, url); } }; document.addEventListener('click', function (e) { var cl = 'ed-menu__show'; var sel = '.' + cl; if (!e.target.matches(sel)) { var btn = document.querySelector(sel); if (btn) btn.classList.remove(cl); } }); api.menu = function (btn) { btn.classList.add('ed-menu__show'); }; })('<style>@import "//gistcdn.githack.com/Fedia/d48bde020c15c4e5f1cf497d2dc1fcca/raw/styles.css"</style>\n<div class="ed-panel" style="display:none">\n <span>🌀</span>\n <span class="ed-tools">\n <button class="ed-btn" onclick="_ed.cmd_block(\'h1\')"><b>H</b></button>\n <button class="ed-btn" onclick="_ed.cmd_block(\'h2\')"><b><small>H</small></b></button>\n <button class="ed-btn" onclick="_ed.cmd_format(\'bold\')"><b><small>b</small></b></button>\n <button class="ed-btn" onclick="_ed.cmd_format(\'italic\')"><i><small>i</small></i></button>\n <button class="ed-btn" onclick="_ed.cmd_clear()"><i><b>T</b></i><small>x</small></button>\n <button class="ed-btn" onclick="_ed.menu(this)">+</button>\n <div class="ed-menu">\n <button class="ed-btn" onclick="_ed.cmd_link()">🔗 Link</button>\n </div>\n </span>\n <span class="ed-actions">\n <button class="ed-btn" onclick="_ed.save()">💾</button>\n <button class="ed-btn" onclick="_ed.menu(this)">⠇</button>\n <div class="ed-menu">\n <button class="ed-btn ed-btn-deletepage" onclick="_ed.delete()">🗑️&nbsp;Delete</button>\n </div>\n </span>\n</div>\n<div id="ed-gauth"></div>'); //- Image upload plugin (function (api) { var btn_html = '<button class="ed-btn" onclick="_ed.cmd_img()">📷 Picture</button>'; var placeholder_img = 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAge3t3fX0ge3tofX0nPjxkZWZzPjxzeW1ib2wgaWQ9J2EnIHZpZXdCb3g9JzAgMCA5MCA2Nicgb3BhY2l0eT0nMC4zJz48cGF0aCBkPSdNODUgNXY1Nkg1VjVoODBtNS01SDB2NjZoOTBWMHonLz48Y2lyY2xlIGN4PScxOCcgY3k9JzIwJyByPSc2Jy8+PHBhdGggZD0nTTU2IDE0TDM3IDM5bC04LTYtMTcgMjNoNjd6Jy8+PC9zeW1ib2w+PC9kZWZzPjx1c2UgeGxpbms6aHJlZj0nI2EnIHdpZHRoPScyMCUnIHg9JzQwJScvPjwvc3ZnPg=='; document.addEventListener('beforeedit', function (e) { init(e.target); }); function init(editable) { document.querySelector('.ed-tools .ed-menu').insertAdjacentHTML('beforeend', btn_html); api.editor.conf.schema += ', p > img'; editable.addEventListener('click', function (e) { var el = e.target; if (editable.isContentEditable && el.matches('img')) { e.preventDefault(); img_upload(el); } }); } api.cmd_img = function () { var sel = window.getSelection(); if (sel.rangeCount) { var range = sel.getRangeAt(0); var img = new Image(); img.src = placeholder_img; range.insertNode(img); } }; function readFile(file, cb) { var fr = new FileReader(); fr.onloadend = function (e) { var uri = fr.result; cb(null, uri.substr(uri.indexOf(',') + 1), fr); }; fr.onerror = function (e) { cb(e, null); }; fr.readAsDataURL(file); } function img_upload(dest) { var input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.onchange = function () { var f = input.files[0]; if (!f || f.type.substr(0, 6) !== 'image/') return; var path = 'img/' + f.name; readFile(f, function (err, data, reader) { if (err) return alert(err); dest.src = reader.result; api.uploadFile(path, data, { 'Content-Type': f.type, 'Content-Encoding': 'base64' }).then(function () { dest.src = '/' + path + '?v' + Date.now(); }); }); }; input.click(); } })(_ed);