/* Riot v2.5.0, @license MIT */ ;(function(window, undefined) { 'use strict'; var riot = { version: 'v2.5.0', 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 */ GLOBAL_MIXIN = '__global_mixin', // riot specific prefixes RIOT_PREFIX = 'riot-', RIOT_TAG = RIOT_PREFIX + 'tag', RIOT_TAG_IS = 'data-is', // 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|(?:un)?mount|mixin|is(?:Mounted|Loop)|tags|parent|opts|trigger|o(?:n|ff|ne))$/, // SVG tags list https://www.w3.org/TR/SVG/attindex.html#PresentationAttributes SVG_TAGS_LIST = ['altGlyph', 'animate', 'animateColor', 'circle', 'clipPath', 'defs', 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feFlood', 'feGaussianBlur', 'feImage', 'feMerge', 'feMorphology', 'feOffset', 'feSpecularLighting', 'feTile', 'feTurbulence', 'filter', 'font', 'foreignObject', 'g', 'glyph', 'glyphRef', 'image', 'line', 'linearGradient', 'marker', 'mask', 'missing-glyph', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'stop', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tref', 'tspan', 'use'], // version# for IE 8-11, 0 for others IE_VERSION = (window && window.document || {}).documentMode | 0, // detect firefox to fix #1374 FIREFOX = window && !!window.InstallTrigger /* istanbul ignore next */ riot.observable = function(el) { /** * Extend the original object or create a new empty one * @type { Object } */ el = el || {} /** * Private variables */ var callbacks = {}, slice = Array.prototype.slice /** * Private Methods */ /** * Helper function needed to get and loop all the events in a string * @param { String } e - event string * @param {Function} fn - callback */ function onEachEvent(e, fn) { var es = e.split(' '), l = es.length, i = 0, name, indx for (; i < l; i++) { name = es[i] indx = name.indexOf('.') if (name) fn( ~indx ? name.substring(0, indx) : name, i, ~indx ? name.slice(indx + 1) : null) } } /** * Public Api */ // extend the el object adding the observable methods Object.defineProperties(el, { /** * 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 */ on: { value: function(events, fn) { if (typeof fn != 'function') return el onEachEvent(events, function(name, pos, ns) { (callbacks[name] = callbacks[name] || []).push(fn) fn.typed = pos > 0 fn.ns = ns }) return el }, enumerable: false, writable: false, configurable: false }, /** * Removes the given space separated list of `events` listeners * @param { String } events - events ids * @param { Function } fn - callback function * @returns { Object } el */ off: { value: function(events, fn) { if (events == '*' && !fn) callbacks = {} else { onEachEvent(events, function(name, pos, ns) { if (fn || ns) { var arr = callbacks[name] for (var i = 0, cb; cb = arr && arr[i]; ++i) { if (cb == fn || ns && cb.ns == ns) arr.splice(i--, 1) } } else delete callbacks[name] }) } return el }, enumerable: false, writable: false, configurable: false }, /** * 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 */ one: { value: function(events, fn) { function on() { el.off(events, on) fn.apply(el, arguments) } return el.on(events, on) }, enumerable: false, writable: false, configurable: false }, /** * Execute all callback functions that listen to * the given space separated list of `events` * @param { String } events - events ids * @returns { Object } el */ trigger: { value: function(events) { // getting the arguments var arglen = arguments.length - 1, args = new Array(arglen), fns for (var i = 0; i < arglen; i++) { args[i] = arguments[i + 1] // skip first argument } onEachEvent(events, function(name, pos, ns) { fns = slice.call(callbacks[name] || [], 0) for (var i = 0, fn; fn = fns[i]; ++i) { if (fn.busy) continue fn.busy = 1 if (!ns || fn.ns == ns) 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 }, enumerable: false, writable: false, configurable: false } }) 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] || '' : (loc ? getPathFromRoot(href) : 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() // assign sub-router's main method var router = newSubRouter.m.bind(newSubRouter) // stop only this sub-router router.stop = newSubRouter.s.bind(newSubRouter) return router } /** * 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.4.0 */ /** * 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)) { // eslint-disable-line 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.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]) } 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 /* eslint-disable */ return new Function('E', expr + ';') /* eslint-enable */ } var CH_IDEXPR = '\u2057', RE_CSNAME = /^(?:(-?[_A-Za-z\xA0-\xFF][-\w\xA0-\xFF]*)|\u2057(\d+)~):/, RE_QBLOCK = RegExp(brackets.S_QBLOCKS, 'g'), RE_DQUOTE = /\u2057/g, RE_QBMARK = /\u2057(\d+)~/g function _getTmpl (str) { var qstr = [], expr, parts = brackets.split(str.replace(RE_DQUOTE, '"'), 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 } function _parseExpr (expr, asText, qstr) { expr = expr .replace(RE_QBLOCK, function (s, div) { return s.length > 2 && !div ? CH_IDEXPR + (qstr.push(s) - 1) + '~' : s }) .replace(/\s+/g, ' ').trim() .replace(/\ ?([[\({},?\.:])\ ?/g, '$1') if (expr) { var list = [], cnt = 0, match while (expr && (match = expr.match(RE_CSNAME)) && !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 // eslint-disable-next-line max-len 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.4.0' 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 _mkdom() { var reHasYield = /|>([\S\s]*?)<\/yield\s*>|>)/ig, reYieldSrc = /]*)['"]\s*>([\S\s]*?)<\/yield\s*>/ig, reYieldDest = /|>([\S\s]*?)<\/yield\s*>)/ig var rootEls = { tr: 'tbody', th: 'tr', td: 'tr', col: 'colgroup' }, tblTags = IE_VERSION && IE_VERSION < 10 ? SPECIAL_TAGS_REGEX : /^(?:t(?:body|head|foot|[rhd])|caption|col(?:group)?)$/ /** * Creates a DOM element to wrap the given content. Normally an `DIV`, but can be * also a `TABLE`, `SELECT`, `TBODY`, `TR`, or `COLGROUP` element. * * @param {string} templ - The template coming from the custom tag definition * @param {string} [html] - HTML content that comes from the DOM element where you * will mount the tag, mostly the original tag in the page * @returns {HTMLElement} DOM element with _templ_ merged through `YIELD` with the _html_. */ function _mkdom(templ, html) { var match = templ && templ.match(/^\s*<([-\w]+)/), tagName = match && match[1].toLowerCase(), el = mkEl('div', isSVGTag(tagName)) // replace all the yield tags with the tag inner html templ = replaceYield(templ, html) /* istanbul ignore next */ if (tblTags.test(tagName)) el = specialTags(el, templ, tagName) else setInnerHTML(el, templ) el.stub = true return el } /* Creates the root element for table or 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() + ' 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: getOuterHTML(dom) }, useRoot = SPECIAL_TAGS_REGEX.test(tagName), root = dom.parentNode, ref = document.createTextNode(''), child = getTag(dom), isOption = tagName.toLowerCase() === 'option', // 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 var i = 0, itemsLength = items.length for (; i < itemsLength; i++) { // reorder only if the items are objects var item = items[i], _mustReorder = mustReorder && typeof item == T_OBJECT && !hasKeys, 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 || !tags[i]) { // fix 1581 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) // #1374 some browsers reset selected here oldItems.splice(i, 0, item) } tags.splice(i, 0, tag) pos = i // handled here so no move } else tag.update(item, true) // reorder the tag if it's not located in its previous position if ( pos !== i && _mustReorder && tags[i] // fix 1581 unable to reproduce it in a test! ) { // 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 root.insertBefore(frag, ref) if (isOption) { // #1374 FireFox bug in