UNPKG

paperapp

Version:

deadsimple json-based SPA app-generator for gitlab (static pages)

640 lines (580 loc) 20.9 kB
window.paperapp = function(opts) { var $ = document.querySelector.bind(document) $.bot = /bot|googlebot|crawler|spider|robot|crawling/i.test(navigator.userAgent) $.extend = function(a, b) { for (var i in b) a[i] = b[i] return a } $.addClass = function(el, classname) { if( !el ) return $.delClass(el, classname) el.className += ' ' + classname } $.delClass = function(el, classname) { if( !el ) return el.className = el.className.replace(RegExp("[ ]?" + classname, "g"), '') } $.toSlug = function(str){ return String(str) .toLowerCase() .replace(/ /g,'-') } /* FUNCTIONAL CANDY */ $.curry = function(fn){ var args = Array.prototype.slice.call(arguments, 1); return function() { return fn.apply(this, args.concat(Array.prototype.slice.call(arguments, 0))); } } $.mapasync = function mapasync(arr, cb, done) { if( !arr || arr.length == 0 ) done() var f, funcs, i, k, v; funcs = []; i = 0; for (k in arr) { v = arr[k]; f = function(i, v) { return function() { var e, error; try { if (funcs[i + 1] != null) return cb(v, i, funcs[i + 1]); else return cb(v, i, done); } catch (error) { e = error; return done(new Error(e)); } } } funcs.push(f(i++, v)) } return funcs[0]() } $.equals = function(prop, val, obj){ return obj ? obj[prop] == val : false } $.window = window // Test via a getter in the options object to see if the passive property is accessed $.passive = undefined try { var opts = Object.defineProperty({}, 'passive', { get: function() { $.passive = { passive: true } } }); window.addEventListener("testPassive", null, opts); window.removeEventListener("testPassive", null, opts); } catch (e) {} $.require = function(url, type, next) { console.log("require("+url+")") type = type == 'js' ? 'js' : 'css' var tag = document.createElement(type == 'js' ? 'script' : 'link'); if (type == 'css') tag.rel = "stylesheet" tag[type == 'js' ? 'src' : 'href'] = url; tag.addEventListener('load', function() { next(tag); }, $.passive); tag.addEventListener('error', function() { throw tag console.log('require(' + url + ') failed') }, $.passive); document.body.appendChild(tag); } $.createSelect = function(name, opts) { var wrapper = $.Element({ class: "select", attr: { required: "required" } }) var select = $.Element({ format: '<select>', id: name }) wrapper.appendChild(select) select.appendChild($.Element({ format: '<option>', value: opts.default || '', attr: { disabled: opts.disabled ? "disabled" : undefined, selected: "selected" } })) opts.enum.map(function(v) { select.appendChild($.Element({ format: '<option>', value: v, attr: { value: v } })) }) return wrapper } $.createFormElement = function(name, opts) { elementTag = opts.format == 'textarea' ? '<textarea>' : '<input>' if (opts.enum) return $.createSelect(name, opts) return $.Element({ title: opts.title || name, format: elementTag, attr: { type: opts.format == 'textarea' ? undefined : opts.format ? opts.format : 'text', id: name, value: opts.value || null, name: opts.name || null, class: "input", required: 'required', placeholder: opts.default || 'your ' + name } }) } $.createForm = function(opts) { var form = $.Element({ class: "row form center" }) for (var i in opts.properties) { var opt = opts.properties[i] form.appendChild($.Element({ format: '<label>', attr: { id: 'form-' + i }, class: opt.value ? ' ' : 'hide', value: opt.title || i })) var input = $.createFormElement(i, opt) input.oninput = function(label, opt, e) { $(label).className = $(label).className.replace(/hide/, '') + (String(this.value).length || this.selectedIndex ? '' : ' hide') } .bind(input, '#form-' + i, opt) form.appendChild(input) } if ($.get(opts, 'options.savebutton')) form.appendChild($.Element({ format: "<button>", class: "bright-bg", value: opts.options.savebutton })) return form } $.hrefToHash = function(href){ var htmlfile = String(document.location.pathname).replace(/.*\//,'').replace('.html','') var extUrl = href.match(/^http/) if( document.location.search || htmlfile || extUrl ) return href var obj = {} var query = href[0] == '?' ? href.substr(1).split("&") : false if( !query ) return '#{"c":"'+ href.replace(/\.html$/, '') + '"}' query.map( function(q){ var parts = q.split("=") obj[ parts[0] ] = parts[1] }) return '#'+JSON.stringify(obj) } $.Element = function(opts) { opts = opts || {} if (opts.type == 'object') return $.createForm(opts) var format = $.get(opts, 'format') || 'div' var tag = format ? format.replace(/[<>]/gi, '') : 'div' if (window.paperapp.elements[format] && !opts.custom) return window.paperapp.elements[format](opts) var el = document.createElement(tag) if (opts.class) el.className = $.renderString(opts.class, $) opts.attr = opts.attr || {} opts.style = opts.style || {} for (var i in opts.attr ) opts.attr[i] = $.renderString(opts.attr[i],$) for (var i in opts.style ) opts.style[i] = $.renderString(opts.style[i],$) // SEO optimization if( opts.attr.href ) opts.attr.href = $.hrefToHash( opts.attr.href ) if (opts.style) $.extend(el.style, opts.style) if (opts.attr) for (var i in opts.attr) el.setAttribute(i, $.renderString(opts.attr[i], $)) if (opts.value){ var isUrl = opts.value.match(/\$\{.*[\/].*\}$/) if( !isUrl ) el.innerHTML += $.renderString(opts.value, $) else{ var url = opts.value.replace(/(\$\{|\})/g,'') $.request('get',url,function(el,res){ el.innerHTML = res $.pub('/request/done', url ) }.bind(null,el) ) } } opts.el = el return el } $.all = function(selector) { return [].slice.call(document.querySelectorAll(selector)) } $.getDefaultOpts = function() { var args = decodeURIComponent(String(document.location.hash).substr(1)) args = args[0] == '{' ? new Function("return " + decodeURIComponent(args))() : {} var opts = $.extend({ json: 'https://gist.githubusercontent.com/coderofsalvation/9c801b0d43c6dc67dcfb01bf70c966c8/raw/app.json?'+(new Date().getTime()), css: 'https://2wa.gitlab.io/paperapp.js.example/app.css', js: 'https://2wa.gitlab.io/paperapp.js.example/app.js', msg_loading: "", boot: false }, args) return opts } $.recordTime = function recordTime(key){ $.duration[key] = new Date().getTime() - $.duration.pageload } $.loading = function(msg){ if( msg ){ $('#loader > #msg').innerHTML = msg if( !document.body.className.match(/loading/) ) $.addClass(document.body, 'loading') }else $.delClass( document.body, "loading") } $.init = function(opts) { if( $.bot || document.location.length > 1 ) return $.duration = paperapp.duration var done = function(){ $.pub('hashchange') // triggers 'initRouter' $.recordTime('/init/done') $.pub("/init/done") } opts = $.extend($.getDefaultOpts(),opts||{}) $('#loader > #msg').innerHTML = opts.msg_loading $.pub("/init/opts",opts) function init(){ $.request('get', opts.json, function(res) { $.require(opts.css, 'css', function() { $ = $.extend($, JSON.parse(res)) $.opts = opts $.initRouter() $.recordTime('/init') $.pub("/init") $.components = $.components || [] if( opts.debug ) $.components.push('https://cdn.rawgit.com/coderofsalvation/fdc7c8c736d9fdd7a153d18a7b82512e/raw/debug.js') $.loadComponents(opts,done) }) }) } if( opts.js ) $.require( opts.js,'js', init) else init() } $.loadComponents = function(opts,done){ $.mapasync( ($.components||[]), function(c,k,next){ $.require( $.renderString(c,$) , c.match(/css/) ? 'css' :'js', next.bind(this)) }, function(err){ if(err) console.error(err) $('.wrapper').innerHTML = '' // erase leftovers $.cards.map(function(c) { $.addCard(c) }) if( $.wallpaper && window.innerWidth > 999 ){ $('#wallpaper').style.backgroundImage = 'url('+$.wallpaper+')' } $('#menu select').onchange = $.pub.bind($, "/menu/change") $.pub('/init/loadcomponents') $.recordTime('/init/loadcomponents') done() }) } $.addCard = function(opts) { opts = $.extend(opts, { style: { display: "none" } }) var card = $.Element(opts) card.className = "row card "+$.toSlug(opts.title) if (opts.items){ var add = function(i) { card.appendChild($.Element($.extend(i, { parent: card }))) } if( $.get($,'header.items') && !$.opts.noheader ) $.header.items.map(add) opts.items.map( add ) if( $.get($,'footer.items') && !$.opts.nofooter ) $.footer.items.map(add) } var parent = opts.parent || $('.wrapper') parent.appendChild(card) if( opts.menu === false ) return $('#menu select').appendChild($.Element({ format: "<option>", value: opts.title, attr: { value: opts.title } })) } $.get = function(xs, x, fallback ) { return String(x).split('.').reduce(function(acc, x) { if (acc == null || acc == undefined ) return fallback; return new Function("x","acc","return acc" + (x[0] == '[' ? x : '.'+x) )(x, acc) || fallback }, xs) } $.assign = function(obj, path, value) { var last var o = obj path = String(path) var vars = path.split(".") var lastVar = vars[vars.length - 1] vars.map(function(v) { if (lastVar == v) return o = (new Function("o","return o." + v)(o) || new Function("o","return o."+v+" = {}")(o)) last = v }) new Function("o","v","o." + lastVar + " = v")(o, value) $.pub(path, value) return $ } $.fullscreen = function toggleFullScreen(toggle) { if ((document.fullScreenElement && document.fullScreenElement !== null) || (!document.mozFullScreen && !document.webkitIsFullScreen)) { if (document.documentElement.requestFullScreen) { document.documentElement.requestFullScreen(); } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullScreen) { document.documentElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); } } else { if (document.cancelFullScreen) { document.cancelFullScreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } } } $.set = $.assign.bind($, $) // tiny pubsub var e = $.e = {}; $.pub = function(window, name, data){ //window.setTimeout( function(){ console.log("$.pub('"+name+"',...)") },5) (e[name] = e[name] || new window.Event(name)).data = data; window.dispatchEvent(e[name]); }.bind(window, window) $.trigger = $.pub // alias $.sub = $.on = function(name, handler, context) { if( typeof handler == 'object'){ handler.map( $.curry( $.sub, name ) ) return } if( window.navigator ) console.log("$.sub('" + name + "')") ehandler = function(e) { handler(e.data) } window.addEventListener(name, ehandler.bind(context), $.passive); } $.unsub = $.off = function(name, handler, context) { removeEventListener(name, handler.bind(context)); } $.initDone = function initDone() { $('#loader').style.display = 'none' $('#fullscreen').addEventListener('click', $.fullscreen) if( opts.nomenu ) $('#menu').style.display = 'none' if( opts.nofullscreen ) $('#fullscreen').style.display = 'none' $.pub('/route/change', $.store.route ) // slide in menu var body = $('body') var variations = ['','','','','','a','b','c','d','f','g','h'] $.addClass(body, variations[ Math.floor(Math.random()*variations.length) ]) $.delClass(body, 'loading') $.addClass(body, 'loaded') $.addClass($('.wrapper'), 'loaded') delete $.opts.card } $.renderVar = function renderString(es6_template) { return es6_template[0] == '$' ? $.get($, es6_template.replace(/[\$\{\}]/g, '') ) : es6_template } $.renderString = function renderString(es6_template, data) { if (typeof es6_template != 'string' || !data) return es6_template if (typeof data == 'function') { var _data = {$:$} for (var i in data) if (typeof data[i] != 'function') _data[i] = data[i] data = _data } var reg = /\$\{(.*?)\}/gm; var res; while (res = reg.exec(es6_template)) { es6_template = es6_template.replace(res[0], $.get(data, res[1]) || $.get({ window: window, $:$ }, res[1]) || "") reg.lastIndex = 0 // reset so duplicate template-vars will be processed } return es6_template } $.initQuery = function(){ if( !document.location.search ) return document.location.search .substr(1) .split("&") .map( function(q){ var parts = q.split("=") $.assign($, 'store.route.'+parts[0], parts[1]) }) } $.initRouter = function(){ $.assign($, 'store.route', {}) $.initQuery() var updateRoute = function(){ console.log("/route/change "+JSON.stringify($.store.route) ) if( document.location.hash.match(/^#\{/) ) $.store.route = $.extend( $.store.route, JSON.parse( decodeURIComponent(document.location.hash.substr(1)) ) ) $.pub('/route/change', $.store.route ) } updateRoute() window.addEventListener('hashchange', updateRoute ) } // request(method, url, payload, callback) $._request = function(m, u, c, x, d) { with (x = new XMLHttpRequest) return onreadystatechange = function() { readyState ^ 4 || c(this) } , open(m, u, c), send(d), x } $.request = function(m, u, c, x, d) { var key = m + ' ' + String(u).replace(/\?[0-9]+/,'') if (!$.get(window, 'navigator.onLine')) return c(window.localStorage.getItem(key)) $._request(m, u, function(res) { var res = res.responseText || '' window.localStorage.setItem(key, res) c(res) }) } $.hideCard = function(c){ if( !c.el ) return $.delClass(c.el, 'slidein') c.el.style.display = 'none' $.delClass(document.body, $.toSlug(c.title)) } $.showCard = function(title, nohash){ var c = $.cards.find( $.equals.bind(null, 'title', title) ) if( !c.el ) return if( c.el.style.display == '' ) return // already shown if( !c ) return $.cards.map( $.hideCard ) $.addClass(document.body, $.toSlug(c.title)) $.addClass(c.el, 'slidein') $.loading(false) c.el.style.display = '' c.el.style.visibility = 'visible' //if( !nohash && !c.homepage ) document.location.hash = '#'+JSON.stringify({c:c.title}) } $.sub('/init/done', setTimeout.bind(window, $.initDone, 1000)) $.sub('/menu/change', function(e) { var i = 0 var selected = false $.cards.map ( function(c){ $.hideCard(c) if( selected ) return var htmlfile = String(document.location.pathname).replace(/.*\//,'').replace('.html','') var menuValue = e.target.options[e.target.selectedIndex] ? e.target.options[e.target.selectedIndex].value : -1 var boot = $.opts.boot var query = $.get($,'store.route.c') // during first page-render, show card which : if( (!boot && c.title == query) || // - matches (card) 'c'-property of document.location.search ($.store.route.c) (!boot && !query && c.title == $.get($,'opts.c')) || // - matches (card) 'c'-property of hash ($.opts.card) (!boot && !query && htmlfile == $.toSlug(c.title) ) || // - matches current url-file (products.html e.g.) (!boot && !query && !htmlfile && c.homepage ) || // - matches 'homepage' set to true (boot && c.title == menuValue ) // - matches current selected option of menu when already booted (ignore document.location.search && hash) ) { $.opts.boot = selected = c.title e.target.selectedIndex = i $.showCard(c.title) } i+=1 }) }) if (opts && opts.cards) $.init(opts) return $ } window.paperapp.elements = { "img": function(opts) { var el = $.Element( $.extend(opts,{ custom:true, format:'div', class: 'img', style: $.extend({ 'backgroundImage': 'url(' + opts.src + ')', }, opts.style) })) return el }, "iframe": function(opts) { var el = $.Element( $.extend(opts,{ custom:true, format:"iframe", attr: $.extend({ src: opts.src, frameborder:'0', hspace:'0', vspace:'0', marginheight:'0', marginwidth:'0', allowtransparency:"true" }, opts.attr||{} ), style: $.extend({ background: $.get(opts,'opts.style.background','none transparent'), width: '100%', height: '33vh', overflow: 'hidden' }, opts.style||{} ) })) return el } } window.paperapp.duration = {pageload:new Date().getTime()} JSON._parse = JSON.parse JSON.parse = function parse(txt, reviver, context) { context = context || 20 try { return JSON._parse(txt, reviver) } catch (e) { if (typeof txt !== 'string') { const isEmptyArray = Array.isArray(txt) && txt.length === 0 const errorMessage = 'Cannot parse ' + (isEmptyArray ? 'an empty array' : String(txt)) throw new TypeError(errorMessage) } const syntaxErr = e.message.match(/^Unexpected token.*position\s+(\d+)/i) const errIdx = syntaxErr ? +syntaxErr[1] : e.message.match(/^Unexpected end of JSON.*/i) ? txt.length - 1 : null if (errIdx != null) { const start = errIdx <= context ? 0 : errIdx - context const end = errIdx + context >= txt.length ? txt.length : errIdx + context e.message += ` while parsing near '${ start === 0 ? '' : '...' }${txt.slice(start, end)}${ end === txt.length ? '' : '...' }'` } else { e.message += ` while parsing '${txt.slice(0, context * 2)}'` } throw e } }