Created
July 31, 2015 11:22
-
-
Save cognitom/b185a7a1abfc3b876005 to your computer and use it in GitHub Desktop.
Riot.js WIP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Riot WIP, @license MIT, (c) 2015 Muut Inc. + contributors */ | |
;(function(window, undefined) { | |
'use strict' | |
var riot = { version: 'WIP', settings: {} } | |
// This globals 'const' helps code size reduction | |
// for typeof == '' comparisons | |
var T_STRING = 'string', | |
T_OBJECT = 'object', | |
T_UNDEF = 'undefined' | |
// for IE8 and rest of the world | |
/* istanbul ignore next */ | |
var isArray = Array.isArray || (function () { | |
var _ts = Object.prototype.toString | |
return function (v) { return _ts.call(v) === '[object Array]' } | |
})() | |
// Version# for IE 8-11, 0 for others | |
var ieVersion = (function (win) { | |
return (window && window.document || {}).documentMode | 0 | |
})() | |
riot.observable = function(el) { | |
el = el || {} | |
var callbacks = {}, | |
_id = 0 | |
el.on = function(events, fn) { | |
if (isFunction(fn)) { | |
if (typeof fn.id === T_UNDEF) fn._id = _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) | |
} | |
} 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 | |
} | |
riot.mixin = (function() { | |
var mixins = {} | |
return function(name, mixin) { | |
if (!mixin) return mixins[name] | |
mixins[name] = mixin | |
} | |
})() | |
;(function(riot, evt, win) { | |
// browsers only | |
if (!win) return | |
var loc = win.location, | |
fns = riot.observable(), | |
started = false, | |
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 | |
} | |
r.stop = function () { | |
if (!started) return | |
win.removeEventListener ? win.removeEventListener(evt, emit, false) : win.detachEvent('on' + evt, emit) | |
fns.off('*') | |
started = false | |
} | |
r.start = function () { | |
if (started) return | |
win.addEventListener ? win.addEventListener(evt, emit, false) : win.attachEvent('on' + evt, emit) | |
started = true | |
} | |
// autostart the router | |
r.start() | |
})(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('<h1>{ title }</h1>{ 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) { | |
var cachedBrackets, | |
r, | |
b, | |
re = /[{}]/g | |
return function(x) { | |
// make sure we use the current setting | |
var s = riot.settings.brackets || orig | |
// recreate cached vars if needed | |
if (cachedBrackets !== s) { | |
cachedBrackets = s | |
b = s.split(' ') | |
r = b.map(function (e) { return e.replace(/(?=.)/g, '\\') }) | |
} | |
// if regexp given, rewrite it with current brackets (only if differ from default) | |
return x instanceof RegExp ? ( | |
s === orig ? x : | |
new RegExp(x.source.replace(re, function(b) { return r[~~(b === '}')] }), x.global ? 'g' : '') | |
) : | |
// else, get specific bracket | |
b[x] | |
} | |
})('{ }') | |
var tmpl = (function() { | |
var cache = {}, | |
reVars = /(['"\/]).*?[^\\]\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 <b>{x}</b> | |
!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(reVars, 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') | |
+ '}catch(e){' | |
+ '}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 b0 = brackets(0), | |
els = expr.trim().slice(b0.length).match(/^\s*(\S+?)\s*(?:,\s*(\S+))?\s+in\s+(.+)$/) | |
return els ? { key: els[1], pos: els[2], val: b0 + els[3] } : { val: expr } | |
} | |
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 tagName = getTagName(dom), | |
template = dom.outerHTML, | |
hasImpl = !!tagImpl[tagName], | |
impl = tagImpl[tagName] || { | |
tmpl: template | |
}, | |
root = dom.parentNode, | |
placeholder = document.createComment('riot placeholder'), | |
tags = [], | |
child = getTag(dom), | |
checksum | |
root.insertBefore(placeholder, dom) | |
expr = loopKeys(expr) | |
// clean template code | |
parent | |
.one('premount', function () { | |
if (root.stub) root = parent.root | |
// remove the original DOM node | |
dom.parentNode.removeChild(dom) | |
}) | |
.on('update', function () { | |
var items = tmpl(expr.val, parent) | |
// object loop. any changes cause full redraw | |
if (!isArray(items)) { | |
checksum = items ? JSON.stringify(items) : '' | |
items = !items ? [] : | |
Object.keys(items).map(function (key) { | |
return mkitem(expr, key, items[key]) | |
}) | |
} | |
var frag = document.createDocumentFragment(), | |
i = tags.length, | |
j = items.length | |
// unmount leftover items | |
while (i > j) { | |
tags[--i].unmount() | |
tags.splice(i, 1) | |
} | |
for (i = 0; i < j; ++i) { | |
var _item = !checksum && !!expr.key ? mkitem(expr, items[i], i) : items[i] | |
if (!tags[i]) { | |
// mount new | |
(tags[i] = new Tag(impl, { | |
parent: parent, | |
isLoop: true, | |
hasImpl: hasImpl, | |
root: hasImpl ? dom.cloneNode() : root, | |
item: _item | |
}, dom.innerHTML) | |
).mount() | |
frag.appendChild(tags[i].root) | |
} else | |
tags[i].update(_item) | |
tags[i]._item = _item | |
} | |
root.insertBefore(frag, placeholder) | |
if (child) parent.tags[tagName] = tags | |
}).one('updated', function() { | |
var keys = Object.keys(parent)// only set new values | |
walk(root, function(node) { | |
// only set element node and not isLoop | |
if (node.nodeType == 1 && !node.isLoop && !node._looped) { | |
node._visited = false // reset _visited for loop node | |
node._looped = true // avoid set multiple each | |
setNamed(node, parent, keys) | |
} | |
}) | |
}) | |
} | |
function parseNamedElements(root, parent, childTags) { | |
walk(root, function(dom) { | |
if (dom.nodeType == 1) { | |
dom.isLoop = dom.isLoop || (dom.parentNode && dom.parentNode.isLoop || dom.getAttribute('each')) ? 1 : 0 | |
// custom child tag | |
var child = getTag(dom) | |
if (child && !dom.isLoop) { | |
var tag = new Tag(child, { root: dom, parent: parent }, dom.innerHTML), | |
tagName = getTagName(dom), | |
ptag = parent, | |
cachedTag | |
while (!getTag(ptag.root)) { | |
if (!ptag.parent) break | |
ptag = ptag.parent | |
} | |
// fix for the parent attribute in the looped elements | |
tag.parent = ptag | |
cachedTag = ptag.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 (!isArray(cachedTag)) | |
ptag.tags[tagName] = [cachedTag] | |
// add the new nested tag to the array | |
ptag.tags[tagName].push(tag) | |
} else { | |
ptag.tags[tagName] = tag | |
} | |
// empty the child node once we got its template | |
// to avoid that its children get compiled multiple times | |
dom.innerHTML = '' | |
childTags.push(tag) | |
} | |
if (!dom.isLoop) | |
setNamed(dom, parent, []) | |
} | |
}) | |
} | |
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, innerHTML) { | |
var self = riot.observable(this), | |
opts = inherit(conf.opts) || {}, | |
dom = mkdom(impl.tmpl), | |
parent = conf.parent, | |
isLoop = conf.isLoop, | |
hasImpl = conf.hasImpl, | |
item = cleanUpData(conf.item), | |
expressions = [], | |
childTags = [], | |
root = conf.root, | |
fn = impl.fn, | |
tagName = root.tagName.toLowerCase(), | |
attr = {}, | |
propsInSyncWithParent = [], | |
loopDom, | |
TAG_ATTRIBUTES = /([\w\-]+)\s?=\s?['"]([^'"]+)["']/gim | |
if (fn && root._tag) { | |
root._tag.unmount(true) | |
} | |
// not yet mounted | |
this.isMounted = false | |
root.isLoop = isLoop | |
if (impl.attrs) { | |
var attrs = impl.attrs.match(TAG_ATTRIBUTES) | |
each(attrs, function(a) { | |
var kv = a.split(/\s?=\s?/) | |
root.setAttribute(kv[0], kv[1].replace(/['"]/g, '')) | |
}) | |
} | |
// keep a reference to the tag just created | |
// so we will be able to mount this tag multiple times | |
root._tag = this | |
// create a unique id to this tag | |
// it could be handy to use it also to improve the virtual dom rendering speed | |
this._id = fastAbs(~~(Date.now() * Math.random())) | |
extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item) | |
// grab attributes | |
each(root.attributes, function(el) { | |
var val = el.value | |
// remember attributes with expressions only | |
if (brackets(/\{.*\}/).test(val)) attr[el.name] = val | |
}) | |
if (dom.innerHTML && !/select|optgroup|tr/.test(tagName)) | |
// replace all the yield tags with the tag inner html | |
dom.innerHTML = replaceYield(dom.innerHTML, innerHTML) | |
// options | |
function updateOpts() { | |
var ctx = hasImpl && isLoop ? self : parent || self | |
// update opts from current DOM attributes | |
each(root.attributes, function(el) { | |
opts[el.name] = tmpl(el.value, ctx) | |
}) | |
// recover those with expressions | |
each(Object.keys(attr), function(name) { | |
opts[name] = tmpl(attr[name], ctx) | |
}) | |
} | |
function normalizeData(data) { | |
for (var key in item) { | |
if (typeof self[key] !== T_UNDEF) | |
self[key] = data[key] | |
} | |
} | |
function inheritFromParent () { | |
if (!self.parent || !isLoop) return | |
each(Object.keys(self.parent), function(k) { | |
// some properties must be always in sync with the parent tag | |
var mustSync = ~propsInSyncWithParent.indexOf(k) | |
if (typeof self[k] === T_UNDEF || mustSync) { | |
// track the property to keep in sync | |
// so we can keep it updated | |
if (!mustSync) propsInSyncWithParent.push(k) | |
self[k] = self.parent[k] | |
} | |
}) | |
} | |
this.update = function(data) { | |
// make sure the data passed will not override | |
// the component core methods | |
data = cleanUpData(data) | |
// inherit properties from the parent | |
inheritFromParent() | |
// normalize the tag properties in case an item object was initially passed | |
if (data && typeof item === T_OBJECT || isArray(item)) { | |
normalizeData(data) | |
item = data | |
} | |
extend(self, data) | |
updateOpts() | |
self.trigger('update', data) | |
update(expressions, self) | |
self.trigger('updated') | |
} | |
this.mixin = function() { | |
each(arguments, function(mix) { | |
mix = typeof mix === T_STRING ? riot.mixin(mix) : mix | |
each(Object.keys(mix), function(key) { | |
// bind methods to self | |
if (key != 'init') | |
self[key] = isFunction(mix[key]) ? mix[key].bind(self) : mix[key] | |
}) | |
// init method will be called automatically | |
if (mix.init) mix.init.bind(self)() | |
}) | |
} | |
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) | |
if (!self.parent || hasImpl) parseExpressions(self.root, self, expressions) // top level before update, empty root | |
if (!self.parent || isLoop) self.update(item) | |
// internal use only, fixes #403 | |
self.trigger('premount') | |
if (isLoop && !hasImpl) { | |
// update the root attribute for the looped elements | |
self.root = root = loopDom = dom.firstChild | |
} else { | |
while (dom.firstChild) root.appendChild(dom.firstChild) | |
if (root.stub) self.root = root = parent.root | |
} | |
// if it's not a child tag we can trigger its mount event | |
if (!self.parent || self.parent.isMounted) { | |
self.isMounted = true | |
self.trigger('mount') | |
} | |
// otherwise we need to wait that the parent event gets triggered | |
else self.parent.one('mount', function() { | |
// avoid to trigger the `mount` event for the tags | |
// not visible included in an if statement | |
if (!isInStub(self.root)) { | |
self.parent.isMounted = self.isMounted = true | |
self.trigger('mount') | |
} | |
}) | |
} | |
this.unmount = function(keepRootTag) { | |
var el = loopDom || root, | |
p = el.parentNode | |
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 (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 | |
parent.tags[tagName] = undefined | |
else | |
while (el.firstChild) el.removeChild(el.firstChild) | |
if (!keepRootTag) | |
p.removeChild(el) | |
} | |
self.trigger('unmount') | |
toggle() | |
self.off('*') | |
// somehow ie8 does not like `delete root._tag` | |
root._tag = null | |
} | |
function toggle(isMount) { | |
// mount/unmount children | |
each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() }) | |
// listen/unlisten parent (events flow one way from parent to children) | |
if (parent) { | |
var evt = isMount ? 'on' : 'off' | |
// the loop tags will be always in sync with the parent automatically | |
if (isLoop) | |
parent[evt]('unmount', self.unmount) | |
else | |
parent[evt]('update', self.update)[evt]('unmount', self.unmount) | |
} | |
} | |
// named elements available for fn | |
parseNamedElements(dom, this, childTags) | |
} | |
function setEventHandler(name, handler, dom, tag) { | |
dom[name] = function(e) { | |
var item = tag._item, | |
ptag = tag.parent | |
if (!item) | |
while (ptag) { | |
item = ptag._item | |
ptag = item ? false : ptag.parent | |
} | |
// cross browser event fix | |
e = e || window.event | |
// ignore error on some browsers | |
try { | |
e.currentTarget = dom | |
if (!e.target) e.target = e.srcElement | |
if (!e.which) e.which = e.charCode || e.keyCode | |
} catch (ignored) { '' } | |
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 | |
} | |
if (!e.preventUpdate) { | |
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) | |
} | |
} | |
function update(expressions, tag) { | |
each(expressions, function(expr, i) { | |
var dom = expr.dom, | |
attrName = 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 (!attrName) return dom.nodeValue = value.toString() | |
// remove original attribute | |
remAttr(dom, attrName) | |
// event handler | |
if (isFunction(value)) { | |
setEventHandler(attrName, value, dom, tag) | |
// if- conditional | |
} else if (attrName == 'if') { | |
var stub = expr.stub | |
// add to DOM | |
if (value) { | |
if (stub) { | |
insertTo(stub.parentNode, stub, dom) | |
dom.inStub = false | |
// avoid to trigger the mount event if the tags is not visible yet | |
// maybe we can optimize this avoiding to mount the tag at all | |
if (!isInStub(dom)) { | |
walk(dom, function(el) { | |
if (el._tag && !el._tag.isMounted) el._tag.isMounted = !!el._tag.trigger('mount') | |
}) | |
} | |
} | |
// remove from DOM | |
} else { | |
stub = expr.stub = stub || document.createTextNode('') | |
insertTo(dom.parentNode, dom, stub) | |
dom.inStub = true | |
} | |
// show / hide | |
} else if (/^(show|hide)$/.test(attrName)) { | |
if (attrName == 'hide') value = !value | |
dom.style.display = value ? '' : 'none' | |
// field value | |
} else if (attrName == 'value') { | |
dom.value = value | |
// <img src="{ expr }"> | |
} else if (attrName.slice(0, 5) == 'riot-' && attrName != 'riot-tag') { | |
attrName = attrName.slice(5) | |
value ? dom.setAttribute(attrName, value) : remAttr(dom, attrName) | |
} else { | |
if (expr.bool) { | |
dom[attrName] = value | |
if (!value) return | |
value = attrName | |
} | |
if (typeof value !== T_OBJECT) dom.setAttribute(attrName, 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 isFunction(v) { | |
return typeof v === 'function' || false // avoid IE problems | |
} | |
function remAttr(dom, name) { | |
dom.removeAttribute(name) | |
} | |
function fastAbs(nr) { | |
return (nr ^ (nr >> 31)) - (nr >> 31) | |
} | |
function getTag(dom) { | |
var tagName = dom.tagName.toLowerCase() | |
return tagImpl[dom.getAttribute(RIOT_TAG) || tagName] | |
} | |
function getTagName(dom) { | |
var child = getTag(dom), | |
namedTag = dom.getAttribute('name'), | |
tagName = namedTag && namedTag.indexOf(brackets(0)) < 0 ? namedTag : child ? child.name : dom.tagName.toLowerCase() | |
return tagName | |
} | |
function extend(src) { | |
var obj, args = arguments | |
for (var i = 1; i < args.length; ++i) { | |
if ((obj = args[i])) { | |
for (var key in obj) { // eslint-disable-line guard-for-in | |
src[key] = obj[key] | |
} | |
} | |
} | |
return src | |
} | |
// with this function we avoid that the current Tag methods get overridden | |
function cleanUpData(data) { | |
if (!(data instanceof Tag)) return data | |
var o = {}, | |
blackList = ['update', 'root', 'mount', 'unmount', 'mixin', 'isMounted', 'isloop', 'tags', 'parent', 'opts'] | |
for (var key in data) { | |
if (!~blackList.indexOf(key)) | |
o[key] = data[key] | |
} | |
return o | |
} | |
function mkdom(template) { | |
var checkie = ieVersion && ieVersion < 10, | |
matches = /^\s*<([\w-]+)/.exec(template), | |
tagName = matches ? matches[1].toLowerCase() : '', | |
rootTag = (tagName === 'th' || tagName === 'td') ? 'tr' : | |
(tagName === 'tr' ? 'tbody' : 'div'), | |
el = mkEl(rootTag) | |
el.stub = true | |
if (checkie) { | |
if (tagName === 'optgroup') | |
optgroupInnerHTML(el, template) | |
else if (tagName === 'option') | |
optionInnerHTML(el, template) | |
else if (rootTag !== 'div') | |
tbodyInnerHTML(el, template, tagName) | |
else | |
checkie = 0 | |
} | |
if (!checkie) 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 isInStub(dom) { | |
while (dom) { | |
if (dom.inStub) return true | |
dom = dom.parentNode | |
} | |
return false | |
} | |
function mkEl(name) { | |
return document.createElement(name) | |
} | |
function replaceYield (tmpl, innerHTML) { | |
return tmpl.replace(/<(yield)\/?>(<\/\1>)?/gim, innerHTML || '') | |
} | |
function $$(selector, ctx) { | |
return (ctx || document).querySelectorAll(selector) | |
} | |
function $(selector, ctx) { | |
return (ctx || document).querySelector(selector) | |
} | |
function inherit(parent) { | |
function Child() {} | |
Child.prototype = parent | |
return new Child() | |
} | |
function setNamed(dom, parent, keys) { | |
each(dom.attributes, function(attr) { | |
if (dom._visited) return | |
if (attr.name === 'id' || attr.name === 'name') { | |
dom._visited = true | |
var p, v = attr.value | |
if (~keys.indexOf(v)) return | |
p = parent[v] | |
if (!p) | |
parent[v] = dom | |
else | |
isArray(p) ? p.push(dom) : (parent[v] = [p, dom]) | |
} | |
}) | |
} | |
/** | |
* | |
* Hacks needed for the old internet explorer versions [lower than IE10] | |
* | |
*/ | |
/* istanbul ignore next */ | |
function tbodyInnerHTML(el, html, tagName) { | |
var div = mkEl('div'), | |
loops = /td|th/.test(tagName) ? 3 : 2, | |
child | |
div.innerHTML = '<table>' + html + '</table>' | |
child = div.firstChild | |
while (loops--) child = child.firstChild | |
el.appendChild(child) | |
} | |
/* istanbul ignore next */ | |
function optionInnerHTML(el, html) { | |
var opt = mkEl('option'), | |
valRegx = /value=[\"'](.+?)[\"']/, | |
selRegx = /selected=[\"'](.+?)[\"']/, | |
eachRegx = /each=[\"'](.+?)[\"']/, | |
ifRegx = /if=[\"'](.+?)[\"']/, | |
innerRegx = />([^<]*)</, | |
valuesMatch = html.match(valRegx), | |
selectedMatch = html.match(selRegx), | |
innerValue = html.match(innerRegx), | |
eachMatch = html.match(eachRegx), | |
ifMatch = html.match(ifRegx) | |
if (innerValue) opt.innerHTML = innerValue[1] | |
else opt.innerHTML = html | |
if (valuesMatch) opt.value = valuesMatch[1] | |
if (selectedMatch) opt.setAttribute('riot-selected', selectedMatch[1]) | |
if (eachMatch) opt.setAttribute('each', eachMatch[1]) | |
if (ifMatch) opt.setAttribute('if', ifMatch[1]) | |
el.appendChild(opt) | |
} | |
/* istanbul ignore next */ | |
function optgroupInnerHTML(el, html) { | |
var opt = mkEl('optgroup'), | |
labelRegx = /label=[\"'](.+?)[\"']/, | |
elementRegx = /^<([^>]*)>/, | |
tagRegx = /^<([^ \>]*)/, | |
labelMatch = html.match(labelRegx), | |
elementMatch = html.match(elementRegx), | |
tagMatch = html.match(tagRegx), | |
innerContent = html | |
if (elementMatch) { | |
var options = html.slice(elementMatch[1].length+2, -tagMatch[1].length-3).trim() | |
innerContent = options | |
} | |
if (labelMatch) opt.setAttribute('riot-label', labelMatch[1]) | |
if (innerContent) { | |
var innerOpt = mkEl('div') | |
optionInnerHTML(innerOpt, innerContent) | |
opt.appendChild(innerOpt.firstChild) | |
} | |
el.appendChild(opt) | |
} | |
/* | |
Virtual dom is an array of custom tags on the document. | |
Updates and unmounts propagate downwards from parent to children. | |
*/ | |
var virtualDom = [], | |
tagImpl = {}, | |
styleNode | |
var RIOT_TAG = 'riot-tag' | |
function injectStyle(css) { | |
if (riot.render) return // skip injection on the server | |
if (!styleNode) { | |
styleNode = mkEl('style') | |
styleNode.setAttribute('type', 'text/css') | |
} | |
var head = document.head || document.getElementsByTagName('head')[0] | |
if (styleNode.styleSheet) | |
styleNode.styleSheet.cssText += css | |
else | |
styleNode.innerHTML += css | |
if (!styleNode._rendered) | |
if (styleNode.styleSheet) { | |
document.body.appendChild(styleNode) | |
} else { | |
var rs = $('style[type=riot]') | |
if (rs) { | |
rs.parentNode.insertBefore(styleNode, rs) | |
rs.parentNode.removeChild(rs) | |
} else head.appendChild(styleNode) | |
} | |
styleNode._rendered = true | |
} | |
function mountTo(root, tagName, opts) { | |
var tag = tagImpl[tagName], | |
// cache the inner HTML to fix #855 | |
innerHTML = root._innerHTML = root._innerHTML || root.innerHTML | |
// clear the inner html | |
root.innerHTML = '' | |
if (tag && root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML) | |
if (tag && tag.mount) { | |
tag.mount() | |
virtualDom.push(tag) | |
return tag.on('unmount', function() { | |
virtualDom.splice(virtualDom.indexOf(tag), 1) | |
}) | |
} | |
} | |
riot.tag = function(name, html, css, attrs, fn) { | |
if (isFunction(attrs)) { | |
fn = attrs | |
if (/^[\w\-]+\s?=/.test(css)) { | |
attrs = css | |
css = '' | |
} else attrs = '' | |
} | |
if (css) { | |
if (isFunction(css)) fn = css | |
else injectStyle(css) | |
} | |
tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } | |
return name | |
} | |
riot.mount = function(selector, tagName, opts) { | |
var els, | |
allTags, | |
tags = [] | |
// helper functions | |
function addRiotTags(arr) { | |
var list = '' | |
each(arr, function (e) { | |
list += ', *[riot-tag="'+ e.trim() + '"]' | |
}) | |
return list | |
} | |
function selectAllTags() { | |
var keys = Object.keys(tagImpl) | |
return keys + addRiotTags(keys) | |
} | |
function pushTags(root) { | |
if (root.tagName) { | |
if (tagName && !root.getAttribute(RIOT_TAG)) | |
root.setAttribute(RIOT_TAG, tagName) | |
var tag = mountTo(root, | |
tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts) | |
if (tag) tags.push(tag) | |
} | |
else if (root.length) { | |
each(root, pushTags) // assume nodeList | |
} | |
} | |
// ----- mount code ----- | |
if (typeof tagName === T_OBJECT) { | |
opts = tagName | |
tagName = 0 | |
} | |
// crawl the DOM to find the tag | |
if (typeof selector === T_STRING) { | |
if (selector === '*') | |
// select all the tags registered | |
// and also the tags found with the riot-tag attribute set | |
selector = allTags = selectAllTags() | |
else | |
// or just the ones named like the selector | |
selector += addRiotTags(selector.split(',')) | |
els = $$(selector) | |
} | |
else | |
// probably you have passed already a tag or a NodeList | |
els = selector | |
// select all the registered and mount them inside their root elements | |
if (tagName === '*') { | |
// get all custom tags | |
tagName = allTags || selectAllTags() | |
// if the root els it's just a single tag | |
if (els.tagName) | |
els = $$(tagName, els) | |
else { | |
// select all the children for all the different root elements | |
var nodeList = [] | |
each(els, function (_el) { | |
nodeList.push($$(tagName, _el)) | |
}) | |
els = nodeList | |
} | |
// get rid of the tagName | |
tagName = 0 | |
} | |
if (els.tagName) | |
pushTags(els) | |
else | |
each(els, pushTags) | |
return tags | |
} | |
// update everything | |
riot.update = function() { | |
return each(virtualDom, function(tag) { | |
tag.update() | |
}) | |
} | |
// @deprecated | |
riot.mountTo = riot.mount | |
var parsers = { | |
html: {}, | |
css: {}, | |
js: { | |
coffee: function(js) { | |
return CoffeeScript.compile(js, { bare: true }) | |
}, | |
es6: function(js) { | |
return babel.transform(js, { blacklist: ['useStrict'] }).code | |
}, | |
none: function(js) { | |
return js | |
} | |
} | |
} | |
// fix 913 | |
parsers.js.javascript = parsers.js.none | |
// 4 the nostalgics | |
parsers.js.coffeescript = parsers.js.coffee | |
riot.parsers = parsers | |
var BOOL_ATTR = ('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,'+ | |
'defaultchecked,defaultmuted,defaultselected,defer,disabled,draggable,enabled,formnovalidate,hidden,'+ | |
'indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,'+ | |
'pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,spellcheck,translate,truespeed,'+ | |
'typemustmatch,visible').split(','), | |
// these cannot be auto-closed | |
VOID_TAGS = 'area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr'.split(','), | |
/* | |
Following attributes give error when parsed on browser with { exrp_values } | |
'd' describes the SVG <path>, Chrome gives error if the value is not valid format | |
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d | |
*/ | |
PREFIX_ATTR = ['style', 'src', 'd'], | |
LINE_TAG = /^<([\w\-]+)>(.*)<\/\1>/gim, | |
QUOTE = /=({[^}]+})([\s\/\>]|$)/g, | |
SET_ATTR = /([\w\-]+)=(["'])([^\2]+?)\2/g, | |
EXPR = /{\s*([^}]+)\s*}/g, | |
// (tagname) (html) (javascript) endtag | |
CUSTOM_TAG = /^<([\w\-]+)\s?([^>]*)>([^\x00]*[\w\/}"']>$)?([^\x00]*?)^<\/\1>/gim, | |
SCRIPT = /<script(\s+type=['"]?([^>'"]+)['"]?)?>([^\x00]*?)<\/script>/gm, | |
STYLE = /<style(\s+type=['"]?([^>'"]+)['"]?|\s+scoped)?>([^\x00]*?)<\/style>/gm, | |
CSS_SELECTOR = /(^|\}|\{)\s*([^\{\}]+)\s*(?=\{)/g, | |
CSS_COMMENT = /\/\*[^\x00]*?\*\//gm, | |
HTML_COMMENT = /<!--.*?-->/g, | |
CLOSED_TAG = /<([\w\-]+)([^>]*)\/\s*>/g, | |
LINE_COMMENT = /^\s*\/\/.*$/gm, | |
JS_COMMENT = /\/\*[^\x00]*?\*\//gm, | |
INPUT_NUMBER = /(<input\s[^>]*?)type=['"]number['"]/gm | |
function mktag(name, html, css, attrs, js) { | |
return 'riot.tag(\'' | |
+ name + '\', \'' | |
+ html + '\'' | |
+ (css ? ', \'' + css + '\'' : '') | |
+ (attrs ? ', \'' + attrs.replace(/'/g, "\\'") + '\'' : '') | |
+ ', function(opts) {' + js + '\n});' | |
} | |
function compileHTML(html, opts, type) { | |
var brackets = riot.util.brackets | |
// foo={ bar } --> foo="{ bar }" | |
html = html.replace(brackets(QUOTE), '="$1"$2') | |
// whitespace | |
html = opts.whitespace ? html.replace(/\n/g, '\\n') : html.replace(/\s+/g, ' ') | |
// strip comments | |
html = html.trim().replace(HTML_COMMENT, '') | |
// input type=numbr | |
html = html.replace(INPUT_NUMBER, '$1riot-type='+brackets(0)+'"number"'+brackets(1)) // fake expression | |
// alter special attribute names | |
html = html.replace(SET_ATTR, function(full, name, _, expr) { | |
if (expr.indexOf(brackets(0)) >= 0) { | |
name = name.toLowerCase() | |
if (PREFIX_ATTR.indexOf(name) >= 0) name = 'riot-' + name | |
// IE8 looses boolean attr values: `checked={ expr }` --> `__checked={ expr }` | |
else if (BOOL_ATTR.indexOf(name) >= 0) name = '__' + name | |
} | |
return name + '="' + expr + '"' | |
}) | |
// run expressions trough parser | |
if (opts.expr) { | |
html = html.replace(brackets(EXPR), function(_, expr) { | |
var ret = compileJS(expr, opts, type).trim().replace(/\r?\n|\r/g, '').trim() | |
if (ret.slice(-1) == ';') ret = ret.slice(0, -1) | |
return brackets(0) + ret + brackets(1) | |
}) | |
} | |
// <foo/> -> <foo></foo> | |
html = html.replace(CLOSED_TAG, function(_, name, attr) { | |
var tag = '<' + name + (attr ? ' ' + attr.trim() : '') + '>' | |
// Do not self-close HTML5 void tags | |
if (VOID_TAGS.indexOf(name.toLowerCase()) == -1) tag += '</' + name + '>' | |
return tag | |
}) | |
// escape single quotes | |
html = html.replace(/'/g, "\\'") | |
// \{ jotain \} --> \\{ jotain \\} | |
html = html.replace(brackets(/\\{|\\}/g), '\\$&') | |
// compact: no whitespace between tags | |
if (opts.compact) html = html.replace(/> </g, '><') | |
return html | |
} | |
function riotjs(js) { | |
// strip comments | |
js = js.replace(LINE_COMMENT, '').replace(JS_COMMENT, '') | |
// ES6 method signatures | |
var lines = js.split('\n'), | |
es6Ident = '' | |
lines.forEach(function(line, i) { | |
var l = line.trim() | |
// method start | |
if (l[0] != '}' && l.indexOf('(') > 0 && l.indexOf('function') == -1) { | |
var end = /[{}]/.exec(l.slice(-1)), | |
m = end && /(\s+)([\w]+)\s*\(([\w,\s]*)\)\s*\{/.exec(line) | |
if (m && !/^(if|while|switch|for|catch)$/.test(m[2])) { | |
lines[i] = m[1] + 'this.' + m[2] + ' = function(' + m[3] + ') {' | |
// foo() { } | |
if (end[0] == '}') { | |
lines[i] += ' ' + l.slice(m[0].length - 1, -1) + '}.bind(this)' | |
} else { | |
es6Ident = m[1] | |
} | |
} | |
} | |
// method end | |
if (line.slice(0, es6Ident.length + 1) == es6Ident + '}') { | |
lines[i] = es6Ident + '}.bind(this);' | |
es6Ident = '' | |
} | |
}) | |
return lines.join('\n') | |
} | |
function scopedCSS (tag, style, type) { | |
// 1. Remove CSS comments | |
// 2. Find selectors and separate them by conmma | |
// 3. keep special selectors as is | |
// 4. prepend tag and [riot-tag] | |
return style.replace(CSS_COMMENT, '').replace(CSS_SELECTOR, function (m, p1, p2) { | |
return p1 + ' ' + p2.split(/\s*,\s*/g).map(function(sel) { | |
var s = sel.trim() | |
var t = (/:scope/.test(s) ? '' : ' ') + s.replace(/:scope/, '') | |
return s[0] == '@' || s == 'from' || s == 'to' || /%$/.test(s) ? s : | |
tag + t + ', [riot-tag="' + tag + '"]' + t | |
}).join(',') | |
}).trim() | |
} | |
function compileJS(js, opts, type) { | |
var parser = opts.parser || (type ? riot.parsers.js[type] : riotjs) | |
if (!parser) throw new Error('Parser not found "' + type + '"') | |
return parser(js, opts) | |
} | |
function compileTemplate(lang, html) { | |
var parser = riot.parsers.html[lang] | |
if (!parser) throw new Error('Template parser not found "' + lang + '"') | |
return parser(html) | |
} | |
function compileCSS(style, tag, type) { | |
if (type == 'scoped-css') style = scopedCSS(tag, style) | |
else if (riot.parsers.css[type]) style = riot.parsers.css[type](tag, style) | |
return style.replace(/\s+/g, ' ').replace(/\\/g, '\\\\').replace(/'/g, "\\'").trim() | |
} | |
function compile(src, opts) { | |
opts = opts || {} | |
if (opts.brackets) riot.settings.brackets = opts.brackets | |
if (opts.template) src = compileTemplate(opts.template, src) | |
src = src.replace(LINE_TAG, function(_, tagName, html) { | |
return mktag(tagName, compileHTML(html, opts), '', '', '') | |
}) | |
return src.replace(CUSTOM_TAG, function(_, tagName, attrs, html, js) { | |
html = html || '' | |
attrs = compileHTML(attrs, '', '') | |
// js wrapped inside <script> tag | |
var type = opts.type | |
if (!js.trim()) { | |
html = html.replace(SCRIPT, function(_, fullType, _type, script) { | |
if (_type) type = _type.replace('text/', '') | |
js = script | |
return '' | |
}) | |
} | |
// styles in <style> tag | |
var styleType = 'css', | |
style = '' | |
html = html.replace(STYLE, function(_, fullType, _type, _style) { | |
if (fullType && fullType.trim() == 'scoped') styleType = 'scoped-css' | |
else if (_type) styleType = _type.replace('text/', '') | |
style = _style | |
return '' | |
}) | |
return mktag( | |
tagName, | |
compileHTML(html, opts, type), | |
compileCSS(style, tagName, styleType), | |
attrs, | |
compileJS(js, opts, type) | |
) | |
}) | |
} | |
var doc = window.document, | |
promise, | |
ready | |
function GET(url, fn) { | |
var req = new XMLHttpRequest() | |
req.onreadystatechange = function() { | |
if (req.readyState == 4 && req.status == 200) fn(req.responseText) | |
} | |
req.open('GET', url, true) | |
req.send('') | |
} | |
function unindent(src) { | |
var ident = /[ \t]+/.exec(src) | |
if (ident) src = src.replace(new RegExp('^' + ident[0], 'gm'), '') | |
return src | |
} | |
function globalEval(js) { | |
var node = doc.createElement('script'), | |
root = doc.documentElement | |
node.text = compile(js) | |
root.appendChild(node) | |
root.removeChild(node) | |
} | |
function compileScripts(fn) { | |
var scripts = doc.querySelectorAll('script[type="riot/tag"]'), | |
scriptsAmount = scripts.length | |
function done() { | |
promise.trigger('ready') | |
ready = true | |
fn && fn() | |
} | |
if (!scriptsAmount) { | |
done() | |
} else { | |
[].map.call(scripts, function(script) { | |
var url = script.getAttribute('src') | |
function compileTag(source) { | |
globalEval(source) | |
scriptsAmount-- | |
if (!scriptsAmount) { | |
done() | |
} | |
} | |
return url ? GET(url, compileTag) : compileTag(unindent(script.innerHTML)) | |
}) | |
} | |
} | |
riot.compile = function(arg, fn) { | |
// string | |
if (typeof arg === T_STRING) { | |
// compile & return | |
if (arg.trim()[0] == '<') { | |
var js = unindent(compile(arg)) | |
if (!fn) globalEval(js) | |
return js | |
// URL | |
} else { | |
return GET(arg, function(str) { | |
var js = unindent(compile(str)) | |
globalEval(js) | |
fn && fn(js, str) | |
}) | |
} | |
} | |
// must be a function | |
if (typeof arg !== 'function') arg = undefined | |
// all compiled | |
if (ready) return arg && arg() | |
// add to queue | |
if (promise) { | |
arg && promise.on('ready', arg) | |
// grab riot/tag elements + load & execute them | |
} else { | |
promise = riot.observable() | |
compileScripts(arg) | |
} | |
} | |
// reassign mount methods | |
var mount = riot.mount | |
riot.mount = function(a, b, c) { | |
var ret | |
riot.compile(function() { ret = mount(a, b, c) }) | |
return ret | |
} | |
// @deprecated | |
riot.mountTo = riot.mount | |
// share methods for other riot parts, e.g. compiler | |
riot.util = { brackets: brackets, tmpl: tmpl } | |
// support CommonJS, AMD & browser | |
/* istanbul ignore next */ | |
if (typeof exports === T_OBJECT) | |
module.exports = riot | |
else if (typeof define === 'function' && define.amd) | |
define(function() { return window.riot = riot }) | |
else | |
window.riot = riot | |
})(typeof window != 'undefined' ? window : void 0); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment