Example using deferred unmount for riotjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Riot WIP, @license MIT */ | |
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | |
typeof define === 'function' && define.amd ? define(factory) : | |
(global.riot = factory()); | |
}(this, (function () { 'use strict'; | |
var __VIRTUAL_DOM = []; | |
var __TAG_IMPL = {}; | |
var GLOBAL_MIXIN = '__global_mixin'; | |
var RIOT_PREFIX = 'riot-'; | |
var RIOT_TAG_IS = 'data-is'; | |
var T_STRING = 'string'; | |
var T_OBJECT = 'object'; | |
var T_UNDEF = 'undefined'; | |
var T_FUNCTION = 'function'; | |
var XLINK_NS = 'http://www.w3.org/1999/xlink'; | |
var XLINK_REGEX = /^xlink:(\w+)/; | |
var WIN = typeof window === T_UNDEF ? undefined : window; | |
var RE_SPECIAL_TAGS = /^(?:t(?:body|head|foot|[rhd])|caption|col(?:group)?|opt(?:ion|group))$/; | |
var RE_SPECIAL_TAGS_NO_OPTION = /^(?:t(?:body|head|foot|[rhd])|caption|col(?:group)?)$/; | |
var RE_RESERVED_NAMES = /^(?:_(?:item|id|parent)|update|root|(?:un)?mount|mixin|is(?:Mounted|Loop)|tags|parent|opts|trigger|o(?:n|ff|ne))$/; | |
var RE_SVG_TAGS = /^(altGlyph|animate(?:Color)?|circle|clipPath|defs|ellipse|fe(?:Blend|ColorMatrix|ComponentTransfer|Composite|ConvolveMatrix|DiffuseLighting|DisplacementMap|Flood|GaussianBlur|Image|Merge|Morphology|Offset|SpecularLighting|Tile|Turbulence)|filter|font|foreignObject|g(?:lyph)?(?:Ref)?|image|line(?:arGradient)?|ma(?:rker|sk)|missing-glyph|path|pattern|poly(?:gon|line)|radialGradient|rect|stop|svg|switch|symbol|text(?:Path)?|tref|tspan|use)$/; | |
var RE_HTML_ATTRS = /([-\w]+) ?= ?(?:"([^"]*)|'([^']*)|({[^}]*}))/g; | |
var RE_BOOL_ATTRS = /^(?: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 IE_VERSION = (WIN && WIN.document || {}).documentMode | 0; | |
var FIREFOX = WIN && !!WIN.InstallTrigger | |
/** | |
* Check whether a DOM node must be considered a part of an svg document | |
* @param { String } name - | |
* @returns { Boolean } - | |
*/ | |
function isSVGTag(name) { | |
return RE_SVG_TAGS.test(name) | |
} | |
/** | |
* Check Check if the passed argument is undefined | |
* @param { String } value - | |
* @returns { Boolean } - | |
*/ | |
function isBoolAttr(value) { | |
return RE_BOOL_ATTRS.test(value) | |
} | |
/** | |
* Check if passed argument is a function | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isFunction(value) { | |
return typeof value === T_FUNCTION || false // avoid IE problems | |
} | |
/** | |
* Check if passed argument is an object, exclude null | |
* NOTE: use isObject(x) && !isArray(x) to excludes arrays. | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isObject(value) { | |
return value && typeof value === T_OBJECT // typeof null is 'object' | |
} | |
/** | |
* Check if passed argument is undefined | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isUndefined(value) { | |
return typeof value === T_UNDEF | |
} | |
/** | |
* Check if passed argument is a string | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isString(value) { | |
return typeof value === T_STRING | |
} | |
/** | |
* Check if passed argument is empty. Different from falsy, because we dont consider 0 or false to be blank | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isBlank(value) { | |
return isUndefined(value) || value === null || value === '' | |
} | |
/** | |
* Check if passed argument is a kind of array | |
* @param { * } value - | |
* @returns { Boolean } - | |
*/ | |
function isArray(value) { | |
return Array.isArray(value) || value instanceof Array | |
} | |
/** | |
* Check whether object's property could be overridden | |
* @param { Object } obj - source object | |
* @param { String } key - object property | |
* @returns { Boolean } - | |
*/ | |
function isWritable(obj, key) { | |
var descriptor = Object.getOwnPropertyDescriptor(obj, key) | |
return isUndefined(obj[key]) || descriptor && descriptor.writable | |
} | |
/** | |
* Check if passed argument is a reserved name | |
* @param { String } value - | |
* @returns { Boolean } - | |
*/ | |
function isReservedName(value) { | |
return RE_RESERVED_NAMES.test(value) | |
} | |
var check = Object.freeze({ | |
isSVGTag: isSVGTag, | |
isBoolAttr: isBoolAttr, | |
isFunction: isFunction, | |
isObject: isObject, | |
isUndefined: isUndefined, | |
isString: isString, | |
isBlank: isBlank, | |
isArray: isArray, | |
isWritable: isWritable, | |
isReservedName: isReservedName | |
}); | |
/** | |
* 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) | |
} | |
/** | |
* Create a document fragment | |
* @returns { Object } document fragment | |
*/ | |
function createFrag() { | |
return document.createDocumentFragment() | |
} | |
/** | |
* Create a document text node | |
* @returns { Object } create a text node to use as placeholder | |
*/ | |
function createDOMPlaceholder() { | |
return document.createTextNode('') | |
} | |
/** | |
* Create a generic DOM node | |
* @param { String } name - name of the DOM node we want to create | |
* @param { Boolean } isSvg - should we use a SVG as parent node? | |
* @returns { Object } DOM node just created | |
*/ | |
function mkEl(name, isSvg) { | |
return isSvg ? | |
document.createElementNS('http://www.w3.org/2000/svg', 'svg') : | |
document.createElement(name) | |
} | |
/** | |
* Get the outer html of any DOM node SVGs included | |
* @param { Object } el - DOM node to parse | |
* @returns { String } el.outerHTML | |
*/ | |
function getOuterHTML(el) { | |
if (el.outerHTML) | |
{ return el.outerHTML } | |
// some browsers do not support outerHTML on the SVGs tags | |
else { | |
var container = mkEl('div') | |
container.appendChild(el.cloneNode(true)) | |
return container.innerHTML | |
} | |
} | |
/** | |
* Set the inner html of any DOM node SVGs included | |
* @param { Object } container - DOM node where we'll inject new html | |
* @param { String } html - html to inject | |
*/ | |
function setInnerHTML(container, html) { | |
if (!isUndefined(container.innerHTML)) | |
{ container.innerHTML = html } | |
// some browsers do not support innerHTML on the SVGs tags | |
else { | |
var doc = new DOMParser().parseFromString(html, 'application/xml') | |
var node = container.ownerDocument.importNode(doc.documentElement, true) | |
container.appendChild(node) | |
} | |
} | |
/** | |
* 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) | |
} | |
/** | |
* 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) { | |
var xlink = XLINK_REGEX.exec(name) | |
if (xlink && xlink[1]) | |
{ dom.setAttributeNS(XLINK_NS, xlink[1], val) } | |
else | |
{ dom.setAttribute(name, val) } | |
} | |
/** | |
* Insert safely a tag to fix #1962 #1649 | |
* @param { HTMLElement } root - children container | |
* @param { HTMLElement } curr - node to insert | |
* @param { HTMLElement } next - node that should preceed the current node inserted | |
*/ | |
function safeInsert(root, curr, next) { | |
root.insertBefore(curr, next.parentNode && next) | |
} | |
/** | |
* 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 walkAttrs(html, fn) { | |
if (!html) | |
{ return } | |
var m | |
while (m = RE_HTML_ATTRS.exec(html)) | |
{ fn(m[1].toLowerCase(), m[2] || m[3] || m[4]) } | |
} | |
/** | |
* 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 | |
* @param { Object } context - fn can optionally return an object, which is passed to children | |
*/ | |
function walkNodes(dom, fn, context) { | |
if (dom) { | |
var res = fn(dom, context) | |
var next | |
// stop the recursion | |
if (res === false) { return } | |
dom = dom.firstChild | |
while (dom) { | |
next = dom.nextSibling | |
walkNodes(dom, fn, res) | |
dom = next | |
} | |
} | |
} | |
var dom = Object.freeze({ | |
$$: $$, | |
$: $, | |
createFrag: createFrag, | |
createDOMPlaceholder: createDOMPlaceholder, | |
mkEl: mkEl, | |
getOuterHTML: getOuterHTML, | |
setInnerHTML: setInnerHTML, | |
remAttr: remAttr, | |
getAttr: getAttr, | |
setAttr: setAttr, | |
safeInsert: safeInsert, | |
walkAttrs: walkAttrs, | |
walkNodes: walkNodes | |
}); | |
var styleNode; | |
var cssTextProp; | |
var byName = {}; | |
var remainder = [] | |
// skip the following code on the server | |
if (WIN) { | |
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 | |
})() | |
cssTextProp = styleNode.styleSheet | |
} | |
/** | |
* Object that will be used to inject and manage the css of every tag instance | |
*/ | |
var styleManager = { | |
styleNode: styleNode, | |
/** | |
* Save a tag style to be later injected into DOM | |
* @param { String } css - css string | |
* @param { String } name - if it's passed we will map the css to a tagname | |
*/ | |
add: function add(css, name) { | |
if (name) { byName[name] = css } | |
else { remainder.push(css) } | |
}, | |
/** | |
* Inject all previously saved tag styles into DOM | |
* innerHTML seems slow: http://jsperf.com/riot-insert-style | |
*/ | |
inject: function inject() { | |
if (!WIN) { return } | |
var style = Object.keys(byName) | |
.map(function(k) { return byName[k] }) | |
.concat(remainder).join('\n') | |
if (cssTextProp) { cssTextProp.cssText = style } | |
else { styleNode.innerHTML = style } | |
} | |
} | |
/** | |
* The riot template engine | |
* @version v2.4.1 | |
*/ | |
/** | |
* riot.util.brackets | |
* | |
* - `brackets ` - Returns a string or regex based on its parameter | |
* - `brackets.set` - Change the current riot brackets | |
* | |
* @module | |
*/ | |
/* global riot */ | |
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, | |
UNSUPPORTED = RegExp('[\\' + 'x00-\\x1F<>a-zA-Z0-9\'",;\\\\]'), | |
NEED_ESCAPE = /(?=[[\]()*+?.^$|])/g, | |
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 || UNSUPPORTED.test(pair)) { | |
throw new Error('Unsupported brackets "' + pair + '"') | |
} | |
arr = arr.concat(pair.replace(NEED_ESCAPE, '\\').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 | |
// istanbul ignore next | |
_tmpl.clearCache = function () { _cache = {} } | |
_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 + ';') // eslint-disable-line no-new-func | |
} | |
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 | |
} | |
_tmpl.version = brackets.version = 'v2.4.1' | |
return _tmpl | |
})() | |
/** | |
* Simple object prototypal inheritance | |
* @param { Object } parent - parent object | |
* @returns { Object } child instance | |
*/ | |
function inherit(parent) { | |
return Object.assign ? Object.assign({}, parent) : extend({}, parent) | |
} | |
/** | |
* Specialized function for looping an array-like collection with `each={}` | |
* @param { Array } list - collection of items | |
* @param {Function} fn - callback function | |
* @returns { Array } the array looped | |
*/ | |
function each(list, fn) { | |
var len = list ? list.length : 0 | |
for (var i = 0, el; i < len; ++i) { | |
el = list[i] | |
// return false -> current item was removed by fn during the loop | |
if (fn(el, i) === false) | |
{ i-- } | |
} | |
return list | |
} | |
/** | |
* Check whether an array contains an item | |
* @param { Array } array - target array | |
* @param { * } item - item to test | |
* @returns { Boolean } - | |
*/ | |
function contains(array, item) { | |
return ~array.indexOf(item) | |
} | |
/** | |
* Convert a string containing dashes to camel case | |
* @param { String } str - input string | |
* @returns { String } my-string -> myString | |
*/ | |
function toCamel(str) { | |
return str.replace(/-(\w)/g, function (_, c) { return c.toUpperCase(); }) | |
} | |
/** | |
* Faster String startsWith alternative | |
* @param { String } str - source string | |
* @param { String } value - test string | |
* @returns { Boolean } - | |
*/ | |
function startsWith(str, value) { | |
return str.slice(0, value.length) === value | |
} | |
/** | |
* 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: true | |
}, options)) | |
return el | |
} | |
/** | |
* 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 | |
} | |
var misc = Object.freeze({ | |
inherit: inherit, | |
each: each, | |
contains: contains, | |
toCamel: toCamel, | |
startsWith: startsWith, | |
defineProperty: defineProperty, | |
extend: extend | |
}); | |
var 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 | |
/** | |
* Public Api | |
*/ | |
// extend the el object adding the observable methods | |
Object.defineProperties(el, { | |
/** | |
* Listen to the given `event` ands | |
* execute the `callback` each time an event is triggered. | |
* @param { String } event - event id | |
* @param { Function } fn - callback function | |
* @returns { Object } el | |
*/ | |
on: { | |
value: function(event, fn) { | |
if (typeof fn == 'function') | |
{ (callbacks[event] = callbacks[event] || []).push(fn) } | |
return el | |
}, | |
enumerable: false, | |
writable: false, | |
configurable: false | |
}, | |
/** | |
* Removes the given `event` listeners | |
* @param { String } event - event id | |
* @param { Function } fn - callback function | |
* @returns { Object } el | |
*/ | |
off: { | |
value: function(event, fn) { | |
if (event == '*' && !fn) { callbacks = {} } | |
else { | |
if (fn) { | |
var arr = callbacks[event] | |
for (var i = 0, cb; cb = arr && arr[i]; ++i) { | |
if (cb == fn) { arr.splice(i--, 1) } | |
} | |
} else { delete callbacks[event] } | |
} | |
return el | |
}, | |
enumerable: false, | |
writable: false, | |
configurable: false | |
}, | |
/** | |
* Listen to the given `event` and | |
* execute the `callback` at most once | |
* @param { String } event - event id | |
* @param { Function } fn - callback function | |
* @returns { Object } el | |
*/ | |
one: { | |
value: function(event, fn) { | |
function on() { | |
el.off(event, on) | |
fn.apply(el, arguments) | |
} | |
return el.on(event, on) | |
}, | |
enumerable: false, | |
writable: false, | |
configurable: false | |
}, | |
/** | |
* Execute all callback functions that listen to | |
* the given `event` | |
* @param { String } event - event id | |
* @returns { Object } el | |
*/ | |
trigger: { | |
value: function(event) { | |
var arguments$1 = arguments; | |
// getting the arguments | |
var arglen = arguments.length - 1, | |
args = new Array(arglen), | |
fns, | |
fn, | |
i | |
for (i = 0; i < arglen; i++) { | |
args[i] = arguments$1[i + 1] // skip first argument | |
} | |
fns = slice.call(callbacks[event] || [], 0) | |
for (i = 0; fn = fns[i]; ++i) { | |
fn.apply(el, args) | |
if (fns[i] !== fn) { i-- } | |
} | |
if (callbacks['*'] && event != '*') | |
{ el.trigger.apply(el, ['*', event].concat(args)) } | |
return el | |
}, | |
enumerable: false, | |
writable: false, | |
configurable: false | |
} | |
}) | |
return el | |
} | |
var EVENTS_PREFIX_REGEX = /^on/ | |
/** | |
* Trigger DOM events | |
* @param { HTMLElement } dom - dom element target of the event | |
* @param { Function } handler - user function | |
* @param { Object } e - event object | |
*/ | |
function handleEvent(dom, handler, e) { | |
var ptag = this._parent, | |
item = this._item | |
if (!item) | |
{ while (ptag && !item) { | |
item = ptag._item | |
ptag = ptag._parent | |
} } | |
// 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 | |
handler.call(this, e) | |
if (!e.preventUpdate) { | |
getImmediateCustomParentTag(this).update() | |
} | |
} | |
/** | |
* 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) { | |
var eventName, | |
cb = handleEvent.bind(tag, dom, handler) | |
if (!dom.addEventListener) { | |
dom[name] = cb | |
return | |
} | |
// normalize event name | |
eventName = name.replace(EVENTS_PREFIX_REGEX, '') | |
// cache the callback directly on the DOM node | |
if (!dom._riotEvents) { dom._riotEvents = {} } | |
if (dom._riotEvents[name]) | |
{ dom.removeEventListener(eventName, dom._riotEvents[name]) } | |
dom._riotEvents[name] = cb | |
dom.addEventListener(eventName, cb, false) | |
} | |
/** | |
* Update dynamically created riot-tag with changing expressions | |
* @param { Object } expr - expression tag and expression info | |
* @param { Tag } parent - parent for tag creation | |
*/ | |
function updateRtag(expr, parent) { | |
var tagName = tmpl(expr.value, parent), | |
conf | |
if (expr.tag && expr.tagName === tagName) { | |
expr.tag.update() | |
return | |
} | |
// sync _parent to accommodate changing tagnames | |
if (expr.tag) { | |
var delName = expr.tag.opts.dataIs, | |
tags = expr.tag._parent.tags | |
arrayishRemove(tags, delName, expr.tag) | |
} | |
expr.impl = __TAG_IMPL[tagName] | |
conf = {root: expr.dom, parent: parent, hasImpl: true, tagName: tagName} | |
expr.tag = initChildTag(expr.impl, conf, expr.dom.innerHTML, parent) | |
expr.tagName = tagName | |
expr.tag.mount() | |
expr.tag.update() | |
// parent is the placeholder tag, not the dynamic tag so clean up | |
parent.on('unmount', function () { | |
var delName = expr.tag.opts.dataIs, | |
tags = expr.tag.parent.tags, | |
_tags = expr.tag._parent.tags | |
arrayishRemove(tags, delName, expr.tag) | |
arrayishRemove(_tags, delName, expr.tag) | |
expr.tag.unmount() | |
}) | |
} | |
/** | |
* Update on single tag expression | |
* @this Tag | |
* @param { Object } expr - expression logic | |
* @returns { undefined } | |
*/ | |
function updateExpression(expr) { | |
var dom = expr.dom, | |
attrName = expr.attr, | |
value = tmpl(expr.expr, this), | |
isValueAttr = attrName === 'value', | |
isVirtual = expr.root && expr.root.tagName === 'VIRTUAL', | |
parent = dom && (expr.parent || dom.parentNode), | |
old | |
if (expr.bool) | |
{ value = value ? attrName : false } | |
else if (isUndefined(value) || value === null) | |
{ value = '' } | |
if (expr._riot_id) { // if it's a tag | |
if (expr.isMounted) { | |
expr.update() | |
// if it hasn't been mounted yet, do that now. | |
} else { | |
expr.mount() | |
if (isVirtual) { | |
var frag = document.createDocumentFragment() | |
makeVirtual.call(expr, frag) | |
expr.root.parentElement.replaceChild(frag, expr.root) | |
} | |
} | |
return | |
} | |
old = expr.value | |
expr.value = value | |
if (expr.update) { | |
expr.update() | |
return | |
} | |
if (old === value) { return } | |
if (expr.isRtag && value) { return updateRtag(expr, this) } | |
// no change, so nothing more to do | |
if (isValueAttr && dom.value === value) { return } | |
// textarea and text nodes have no attribute name | |
if (!attrName) { | |
// about #815 w/o replace: the browser converts the value to a string, | |
// the comparison by "==" does too, but not in the server | |
value += '' | |
// test for parent avoids error with invalid assignment to nodeValue | |
if (parent) { | |
// cache the parent node because somehow it will become null on IE | |
// on the next iteration | |
expr.parent = parent | |
if (parent.tagName === 'TEXTAREA') { | |
parent.value = value // #1113 | |
if (!IE_VERSION) { dom.nodeValue = value } // #1625 IE throws here, nodeValue | |
} // will be available on 'updated' | |
else { dom.nodeValue = value } | |
} | |
return | |
} | |
// remove original attribute | |
remAttr(dom, attrName) | |
// event handler | |
if (isFunction(value)) { | |
setEventHandler(attrName, value, dom, this) | |
// show / hide | |
} else if (/^(show|hide)$/.test(attrName)) { | |
if (attrName === 'hide') { value = !value } | |
dom.style.display = value ? '' : 'none' | |
// field value | |
} else if (isValueAttr) { | |
dom.value = value | |
// <img src="{ expr }"> | |
} else if (startsWith(attrName, RIOT_PREFIX) && attrName !== RIOT_TAG_IS) { | |
if (value) | |
{ setAttr(dom, attrName.slice(RIOT_PREFIX.length), value) } | |
} else { | |
// <select> <option selected={true}> </select> | |
if (attrName === 'selected' && parent && /^(SELECT|OPTGROUP)$/.test(parent.nodeName) && value) | |
{ parent.value = dom.value } | |
if (expr.bool) { | |
dom[attrName] = value | |
if (!value) { return } | |
} | |
if (value === 0 || value && typeof value !== T_OBJECT) | |
{ setAttr(dom, attrName, value) } | |
} | |
} | |
/** | |
* Update all the expressions in a Tag instance | |
* @this Tag | |
* @param { Array } expressions - expression that must be re evaluated | |
*/ | |
function update$1$1(expressions) { | |
each(expressions, updateExpression.bind(this)) | |
} | |
var IfExpr = { | |
init: function init(dom, parentTag, expr) { | |
remAttr(dom, 'if') | |
this.parentTag = parentTag | |
this.expr = expr | |
this.stub = document.createTextNode('') | |
this.pristine = dom | |
var p = dom.parentNode | |
p.insertBefore(this.stub, dom) | |
p.removeChild(dom) | |
return this | |
}, | |
update: function update$1() { | |
var newValue = tmpl(this.expr, this.parentTag) | |
if (newValue && !this.current) { // insert | |
this.current = this.pristine.cloneNode(true) | |
this.stub.parentNode.insertBefore(this.current, this.stub) | |
this.expressions = [] | |
parseExpressions.apply(this.parentTag, [this.current, this.expressions, true]) | |
} | |
else if (!newValue && this.current) { // remove | |
unmountAll(this.expressions) | |
this.current.parentNode.removeChild(this.current) | |
this.current = null | |
this.expressions = [] | |
} | |
if (newValue) { update$1$1.call(this.parentTag, this.expressions) } | |
}, | |
unmount: function unmount() { | |
unmountAll(this.expressions || []) | |
delete this.pristine | |
delete this.parentNode | |
delete this.stub | |
} | |
} | |
var RefExpr = { | |
init: function init(dom, attrName, attrValue, parent) { | |
this.dom = dom | |
this.attr = attrName | |
this.rawValue = attrValue | |
this.parent = parent | |
this.hasExp = tmpl.hasExpr(attrValue) | |
this.firstRun = true | |
return this | |
}, | |
update: function update() { | |
var value = this.rawValue | |
if (this.hasExp) | |
{ value = tmpl(this.rawValue, this.parent) } | |
// if nothing changed, we're done | |
if (!this.firstRun && value === this.value) { return } | |
var customParent = this.parent && getImmediateCustomParentTag(this.parent) | |
// if the referenced element is a custom tag, then we set the tag itself, rather than DOM | |
var tagOrDom = this.tag || this.dom | |
// the name changed, so we need to remove it from the old key (if present) | |
if (!isBlank(this.value) && customParent) | |
{ arrayishRemove(customParent.refs, this.value, tagOrDom) } | |
if (isBlank(value)) { | |
// if the value is blank, we remove it | |
remAttr(this.dom, this.attr) | |
} else { | |
// add it to the refs of parent tag (this behavior was changed >=3.0) | |
if (customParent) { arrayishAdd(customParent.refs, value, tagOrDom) } | |
// set the actual DOM attr | |
setAttr(this.dom, this.attr, value) | |
} | |
this.value = value | |
this.firstRun = false | |
}, | |
unmount: function unmount() { | |
var tagOrDom = this.tag || this.dom | |
var customParent = this.parent && getImmediateCustomParentTag(this.parent) | |
if (!isBlank(this.value) && customParent) | |
{ arrayishRemove(customParent.refs, this.value, tagOrDom) } | |
delete this.dom | |
delete this.parent | |
} | |
} | |
/** | |
* 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 | |
* @param { Object } base - prototype object for the new item | |
* @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, base) { | |
var item = base ? Object.create(base) : {} | |
item[expr.key] = key | |
if (expr.pos) { item[expr.pos] = val } | |
return item | |
} | |
/** | |
* Unmount the redundant tags | |
* @param { Array } items - see maintainOrder param | |
* @param { Array } tags - array containing all the children tags | |
* @param { String } tagName - key used to identify the type of tag | |
* @param { Object } parent - parent tag to remove the child from | |
* @param { Boolean } maintainOrder - if true, param items contains items | |
* to keep. If false (default), param items contains current items to loop | |
* through. | |
*/ | |
function unmountRedundant(items, tags, tagName, parent, maintainOrder) { | |
if (maintainOrder) { | |
each(tags, function(tag, i) { | |
if (tag === undefined) { return } // fix for each function not reevaluating the length | |
if (!~items.indexOf(tag)) { | |
var t = tags.splice(i, 1)[0] | |
t.unmount() | |
arrayishRemove(parent.tags, tagName, t, true) | |
return false | |
} | |
}) | |
} else { | |
var i = tags.length, | |
j = items.length, | |
t | |
while (i > j) { | |
t = tags[--i] | |
tags.splice(i, 1) | |
t.unmount() | |
arrayishRemove(parent.tags, tagName, t, true) | |
} | |
} | |
} | |
/** | |
* Move the nested custom tags in non custom loop tags | |
* @this Tag | |
* @param { Number } i - current position of the loop tag | |
*/ | |
function moveNestedTags(i) { | |
var this$1 = this; | |
each(Object.keys(this.tags), function (tagName) { | |
var tag = this$1.tags[tagName] | |
if (isArray(tag)) | |
{ each(tag, function (t) { | |
moveChildTag.apply(t, [tagName, i]) | |
}) } | |
else | |
{ moveChildTag.apply(tag, [tagName, i]) } | |
}) | |
} | |
/** | |
* Move a child tag | |
* @this Tag | |
* @param { HTMLElement } root - dom node containing all the loop children | |
* @param { Tag } nextTag - instance of the next tag preceding the one we want to move | |
* @param { Boolean } isVirtual - is it a virtual tag? | |
*/ | |
function move(root, nextTag, isVirtual) { | |
if (isVirtual) | |
{ moveVirtual.apply(this, [root, nextTag]) } | |
else | |
{ safeInsert(root, this.root, nextTag.root) } | |
} | |
/** | |
* Insert and mount a child tag | |
* @this Tag | |
* @param { HTMLElement } root - dom node containing all the loop children | |
* @param { Tag } nextTag - instance of the next tag preceding the one we want to insert | |
* @param { Boolean } isVirtual - is it a virtual tag? | |
*/ | |
function insert(root, nextTag, isVirtual) { | |
if (isVirtual) | |
{ makeVirtual.apply(this, [root, nextTag]) } | |
else | |
{ safeInsert(root, this.root, nextTag.root) } | |
} | |
/** | |
* Append a new tag into the DOM | |
* @this Tag | |
* @param { HTMLElement } root - dom node containing all the loop children | |
* @param { Boolean } isVirtual - is it a virtual tag? | |
*/ | |
function append(root, isVirtual) { | |
if (isVirtual) | |
{ makeVirtual.call(this, root) } | |
else | |
{ root.appendChild(this.root) } | |
} | |
/** | |
* Update option nodes that seem to be buggy on Firefox see also #1374 | |
* @param { HTMLElement } root - <select> tag | |
*/ | |
function updateSelect(root) { | |
for (var n = 0; n < root.length; n++) { | |
if (root[n].__riot1374) { | |
root.selectedIndex = n // clear other options | |
delete root[n].__riot1374 | |
break | |
} | |
} | |
} | |
/** | |
* Manage tags having the 'each' | |
* @param { HTMLElement } 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 | |
* @returns { Object } expression object for this each loop | |
*/ | |
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 = __TAG_IMPL[tagName] || { tmpl: getOuterHTML(dom) }, | |
useRoot = RE_SPECIAL_TAGS.test(tagName), | |
root = dom.parentNode, | |
ref = createDOMPlaceholder(), | |
child = getTag(dom), | |
ifExpr = getAttr(dom, 'if'), | |
isOption = tagName.toLowerCase() === 'option', // the option tags must be treated differently | |
tags = [], | |
oldItems = [], | |
hasKeys, | |
isLoop = true, | |
isAnonymous = !__TAG_IMPL[tagName], | |
isVirtual = dom.tagName === 'VIRTUAL', | |
toKeep = [] | |
// parse the each expression | |
expr = tmpl.loopKeys(expr) | |
expr.isLoop = true | |
if (ifExpr) { remAttr(dom, 'if') } | |
// insert a marked where the loop tags will be injected | |
root.insertBefore(ref, dom) | |
root.removeChild(dom) | |
expr.update = function updateEach() { | |
// get the new items collection | |
var items = tmpl(expr.val, parent), | |
parentNode, | |
frag, | |
placeholder | |
root = ref.parentNode | |
if (parentNode) { | |
placeholder = createDOMPlaceholder('') | |
parentNode.insertBefore(placeholder, root) | |
parentNode.removeChild(root) | |
} else { | |
frag = createFrag() | |
} | |
// 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]) | |
}) : [] | |
} | |
if (ifExpr) { | |
items = items.filter(function(item, i) { | |
var context = mkitem(expr, item, i, parent) | |
return !!tmpl(ifExpr, context) | |
}) | |
} | |
// loop all the new items | |
each(items, function(item, i) { | |
// reorder only if the items are objects | |
var | |
_mustReorder = mustReorder && typeof item === T_OBJECT && !hasKeys, | |
oldPos = oldItems.indexOf(item), | |
pos = ~oldPos && (_mustReorder || isFunction(tags[oldPos].beforeUnmount) ) ? 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 | |
) { | |
var mustAppend = i === tags.length | |
tag = new Tag$$1(impl, { | |
parent: parent, | |
isLoop: isLoop, | |
isAnonymous: isAnonymous, | |
root: useRoot ? root : dom.cloneNode(), | |
item: item | |
}, dom.innerHTML) | |
// mount the tag | |
tag.mount() | |
if (mustAppend) | |
{ append.apply(tag, [frag || root, isVirtual]) } | |
else | |
{ insert.apply(tag, [root, tags[i], isVirtual]) } | |
if (!mustAppend) { oldItems.splice(i, 0, item) } | |
tags.splice(i, 0, tag) | |
if (child) { arrayishAdd(parent.tags, tagName, tag, true) } | |
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) { | |
move.apply(tag, [root, tags[i], isVirtual]) | |
// 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.call(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) | |
if (isFunction(tag.beforeUnmount)) | |
{ toKeep.push(tag) } | |
}) | |
// remove the redundant tags | |
unmountRedundant(toKeep.length ? toKeep : items, tags, tagName, parent, toKeep.length) | |
// #1374 FireFox bug in <option selected={expression}> | |
if (isOption && FIREFOX && !root.multiple) { updateSelect(root) } | |
// clone the items array | |
oldItems = items.slice() | |
// empty the items we are keeping | |
toKeep = [] | |
if (frag) { | |
root.insertBefore(frag, ref) | |
} else { | |
parentNode.insertBefore(root, placeholder) | |
parentNode.removeChild(placeholder) | |
} | |
} | |
expr.unmount = function() { | |
each(tags, function(t) { t.unmount() }) | |
} | |
return expr | |
} | |
/** | |
* Walk the tag DOM to detect the expressions to evaluate | |
* @this Tag | |
* @param { HTMLElement } root - root tag where we will start digging the expressions | |
* @param { Array } expressions - empty array where the expressions will be added | |
* @param { Boolean } mustIncludeRoot - flag to decide whether the root must be parsed as well | |
*/ | |
function parseExpressions(root, expressions, mustIncludeRoot) { | |
var this$1 = this; | |
var base = {parent: {children: expressions}} | |
walkNodes(root, function (dom, ctx) { | |
var type = dom.nodeType, parent = ctx.parent, attr, expr, tagImpl | |
if (!mustIncludeRoot && dom === root) { return {parent: parent} } | |
// text node | |
if (type === 3 && dom.parentNode.tagName !== 'STYLE' && tmpl.hasExpr(dom.nodeValue)) | |
{ parent.children.push({dom: dom, expr: dom.nodeValue}) } | |
if (type !== 1) { return ctx } // not an element | |
// loop. each does it's own thing (for now) | |
if (attr = getAttr(dom, 'each')) { | |
parent.children.push(_each(dom, this$1, attr)) | |
return false | |
} | |
// if-attrs become the new parent. Any following expressions (either on the current | |
// element, or below it) become children of this expression. | |
if (attr = getAttr(dom, 'if')) { | |
parent.children.push(Object.create(IfExpr).init(dom, this$1, attr)) | |
return false | |
} | |
if (expr = getAttr(dom, RIOT_TAG_IS)) { | |
if (tmpl.hasExpr(expr)) { | |
parent.children.push({isRtag: true, expr: expr, dom: dom}) | |
return false | |
} | |
} | |
// if this is a tag, stop traversing here. | |
// we ignore the root, since parseExpressions is called while we're mounting that root | |
tagImpl = getTag(dom) | |
if (tagImpl && (dom !== root || mustIncludeRoot)) { | |
var conf = {root: dom, parent: this$1, hasImpl: true} | |
parent.children.push(initChildTag(tagImpl, conf, dom.innerHTML, this$1)) | |
return false | |
} | |
// attribute expressions | |
parseAttributes.apply(this$1, [dom, dom.attributes, function(attr, expr) { | |
if (!expr) { return } | |
parent.children.push(expr) | |
}]) | |
// whatever the parent is, all child elements get the same parent. | |
// If this element had an if-attr, that's the parent for all child elements | |
return {parent: parent} | |
}, base) | |
} | |
/** | |
* Calls `fn` for every attribute on an element. If that attr has an expression, | |
* it is also passed to fn. | |
* @this Tag | |
* @param { HTMLElement } dom - dom node to parse | |
* @param { Array } attrs - array of attributes | |
* @param { Function } fn - callback to exec on any iteration | |
*/ | |
function parseAttributes(dom, attrs, fn) { | |
var this$1 = this; | |
each(attrs, function (attr) { | |
var name = attr.name, bool = isBoolAttr(name), expr | |
if (~['ref', 'data-ref'].indexOf(name)) { | |
expr = Object.create(RefExpr).init(dom, name, attr.value, this$1) | |
} else if (tmpl.hasExpr(attr.value)) { | |
expr = {dom: dom, expr: attr.value, attr: attr.name, bool: bool} | |
} | |
fn(attr, expr) | |
}) | |
} | |
/* | |
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 reHasYield = /<yield\b/i; | |
var reYieldAll = /<yield\s*(?:\/>|>([\S\s]*?)<\/yield\s*>|>)/ig; | |
var reYieldSrc = /<yield\s+to=['"]([^'">]*)['"]\s*>([\S\s]*?)<\/yield\s*>/ig; | |
var reYieldDest = /<yield\s+from=['"]?([-\w]+)['"]?\s*(?:\/>|>([\S\s]*?)<\/yield\s*>)/ig; | |
var rootEls = { tr: 'tbody', th: 'tr', td: 'tr', col: 'colgroup' }; | |
var tblTags = IE_VERSION && IE_VERSION < 10 ? RE_SPECIAL_TAGS : RE_SPECIAL_TAGS_NO_OPTION; | |
var GENERIC = 'div' | |
/* | |
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() + '</' + parent | |
parent = el.firstChild | |
// returns the immediate parent if tr/th/td/col is the only element, if not | |
// returns the whole tree, as this can include additional elements | |
if (select) { | |
parent.selectedIndex = -1 // for IE9, compatible w/current riot behavior | |
} else { | |
// avoids insertion of cointainer inside container (ex: tbody inside tbody) | |
var tname = rootEls[tagName] | |
if (tname && parent.childElementCount === 1) { parent = $(tname, parent) } | |
} | |
return parent | |
} | |
/* | |
Replace the yield tag from any tag template with the innerHTML of the | |
original tag in the page | |
*/ | |
function replaceYield(templ, html) { | |
// do nothing if no yield | |
if (!reHasYield.test(templ)) { return templ } | |
// be careful with #1343 - string on the source having `$1` | |
var src = {} | |
html = html && html.replace(reYieldSrc, function (_, ref, text) { | |
src[ref] = src[ref] || text // preserve first definition | |
return '' | |
}).trim() | |
return templ | |
.replace(reYieldDest, function (_, ref, def) { // yield with from - to attrs | |
return src[ref] || def || '' | |
}) | |
.replace(reYieldAll, function (_, def) { // yield without any "from" | |
return html || def || '' | |
}) | |
} | |
/** | |
* 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(GENERIC, 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 | |
} | |
/** | |
* Another way to create a riot tag a bit more es6 friendly | |
* @param { HTMLElement } el - tag DOM selector or DOM node/s | |
* @param { Object } opts - tag logic | |
* @returns { Tag } new riot tag instance | |
*/ | |
function Tag$1(el, opts) { | |
// get the tag properties from the class constructor | |
var ref = this; | |
var name = ref.name; | |
var tmpl = ref.tmpl; | |
var css = ref.css; | |
var attrs = ref.attrs; | |
var onCreate = ref.onCreate; | |
// register a new tag and cache the class prototype | |
if (!__TAG_IMPL[name]) { | |
tag(name, tmpl, css, attrs, onCreate) | |
// cache the class constructor | |
__TAG_IMPL[name].class = this.constructor | |
} | |
// mount the tag using the class instance | |
mountTo(el, name, opts, this) | |
return this | |
} | |
/** | |
* Create a new riot tag implementation | |
* @param { String } name - name/id of the new riot tag | |
* @param { String } tmpl - 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 | |
*/ | |
function tag(name, tmpl, 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) } | |
} | |
name = name.toLowerCase() | |
__TAG_IMPL[name] = { name: name, tmpl: tmpl, 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 } tmpl - 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 | |
*/ | |
function tag2(name, tmpl, css, attrs, fn) { | |
if (css) | |
{ styleManager.add(css, name) } | |
var exists = !!__TAG_IMPL[name] | |
__TAG_IMPL[name] = { name: name, tmpl: tmpl, attrs: attrs, fn: fn } | |
if (exists && riot.util.hotReloader) | |
{ riot.util.hotReloader(name) } | |
return name | |
} | |
/** | |
* Mount a tag using a specific tag implementation | |
* @param { * } selector - tag DOM selector or DOM node/s | |
* @param { String } tagName - tag implementation name | |
* @param { Object } opts - tag logic | |
* @returns { Array } new tags instances | |
*/ | |
function mount$1(selector, tagName, opts) { | |
var tags = [] | |
function pushTagsTo(root) { | |
if (root.tagName) { | |
var riotTag = getAttr(root, RIOT_TAG_IS) | |
// have tagName? force riot-tag to be the same | |
if (tagName && riotTag !== tagName) { | |
riotTag = tagName | |
setAttr(root, RIOT_TAG_IS, tagName) | |
} | |
var tag = mountTo(root, riotTag || root.tagName.toLowerCase(), opts) | |
if (tag) | |
{ tags.push(tag) } | |
} else if (root.length) | |
{ each(root, pushTagsTo) } // assume nodeList | |
} | |
// inject styles into DOM | |
styleManager.inject() | |
if (isObject(tagName)) { | |
opts = tagName | |
tagName = 0 | |
} | |
var elem | |
var allTags | |
// crawl the DOM to find the tag | |
if (isString(selector)) { | |
selector = selector === '*' ? | |
// select all registered tags | |
// & tags found with the riot-tag attribute set | |
allTags = selectTags() : | |
// or just the ones named like the selector | |
selector + selectTags(selector.split(/, */)) | |
// make sure to pass always a selector | |
// to the querySelectorAll function | |
elem = selector ? $$(selector) : [] | |
} | |
else | |
// probably you have passed already a tag or a NodeList | |
{ elem = selector } | |
// select all the registered and mount them inside their root elements | |
if (tagName === '*') { | |
// get all custom tags | |
tagName = allTags || selectTags() | |
// if the root els it's just a single tag | |
if (elem.tagName) | |
{ elem = $$(tagName, elem) } | |
else { | |
// select all the children for all the different root elements | |
var nodeList = [] | |
each(elem, function (_el) { return nodeList.push($$(tagName, _el)); }) | |
elem = nodeList | |
} | |
// get rid of the tagName | |
tagName = 0 | |
} | |
pushTagsTo(elem) | |
return tags | |
} | |
// Create a mixin that could be globally shared across all the tags | |
var mixins = {} | |
var globals = mixins[GLOBAL_MIXIN] = {} | |
var _id = 0 | |
/** | |
* Create/Return a mixin by its name | |
* @param { String } name - mixin name (global mixin if object) | |
* @param { Object } mix - mixin logic | |
* @param { Boolean } g - is global? | |
* @returns { Object } the mixin logic | |
*/ | |
function mixin(name, mix, g) { | |
// Unnamed global | |
if (isObject(name)) { | |
mixin(("__unnamed_" + (_id++)), name, true) | |
return | |
} | |
var store = g ? globals : mixins | |
// Getter | |
if (!mix) { | |
if (isUndefined(store[name])) | |
{ throw new Error('Unregistered mixin: ' + name) } | |
return store[name] | |
} | |
// Setter | |
store[name] = isFunction(mix) ? | |
extend(mix.prototype, store[name] || {}) && mix : | |
extend(store[name] || {}, mix) | |
} | |
/** | |
* Update all the tags instances created | |
* @returns { Array } all the tags instances | |
*/ | |
function update$2() { | |
return each(__VIRTUAL_DOM, function (tag) { return tag.update(); }) | |
} | |
function unregister(name) { | |
delete __TAG_IMPL[name] | |
} | |
// counter to give a unique id to all the Tag instances | |
var __uid = 0 | |
/** | |
* We need to update opts for this tag. That requires updating the expressions | |
* in any attributes on the tag, and then copying the result onto opts. | |
* @this Tag | |
* @param {Boolean} isLoop - is it a loop tag? | |
* @param { Tag } parent - parent tag node | |
* @param { Boolean } isAnonymous - is it a tag without any impl? (a tag not registered) | |
* @param { Object } opts - tag options | |
* @param { Array } instAttrs - tag attributes array | |
*/ | |
function updateOpts(isLoop, parent, isAnonymous, opts, instAttrs) { | |
// isAnonymous `each` tags treat `dom` and `root` differently. In this case | |
// (and only this case) we don't need to do updateOpts, because the regular parse | |
// will update those attrs. Plus, isAnonymous tags don't need opts anyway | |
if (isLoop && isAnonymous) { return } | |
var ctx = !isAnonymous && isLoop ? this : parent || this | |
each(instAttrs, function (attr) { | |
if (attr.expr) { update$1$1.call(ctx, [attr.expr]) } | |
opts[toCamel(attr.name)] = attr.expr ? attr.expr.value : attr.value | |
}) | |
} | |
/** | |
* Tag class | |
* @constructor | |
* @param { Object } impl - it contains the tag template, and logic | |
* @param { Object } conf - tag options | |
* @param { String } innerHTML - html that eventually we need to inject in the tag | |
*/ | |
function Tag$$1(impl, conf, innerHTML) { | |
var opts = inherit(conf.opts), | |
parent = conf.parent, | |
isLoop = conf.isLoop, | |
isAnonymous = conf.isAnonymous, | |
item = cleanUpData(conf.item), | |
instAttrs = [], // All attributes on the Tag when it's first parsed | |
implAttrs = [], // expressions on this type of Tag | |
expressions = [], | |
root = conf.root, | |
tagName = conf.tagName || root.tagName.toLowerCase(), | |
propsInSyncWithParent = [], | |
dom | |
// make this tag observable | |
observable(this) | |
// only call unmount if we have a valid __TAG_IMPL (has name property) | |
if (impl.name && root._tag) { root._tag.unmount(true) } | |
// not yet mounted | |
this.isMounted = false | |
root.isLoop = isLoop | |
defineProperty(this, '_internal', { | |
isAnonymous: isAnonymous, | |
instAttrs: instAttrs, | |
innerHTML: innerHTML, | |
// these vars will be needed only for the virtual tags | |
virts: [], | |
tail: null, | |
head: null | |
}) | |
// 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 }, item) | |
// protect the "tags" and "refs" property from being overridden | |
defineProperty(this, 'tags', {}) | |
defineProperty(this, 'refs', {}) | |
dom = mkdom(impl.tmpl, innerHTML) | |
/** | |
* Update the tag expressions and options | |
* @param { * } data - data we want to use to extend the tag properties | |
* @returns { Tag } | |
*/ | |
defineProperty(this, 'update', function tagUpdate(data) { | |
if (isFunction(this.shouldUpdate) && !this.shouldUpdate()) { return } | |
// make sure the data passed will not override | |
// the component core methods | |
data = cleanUpData(data) | |
// inherit properties from the parent, but only for isAnonymous tags | |
if (isLoop && isAnonymous) { inheritFrom.apply(this, [this.parent, propsInSyncWithParent]) } | |
extend(this, data) | |
updateOpts.apply(this, [isLoop, parent, isAnonymous, opts, instAttrs]) | |
if (this.isMounted) { this.trigger('update', data) } | |
update$1$1.call(this, expressions) | |
if (this.isMounted) { this.trigger('updated') } | |
return this | |
}) | |
/** | |
* Add a mixin to this tag | |
* @returns { Tag } | |
*/ | |
defineProperty(this, 'mixin', function tagMixin() { | |
var this$1 = this; | |
each(arguments, function (mix) { | |
var instance, | |
props = [], | |
obj | |
mix = isString(mix) ? mixin(mix) : mix | |
// check if the mixin is a function | |
if (isFunction(mix)) { | |
// create the new mixin instance | |
instance = new mix() | |
} else { instance = mix } | |
var proto = Object.getPrototypeOf(instance) | |
// build multilevel prototype inheritance chain property list | |
do { props = props.concat(Object.getOwnPropertyNames(obj || instance)) } | |
while (obj = Object.getPrototypeOf(obj || instance)) | |
// loop the keys in the function prototype or the all object keys | |
each(props, function (key) { | |
// bind methods to this | |
// allow mixins to override other properties/parent mixins | |
if (key !== 'init') { | |
// check for getters/setters | |
var descriptor = Object.getOwnPropertyDescriptor(instance, key) || Object.getOwnPropertyDescriptor(proto, key) | |
var hasGetterSetter = descriptor && (descriptor.get || descriptor.set) | |
// apply method only if it does not already exist on the instance | |
if (!this$1.hasOwnProperty(key) && hasGetterSetter) { | |
Object.defineProperty(this$1, key, descriptor) | |
} else { | |
this$1[key] = isFunction(instance[key]) ? | |
instance[key].bind(this$1) : | |
instance[key] | |
} | |
} | |
}) | |
// init method will be called automatically | |
if (instance.init) | |
{ instance.init.bind(this$1)() } | |
}) | |
return this | |
}) | |
/** | |
* Mount the current tag instance | |
* @returns { Tag } | |
*/ | |
defineProperty(this, 'mount', function tagMount() { | |
var this$1 = this; | |
root._tag = this // keep a reference to the tag just created | |
// add global mixins | |
var globalMixin = mixin(GLOBAL_MIXIN) | |
if (globalMixin) | |
{ for (var i in globalMixin) | |