paperapp
Version:
deadsimple json-based SPA app-generator for gitlab (static pages)
640 lines (580 loc) • 20.9 kB
JavaScript
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
}
}