/* Riot v2.3.15, @license MIT, (c) 2015 Muut Inc. + contributors */ ;(function(window, undefined) { 'use strict'; var riot = { version: 'v2.3.15', settings: {} }, // be aware, internal usage // ATTENTION: prefix the global dynamic variables with `__` // counter to give a unique id to all the Tag instances __uid = 0, // tags instances cache __virtualDom = [], // tags implementation cache __tagImpl = {}, /** * Const */ // riot specific prefixes RIOT_PREFIX = 'riot-', RIOT_TAG = RIOT_PREFIX + 'tag', // for typeof == '' comparisons T_STRING = 'string', T_OBJECT = 'object', T_UNDEF = 'undefined', T_FUNCTION = 'function', // special native tags that cannot be treated like the others SPECIAL_TAGS_REGEX = /^(?:t(?:body|head|foot|[rhd])|caption|col(?:group)?|opt(?:ion|group))$/, RESERVED_WORDS_BLACKLIST = ['_item', '_id', '_parent', 'update', 'root', 'mount', 'unmount', 'mixin', 'isMounted', 'isLoop', 'tags', 'parent', 'opts', 'trigger', 'on', 'off', 'one'], // version# for IE 8-11, 0 for others IE_VERSION = (window && window.document || {}).documentMode | 0 /* istanbul ignore next */ riot.observable = function(el) { /** * Extend the original object or create a new empty one * @type { Object } */ el = el || {} /** * Private variables and methods */ var callbacks = {}, slice = Array.prototype.slice, onEachEvent = function(e, fn) { e.replace(/\S+/g, fn) }, defineProperty = function (key, value) { Object.defineProperty(el, key, { value: value, enumerable: false, writable: false, configurable: false }) } /** * Listen to the given space separated list of `events` and execute the `callback` each time an event is triggered. * @param { String } events - events ids * @param { Function } fn - callback function * @returns { Object } el */ defineProperty('on', function(events, fn) { if (typeof fn != 'function') return el onEachEvent(events, function(name, pos) { (callbacks[name] = callbacks[name] || []).push(fn) fn.typed = pos > 0 }) return el }) /** * Removes the given space separated list of `events` listeners * @param { String } events - events ids * @param { Function } fn - callback function * @returns { Object } el */ defineProperty('off', function(events, fn) { if (events == '*' && !fn) callbacks = {} else { onEachEvent(events, function(name) { if (fn) { var arr = callbacks[name] for (var i = 0, cb; cb = arr && arr[i]; ++i) { if (cb == fn) arr.splice(i--, 1) } } else delete callbacks[name] }) } return el }) /** * Listen to the given space separated list of `events` and execute the `callback` at most once * @param { String } events - events ids * @param { Function } fn - callback function * @returns { Object } el */ defineProperty('one', function(events, fn) { function on() { el.off(events, on) fn.apply(el, arguments) } return el.on(events, on) }) /** * Execute all callback functions that listen to the given space separated list of `events` * @param { String } events - events ids * @returns { Object } el */ defineProperty('trigger', function(events) { // getting the arguments // skipping the first one var args = slice.call(arguments, 1), fns onEachEvent(events, function(name) { fns = slice.call(callbacks[name] || [], 0) for (var i = 0, fn; fn = fns[i]; ++i) { if (fn.busy) return fn.busy = 1 fn.apply(el, fn.typed ? [name].concat(args) : args) if (fns[i] !== fn) { i-- } fn.busy = 0 } if (callbacks['*'] && name != '*') el.trigger.apply(el, ['*', name].concat(args)) }) return el }) return el } /* istanbul ignore next */ ;(function(riot) { /** * Simple client-side router * @module riot-route */ var RE_ORIGIN = /^.+?\/+[^\/]+/, EVENT_LISTENER = 'EventListener', REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER, ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER, HAS_ATTRIBUTE = 'hasAttribute', REPLACE = 'replace', POPSTATE = 'popstate', HASHCHANGE = 'hashchange', TRIGGER = 'trigger', MAX_EMIT_STACK_LEVEL = 3, win = typeof window != 'undefined' && window, doc = typeof document != 'undefined' && document, hist = win && history, loc = win && (hist.location || win.location), // see html5-history-api prot = Router.prototype, // to minify more clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click', started = false, central = riot.observable(), routeFound = false, debouncedEmit, base, current, parser, secondParser, emitStack = [], emitStackLevel = 0 /** * Default parser. You can replace it via router.parser method. * @param {string} path - current path (normalized) * @returns {array} array */ function DEFAULT_PARSER(path) { return path.split(/[/?#]/) } /** * Default parser (second). You can replace it via router.parser method. * @param {string} path - current path (normalized) * @param {string} filter - filter string (normalized) * @returns {array} array */ function DEFAULT_SECOND_PARSER(path, filter) { var re = new RegExp('^' + filter[REPLACE](/\*/g, '([^/?#]+?)')[REPLACE](/\.\./, '.*') + '$'), args = path.match(re) if (args) return args.slice(1) } /** * Simple/cheap debounce implementation * @param {function} fn - callback * @param {number} delay - delay in seconds * @returns {function} debounced function */ function debounce(fn, delay) { var t return function () { clearTimeout(t) t = setTimeout(fn, delay) } } /** * Set the window listeners to trigger the routes * @param {boolean} autoExec - see route.start */ function start(autoExec) { debouncedEmit = debounce(emit, 1) win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit) win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit) doc[ADD_EVENT_LISTENER](clickEvent, click) if (autoExec) emit(true) } /** * Router class */ function Router() { this.$ = [] riot.observable(this) // make it observable central.on('stop', this.s.bind(this)) central.on('emit', this.e.bind(this)) } function normalize(path) { return path[REPLACE](/^\/|\/$/, '') } function isString(str) { return typeof str == 'string' } /** * Get the part after domain name * @param {string} href - fullpath * @returns {string} path from root */ function getPathFromRoot(href) { return (href || loc.href || '')[REPLACE](RE_ORIGIN, '') } /** * Get the part after base * @param {string} href - fullpath * @returns {string} path from base */ function getPathFromBase(href) { return base[0] == '#' ? (href || loc.href || '').split(base)[1] || '' : getPathFromRoot(href)[REPLACE](base, '') } function emit(force) { // the stack is needed for redirections var isRoot = emitStackLevel == 0 if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) return emitStackLevel++ emitStack.push(function() { var path = getPathFromBase() if (force || path != current) { central[TRIGGER]('emit', path) current = path } }) if (isRoot) { while (emitStack.length) { emitStack[0]() emitStack.shift() } emitStackLevel = 0 } } function click(e) { if ( e.which != 1 // not left click || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys || e.defaultPrevented // or default prevented ) return var el = e.target while (el && el.nodeName != 'A') el = el.parentNode if ( !el || el.nodeName != 'A' // not A tag || el[HAS_ATTRIBUTE]('download') // has download attr || !el[HAS_ATTRIBUTE]('href') // has no href attr || el.target && el.target != '_self' // another window or frame || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) == -1 // cross origin ) return if (el.href != loc.href) { if ( el.href.split('#')[0] == loc.href.split('#')[0] // internal jump || base != '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base || !go(getPathFromBase(el.href), el.title || doc.title) // route not found ) return } e.preventDefault() } /** * Go to the path * @param {string} path - destination path * @param {string} title - page title * @param {boolean} shouldReplace - use replaceState or pushState * @returns {boolean} - route not found flag */ function go(path, title, shouldReplace) { if (hist) { // if a browser path = base + normalize(path) title = title || doc.title // browsers ignores the second parameter `title` shouldReplace ? hist.replaceState(null, title, path) : hist.pushState(null, title, path) // so we need to set it manually doc.title = title routeFound = false emit() return routeFound } // Server-side usage: directly execute handlers for the path return central[TRIGGER]('emit', getPathFromBase(path)) } /** * Go to path or set action * a single string: go there * two strings: go there with setting a title * two strings and boolean: replace history with setting a title * a single function: set an action on the default route * a string/RegExp and a function: set an action on the route * @param {(string|function)} first - path / action / filter * @param {(string|RegExp|function)} second - title / action * @param {boolean} third - replace flag */ prot.m = function(first, second, third) { if (isString(first) && (!second || isString(second))) go(first, second, third || false) else if (second) this.r(first, second) else this.r('@', first) } /** * Stop routing */ prot.s = function() { this.off('*') this.$ = [] } /** * Emit * @param {string} path - path */ prot.e = function(path) { this.$.concat('@').some(function(filter) { var args = (filter == '@' ? parser : secondParser)(normalize(path), normalize(filter)) if (typeof args != 'undefined') { this[TRIGGER].apply(null, [filter].concat(args)) return routeFound = true // exit from loop } }, this) } /** * Register route * @param {string} filter - filter for matching to url * @param {function} action - action to register */ prot.r = function(filter, action) { if (filter != '@') { filter = '/' + normalize(filter) this.$.push(filter) } this.on(filter, action) } var mainRouter = new Router() var route = mainRouter.m.bind(mainRouter) /** * Create a sub router * @returns {function} the method of a new Router object */ route.create = function() { var newSubRouter = new Router() // stop only this sub-router newSubRouter.m.stop = newSubRouter.s.bind(newSubRouter) // return sub-router's main method return newSubRouter.m.bind(newSubRouter) } /** * Set the base of url * @param {(str|RegExp)} arg - a new base or '#' or '#!' */ route.base = function(arg) { base = arg || '#' current = getPathFromBase() // recalculate current path } /** Exec routing right now **/ route.exec = function() { emit(true) } /** * Replace the default router to yours * @param {function} fn - your parser function * @param {function} fn2 - your secondParser function */ route.parser = function(fn, fn2) { if (!fn && !fn2) { // reset parser for testing... parser = DEFAULT_PARSER secondParser = DEFAULT_SECOND_PARSER } if (fn) parser = fn if (fn2) secondParser = fn2 } /** * Helper function to get url query as an object * @returns {object} parsed query */ route.query = function() { var q = {} var href = loc.href || current href[REPLACE](/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v }) return q } /** Stop routing **/ route.stop = function () { if (started) { if (win) { win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit) win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit) doc[REMOVE_EVENT_LISTENER](clickEvent, click) } central[TRIGGER]('stop') started = false } } /** * Start routing * @param {boolean} autoExec - automatically exec after starting if true */ route.start = function (autoExec) { if (!started) { if (win) { if (document.readyState == 'complete') start(autoExec) // the timeout is needed to solve // a weird safari bug https://github.com/riot/route/issues/33 else win[ADD_EVENT_LISTENER]('load', function() { setTimeout(function() { start(autoExec) }, 1) }) } started = true } } /** Prepare the router **/ route.base() route.parser() riot.route = route })(riot) /* istanbul ignore next */ /** * The riot template engine * @version v2.3.21 */ /** * riot.util.brackets * * - `brackets ` - Returns a string or regex based on its parameter * - `brackets.set` - Change the current riot brackets * * @module */ var brackets = (function (UNDEF) { var REGLOB = 'g', R_MLCOMMS = /\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//g, R_STRINGS = /"[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'/g, S_QBLOCKS = R_STRINGS.source + '|' + /(?:\breturn\s+|(?:[$\w\)\]]|\+\+|--)\s*(\/)(?![*\/]))/.source + '|' + /\/(?=[^*\/])[^[\/\\]*(?:(?:\[(?:\\.|[^\]\\]*)*\]|\\.)[^[\/\\]*)*?(\/)[gim]*/.source, FINDBRACES = { '(': RegExp('([()])|' + S_QBLOCKS, REGLOB), '[': RegExp('([[\\]])|' + S_QBLOCKS, REGLOB), '{': RegExp('([{}])|' + S_QBLOCKS, REGLOB) }, DEFAULT = '{ }' var _pairs = [ '{', '}', '{', '}', /{[^}]*}/, /\\([{}])/g, /\\({)|{/g, RegExp('\\\\(})|([[({])|(})|' + S_QBLOCKS, REGLOB), DEFAULT, /^\s*{\^?\s*([$\w]+)(?:\s*,\s*(\S+))?\s+in\s+(\S.*)\s*}/, /(^|[^\\]){=[\S\s]*?}/ ] var cachedBrackets = UNDEF, _regex, _cache = [], _settings function _loopback (re) { return re } function _rewrite (re, bp) { if (!bp) bp = _cache return new RegExp( re.source.replace(/{/g, bp[2]).replace(/}/g, bp[3]), re.global ? REGLOB : '' ) } function _create (pair) { if (pair === DEFAULT) return _pairs var arr = pair.split(' ') if (arr.length !== 2 || /[\x00-\x1F<>a-zA-Z0-9'",;\\]/.test(pair)) { throw new Error('Unsupported brackets "' + pair + '"') } arr = arr.concat(pair.replace(/(?=[[\]()*+?.^$|])/g, '\\').split(' ')) arr[4] = _rewrite(arr[1].length > 1 ? /{[\S\s]*?}/ : _pairs[4], arr) arr[5] = _rewrite(pair.length > 3 ? /\\({|})/g : _pairs[5], arr) arr[6] = _rewrite(_pairs[6], arr) arr[7] = RegExp('\\\\(' + arr[3] + ')|([[({])|(' + arr[3] + ')|' + S_QBLOCKS, REGLOB) arr[8] = pair return arr } function _brackets (reOrIdx) { return reOrIdx instanceof RegExp ? _regex(reOrIdx) : _cache[reOrIdx] } _brackets.split = function split (str, tmpl, _bp) { // istanbul ignore next: _bp is for the compiler if (!_bp) _bp = _cache var parts = [], match, isexpr, start, pos, re = _bp[6] isexpr = start = re.lastIndex = 0 while (match = re.exec(str)) { pos = match.index if (isexpr) { if (match[2]) { re.lastIndex = skipBraces(str, match[2], re.lastIndex) continue } if (!match[3]) continue } if (!match[1]) { unescapeStr(str.slice(start, pos)) start = re.lastIndex re = _bp[6 + (isexpr ^= 1)] re.lastIndex = start } } if (str && start < str.length) { unescapeStr(str.slice(start)) } return parts function unescapeStr (s) { if (tmpl || isexpr) parts.push(s && s.replace(_bp[5], '$1')) else parts.push(s) } function skipBraces (s, ch, ix) { var match, recch = FINDBRACES[ch] recch.lastIndex = ix ix = 1 while (match = recch.exec(s)) { if (match[1] && !(match[1] === ch ? ++ix : --ix)) break } return ix ? s.length : recch.lastIndex } } _brackets.hasExpr = function hasExpr (str) { return _cache[4].test(str) } _brackets.loopKeys = function loopKeys (expr) { var m = expr.match(_cache[9]) return m ? { key: m[1], pos: m[2], val: _cache[0] + m[3].trim() + _cache[1] } : { val: expr.trim() } } _brackets.hasRaw = function (src) { return _cache[10].test(src) } _brackets.array = function array (pair) { return pair ? _create(pair) : _cache } function _reset (pair) { if ((pair || (pair = DEFAULT)) !== _cache[8]) { _cache = _create(pair) _regex = pair === DEFAULT ? _loopback : _rewrite _cache[9] = _regex(_pairs[9]) _cache[10] = _regex(_pairs[10]) } cachedBrackets = pair } function _setSettings (o) { var b o = o || {} b = o.brackets Object.defineProperty(o, 'brackets', { set: _reset, get: function () { return cachedBrackets }, enumerable: true }) _settings = o _reset(b) } Object.defineProperty(_brackets, 'settings', { set: _setSettings, get: function () { return _settings } }) /* istanbul ignore next: in the browser riot is always in the scope */ _brackets.settings = typeof riot !== 'undefined' && riot.settings || {} _brackets.set = _reset _brackets.R_STRINGS = R_STRINGS _brackets.R_MLCOMMS = R_MLCOMMS _brackets.S_QBLOCKS = S_QBLOCKS return _brackets })() /** * @module tmpl * * tmpl - Root function, returns the template value, render with data * tmpl.hasExpr - Test the existence of a expression inside a string * tmpl.loopKeys - Get the keys for an 'each' loop (used by `_each`) */ var tmpl = (function () { var _cache = {} function _tmpl (str, data) { if (!str) return str return (_cache[str] || (_cache[str] = _create(str))).call(data, _logErr) } _tmpl.haveRaw = brackets.hasRaw _tmpl.hasExpr = brackets.hasExpr _tmpl.loopKeys = brackets.loopKeys _tmpl.errorHandler = null function _logErr (err, ctx) { if (_tmpl.errorHandler) { err.riotData = { tagName: ctx && ctx.root && ctx.root.tagName, _riot_id: ctx && ctx._riot_id //eslint-disable-line camelcase } _tmpl.errorHandler(err) } } function _create (str) { var expr = _getTmpl(str) if (expr.slice(0, 11) !== 'try{return ') expr = 'return ' + expr return new Function('E', expr + ';') } var RE_QBLOCK = RegExp(brackets.S_QBLOCKS, 'g'), RE_QBMARK = /\x01(\d+)~/g function _getTmpl (str) { var qstr = [], expr, parts = brackets.split(str.replace(/\u2057/g, '"'), 1) if (parts.length > 2 || parts[0]) { var i, j, list = [] for (i = j = 0; i < parts.length; ++i) { expr = parts[i] if (expr && (expr = i & 1 ? _parseExpr(expr, 1, qstr) : '"' + expr .replace(/\\/g, '\\\\') .replace(/\r\n?|\n/g, '\\n') .replace(/"/g, '\\"') + '"' )) list[j++] = expr } expr = j < 2 ? list[0] : '[' + list.join(',') + '].join("")' } else { expr = _parseExpr(parts[1], 0, qstr) } if (qstr[0]) expr = expr.replace(RE_QBMARK, function (_, pos) { return qstr[pos] .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') }) return expr } var RE_BREND = { '(': /[()]/g, '[': /[[\]]/g, '{': /[{}]/g }, CS_IDENT = /^(?:(-?[_A-Za-z\xA0-\xFF][-\w\xA0-\xFF]*)|\x01(\d+)~):/ function _parseExpr (expr, asText, qstr) { if (expr[0] === '=') expr = expr.slice(1) expr = expr .replace(RE_QBLOCK, function (s, div) { return s.length > 2 && !div ? '\x01' + (qstr.push(s) - 1) + '~' : s }) .replace(/\s+/g, ' ').trim() .replace(/\ ?([[\({},?\.:])\ ?/g, '$1') if (expr) { var list = [], cnt = 0, match while (expr && (match = expr.match(CS_IDENT)) && !match.index ) { var key, jsb, re = /,|([[{(])|$/g expr = RegExp.rightContext key = match[2] ? qstr[match[2]].slice(1, -1).trim().replace(/\s+/g, ' ') : match[1] while (jsb = (match = re.exec(expr))[1]) skipBraces(jsb, re) jsb = expr.slice(0, match.index) expr = RegExp.rightContext list[cnt++] = _wrapExpr(jsb, 1, key) } expr = !cnt ? _wrapExpr(expr, asText) : cnt > 1 ? '[' + list.join(',') + '].join(" ").trim()' : list[0] } return expr function skipBraces (ch, re) { var mm, lv = 1, ir = RE_BREND[ch] ir.lastIndex = re.lastIndex while (mm = ir.exec(expr)) { if (mm[0] === ch) ++lv else if (!--lv) break } re.lastIndex = lv ? expr.length : ir.lastIndex } } // istanbul ignore next: not both var JS_CONTEXT = '"in this?this:' + (typeof window !== 'object' ? 'global' : 'window') + ').', JS_VARNAME = /[,{][$\w]+:|(^ *|[^$\w\.])(?!(?:typeof|true|false|null|undefined|in|instanceof|is(?:Finite|NaN)|void|NaN|new|Date|RegExp|Math)(?![$\w]))([$_A-Za-z][$\w]*)/g, JS_NOPROPS = /^(?=(\.[$\w]+))\1(?:[^.[(]|$)/ function _wrapExpr (expr, asText, key) { var tb expr = expr.replace(JS_VARNAME, function (match, p, mvar, pos, s) { if (mvar) { pos = tb ? 0 : pos + match.length if (mvar !== 'this' && mvar !== 'global' && mvar !== 'window') { match = p + '("' + mvar + JS_CONTEXT + mvar if (pos) tb = (s = s[pos]) === '.' || s === '(' || s === '[' } else if (pos) { tb = !JS_NOPROPS.test(s.slice(pos)) } } return match }) if (tb) { expr = 'try{return ' + expr + '}catch(e){E(e,this)}' } if (key) { expr = (tb ? 'function(){' + expr + '}.call(this)' : '(' + expr + ')' ) + '?"' + key + '":""' } else if (asText) { expr = 'function(v){' + (tb ? expr.replace('return ', 'v=') : 'v=(' + expr + ')' ) + ';return v||v===0?v:""}.call(this)' } return expr } // istanbul ignore next: compatibility fix for beta versions _tmpl.parse = function (s) { return s } _tmpl.version = brackets.version = 'v2.3.21' return _tmpl })() /* lib/browser/tag/mkdom.js Includes hacks needed for the Internet Explorer version 9 and below See: http://kangax.github.io/compat-table/es5/#ie8 http://codeplanet.io/dropping-ie8/ */ var mkdom = (function (checkIE) { var reToSrc = /([\S\s]+?)<\/yield\s*>/.source, rootEls = { tr: 'tbody', th: 'tr', td: 'tr', col: 'colgroup' }, GENERIC = 'div' checkIE = checkIE && checkIE < 10 var tblTags = checkIE ? SPECIAL_TAGS_REGEX : /^(?:t(?:body|head|foot|[rhd])|caption|col(?:group)?)$/ // creates any dom element in a div, table, or colgroup container function _mkdom(templ, html) { var match = templ && templ.match(/^\s*<([-\w]+)/), tagName = match && match[1].toLowerCase(), el = mkEl(GENERIC) // replace all the yield tags with the tag inner html templ = replaceYield(templ, html || '') /* istanbul ignore next */ //if ((checkIE || !startsWith(tagName, 'opt')) && SPECIAL_TAGS_REGEX.test(tagName)) if (tblTags.test(tagName)) el = specialTags(el, templ, tagName) else el.innerHTML = templ el.stub = true return el } // creates the root element for table and select child elements // tr/th/td/thead/tfoot/tbody/caption/col/colgroup/option/optgroup function specialTags(el, templ, tagName) { var select = tagName[0] === 'o', parent = select ? 'select>' : 'table>' // trim() is important here, this ensures we don't have artifacts, // so we can check if we have only one element inside the parent el.innerHTML = '<' + parent + templ.trim() + '|>\s*<\/yield\s*>)/ig, function (str, ref) { var m = html.match(RegExp(reToSrc.replace('@', ref), 'i')) ++n return m && m[2] || '' }) // yield without any "from", replace yield in templ with the innerHTML return n ? templ : templ.replace(/|>\s*<\/yield\s*>)/gi, html) } return _mkdom })(IE_VERSION) /** * Convert the item looped into an object used to extend the child tag properties * @param { Object } expr - object containing the keys used to extend the children tags * @param { * } key - value to assign to the new object returned * @param { * } val - value containing the position of the item in the array * @returns { Object } - new object containing the values of the original item * * The variables 'key' and 'val' are arbitrary. * They depend on the collection type looped (Array, Object) * and on the expression used on the each tag * */ function mkitem(expr, key, val) { var item = {} item[expr.key] = key if (expr.pos) item[expr.pos] = val return item } /** * Unmount the redundant tags * @param { Array } items - array containing the current items to loop * @param { Array } tags - array containing all the children tags */ function unmountRedundant(items, tags) { var i = tags.length, j = items.length, t while (i > j) { t = tags[--i] tags.splice(i, 1) t.unmount() } } /** * Move the nested custom tags in non custom loop tags * @param { Object } child - non custom loop tag * @param { Number } i - current position of the loop tag */ function moveNestedTags(child, i) { Object.keys(child.tags).forEach(function(tagName) { var tag = child.tags[tagName] if (isArray(tag)) each(tag, function (t) { moveChildTag(t, tagName, i) }) else moveChildTag(tag, tagName, i) }) } /** * Adds the elements for a virtual tag * @param { Tag } tag - the tag whose root's children will be inserted or appended * @param { Node } src - the node that will do the inserting or appending * @param { Tag } target - only if inserting, insert before this tag's first child */ function addVirtual(tag, src, target) { var el = tag._root, sib tag._virts = [] while (el) { sib = el.nextSibling if (target) src.insertBefore(el, target._root) else src.appendChild(el) tag._virts.push(el) // hold for unmounting el = sib } } /** * Move virtual tag and all child nodes * @param { Tag } tag - first child reference used to start move * @param { Node } src - the node that will do the inserting * @param { Tag } target - insert before this tag's first child * @param { Number } len - how many child nodes to move */ function moveVirtual(tag, src, target, len) { var el = tag._root, sib, i = 0 for (; i < len; i++) { sib = el.nextSibling src.insertBefore(el, target._root) el = sib } } /** * Manage tags having the 'each' * @param { Object } dom - DOM node we need to loop * @param { Tag } parent - parent tag instance where the dom node is contained * @param { String } expr - string contained in the 'each' attribute */ function _each(dom, parent, expr) { // remove the each property from the original tag remAttr(dom, 'each') var mustReorder = typeof getAttr(dom, 'no-reorder') !== T_STRING || remAttr(dom, 'no-reorder'), tagName = getTagName(dom), impl = __tagImpl[tagName] || { tmpl: dom.outerHTML }, useRoot = SPECIAL_TAGS_REGEX.test(tagName), root = dom.parentNode, ref = document.createTextNode(''), child = getTag(dom), isOption = /^option$/i.test(tagName), // the option tags must be treated differently tags = [], oldItems = [], hasKeys, isVirtual = dom.tagName == 'VIRTUAL' // parse the each expression expr = tmpl.loopKeys(expr) // insert a marked where the loop tags will be injected root.insertBefore(ref, dom) // clean template code parent.one('before-mount', function () { // remove the original DOM node dom.parentNode.removeChild(dom) if (root.stub) root = parent.root }).on('update', function () { // get the new items collection var items = tmpl(expr.val, parent), // create a fragment to hold the new DOM nodes to inject in the parent tag frag = document.createDocumentFragment() // object loop. any changes cause full redraw if (!isArray(items)) { hasKeys = items || false items = hasKeys ? Object.keys(items).map(function (key) { return mkitem(expr, key, items[key]) }) : [] } // loop all the new items items.forEach(function(item, i) { // reorder only if the items are objects var _mustReorder = mustReorder && item instanceof Object, oldPos = oldItems.indexOf(item), pos = ~oldPos && _mustReorder ? oldPos : i, // does a tag exist in this position? tag = tags[pos] item = !hasKeys && expr.key ? mkitem(expr, item, i) : item // new tag if ( !_mustReorder && !tag // with no-reorder we just update the old tags || _mustReorder && !~oldPos || !tag // by default we always try to reorder the DOM elements ) { tag = new Tag(impl, { parent: parent, isLoop: true, hasImpl: !!__tagImpl[tagName], root: useRoot ? root : dom.cloneNode(), item: item }, dom.innerHTML) tag.mount() if (isVirtual) tag._root = tag.root.firstChild // save reference for further moves or inserts // this tag must be appended if (i == tags.length) { if (isVirtual) addVirtual(tag, frag) else frag.appendChild(tag.root) } // this tag must be insert else { if (isVirtual) addVirtual(tag, root, tags[i]) else root.insertBefore(tag.root, tags[i].root) oldItems.splice(i, 0, item) } tags.splice(i, 0, tag) pos = i // handled here so no move } else tag.update(item) // reorder the tag if it's not located in its previous position if (pos !== i && _mustReorder) { // update the DOM if (isVirtual) moveVirtual(tag, root, tags[i], dom.childNodes.length) else root.insertBefore(tag.root, tags[i].root) // update the position attribute if it exists if (expr.pos) tag[expr.pos] = i // move the old tag instance tags.splice(i, 0, tags.splice(pos, 1)[0]) // move the old item oldItems.splice(i, 0, oldItems.splice(pos, 1)[0]) // if the loop tags are not custom // we need to move all their custom tags into the right position if (!child && tag.tags) moveNestedTags(tag, i) } // cache the original item to use it in the events bound to this node // and its children tag._item = item // cache the real parent tag internally defineProperty(tag, '_parent', parent) }) // remove the redundant tags unmountRedundant(items, tags) // insert the new nodes if (isOption) root.appendChild(frag) else root.insertBefore(frag, ref) // set the 'tags' property of the parent tag // if child is 'undefined' it means that we don't need to set this property // for example: // we don't need store the `myTag.tags['div']` property if we are looping a div tag // but we need to track the `myTag.tags['child']` property looping a custom child node named `child` if (child) parent.tags[tagName] = tags // clone the items array oldItems = items.slice() }) } /** * Object that will be used to inject and manage the css of every tag instance */ var styleManager = (function(_riot) { if (!window) return { // skip injection on the server add: function () {}, inject: function () {} } var styleNode = (function () { // create a new style element with the correct type var newNode = mkEl('style') setAttr(newNode, 'type', 'text/css') // replace any user node or insert the new one into the head var userNode = $('style[type=riot]') if (userNode) { if (userNode.id) newNode.id = userNode.id userNode.parentNode.replaceChild(newNode, userNode) } else document.getElementsByTagName('head')[0].appendChild(newNode) return newNode })() // Create cache and shortcut to the correct property var cssTextProp = styleNode.styleSheet, stylesToInject = '' // Expose the style node in a non-modificable property Object.defineProperty(_riot, 'styleNode', { value: styleNode, writable: true }) /** * Public api */ return { /** * Save a tag style to be later injected into DOM * @param { String } css [description] */ add: function(css) { stylesToInject += css }, /** * Inject all previously saved tag styles into DOM * innerHTML seems slow: http://jsperf.com/riot-insert-style */ inject: function() { if (stylesToInject) { if (cssTextProp) cssTextProp.cssText += stylesToInject else styleNode.innerHTML += stylesToInject stylesToInject = '' } } } })(riot) function parseNamedElements(root, tag, childTags, forceParsingNamed) { walk(root, function(dom) { if (dom.nodeType == 1) { dom.isLoop = dom.isLoop || (dom.parentNode && dom.parentNode.isLoop || getAttr(dom, 'each')) ? 1 : 0 // custom child tag if (childTags) { var child = getTag(dom) if (child && !dom.isLoop) childTags.push(initChildTag(child, {root: dom, parent: tag}, dom.innerHTML, tag)) } if (!dom.isLoop || forceParsingNamed) setNamed(dom, tag, []) } }) } function parseExpressions(root, tag, expressions) { function addExpr(dom, val, extra) { if (tmpl.hasExpr(val)) { expressions.push(extend({ dom: dom, expr: val }, extra)) } } walk(root, function(dom) { var type = dom.nodeType, attr // text node if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue) if (type != 1) return /* element */ // loop attr = getAttr(dom, '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) || {}, 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 = [], dom if (fn && root._tag) root._tag.unmount(true) // not yet mounted this.isMounted = false root.isLoop = isLoop // 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 defineProperty(this, '_riot_id', ++__uid) // base 1 allows test !t._riot_id 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 (tmpl.hasExpr(val)) attr[el.name] = val }) dom = mkdom(impl.tmpl, innerHTML) // options function updateOpts() { var ctx = hasImpl && isLoop ? self : parent || self // update opts from current DOM attributes each(root.attributes, function(el) { var val = el.value opts[toCamel(el.name)] = tmpl.hasExpr(val) ? tmpl(val, ctx) : val }) // recover those with expressions each(Object.keys(attr), function(name) { opts[toCamel(name)] = tmpl(attr[name], ctx) }) } function normalizeData(data) { for (var key in item) { if (typeof self[key] !== T_UNDEF && isWritable(self, key)) 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 = !contains(RESERVED_WORDS_BLACKLIST, k) && contains(propsInSyncWithParent, 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] } }) } defineProperty(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) { normalizeData(data) item = data } extend(self, data) updateOpts() self.trigger('update', data) update(expressions, self) // the updated event will be triggered // once the DOM will be ready and all the reflows are completed // this is useful if you want to get the "real" root properties // 4 ex: root.offsetWidth ... rAF(function() { self.trigger('updated') }) return this }) defineProperty(this, 'mixin', function() { each(arguments, function(mix) { var instance mix = typeof mix === T_STRING ? riot.mixin(mix) : mix // check if the mixin is a function if (isFunction(mix)) { // create the new mixin instance instance = new mix() // save the prototype to loop it afterwards mix = mix.prototype } else instance = mix // loop the keys in the function prototype or the all object keys each(Object.getOwnPropertyNames(mix), function(key) { // bind methods to self if (key != 'init') self[key] = isFunction(instance[key]) ? instance[key].bind(self) : instance[key] }) // init method will be called automatically if (instance.init) instance.init.bind(self)() }) return this }) defineProperty(this, 'mount', function() { updateOpts() // initialiation if (fn) fn.call(self, opts) // parse layout after init. fn may calculate args for nested custom tags parseExpressions(dom, self, expressions) // mount the child tags toggle(true) // update the root adding custom attributes coming from the compiler // it fixes also #1087 if (impl.attrs || hasImpl) { walkAttributes(impl.attrs, function (k, v) { setAttr(root, k, v) }) parseExpressions(self.root, self, expressions) } if (!self.parent || isLoop) self.update(item) // internal use only, fixes #403 self.trigger('before-mount') if (isLoop && !hasImpl) { // update the root attribute for the looped elements self.root = root = dom.firstChild } else { while (dom.firstChild) root.appendChild(dom.firstChild) if (root.stub) self.root = root = parent.root } // parse the named dom nodes in the looped child // adding them to the parent as well if (isLoop) parseNamedElements(self.root, self.parent, null, true) // 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') } }) }) defineProperty(this, 'unmount', function(keepRootTag) { var el = root, p = el.parentNode, ptag, tagIndex = __virtualDom.indexOf(self) self.trigger('before-unmount') // remove this tag instance from the global virtualDom variable if (~tagIndex) __virtualDom.splice(tagIndex, 1) if (this._virts) { each(this._virts, function(v) { if (v.parentNode) v.parentNode.removeChild(v) }) } if (p) { if (parent) { ptag = getImmediateCustomParentTag(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(ptag.tags[tagName])) each(ptag.tags[tagName], function(tag, i) { if (tag._riot_id == self._riot_id) ptag.tags[tagName].splice(i, 1) }) else // otherwise just delete the tag instance ptag.tags[tagName] = undefined } else while (el.firstChild) el.removeChild(el.firstChild) if (!keepRootTag) p.removeChild(el) else // the riot-tag attribute isn't needed anymore, remove it remAttr(p, 'riot-tag') } self.trigger('unmount') toggle() self.off('*') self.isMounted = false delete root._tag }) 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) return 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) } /** * Attach an event to a DOM node * @param { String } name - event name * @param { Function } handler - event callback * @param { Object } dom - dom node * @param { Tag } tag - tag instance */ function setEventHandler(name, handler, dom, tag) { dom[name] = function(e) { var ptag = tag._parent, item = tag._item, el if (!item) while (ptag && !item) { item = ptag._item ptag = ptag._parent } // cross browser event fix e = e || window.event // override the event properties if (isWritable(e, 'currentTarget')) e.currentTarget = dom if (isWritable(e, 'target')) e.target = e.srcElement if (isWritable(e, 'which')) e.which = e.charCode || e.keyCode e.item = item // prevent default behaviour (by default) if (handler.call(tag, e) !== true && !/radio|check/.test(dom.type)) { if (e.preventDefault) e.preventDefault() e.returnValue = false } if (!e.preventUpdate) { el = item ? getImmediateCustomParentTag(ptag) : tag el.update() } } } /** * Insert a DOM node replacing another one (used by if- attribute) * @param { Object } root - parent node * @param { Object } node - node replaced * @param { Object } before - node added */ function insertTo(root, node, before) { if (!root) return root.insertBefore(before, node) root.removeChild(node) } /** * Update the expressions in a Tag instance * @param { Array } expressions - expression that must be re evaluated * @param { Tag } tag - tag instance */ 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 (expr.bool) value = value ? attrName : false else if (value == null) value = '' // leave out riot- prefixes from strings inside textarea // fix #815: any value -> string if (parent && parent.tagName == 'TEXTAREA') { value = ('' + value).replace(/riot-/g, '') // change textarea's value parent.value = value } // no change if (expr.value === value) return expr.value = value // text node if (!attrName) { dom.nodeValue = '' + value // #815 related return } // 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 = function() { insertTo(stub.parentNode, stub, dom) }, remove = function() { insertTo(dom.parentNode, dom, stub) } // add to DOM if (value) { if (stub) { add() 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('') // if the parentNode is defined we can easily replace the tag if (dom.parentNode) remove() // otherwise we need to wait the updated event else (tag.parent || tag).one('updated', remove) 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 // } else if (startsWith(attrName, RIOT_PREFIX) && attrName != RIOT_TAG) { if (value) setAttr(dom, attrName.slice(RIOT_PREFIX.length), value) } else { if (expr.bool) { dom[attrName] = value if (!value) return } if (value === 0 || value && typeof value !== T_OBJECT) setAttr(dom, attrName, value) } }) } /** * Specialized function for looping an array-like collection with `each={}` * @param { Array } els - collection of items * @param {Function} fn - callback function * @returns { Array } the array looped */ function each(els, fn) { var len = els ? els.length : 0 for (var i = 0, el; i < len; i++) { el = els[i] // return false -> current item was removed by fn during the loop if (el != null && fn(el, i) === false) i-- } return els } /** * Detect if the argument passed is a function * @param { * } v - whatever you want to pass to this function * @returns { Boolean } - */ function isFunction(v) { return typeof v === T_FUNCTION || false // avoid IE problems } /** * Remove any DOM attribute from a node * @param { Object } dom - DOM node we want to update * @param { String } name - name of the property we want to remove */ function remAttr(dom, name) { dom.removeAttribute(name) } /** * Convert a string containing dashes to camel case * @param { String } string - input string * @returns { String } my-string -> myString */ function toCamel(string) { return string.replace(/-(\w)/g, function(_, c) { return c.toUpperCase() }) } /** * Get the value of any DOM attribute on a node * @param { Object } dom - DOM node we want to parse * @param { String } name - name of the attribute we want to get * @returns { String | undefined } name of the node attribute whether it exists */ function getAttr(dom, name) { return dom.getAttribute(name) } /** * Set any DOM attribute * @param { Object } dom - DOM node we want to update * @param { String } name - name of the property we want to set * @param { String } val - value of the property we want to set */ function setAttr(dom, name, val) { dom.setAttribute(name, val) } /** * Detect the tag implementation by a DOM node * @param { Object } dom - DOM node we need to parse to get its tag implementation * @returns { Object } it returns an object containing the implementation of a custom tag (template and boot function) */ function getTag(dom) { return dom.tagName && __tagImpl[getAttr(dom, RIOT_TAG) || dom.tagName.toLowerCase()] } /** * Add a child tag to its parent into the `tags` object * @param { Object } tag - child tag instance * @param { String } tagName - key where the new tag will be stored * @param { Object } parent - tag instance where the new child tag will be included */ function addChildTag(tag, tagName, parent) { var 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 (!isArray(cachedTag)) // don't add the same tag twice if (cachedTag !== tag) parent.tags[tagName] = [cachedTag] // add the new nested tag to the array if (!contains(parent.tags[tagName], tag)) parent.tags[tagName].push(tag) } else { parent.tags[tagName] = tag } } /** * Move the position of a custom tag in its parent tag * @param { Object } tag - child tag instance * @param { String } tagName - key where the tag was stored * @param { Number } newPos - index where the new tag will be stored */ function moveChildTag(tag, tagName, newPos) { var parent = tag.parent, tags // no parent no move if (!parent) return tags = parent.tags[tagName] if (isArray(tags)) tags.splice(newPos, 0, tags.splice(tags.indexOf(tag), 1)[0]) else addChildTag(tag, tagName, parent) } /** * Create a new child tag including it correctly into its parent * @param { Object } child - child tag implementation * @param { Object } opts - tag options containing the DOM node where the tag will be mounted * @param { String } innerHTML - inner html of the child node * @param { Object } parent - instance of the parent tag including the child custom tag * @returns { Object } instance of the new child tag just created */ function initChildTag(child, opts, innerHTML, parent) { var tag = new Tag(child, opts, innerHTML), tagName = getTagName(opts.root), ptag = getImmediateCustomParentTag(parent) // fix for the parent attribute in the looped elements tag.parent = ptag // store the real parent tag // in some cases this could be different from the custom parent tag // for example in nested loops tag._parent = parent // add this tag to the custom parent tag addChildTag(tag, tagName, ptag) // and also to the real parent tag if (ptag !== parent) addChildTag(tag, tagName, parent) // empty the child node once we got its template // to avoid that its children get compiled multiple times opts.root.innerHTML = '' return tag } /** * Loop backward all the parents tree to detect the first custom parent tag * @param { Object } tag - a Tag instance * @returns { Object } the instance of the first custom parent tag found */ function getImmediateCustomParentTag(tag) { var ptag = tag while (!getTag(ptag.root)) { if (!ptag.parent) break ptag = ptag.parent } return ptag } /** * Helper function to set an immutable property * @param { Object } el - object where the new property will be set * @param { String } key - object key where the new property will be stored * @param { * } value - value of the new property * @param { Object } options - set the propery overriding the default options * @returns { Object } - the initial object */ function defineProperty(el, key, value, options) { Object.defineProperty(el, key, extend({ value: value, enumerable: false, writable: false, configurable: false }, options)) return el } /** * Get the tag name of any DOM node * @param { Object } dom - DOM node we want to parse * @returns { String } name to identify this dom node in riot */ function getTagName(dom) { var child = getTag(dom), namedTag = getAttr(dom, 'name'), tagName = namedTag && !tmpl.hasExpr(namedTag) ? namedTag : child ? child.name : dom.tagName.toLowerCase() return tagName } /** * Extend any object with other properties * @param { Object } src - source object * @returns { Object } the resulting extended object * * var obj = { foo: 'baz' } * extend(obj, {bar: 'bar', foo: 'bar'}) * console.log(obj) => {bar: 'bar', foo: 'bar'} * */ function extend(src) { var obj, args = arguments for (var i = 1; i < args.length; ++i) { if (obj = args[i]) { for (var key in obj) { // check if this property of the source object could be overridden if (isWritable(src, key)) src[key] = obj[key] } } } return src } /** * Check whether an array contains an item * @param { Array } arr - target array * @param { * } item - item to test * @returns { Boolean } Does 'arr' contain 'item'? */ function contains(arr, item) { return ~arr.indexOf(item) } /** * Check whether an object is a kind of array * @param { * } a - anything * @returns {Boolean} is 'a' an array? */ function isArray(a) { return Array.isArray(a) || a instanceof Array } /** * Detect whether a property of an object could be overridden * @param { Object } obj - source object * @param { String } key - object property * @returns { Boolean } is this property writable? */ function isWritable(obj, key) { var props = Object.getOwnPropertyDescriptor(obj, key) return typeof obj[key] === T_UNDEF || props && props.writable } /** * With this function we avoid that the internal Tag methods get overridden * @param { Object } data - options we want to use to extend the tag instance * @returns { Object } clean object without containing the riot internal reserved words */ function cleanUpData(data) { if (!(data instanceof Tag) && !(data && typeof data.trigger == T_FUNCTION)) return data var o = {} for (var key in data) { if (!contains(RESERVED_WORDS_BLACKLIST, key)) o[key] = data[key] } return o } /** * Walk down recursively all the children tags starting dom node * @param { Object } dom - starting node where we will start the recursion * @param { Function } fn - callback to transform the child node just found */ function walk(dom, fn) { if (dom) { // stop the recursion if (fn(dom) === false) return else { dom = dom.firstChild while (dom) { walk(dom, fn) dom = dom.nextSibling } } } } /** * Minimize risk: only zero or one _space_ between attr & value * @param { String } html - html string we want to parse * @param { Function } fn - callback function to apply on any attribute found */ function walkAttributes(html, fn) { var m, re = /([-\w]+) ?= ?(?:"([^"]*)|'([^']*)|({[^}]*}))/g while (m = re.exec(html)) { fn(m[1].toLowerCase(), m[2] || m[3] || m[4]) } } /** * Check whether a DOM node is in stub mode, useful for the riot 'if' directive * @param { Object } dom - DOM node we want to parse * @returns { Boolean } - */ function isInStub(dom) { while (dom) { if (dom.inStub) return true dom = dom.parentNode } return false } /** * Create a generic DOM node * @param { String } name - name of the DOM node we want to create * @returns { Object } DOM node just created */ function mkEl(name) { return document.createElement(name) } /** * Shorter and fast way to select multiple nodes in the DOM * @param { String } selector - DOM selector * @param { Object } ctx - DOM node where the targets of our search will is located * @returns { Object } dom nodes found */ function $$(selector, ctx) { return (ctx || document).querySelectorAll(selector) } /** * Shorter and fast way to select a single node in the DOM * @param { String } selector - unique dom selector * @param { Object } ctx - DOM node where the target of our search will is located * @returns { Object } dom node found */ function $(selector, ctx) { return (ctx || document).querySelector(selector) } /** * Simple object prototypal inheritance * @param { Object } parent - parent object * @returns { Object } child instance */ function inherit(parent) { function Child() {} Child.prototype = parent return new Child() } /** * Get the name property needed to identify a DOM node in riot * @param { Object } dom - DOM node we need to parse * @returns { String | undefined } give us back a string to identify this dom node */ function getNamedKey(dom) { return getAttr(dom, 'id') || getAttr(dom, 'name') } /** * Set the named properties of a tag element * @param { Object } dom - DOM node we need to parse * @param { Object } parent - tag instance where the named dom element will be eventually added * @param { Array } keys - list of all the tag instance properties */ function setNamed(dom, parent, keys) { // get the key value we want to add to the tag instance var key = getNamedKey(dom), isArr, // add the node detected to a tag instance using the named property add = function(value) { // avoid to override the tag properties already set if (contains(keys, key)) return // check whether this value is an array isArr = isArray(value) // if the key was never set if (!value) // set it once on the tag instance parent[key] = dom // if it was an array and not yet set else if (!isArr || isArr && !contains(value, dom)) { // add the dom node into the array if (isArr) value.push(dom) else parent[key] = [value, dom] } } // skip the elements with no named properties if (!key) return // check whether this key has been already evaluated if (tmpl.hasExpr(key)) // wait the first updated event only once parent.one('mount', function() { key = getNamedKey(dom) add(parent[key]) }) else add(parent[key]) } /** * Faster String startsWith alternative * @param { String } src - source string * @param { String } str - test string * @returns { Boolean } - */ function startsWith(src, str) { return src.slice(0, str.length) === str } /** * requestAnimationFrame function * Adapted from https://gist.github.com/paulirish/1579671, license MIT */ var rAF = (function (w) { var raf = w.requestAnimationFrame || w.mozRequestAnimationFrame || w.webkitRequestAnimationFrame if (!raf || /iP(ad|hone|od).*OS 6/.test(w.navigator.userAgent)) { // buggy iOS6 var lastTime = 0 raf = function (cb) { var nowtime = Date.now(), timeout = Math.max(16 - (nowtime - lastTime), 0) setTimeout(function () { cb(lastTime = nowtime + timeout) }, timeout) } } return raf })(window || {}) /** * Mount a tag creating new Tag instance * @param { Object } root - dom node where the tag will be mounted * @param { String } tagName - name of the riot tag we want to mount * @param { Object } opts - options to pass to the Tag instance * @returns { Tag } a new Tag instance */ 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() // add this tag to the virtualDom variable if (!contains(__virtualDom, tag)) __virtualDom.push(tag) } return tag } /** * Riot public api */ // share methods for other riot parts, e.g. compiler riot.util = { brackets: brackets, tmpl: tmpl } /** * Create a mixin that could be globally shared across all the tags */ riot.mixin = (function() { var mixins = {} /** * Create/Return a mixin by its name * @param { String } name - mixin name * @param { Object } mixin - mixin logic * @returns { Object } the mixin logic */ return function(name, mixin) { if (!mixin) return mixins[name] mixins[name] = mixin } })() /** * Create a new riot tag implementation * @param { String } name - name/id of the new riot tag * @param { String } html - tag template * @param { String } css - custom tag css * @param { String } attrs - root tag attributes * @param { Function } fn - user function * @returns { String } name/id of the tag just created */ 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 styleManager.add(css) } __tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } return name } /** * Create a new riot tag implementation (for use by the compiler) * @param { String } name - name/id of the new riot tag * @param { String } html - tag template * @param { String } css - custom tag css * @param { String } attrs - root tag attributes * @param { Function } fn - user function * @param { string } [bpair] - brackets used in the compilation * @returns { String } name/id of the tag just created */ riot.tag2 = function(name, html, css, attrs, fn, bpair) { if (css) styleManager.add(css) //if (bpair) riot.settings.brackets = bpair __tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } return name } /** * Mount a tag using a specific tag implementation * @param { String } selector - tag DOM selector * @param { String } tagName - tag implementation name * @param { Object } opts - tag logic * @returns { Array } new tags instances */ riot.mount = function(selector, tagName, opts) { var els, allTags, tags = [] // helper functions function addRiotTags(arr) { var list = '' each(arr, function (e) { if (!/[^-\w]/.test(e)) list += ',*[' + RIOT_TAG + '=' + e.trim() + ']' }) return list } function selectAllTags() { var keys = Object.keys(__tagImpl) return keys + addRiotTags(keys) } function pushTags(root) { var last if (root.tagName) { if (tagName && (!(last = getAttr(root, RIOT_TAG)) || last != tagName)) setAttr(root, 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 ----- // inject styles into DOM styleManager.inject() 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(',')) // make sure to pass always a selector // to the querySelectorAll function els = selector ? $$(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 all the tags instances created * @returns { Array } all the tags instances */ riot.update = function() { return each(__virtualDom, function(tag) { tag.update() }) } /** * Export the Tag constructor */ riot.Tag = Tag /* istanbul ignore next */ /** * @module parsers */ var parsers = (function () { function _req (name) { var parser = window[name] if (parser) return parser throw new Error(name + ' parser not found.') } function extend (obj, props) { if (props) { for (var prop in props) { /* istanbul ignore next */ if (props.hasOwnProperty(prop)) { obj[prop] = props[prop] } } } return obj } var _p = { html: { jade: function (html, opts, url) { opts = extend({ pretty: true, filename: url, doctype: 'html' }, opts) return _req('jade').render(html, opts) } }, css: { less: function (tag, css, opts, url) { var ret opts = extend({ sync: true, syncImport: true, filename: url }, opts) _req('less').render(css, opts, function (err, result) { // istanbul ignore next if (err) throw err ret = result.css }) return ret } }, js: { es6: function (js, opts) { opts = extend({ blacklist: ['useStrict', 'strict', 'react'], sourceMaps: false, comments: false }, opts) return _req('babel').transform(js, opts).code }, babel: function (js, opts, url) { return _req('babel').transform(js, extend({ filename: url }, opts)).code }, coffee: function (js, opts) { return _req('CoffeeScript').compile(js, extend({ bare: true }, opts)) }, livescript: function (js, opts) { return _req('livescript').compile(js, extend({ bare: true, header: false }, opts)) }, typescript: function (js, opts) { return _req('typescript')(js, opts) }, none: function (js) { return js } } } _p.js.javascript = _p.js.none _p.js.coffeescript = _p.js.coffee return _p })() riot.parsers = parsers /** * Compiler for riot custom tags * @version v2.3.22 */ var compile = (function () { var S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source var S_STRINGS = brackets.R_STRINGS.source var HTML_ATTRS = / *([-\w:\xA0-\xFF]+) ?(?:= ?('[^']*'|"[^"]*"|\S+))?/g var HTML_COMMS = RegExp(//.source + '|' + S_LINESTR, 'g') var HTML_TAGS = /<([-\w]+)(?:\s+([^"'\/>]*(?:(?:"[^"]*"|'[^']*'|\/[^>])[^'"\/>]*)*)|\s*)(\/?)>/g var BOOL_ATTRS = RegExp( '^(?:disabled|checked|readonly|required|allowfullscreen|auto(?:focus|play)|' + 'compact|controls|default|formnovalidate|hidden|ismap|itemscope|loop|' + 'multiple|muted|no(?:resize|shade|validate|wrap)?|open|reversed|seamless|' + 'selected|sortable|truespeed|typemustmatch)$') var RIOT_ATTRS = ['style', 'src', 'd'] var VOID_TAGS = /^(?:input|img|br|wbr|hr|area|base|col|embed|keygen|link|meta|param|source|track)$/ var PRE_TAGS = /]*|"[^"]*")*)?>([\S\s]+?)<\/pre\s*>/gi var SPEC_TYPES = /^"(?:number|date(?:time)?|time|month|email|color)\b/i var TRIM_TRAIL = /[ \t]+$/gm var DQ = '"', SQ = "'" function cleanSource (src) { var mm, re = HTML_COMMS if (~src.indexOf('\r')) { src = src.replace(/\r\n?/g, '\n') } re.lastIndex = 0 while (mm = re.exec(src)) { if (mm[0][0] === '<') { src = RegExp.leftContext + RegExp.rightContext re.lastIndex = mm[3] + 1 } } return src } function parseAttribs (str, pcex) { var list = [], match, type, vexp HTML_ATTRS.lastIndex = 0 str = str.replace(/\s+/g, ' ') while (match = HTML_ATTRS.exec(str)) { var k = match[1].toLowerCase(), v = match[2] if (!v) { list.push(k) } else { if (v[0] !== DQ) { v = DQ + (v[0] === SQ ? v.slice(1, -1) : v) + DQ } if (k === 'type' && SPEC_TYPES.test(v)) { type = v } else { if (/\u0001\d/.test(v)) { if (k === 'value') vexp = 1 else if (BOOL_ATTRS.test(k)) k = '__' + k else if (~RIOT_ATTRS.indexOf(k)) k = 'riot-' + k } list.push(k + '=' + v) } } } if (type) { if (vexp) type = DQ + pcex._bp[0] + SQ + type.slice(1, -1) + SQ + pcex._bp[1] + DQ list.push('type=' + type) } return list.join(' ') } function splitHtml (html, opts, pcex) { var _bp = pcex._bp if (html && _bp[4].test(html)) { var jsfn = opts.expr && (opts.parser || opts.type) ? _compileJS : 0, list = brackets.split(html, 0, _bp), expr, israw for (var i = 1; i < list.length; i += 2) { expr = list[i] if (expr[0] === '^') { expr = expr.slice(1) } else if (jsfn) { israw = expr[0] === '=' expr = jsfn(israw ? expr.slice(1) : expr, opts).trim() if (expr.slice(-1) === ';') expr = expr.slice(0, -1) if (israw) expr = '=' + expr } list[i] = '\u0001' + (pcex.push(expr) - 1) + _bp[1] } html = list.join('') } return html } function restoreExpr (html, pcex) { if (pcex.length) { html = html .replace(/\u0001(\d+)/g, function (_, d) { var expr = pcex[d] if (expr[0] === '=') { expr = expr.replace(brackets.R_STRINGS, function (qs) { return qs .replace(//g, '>') }) } return pcex._bp[0] + expr.trim().replace(/[\r\n]+/g, ' ').replace(/"/g, '\u2057') }) } return html } function _compileHTML (html, opts, pcex) { html = splitHtml(html, opts, pcex) .replace(HTML_TAGS, function (_, name, attr, ends) { name = name.toLowerCase() ends = ends && !VOID_TAGS.test(name) ? '>' }) if (!opts.whitespace) { var p = [] if (/]/.test(html)) { html = html.replace(PRE_TAGS, function (q) { p.push(q) return '\u0002' }) } html = html.trim().replace(/\s+/g, ' ') if (p.length) html = html.replace(/\u0002/g, function () { return p.shift() }) } if (opts.compact) html = html.replace(/>[ \t]+<([-\w\/])/g, '><$1') return restoreExpr(html, pcex).replace(TRIM_TRAIL, '') } function compileHTML (html, opts, pcex) { if (Array.isArray(opts)) { pcex = opts opts = {} } else { if (!pcex) pcex = [] if (!opts) opts = {} } pcex._bp = brackets.array(opts.brackets) return _compileHTML(cleanSource(html), opts, pcex) } var JS_ES6SIGN = /^[ \t]*([$_A-Za-z][$\w]*)\s*\([^()]*\)\s*{/m var JS_ES6END = RegExp('[{}]|' + brackets.S_QBLOCKS, 'g') var JS_COMMS = RegExp(brackets.R_MLCOMMS.source + '|//[^\r\n]*|' + brackets.S_QBLOCKS, 'g') function riotjs (js) { var parts = [], match, toes5, pos, name, RE = RegExp if (~js.indexOf('/')) js = rmComms(js, JS_COMMS) while (match = js.match(JS_ES6SIGN)) { parts.push(RE.leftContext) js = RE.rightContext pos = skipBody(js, JS_ES6END) name = match[1] toes5 = !/^(?:if|while|for|switch|catch|function)$/.test(name) name = toes5 ? match[0].replace(name, 'this.' + name + ' = function') : match[0] parts.push(name, js.slice(0, pos)) js = js.slice(pos) if (toes5 && !/^\s*.\s*bind\b/.test(js)) parts.push('.bind(this)') } return parts.length ? parts.join('') + js : js function rmComms (s, r, m) { r.lastIndex = 0 while (m = r.exec(s)) { if (m[0][0] === '/' && !m[1] && !m[2]) { s = RE.leftContext + ' ' + RE.rightContext r.lastIndex = m[3] + 1 } } return s } function skipBody (s, r) { var m, i = 1 r.lastIndex = 0 while (i && (m = r.exec(s))) { if (m[0] === '{') ++i else if (m[0] === '}') --i } return i ? s.length : r.lastIndex } } function _compileJS (js, opts, type, parserOpts, url) { if (!/\S/.test(js)) return '' if (!type) type = opts.type var parser = opts.parser || (type ? parsers.js[type] : riotjs) if (!parser) { throw new Error('JS parser not found: "' + type + '"') } return parser(js, parserOpts, url).replace(/\r\n?/g, '\n').replace(TRIM_TRAIL, '') } function compileJS (js, opts, type, userOpts) { if (typeof opts === 'string') { userOpts = type type = opts opts = {} } if (type && typeof type === 'object') { userOpts = type type = '' } if (!userOpts) userOpts = {} return _compileJS(js, opts || {}, type, userOpts.parserOptions, userOpts.url) } var CSS_SELECTOR = RegExp('([{}]|^)[ ;]*([^@ ;{}][^{}]*)(?={)|' + S_LINESTR, 'g') function scopedCSS (tag, css) { var scope = ':scope' return css.replace(CSS_SELECTOR, function (m, p1, p2) { if (!p2) return m p2 = p2.replace(/[^,]+/g, function (sel) { var s = sel.trim() if (!s || s === 'from' || s === 'to' || s.slice(-1) === '%') { return sel } if (s.indexOf(scope) < 0) { s = tag + ' ' + s + ',[riot-tag="' + tag + '"] ' + s } else { s = s.replace(scope, tag) + ',' + s.replace(scope, '[riot-tag="' + tag + '"]') } return sel.slice(-1) === ' ' ? s + ' ' : s }) return p1 ? p1 + ' ' + p2 : p2 }) } function _compileCSS (css, tag, type, opts) { var scoped = (opts || (opts = {})).scoped if (type) { if (type === 'scoped-css') { scoped = true } else if (parsers.css[type]) { css = parsers.css[type](tag, css, opts.parserOpts || {}, opts.url) } else if (type !== 'css') { throw new Error('CSS parser not found: "' + type + '"') } } css = css.replace(brackets.R_MLCOMMS, '').replace(/\s+/g, ' ').trim() if (scoped) { if (!tag) { throw new Error('Can not parse scoped CSS without a tagName') } css = scopedCSS(tag, css) } return css } function compileCSS (css, type, opts) { if (type && typeof type === 'object') { opts = type type = '' } else if (!opts) opts = {} return _compileCSS(css, opts.tagName, type, opts) } var TYPE_ATTR = /\stype\s*=\s*(?:(['"])(.+?)\1|(\S+))/i var MISC_ATTR = '\\s*=\\s*(' + S_STRINGS + '|{[^}]+}|\\S+)' var END_TAGS = /\/>\n|^<(?:\/?[-\w]+\s*|[-\w]+\s+[-\w:\xA0-\xFF][\S\s]*?)>\n/ function _q (s, r) { if (!s) return "''" s = SQ + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + SQ return r && ~s.indexOf('\n') ? s.replace(/\n/g, '\\n') : s } function mktag (name, html, css, attribs, js, pcex) { var c = ', ', s = '}' + (pcex.length ? ', ' + _q(pcex._bp[8]) : '') + ');' if (js && js.slice(-1) !== '\n') s = '\n' + s return 'riot.tag2(\'' + name + SQ + c + _q(html, 1) + c + _q(css) + c + _q(attribs) + ', function(opts) {\n' + js + s } function splitBlocks (str) { if (/<[-\w]/.test(str)) { var m, k = str.lastIndexOf('<'), n = str.length while (~k) { m = str.slice(k, n).match(END_TAGS) if (m) { k += m.index + m[0].length return [str.slice(0, k), str.slice(k)] } n = k k = str.lastIndexOf('<', k - 1) } } return ['', str] } function getType (attribs) { if (attribs) { var match = attribs.match(TYPE_ATTR) match = match && (match[2] || match[3]) if (match) { return match.replace('text/', '') } } return '' } function getAttrib (attribs, name) { if (attribs) { var match = attribs.match(RegExp('\\s' + name + MISC_ATTR, 'i')) match = match && match[1] if (match) { return (/^['"]/).test(match) ? match.slice(1, -1) : match } } return '' } function getParserOptions (attribs) { var opts = getAttrib(attribs, 'options') return opts ? JSON.parse(opts) : null } function getCode (code, opts, attribs, base) { var type = getType(attribs) return _compileJS(code, opts, type, getParserOptions(attribs), base) } function cssCode (code, opts, attribs, url, tag) { var extraOpts = { parserOpts: getParserOptions(attribs), scoped: attribs && /\sscoped(\s|=|$)/i.test(attribs), url: url } return _compileCSS(code, tag, getType(attribs) || opts.style, extraOpts) } function compileTemplate (html, url, lang, opts) { var parser = parsers.html[lang] if (!parser) { throw new Error('Template parser not found: "' + lang + '"') } return parser(html, opts, url) } var CUST_TAG = RegExp(/^([ \t]*)<([-\w]+)(?:\s+([^'"\/>]+(?:(?:@|\/[^>])[^'"\/>]*)*)|\s*)?(?:\/>|>[ \t]*\n?([\S\s]*)^\1<\/\2\s*>|>(.*)<\/\2\s*>)/ .source.replace('@', S_STRINGS), 'gim'), SCRIPTS = /]*)?>\n?([\S\s]*?)<\/script\s*>/gi, STYLES = /]*)?>\n?([\S\s]*?)<\/style\s*>/gi function compile (src, opts, url) { var parts = [], included if (!opts) opts = {} included = opts.exclude ? function (s) { return opts.exclude.indexOf(s) < 0 } : function () { return 1 } if (!url) url = '' var _bp = brackets.array(opts.brackets) if (opts.template) { src = compileTemplate(src, url, opts.template, opts.templateOptions) } src = cleanSource(src) .replace(CUST_TAG, function (_, indent, tagName, attribs, body, body2) { var jscode = '', styles = '', html = '', pcex = [] pcex._bp = _bp tagName = tagName.toLowerCase() attribs = attribs && included('attribs') ? restoreExpr( parseAttribs( splitHtml(attribs, opts, pcex), pcex), pcex) : '' if ((body || (body = body2)) && /\S/.test(body)) { if (body2) { if (included('html')) html = _compileHTML(body2, opts, pcex) } else { body = body.replace(RegExp('^' + indent, 'gm'), '') body = body.replace(STYLES, function (_m, _attrs, _style) { if (included('css')) { styles += (styles ? ' ' : '') + cssCode(_style, opts, _attrs, url, tagName) } return '' }) body = body.replace(SCRIPTS, function (_m, _attrs, _script) { if (included('js')) { var code = getCode(_script, opts, _attrs, url) if (code) jscode += (jscode ? '\n' : '') + code } return '' }) var blocks = splitBlocks(body.replace(TRIM_TRAIL, '')) if (included('html')) { html = _compileHTML(blocks[0], opts, pcex) } if (included('js')) { body = _compileJS(blocks[1], opts, null, null, url) if (body) jscode += (jscode ? '\n' : '') + body } } } jscode = /\S/.test(jscode) ? jscode.replace(/\n{3,}/g, '\n\n') : '' if (opts.entities) { parts.push({ tagName: tagName, html: html, css: styles, attribs: attribs, js: jscode }) return '' } return mktag(tagName, html, styles, attribs, jscode, pcex) }) if (opts.entities) return parts return src } riot.util.compiler = { compile: compile, html: compileHTML, css: compileCSS, js: compileJS, version: 'v2.3.22' } return compile })() /* Compilation for the browser */ riot.compile = (function () { var promise, // emits the 'ready' event and runs the first callback ready // all the scripts were compiled? // gets the source of an external tag with an async call function GET (url, fn, opts) { var req = new XMLHttpRequest() req.onreadystatechange = function () { if (req.readyState === 4 && (req.status === 200 || !req.status && req.responseText.length)) { fn(req.responseText, opts, url) } } req.open('GET', url, true) req.send('') } // evaluates a compiled tag within the global context function globalEval (js, url) { if (typeof js === T_STRING) { var node = mkEl('script'), root = document.documentElement // make the source available in the "(no domain)" tab // of Chrome DevTools, with a .js extension if (url) js += '\n//# sourceURL=' + url + '.js' node.text = js root.appendChild(node) root.removeChild(node) } } // compiles all the internal and external tags on the page function compileScripts (fn, xopt) { var scripts = $$('script[type="riot/tag"]'), scriptsAmount = scripts.length function done() { promise.trigger('ready') ready = true if (fn) fn() } function compileTag (src, opts, url) { var code = compile(src, opts, url) globalEval(code, url) if (!--scriptsAmount) done() } if (!scriptsAmount) done() else { for (var i = 0; i < scripts.length; ++i) { var script = scripts[i], opts = extend({template: getAttr(script, 'template')}, xopt), url = getAttr(script, 'src') url ? GET(url, compileTag, opts) : compileTag(script.innerHTML, opts) } } } //// Entry point ----- return function (arg, fn, opts) { if (typeof arg === T_STRING) { // 2nd parameter is optional, but can be null if (fn && typeof fn === T_OBJECT) { opts = fn fn = false } // `riot.compile(tag [, callback | true][, options])` if (/^\s*