3219 lines
83 KiB
JavaScript
3219 lines
83 KiB
JavaScript
/* 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 = /<yield\s+to=(['"])?@\1\s*>([\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() + '</' + 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 {
|
|
var tname = rootEls[tagName]
|
|
if (tname && parent.children.length === 1) parent = $(tname, parent)
|
|
}
|
|
return parent
|
|
}
|
|
|
|
/**
|
|
* Replace the yield tag from any tag template with the innerHTML of the
|
|
* original tag in the page
|
|
* @param { String } templ - tag implementation template
|
|
* @param { String } html - original content of the tag in the DOM
|
|
* @returns { String } tag template updated without the yield tag
|
|
*/
|
|
function replaceYield(templ, html) {
|
|
// do nothing if no yield
|
|
if (!/<yield\b/i.test(templ)) return templ
|
|
|
|
// be careful with #1343 - string on the source having `$1`
|
|
var n = 0
|
|
templ = templ.replace(/<yield\s+from=['"]([-\w]+)['"]\s*(?:\/>|>\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(/<yield\s*(?:\/>|>\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
|
|
|
|
// <img src="{ expr }">
|
|
} 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(/<!--(?!>)[\S\s]*?-->/.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 = /<pre(?:\s+(?:[^">]*|"[^"]*")*)?>([\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, '<')
|
|
.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) ? '></' + name : ''
|
|
|
|
if (attr) name += ' ' + parseAttribs(attr, pcex)
|
|
|
|
return '<' + name + ends + '>'
|
|
})
|
|
|
|
if (!opts.whitespace) {
|
|
var p = []
|
|
|
|
if (/<pre[\s>]/.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 = /<script(\s+[^>]*)?>\n?([\S\s]*?)<\/script\s*>/gi,
|
|
|
|
STYLES = /<style(\s+[^>]*)?>\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*</m.test(arg)) {
|
|
var js = compile(arg, opts)
|
|
if (fn !== true) globalEval(js)
|
|
if (isFunction(fn)) fn(js, arg, opts)
|
|
return js
|
|
}
|
|
|
|
// `riot.compile(url [, callback][, options])`
|
|
GET(arg, function (str, opts, url) {
|
|
var js = compile(str, opts, url)
|
|
globalEval(js, url)
|
|
if (fn) fn(js, str, opts)
|
|
})
|
|
|
|
}
|
|
else {
|
|
|
|
// `riot.compile([callback][, options])`
|
|
if (isFunction(arg)) {
|
|
opts = fn
|
|
fn = arg
|
|
} else {
|
|
opts = arg
|
|
fn = undefined
|
|
}
|
|
|
|
if (ready) {
|
|
return fn && fn()
|
|
}
|
|
|
|
if (promise) {
|
|
if (fn) promise.on('ready', fn)
|
|
|
|
} else {
|
|
promise = riot.observable()
|
|
compileScripts(fn, opts)
|
|
}
|
|
}
|
|
}
|
|
|
|
})()
|
|
|
|
// reassign mount methods -----
|
|
var mount = riot.mount
|
|
|
|
riot.mount = function (a, b, c) {
|
|
var ret
|
|
riot.compile(function () { ret = mount(a, b, c) })
|
|
return ret
|
|
}
|
|
// support CommonJS, AMD & browser
|
|
/* istanbul ignore next */
|
|
if (typeof exports === T_OBJECT)
|
|
module.exports = riot
|
|
else if (typeof define === T_FUNCTION && typeof define.amd !== T_UNDEF)
|
|
define(function() { return riot })
|
|
else
|
|
window.riot = riot
|
|
|
|
})(typeof window != 'undefined' ? window : void 0);
|