/* Riot v2.0.14, @license MIT, (c) 2015 Muut Inc. + contributors */
;(function(window) {
// 'use strict' does not allow us to override the events properties https://github.com/muut/riotjs/blob/dev/lib/tag/update.js#L7-L10
// it leads to the following error on firefox "setting a property that has only a getter"
//'use strict'
var riot = { version: 'v2.0.14', settings: {} }
riot.observable = function(el) {
el = el || {}
var callbacks = {},
_id = 0
el.on = function(events, fn) {
if (typeof fn == 'function') {
fn._id = typeof fn._id == 'undefined' ? _id++ : fn._id
events.replace(/\S+/g, function(name, pos) {
(callbacks[name] = callbacks[name] || []).push(fn)
fn.typed = pos > 0
})
}
return el
}
el.off = function(events, fn) {
if (events == '*') callbacks = {}
else {
events.replace(/\S+/g, function(name) {
if (fn) {
var arr = callbacks[name]
for (var i = 0, cb; (cb = arr && arr[i]); ++i) {
if (cb._id == fn._id) { arr.splice(i, 1); i-- }
}
} else {
callbacks[name] = []
}
})
}
return el
}
// only single event supported
el.one = function(name, fn) {
function on() {
el.off(name, on)
fn.apply(el, arguments)
}
return el.on(name, on)
}
el.trigger = function(name) {
var args = [].slice.call(arguments, 1),
fns = callbacks[name] || []
for (var i = 0, fn; (fn = fns[i]); ++i) {
if (!fn.busy) {
fn.busy = 1
fn.apply(el, fn.typed ? [name].concat(args) : args)
if (fns[i] !== fn) { i-- }
fn.busy = 0
}
}
if (callbacks.all && name != 'all') {
el.trigger.apply(el, ['all', name].concat(args))
}
return el
}
return el
}
;(function(riot, evt, window) {
// browsers only
if (!window) return
var loc = window.location,
fns = riot.observable(),
win = window,
current
function hash() {
return loc.href.split('#')[1] || ''
}
function parser(path) {
return path.split('/')
}
function emit(path) {
if (path.type) path = hash()
if (path != current) {
fns.trigger.apply(null, ['H'].concat(parser(path)))
current = path
}
}
var r = riot.route = function(arg) {
// string
if (arg[0]) {
loc.hash = arg
emit(arg)
// function
} else {
fns.on('H', arg)
}
}
r.exec = function(fn) {
fn.apply(null, parser(hash()))
}
r.parser = function(fn) {
parser = fn
}
win.addEventListener ? win.addEventListener(evt, emit, false) : win.attachEvent('on' + evt, emit)
})(riot, 'hashchange', window)
/*
//// How it works?
Three ways:
1. Expressions: tmpl('{ value }', data).
Returns the result of evaluated expression as a raw object.
2. Templates: tmpl('Hi { name } { surname }', data).
Returns a string with evaluated expressions.
3. Filters: tmpl('{ show: !done, highlight: active }', data).
Returns a space separated list of trueish keys (mainly
used for setting html classes), e.g. "show highlight".
// Template examples
tmpl('{ title || "Untitled" }', data)
tmpl('Results are { results ? "ready" : "loading" }', data)
tmpl('Today is { new Date() }', data)
tmpl('{ message.length > 140 && "Message is too long" }', data)
tmpl('This item got { Math.round(rating) } stars', data)
tmpl('
{ title }
{ body }', data)
// Falsy expressions in templates
In templates (as opposed to single expressions) all falsy values
except zero (undefined/null/false) will default to empty string:
tmpl('{ undefined } - { false } - { null } - { 0 }', {})
// will return: " - - - 0"
*/
var brackets = (function(orig, s, b) {
return function(x) {
// make sure we use the current setting
s = riot.settings.brackets || orig
if (b != s) b = s.split(' ')
// if regexp given, rewrite it with current brackets (only if differ from default)
return x && x.test
? s == orig
? x : RegExp(x.source
.replace(/\{/g, b[0].replace(/(?=.)/g, '\\'))
.replace(/\}/g, b[1].replace(/(?=.)/g, '\\')),
x.global ? 'g' : '')
// else, get specific bracket
: b[x]
}
})('{ }')
var tmpl = (function() {
var cache = {},
re_vars = /(['"\/]).*?[^\\]\1|\.\w*|\w*:|\b(?:(?:new|typeof|in|instanceof) |(?:this|true|false|null|undefined)\b|function *\()|([a-z_$]\w*)/gi
// [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ]
// find variable names:
// 1. skip quoted strings and regexps: "a b", 'a b', 'a \'b\'', /a b/
// 2. skip object properties: .name
// 3. skip object literals: name:
// 4. skip javascript keywords
// 5. match var name
// build a template (or get it from cache), render with data
return function(str, data) {
return str && (cache[str] = cache[str] || tmpl(str))(data)
}
// create a template instance
function tmpl(s, p) {
// default template string to {}
s = (s || (brackets(0) + brackets(1)))
// temporarily convert \{ and \} to a non-character
.replace(brackets(/\\{/g), '\uFFF0')
.replace(brackets(/\\}/g), '\uFFF1')
// split string to expression and non-expresion parts
p = split(s, extract(s, brackets(/{/), brackets(/}/)))
return new Function('d', 'return ' + (
// is it a single expression or a template? i.e. {x} or {x}
!p[0] && !p[2] && !p[3]
// if expression, evaluate it
? expr(p[1])
// if template, evaluate all expressions in it
: '[' + p.map(function(s, i) {
// is it an expression or a string (every second part is an expression)
return i % 2
// evaluate the expressions
? expr(s, true)
// process string parts of the template:
: '"' + s
// preserve new lines
.replace(/\n/g, '\\n')
// escape quotes
.replace(/"/g, '\\"')
+ '"'
}).join(',') + '].join("")'
)
// bring escaped { and } back
.replace(/\uFFF0/g, brackets(0))
.replace(/\uFFF1/g, brackets(1))
+ ';')
}
// parse { ... } expression
function expr(s, n) {
s = s
// convert new lines to spaces
.replace(/\n/g, ' ')
// trim whitespace, brackets, strip comments
.replace(brackets(/^[{ ]+|[ }]+$|\/\*.+?\*\//g), '')
// is it an object literal? i.e. { key : value }
return /^\s*[\w- "']+ *:/.test(s)
// if object literal, return trueish keys
// e.g.: { show: isOpen(), done: item.done } -> "show done"
? '[' +
// extract key:val pairs, ignoring any nested objects
extract(s,
// name part: name:, "name":, 'name':, name :
/["' ]*[\w- ]+["' ]*:/,
// expression part: everything upto a comma followed by a name (see above) or end of line
/,(?=["' ]*[\w- ]+["' ]*:)|}|$/
).map(function(pair) {
// get key, val parts
return pair.replace(/^[ "']*(.+?)[ "']*: *(.+?),? *$/, function(_, k, v) {
// wrap all conditional parts to ignore errors
return v.replace(/[^&|=!><]+/g, wrap) + '?"' + k + '":"",'
})
}).join('')
+ '].join(" ").trim()'
// if js expression, evaluate as javascript
: wrap(s, n)
}
// execute js w/o breaking on errors or undefined vars
function wrap(s, nonull) {
s = s.trim()
return !s ? '' : '(function(v){try{v='
// prefix vars (name => data.name)
+ (s.replace(re_vars, function(s, _, v) { return v ? '(d.'+v+'===undefined?'+(typeof window == 'undefined' ? 'global.' : 'window.')+v+':d.'+v+')' : s })
// break the expression if its empty (resulting in undefined value)
|| 'x')
+ '}finally{return '
// default to empty string for falsy values except zero
+ (nonull === true ? '!v&&v!==0?"":v' : 'v')
+ '}}).call(d)'
}
// split string by an array of substrings
function split(str, substrings) {
var parts = []
substrings.map(function(sub, i) {
// push matched expression and part before it
i = str.indexOf(sub)
parts.push(str.slice(0, i), sub)
str = str.slice(i + sub.length)
})
// push the remaining part
return parts.concat(str)
}
// match strings between opening and closing regexp, skipping any inner/nested matches
function extract(str, open, close) {
var start,
level = 0,
matches = [],
re = new RegExp('('+open.source+')|('+close.source+')', 'g')
str.replace(re, function(_, open, close, pos) {
// if outer inner bracket, mark position
if(!level && open) start = pos
// in(de)crease bracket level
level += open ? 1 : -1
// if outer closing bracket, grab the match
if(!level && close != null) matches.push(str.slice(start, pos+close.length))
})
return matches
}
})()
// { key, i in items} -> { key, i, items }
function loopKeys(expr) {
var ret = { val: expr },
els = expr.split(/\s+in\s+/)
if (els[1]) {
ret.val = brackets(0) + els[1]
els = els[0].slice(brackets(0).length).trim().split(/,\s*/)
ret.key = els[0]
ret.pos = els[1]
}
return ret
}
function mkitem(expr, key, val) {
var item = {}
item[expr.key] = key
if (expr.pos) item[expr.pos] = val
return item
}
/* Beware: heavy stuff */
function _each(dom, parent, expr) {
remAttr(dom, 'each')
var template = dom.outerHTML,
prev = dom.previousSibling,
root = dom.parentNode,
rendered = [],
tags = [],
checksum
expr = loopKeys(expr)
function add(pos, item, tag) {
rendered.splice(pos, 0, item)
tags.splice(pos, 0, tag)
}
// clean template code
parent.one('update', function() {
root.removeChild(dom)
}).one('premount', function() {
if (root.stub) root = parent.root
}).on('update', function() {
var items = tmpl(expr.val, parent)
if (!items) return
// object loop. any changes cause full redraw
if (!Array.isArray(items)) {
var testsum = JSON.stringify(items)
if (testsum == checksum) return
checksum = testsum
// clear old items
each(tags, function(tag) { tag.unmount() })
rendered = []
tags = []
items = Object.keys(items).map(function(key) {
return mkitem(expr, key, items[key])
})
}
// unmount redundant
each(rendered, function(item) {
if (item instanceof Object) {
// skip existing items
if (items.indexOf(item) > -1) {
return
}
} else {
// find all non-objects
var newItems = arrFindEquals(items, item),
oldItems = arrFindEquals(rendered, item)
// if more or equal amount, no need to remove
if (newItems.length >= oldItems.length) {
return
}
}
var pos = rendered.indexOf(item),
tag = tags[pos]
if (tag) {
tag.unmount()
rendered.splice(pos, 1)
tags.splice(pos, 1)
// to let "each" know that this item is removed
return false
}
})
// mount new / reorder
var prev_base = [].indexOf.call(root.childNodes, prev) + 1
each(items, function(item, i) {
// start index search from position based on the current i
var pos = items.indexOf(item, i),
oldPos = rendered.indexOf(item, i)
// if not found, search backwards from current i position
pos < 0 && (pos = items.lastIndexOf(item, i))
oldPos < 0 && (oldPos = rendered.lastIndexOf(item, i))
if (!(item instanceof Object)) {
// find all non-objects
var newItems = arrFindEquals(items, item),
oldItems = arrFindEquals(rendered, item)
// if more, should mount one new
if (newItems.length > oldItems.length) {
oldPos = -1
}
}
// mount new
var nodes = root.childNodes
if (oldPos < 0) {
if (!checksum && expr.key) var _item = mkitem(expr, item, pos)
var tag = new Tag({ tmpl: template }, {
before: nodes[prev_base + pos],
parent: parent,
root: root,
item: _item || item
})
tag.mount()
add(pos, item, tag)
return true
}
// change pos value
if (expr.pos && tags[oldPos][expr.pos] != pos) {
tags[oldPos].one('update', function(item) {
item[expr.pos] = pos
})
tags[oldPos].update()
}
// reorder
if (pos != oldPos) {
root.insertBefore(nodes[prev_base + oldPos], nodes[prev_base + (pos > oldPos ? pos + 1 : pos)])
return add(pos, rendered.splice(oldPos, 1)[0], tags.splice(oldPos, 1)[0])
}
})
rendered = items.slice()
})
}
function parseNamedElements(root, parent, child_tags) {
walk(root, function(dom) {
if (dom.nodeType == 1) {
// custom child tag
var child = getTag(dom)
if (child && !dom.getAttribute('each')) {
var tag = new Tag(child, { root: dom, parent: parent }),
tagName = dom.getAttribute('name') || child.name,
cachedTag = parent.tags[tagName]
// if there are multiple children tags having the same name
if (cachedTag) {
// if the parent tags property is not yet an array
// create it adding the first cached tag
if (!Array.isArray(cachedTag))
parent.tags[tagName] = [cachedTag]
// add the new nested tag to the array
parent.tags[tagName].push(tag)
} else {
parent.tags[tagName] = tag
}
// empty the child node once we got its template
// to avoid that its children get compiled multiple times
dom.innerHTML = ''
child_tags.push(tag)
}
each(dom.attributes, function(attr) {
if (/^(name|id)$/.test(attr.name)) parent[attr.value] = dom
})
}
})
}
function parseExpressions(root, tag, expressions) {
function addExpr(dom, val, extra) {
if (val.indexOf(brackets(0)) >= 0) {
var expr = { dom: dom, expr: val }
expressions.push(extend(expr, extra))
}
}
walk(root, function(dom) {
var type = dom.nodeType
// text node
if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue)
if (type != 1) return
/* element */
// loop
var attr = dom.getAttribute('each')
if (attr) { _each(dom, tag, attr); return false }
// attribute expressions
each(dom.attributes, function(attr) {
var name = attr.name,
bool = name.split('__')[1]
addExpr(dom, attr.value, { attr: bool || name, bool: bool })
if (bool) { remAttr(dom, name); return false }
})
// skip custom tags
if (getTag(dom)) return false
})
}
function Tag(impl, conf) {
var self = riot.observable(this),
opts = inherit(conf.opts) || {},
dom = mkdom(impl.tmpl),
parent = conf.parent,
expressions = [],
child_tags = [],
root = conf.root,
item = conf.item,
fn = impl.fn,
attr = {},
loop_dom
if (fn && root.riot) return
root.riot = true
// create a unique id to this tag
// it could be handy to use it also to improve the virtual dom rendering speed
this._id = ~~(new Date().getTime() * Math.random())
extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item)
// grab attributes
each(root.attributes, function(el) {
attr[el.name] = el.value
})
// options
function updateOpts(rem_attr) {
each(Object.keys(attr), function(name) {
opts[name] = tmpl(attr[name], parent || self)
})
}
this.update = function(data, init) {
extend(self, data, item)
updateOpts()
self.trigger('update', item)
update(expressions, self, item)
self.trigger('updated')
}
this.mount = function() {
updateOpts()
// initialiation
fn && fn.call(self, opts)
toggle(true)
// parse layout after init. fn may calculate args for nested custom tags
parseExpressions(dom, self, expressions)
self.update()
// internal use only, fixes #403
self.trigger('premount')
if (fn) {
while (dom.firstChild) root.appendChild(dom.firstChild)
} else {
loop_dom = dom.firstChild
root.insertBefore(loop_dom, conf.before || null) // null needed for IE8
}
if (root.stub) self.root = root = parent.root
self.trigger('mount')
}
this.unmount = function() {
var el = fn ? root : loop_dom,
p = el.parentNode,
// detect the tag name
tagName = root.tagName.toLowerCase()
if (p) {
if (parent) {
// remove this tag from the parent tags object
// if there are multiple nested tags with same name..
// remove this element form the array
if (Array.isArray(parent.tags[tagName])) {
each(parent.tags[tagName], function(tag, i) {
if (tag._id == self._id)
parent.tags[tagName].splice(i, 1)
})
} else
// otherwise just delete the tag instance
delete parent.tags[tagName]
p.removeChild(el)
} else {
while (el.firstChild) el.removeChild(el.firstChild)
p.removeChild(el)
}
}
self.trigger('unmount')
toggle()
self.off('*')
delete root.riot
}
function toggle(is_mount) {
// mount/unmount children
each(child_tags, function(child) { child[is_mount ? 'mount' : 'unmount']() })
// listen/unlisten parent (events flow one way from parent to children)
if (parent) {
var evt = is_mount ? 'on' : 'off'
parent[evt]('update', self.update)[evt]('unmount', self.unmount)
}
}
// named elements available for fn
parseNamedElements(dom, this, child_tags)
}
function setEventHandler(name, handler, dom, tag, item) {
dom[name] = function(e) {
// cross browser event fix
e = e || window.event
e.which = e.which || e.charCode || e.keyCode
e.target = e.target || e.srcElement
e.currentTarget = dom
e.item = item
// prevent default behaviour (by default)
if (handler.call(tag, e) !== true && !/radio|check/.test(dom.type)) {
e.preventDefault && e.preventDefault()
e.returnValue = false
}
var el = item ? tag.parent : tag
el.update()
}
}
// used by if- attribute
function insertTo(root, node, before) {
if (root) {
root.insertBefore(before, node)
root.removeChild(node)
}
}
// item = currently looped item
function update(expressions, tag, item) {
each(expressions, function(expr) {
var dom = expr.dom,
attr_name = expr.attr,
value = tmpl(expr.expr, tag),
parent = expr.dom.parentNode
if (value == null) value = ''
// leave out riot- prefixes from strings inside textarea
if (parent && parent.tagName == 'TEXTAREA') value = value.replace(/riot-/g, '')
// no change
if (expr.value === value) return
expr.value = value
// text node
if (!attr_name) return dom.nodeValue = value
// remove original attribute
remAttr(dom, attr_name)
// event handler
if (typeof value == 'function') {
setEventHandler(attr_name, value, dom, tag, item)
// if- conditional
} else if (attr_name == 'if') {
var stub = expr.stub
// add to DOM
if (value) {
stub && insertTo(stub.parentNode, stub, dom)
// remove from DOM
} else {
stub = expr.stub = stub || document.createTextNode('')
insertTo(dom.parentNode, dom, stub)
}
// show / hide
} else if (/^(show|hide)$/.test(attr_name)) {
if (attr_name == 'hide') value = !value
dom.style.display = value ? '' : 'none'
// field value
} else if (attr_name == 'value') {
dom.value = value
//
} else if (attr_name.slice(0, 5) == 'riot-') {
attr_name = attr_name.slice(5)
value ? dom.setAttribute(attr_name, value) : remAttr(dom, attr_name)
} else {
if (expr.bool) {
dom[attr_name] = value
if (!value) return
value = attr_name
}
if (typeof value != 'object') dom.setAttribute(attr_name, value)
}
})
}
function each(els, fn) {
for (var i = 0, len = (els || []).length, el; i < len; i++) {
el = els[i]
// return false -> remove current item during loop
if (el != null && fn(el, i) === false) i--
}
return els
}
function remAttr(dom, name) {
dom.removeAttribute(name)
}
// max 2 from objects allowed
function extend(obj, from, from2) {
from && each(Object.keys(from), function(key) {
obj[key] = from[key]
})
return from2 ? extend(obj, from2) : obj
}
function mkdom(template) {
var tag_name = template.trim().slice(1, 3).toLowerCase(),
root_tag = /td|th/.test(tag_name) ? 'tr' : tag_name == 'tr' ? 'tbody' : 'div',
el = document.createElement(root_tag)
el.stub = true
el.innerHTML = template
return el
}
function walk(dom, fn) {
if (dom) {
if (fn(dom) === false) walk(dom.nextSibling, fn)
else {
dom = dom.firstChild
while (dom) {
walk(dom, fn)
dom = dom.nextSibling
}
}
}
}
function arrDiff(arr1, arr2) {
return arr1.filter(function(el) {
return arr2.indexOf(el) < 0
})
}
function arrFindEquals(arr, el) {
return arr.filter(function (_el) {
return _el === el
})
}
function inherit(parent) {
function Child() {}
Child.prototype = parent
return new Child()
}
/*
Virtual dom is an array of custom tags on the document.
Updates and unmounts propagate downwards from parent to children.
*/
var virtual_dom = [],
tag_impl = {}
function getTag(dom) {
return tag_impl[dom.tagName.toLowerCase()]
}
function injectStyle(css) {
var node = document.createElement('style')
node.innerHTML = css
document.head.appendChild(node)
}
function mountTo(root, tagName, opts) {
var tag = tag_impl[tagName]
if (tag && root) tag = new Tag(tag, { root: root, opts: opts })
if (tag && tag.mount) {
tag.mount()
virtual_dom.push(tag)
return tag.on('unmount', function() {
virtual_dom.splice(virtual_dom.indexOf(tag), 1)
})
}
}
riot.tag = function(name, html, css, fn) {
if (typeof css == 'function') fn = css
else if (css) injectStyle(css)
tag_impl[name] = { name: name, tmpl: html, fn: fn }
return name
}
riot.mount = function(selector, tagName, opts) {
if (selector == '*') selector = Object.keys(tag_impl).join(', ')
if (typeof tagName == 'object') { opts = tagName; tagName = 0 }
var tags = []
function push(root) {
var name = tagName || root.tagName.toLowerCase(),
tag = mountTo(root, name, opts)
if (tag) tags.push(tag)
}
// DOM node
if (selector.tagName) {
push(selector)
return tags[0]
// selector or NodeList
} else {
each(typeof selector == 'string' ? document.querySelectorAll(selector) : selector, push)
return tags
}
}
// update everything
riot.update = function() {
return each(virtual_dom, function(tag) {
tag.update()
})
}
// @deprecated
riot.mountTo = riot.mount
// share methods for other riot parts, e.g. compiler
riot.util = { brackets: brackets, tmpl: tmpl }
// support CommonJS, AMD & browser
if (typeof exports === 'object')
module.exports = riot
else if (typeof define === 'function' && define.amd)
define(function() { return riot })
else
window.riot = riot
})(typeof window != 'undefined' ? window : undefined);