mithril
Version:
A framework for building brilliant applications
1,986 lines (1,704 loc) • 56.8 kB
JavaScript
;(function (global, factory) { // eslint-disable-line
"use strict"
/* eslint-disable no-undef */
var m = factory(typeof window !== "undefined" ? window : {})
if (typeof module === "object" && module != null && module.exports) {
module.exports = m
} else if (typeof define === "function" && define.amd) {
define(function () { return m })
} else {
global.m = m
}
/* eslint-enable no-undef */
})(this, function (window, undefined) { // eslint-disable-line
"use strict"
m.version = function () {
return "v0.2.1"
}
// Save these two.
var type = {}.toString
var hasOwn = {}.hasOwnProperty
function isFunction(object) {
return typeof object === "function"
}
function isObject(object) {
return type.call(object) === "[object Object]"
}
function isString(object) {
return type.call(object) === "[object String]"
}
var isArray = Array.isArray || function (object) {
return type.call(object) === "[object Array]"
}
function noop() {}
function forEach(list, f) {
for (var i = 0; i < list.length; i++) {
f(list[i], i)
}
}
function forOwn(obj, f) {
for (var prop in obj) {
if (hasOwn.call(obj, prop)) {
f(obj[prop], prop)
}
}
}
// caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame
// self invoking function needed because of the way mocks work
function initialize(window) {
$document = window.document
$location = window.location
$cancelAnimationFrame = window.cancelAnimationFrame ||
window.clearTimeout
$requestAnimationFrame = window.requestAnimationFrame ||
window.setTimeout
}
initialize(window)
// testing API
m.deps = function (mock) {
initialize(window = mock || window)
return window
}
function gettersetter(store) {
function prop() {
if (arguments.length) store = arguments[0]
return store
}
prop.toJSON = function () {
return store
}
return prop
}
function isPromise(object) {
return object != null && (isObject(object) || isFunction(object)) &&
isFunction(object.then)
}
function simpleResolve(p, callback) {
if (p.then) {
return p.then(callback)
} else {
return callback()
}
}
function propify(promise) {
var prop = m.prop()
promise.then(prop)
prop.then = function (resolve, reject) {
return promise.then(function () {
return resolve(prop())
}, reject)
}
prop.catch = function (reject) {
return promise.then(function () {
return prop()
}, reject)
}
prop.finally = function (callback) {
return promise.then(function (value) {
return simpleResolve(callback(), function () {
return value
})
}, function (reason) {
return simpleResolve(callback(), function () {
throw reason
})
})
}
return prop
}
m.prop = function (store) {
if (isPromise(store)) {
return propify(store)
} else {
return gettersetter(store)
}
}
/**
* @typedef {String} Tag
* A string that looks like -> div.classname#id[param=one][param2=two]
* Which describes a DOM node
*/
function checkForAttrs(pairs) {
return pairs != null && isObject(pairs) &&
!("tag" in pairs || "view" in pairs || "subtree" in pairs)
}
function parseSelector(tag, cell) {
var classes = []
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g
var match
while ((match = parser.exec(tag)) != null) {
if (match[1] === "" && match[2] != null) {
cell.tag = match[2]
} else if (match[1] === "#") {
cell.attrs.id = match[2]
} else if (match[1] === ".") {
classes.push(match[2])
} else if (match[3][0] === "[") {
var pair = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3])
cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true)
}
}
return classes
}
function assignAttrs(target, attrs, classAttr, classes) {
var hasClass = false
if (hasOwn.call(attrs, classAttr)) {
var value = attrs[classAttr]
if (value != null && value !== "") {
hasClass = true
classes.push(value)
}
}
forOwn(attrs, function (value, attr) {
target[attr] = attr === classAttr && hasClass ? "" : value
})
if (classes.length) {
target[classAttr] = classes.join(" ")
}
}
function parameterize(component) {
var args = []
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i])
}
var originalCtrl = component.controller || noop
function Ctrl() {
return originalCtrl.apply(this, args) || this
}
if (originalCtrl !== noop) {
Ctrl.prototype = originalCtrl.prototype
}
var originalView = component.view || noop
function view(ctrl) {
var rest = [ctrl].concat(args)
for (var i = 1; i < arguments.length; i++) {
rest.push(arguments[i])
}
return originalView.apply(component, rest)
}
view.$original = originalView
var output = {controller: Ctrl, view: view}
if (args[0] && args[0].key != null) {
output.attrs = {key: args[0].key}
}
return output
}
m.component = parameterize
/**
* @param {Tag} The DOM node tag
* @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
* @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array,
* or splat (optional)
*/
function m(tag, pairs) {
// The arguments are passed directly like this to delay array
// allocation.
if (isObject(tag)) return parameterize.apply(null, arguments)
if (!isString(tag)) {
throw new TypeError("selector in m(selector, attrs, children) " +
"should be a string")
}
// Degenerate case frequently trips people up. Check for it here so that
// people know it doesn't work.
if (!tag) {
throw new TypeError("selector cannot be an empty string")
}
var hasAttrs = checkForAttrs(pairs)
var args = []
for (var i = hasAttrs ? 2 : 1; i < arguments.length; i++) {
args.push(arguments[i])
}
var children
if (args.length === 1 && isArray(args[0])) {
children = args[0]
} else {
children = args
}
var cell = {
tag: "div",
attrs: {},
children: children
}
assignAttrs(
cell.attrs,
hasAttrs ? pairs : {},
hasAttrs && "class" in pairs ? "class" : "className",
parseSelector(tag, cell)
)
return cell
}
function forKeys(list, f) {
for (var i = 0; i < list.length; i++) {
var attrs = list[i]
attrs = attrs && attrs.attrs
if (attrs && attrs.key != null && f(attrs, i)) {
break
}
}
}
// This function was causing deopts in Chrome.
function dataToString(data) {
// data.toString() might throw or return null if data is the return
// value of Console.log in some versions of Firefox
try {
if (data != null && data.toString() != null) {
return data
}
} catch (e) {
// Swallow all errors here.
}
return ""
}
function flatten(list) {
// recursively flatten array
for (var i = 0; i < list.length; i++) {
if (isArray(list[i])) {
list = list.concat.apply([], list)
// check current index again while there is an array at this
// index.
i--
}
}
return list
}
function insertNode(parent, node, index) {
parent.insertBefore(node, parent.childNodes[index] || null)
}
// the below recursively manages creation/diffing/removal of DOM elements
// based on comparison between `data` and `cached`
//
// the diff algorithm can be summarized as this:
// 1) compare `data` and `cached`
// 2) if they are different, copy `data` to `cached` and update the DOM
// based on what the difference is
// 3) recursively apply this algorithm for every array and for the
// children of every virtual element
//
// the `cached` data structure is essentially the same as the previous
// redraw's `data` data structure, with a few additions:
// - `cached` always has a property called `nodes`, which is a list of
// DOM elements that correspond to the data represented by the
// respective virtual element
// - in order to support attaching `nodes` as a property of `cached`,
// `cached` is *always* a non-primitive object, i.e. if the data was
// a string, then cached is a String instance. If data was `null` or
// `undefined`, cached is `new String("")`
// - `cached also has a `cfgCtx` property, which is the state
// storage object exposed by config(element, isInitialized, context)
// - when `cached` is an Object, it represents a virtual element; when
// it's an Array, it represents a list of elements; when it's a
// String, Number or Boolean, it represents a text node
//
// `parent` is a DOM element used for W3C DOM API calls
// `pTag` is only used for handling a corner case for textarea values
// `pCache` is used to remove nodes in some multi-node cases
// `pIndex` and `index` are used to figure out the offset of nodes.
// They're artifacts from before arrays started being flattened and are
// likely refactorable
// `data` and `cached` are, respectively, the new and old nodes being
// diffed
// `reattach` is a flag indicating whether a parent node was recreated (if
// so, and if this node is reused, then this node must reattach itself to
// the new parent)
// `editable` is a flag that indicates whether an ancestor is
// contenteditable
// `ns` indicates the closest HTML namespace as it cascades down from an
// ancestor
// `cfgs` is a list of config functions to run after the topmost build call
// finishes running
//
// there's logic that relies on the assumption that null and undefined
// data are equivalent to empty strings
// - this prevents lifecycle surprises from procedural helpers that mix
// implicit and explicit return statements (e.g.
// function foo() {if (cond) return m("div")}
// - it simplifies diffing code
function buildContext(
parentElement,
parentTag,
parentCache,
parentIndex,
data,
cached,
shouldReattach,
index,
editable,
namespace,
configs
) {
return {
parent: parentElement,
pTag: parentTag,
pCache: parentCache,
pIndex: parentIndex,
data: data,
cached: cached,
reattach: shouldReattach,
index: index,
editable: editable,
ns: namespace,
cfgs: configs
}
}
function builderBuild(inst) {
inst.data = dataToString(inst.data)
if (inst.data.subtree === "retain") return inst.cached
builderMakeCache(inst)
if (isArray(inst.data)) {
return builderBuildArray(inst)
} else if (inst.data != null && isObject(inst.data)) {
return builderBuildObject(inst)
} else if (isFunction(inst.data)) {
return inst.cached
} else {
return builderHandleTextNode(inst)
}
}
function builderMakeCache(inst) {
if (inst.cached != null) {
if (type.call(inst.cached) === type.call(inst.data)) {
return
}
if (inst.pCache && inst.pCache.nodes) {
var offset = inst.index - inst.pIndex
var end = offset +
(isArray(inst.data) ? inst.data : inst.cached.nodes).length
clear(
inst.pCache.nodes.slice(offset, end),
inst.pCache.slice(offset, end))
} else if (inst.cached.nodes) {
clear(inst.cached.nodes, inst.cached)
}
}
inst.cached = new inst.data.constructor()
// if constructor creates a virtual dom element, use a blank object as
// the base cached node instead of copying the virtual el (#277)
if (inst.cached.tag) inst.cached = {}
inst.cached.nodes = []
}
var DELETION = 1
var INSERTION = 2
var MOVE = 3
function buildArrayKeys(data) {
var guid = 0
forKeys(data, function () {
forEach(data, function (attrs) {
attrs = attrs && attrs.attrs
if (attrs && attrs.key == null) {
attrs.key = "__mithril__" + guid++
}
})
return true
})
}
function builderBuildArrayChild(inst, child, cached, count) {
return builderBuild(buildContext(
inst.parent,
inst.pTag,
inst.cached,
inst.index,
child,
cached,
inst.reattach,
inst.index + count || count,
inst.editable,
inst.ns,
inst.cfgs
))
}
// This is by far the most performance-sensitive method here. If you make
// any changes, be careful to avoid performance regressions. Note that
// variable caching doesn't help, even in the loop.
function builderBuildArray(inst) { // eslint-disable-line max-statements
inst.data = flatten(inst.data)
var nodes = []
var intact = inst.cached.length === inst.data.length
var subArrayCount = 0
// keys algorithm:
// sort elements without recreating them if keys are present
//
// 1) create a map of all existing keys, and mark all for deletion
// 2) add new keys to map and mark them for addition
// 3) if key exists in new list, change action from deletion to a move
// 4) for each key, handle its corresponding action as marked in
// previous steps
var existing = {}
var shouldMaintainIdentities = false
forKeys(inst.cached, function (attrs, i) {
shouldMaintainIdentities = true
existing[attrs.key] = {
action: DELETION,
index: i
}
})
buildArrayKeys(inst.data)
if (shouldMaintainIdentities) {
builderDiffKeys(inst, existing)
}
// end key algorithm
// don't change: faster than forEach
var cacheCount = 0
for (var i = 0, len = inst.data.length; i < len; i++) {
// diff each item in the array
var item = builderBuildArrayChild(
inst,
inst.data[i],
inst.cached[cacheCount],
subArrayCount
)
if (item !== undefined) {
intact = intact && item.nodes.intact
subArrayCount += getSubArrayCount(item)
inst.cached[cacheCount++] = item
}
}
if (!intact) builderDiffArray(inst, nodes)
return inst.cached
}
function builderDiffKeys(inst, existing) {
var keysDiffer = inst.data.length !== inst.cached.length
if (!keysDiffer) {
forKeys(inst.data, function (attrs, i) {
var cachedCell = inst.cached[i]
return keysDiffer =
cachedCell &&
cachedCell.attrs &&
cachedCell.attrs.key !== attrs.key
})
}
if (keysDiffer) {
builderHandleKeysDiffer(inst, existing)
}
}
function builderHandleKeysDiffer(inst, existing) {
var cached = inst.cached.nodes
forKeys(inst.data, function (key, i) {
key = key.key
if (existing[key]) {
existing[key] = {
action: MOVE,
index: i,
from: existing[key].index,
element: cached[existing[key].index] ||
$document.createElement("div")
}
} else {
existing[key] = {
action: INSERTION,
index: i
}
}
})
var actions = []
forOwn(existing, function (value) {
actions.push(value)
})
var changes = actions.sort(sortChanges)
var newCached = new Array(inst.cached.length)
newCached.nodes = inst.cached.nodes.slice()
forEach(changes, function (change) {
var index = change.index
switch (change.action) {
case DELETION:
clear(inst.cached[index].nodes, inst.cached[index])
newCached.splice(index, 1)
break
case INSERTION:
var dummy = $document.createElement("div")
dummy.key = inst.data[index].attrs.key
insertNode(inst.parent, dummy, index)
newCached.splice(index, 0, {
attrs: {key: inst.data[index].attrs.key},
nodes: [dummy]
})
newCached.nodes[index] = dummy
break
case MOVE:
var changeElement = change.element
// changeElement is never null
if (inst.parent.childNodes[index] !== changeElement) {
inst.parent.insertBefore(
changeElement,
inst.parent.childNodes[index] || null
)
}
newCached[index] = inst.cached[change.from]
newCached.nodes[index] = changeElement
}
})
inst.cached = newCached
}
// diffs the array itself
function builderDiffArray(inst, nodes) {
// update the list of DOM nodes by collecting the nodes from each item
for (var i = 0, len = inst.data.length; i < len; i++) {
var item = inst.cached[i]
if (item != null) {
nodes.push.apply(nodes, item.nodes)
}
}
// remove items from the end of the array if the new array is shorter
// than the old one. if errors ever happen here, the issue is most
// likely a bug in the construction of the `cached` data structure
// somewhere earlier in the program
forEach(inst.cached.nodes, function (node, i) {
if (node.parentNode != null && nodes.indexOf(node) < 0) {
clear([node], [inst.cached[i]])
}
})
if (inst.data.length < inst.cached.length) {
inst.cached.length = inst.data.length
}
inst.cached.nodes = nodes
}
function builderInitAttrs(inst) {
var dataAttrs = inst.data.attrs = inst.data.attrs || {}
inst.cached.attrs = inst.cached.attrs || {}
var dataAttrKeys = Object.keys(inst.data.attrs)
builderMaybeRecreateObject(inst, dataAttrKeys)
return dataAttrKeys.length > +("key" in dataAttrs)
}
function builderGetObjectNamespace(inst) {
var data = inst.data
return data.attrs.xmlns ? data.attrs.xmlns :
data.tag === "svg" ? "http://www.w3.org/2000/svg" :
data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" :
inst.ns
}
function builderBuildObject(inst) {
var views = []
var controllers = []
builderMarkViews(inst, views, controllers)
if (!inst.data.tag && controllers.length) {
throw new Error("Component template must return a virtual " +
"element, not an array, string, etc.")
}
var hasKeys = builderInitAttrs(inst)
if (isString(inst.data.tag)) {
return objectBuild({
builder: inst,
hasKeys: hasKeys,
views: views,
controllers: controllers,
ns: builderGetObjectNamespace(inst)
})
}
}
function builderMarkViews(inst, views, controllers) {
var cached = inst.cached && inst.cached.controllers
while (inst.data.view != null) {
builderCheckView(inst, cached, controllers, views)
}
}
var forcing = false
var pendingRequests = 0
function builderCheckView(inst, cached, controllers, views) {
var view = inst.data.view.$original || inst.data.view
var controller = getController(
inst.cached.views,
view,
cached,
inst.data.controller
)
// Faster to coerce to number and check for NaN
var key = +(inst.data && inst.data.attrs && inst.data.attrs.key)
if (pendingRequests === 0 || forcing ||
cached && cached.indexOf(controller) > -1) {
inst.data = inst.data.view(controller)
} else {
inst.data = {tag: "placeholder"}
}
if (inst.data.subtree === "retain") return inst.cached
if (key === key) { // eslint-disable-line no-self-compare
(inst.data.attrs = inst.data.attrs || {}).key = key
}
updateLists(views, controllers, view, controller)
}
var unloaders = []
function unloaderHandler(inst, ev) {
inst.ctrls.splice(inst.ctrls.indexOf(inst.ctrl), 1)
inst.views.splice(inst.views.indexOf(inst.view), 1)
if (inst.ctrl && isFunction(inst.ctrl.onunload)) {
inst.ctrl.onunload(ev)
}
}
function updateLists(views, controllers, view, controller) {
views.push(view)
unloaders[controllers.push(controller) - 1] = {
views: views,
view: view,
ctrl: controller,
ctrls: controllers
}
}
var redrawing = false
m.redraw = function (force) {
if (redrawing) return
redrawing = true
if (force) forcing = true
try {
attemptRedraw(force)
} finally {
redrawing = forcing = false
}
}
var redrawStrategy = m.redraw.strategy = m.prop()
function getController(views, view, cached, controller) {
var index = redrawStrategy() === "diff" && views ?
views.indexOf(view) :
-1
if (index > -1) {
return cached[index]
} else if (isFunction(controller)) {
return new controller()
} else {
return {}
}
}
function builderMaybeRecreateObject(inst, dataAttrKeys) {
// if an element is different enough from the one in cache, recreate it
if (builderElemIsDifferentEnough(inst, dataAttrKeys)) {
if (inst.cached.nodes.length) clear(inst.cached.nodes)
if (inst.cached.cfgCtx &&
isFunction(inst.cached.cfgCtx.onunload)) {
inst.cached.cfgCtx.onunload()
}
if (inst.cached.controllers) {
forEach(inst.cached.controllers, function (controller) {
if (controller.unload) {
controller.onunload({preventDefault: noop})
}
})
}
}
}
// shallow array compare, assumes strings
function arraySortCompare(a, b) {
var len = a.length
if (len !== b.length) return false
// A string-integer map is used to simplify the algorithm from
// two `O(n * log(n))` loops + an `O(n)` loop to just two O(n) loops
// with constant-time (or a super cheap `log(n)`) string key lookup.
var i = 0
var cache = Object.create(null)
while (i < len) cache[b[i]] = i++
while (i !== 0) {
if (cache[a[--i]] === undefined) return false
}
return true
}
function builderElemIsDifferentEnough(inst, dataAttrKeys) {
var data = inst.data
var cached = inst.cached
if (data.tag !== cached.tag) return true
if (!arraySortCompare(dataAttrKeys, Object.keys(cached.attrs))) {
return true
}
if (data.attrs.id !== cached.attrs.id) return true
if (data.attrs.key !== cached.attrs.key) return true
if (redrawStrategy() === "all") {
return !cached.cfgCtx || cached.cfgCtx.retain !== true
} else if (redrawStrategy() === "diff") {
return cached.cfgCtx && cached.cfgCtx.retain === false
} else {
return false
}
}
function objectBuildNewNode(inst) {
var node = objectCreateNode(inst)
inst.builder.cached = objectReconstruct(
inst,
node,
objectCreateAttrs(inst, node),
objectBuildChildren(inst, node)
)
return node
}
function objectBuild(inst) {
var builder = inst.builder
var isNew = builder.cached.nodes.length === 0
var node = isNew ?
objectBuildNewNode(inst) :
objectBuildUpdatedNode(inst)
if (isNew || builder.reattach && node != null) {
insertNode(builder.parent, node, builder.index)
}
builderScheduleConfigs(builder, node, isNew)
return builder.cached
}
function objectCreateNode(inst) {
var data = inst.builder.data
if (inst.ns === undefined) {
if (data.attrs.is) {
return $document.createElement(data.tag, data.attrs.is)
} else {
return $document.createElement(data.tag)
}
} else if (data.attrs.is) {
return $document.createElementNS(inst.ns, data.tag, data.attrs.is)
} else {
return $document.createElementNS(inst.ns, data.tag)
}
}
function objectCreateAttrs(inst, node) {
var data = inst.builder.data
if (inst.hasKeys) {
return setAttributes(node, data.tag, data.attrs, {}, inst.ns)
} else {
return data.attrs
}
}
function objectMakeChild(inst, node, shouldReattach) {
var builder = inst.builder
return builderBuild(buildContext(
node,
builder.data.tag,
undefined,
undefined,
builder.data.children,
builder.cached.children,
shouldReattach,
0,
builder.data.attrs.contenteditable ? node : builder.editable,
inst.ns,
builder.cfgs
))
}
function objectBuildChildren(inst, node) {
var children = inst.builder.data.children
if (children != null && children.length) {
return objectMakeChild(inst, node, true)
} else {
return children
}
}
function objectReconstruct(inst, node, attrs, children) {
var data = inst.builder.data
var cached = {
tag: data.tag,
attrs: attrs,
children: children,
nodes: [node]
}
objectUnloadCachedControllers(inst, cached)
if (cached.children && !cached.children.nodes) {
cached.children.nodes = []
}
// edge case: setting value on <select> doesn't work before children
// exist, so set it again after children have been created
if (data.tag === "select" && "value" in data.attrs) {
setAttributes(node, data.tag, {value: data.attrs.value}, {},
inst.ns)
}
return cached
}
function unloadSingleCachedController(controller) {
if (controller.onunload && controller.onunload.$old) {
controller.onunload = controller.onunload.$old
}
if (pendingRequests && controller.onunload) {
var onunload = controller.onunload
controller.onunload = noop
controller.onunload.$old = onunload
}
}
function objectUnloadCachedControllers(inst, cached) {
if (inst.controllers.length) {
cached.views = inst.views
cached.controllers = inst.controllers
forEach(inst.controllers, unloadSingleCachedController)
}
}
function objectBuildUpdatedNode(inst) {
var cached = inst.builder.cached
var node = cached.nodes[0]
if (inst.hasKeys) {
setAttributes(
node,
inst.builder.data.tag,
inst.builder.data.attrs,
cached.attrs,
inst.ns
)
}
cached.children = objectMakeChild(inst, node, false)
cached.nodes.intact = true
if (inst.controllers.length) {
cached.views = inst.views
cached.controllers = inst.controllers
}
return node
}
function builderScheduleConfigs(inst, node, isNew) {
var data = inst.data
var cached = inst.cached
// They are called after the tree is fully built
var config = data.attrs.config
if (isFunction(config)) {
var context = cached.cfgCtx = cached.cfgCtx || {}
inst.cfgs.push(function () {
return config.call(data, node, !isNew, context, cached)
})
}
}
function builderHandleTextNode(inst) {
if (inst.cached.nodes.length === 0) {
return builderHandleNonexistentNodes(inst)
} else if (inst.cached.valueOf() !== inst.data.valueOf() ||
inst.reattach) {
return builderReattachNodes(inst)
} else {
inst.cached.nodes.intact = true
return inst.cached
}
}
function nodeHasBody(node) {
return !/^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ // eslint-disable-line max-len
.test(node)
}
function builderHandleNonexistentNodes(inst) {
var nodes
if (inst.data.$trusted) {
nodes = injectHTML(inst.parent, inst.index, inst.data)
} else {
nodes = [$document.createTextNode(inst.data)]
if (nodeHasBody(inst.parent.nodeName)) {
insertNode(inst.parent, nodes[0], inst.index)
}
}
var cached
if (typeof inst.data === "string" ||
typeof inst.data === "number" ||
typeof inst.data === "boolean") {
cached = new inst.data.constructor(inst.data)
} else {
cached = inst.data
}
cached.nodes = nodes
return cached
}
function builderReattachNodes(inst) {
var nodes = inst.cached.nodes
if (!inst.editable || inst.editable !== $document.activeElement) {
if (inst.data.$trusted) {
clear(nodes, inst.cached)
nodes = injectHTML(inst.parent, inst.index, inst.data)
} else if (inst.pTag === "textarea") {
// <textarea> uses `value` instead of `nodeValue`.
inst.parent.value = inst.data
} else if (inst.editable) {
// contenteditable nodes use `innerHTML` instead of `nodeValue`.
inst.editable.innerHTML = inst.data
} else {
// was a trusted string
if (nodes[0].nodeType === 1 ||
nodes.length > 1 ||
(nodes[0].nodeValue.trim && !nodes[0].nodeValue.trim())
) {
clear(inst.cached.nodes, inst.cached)
nodes = [$document.createTextNode(inst.data)]
}
builderInjectTextNode(inst, nodes[0])
}
}
inst.cached = new inst.data.constructor(inst.data)
inst.cached.nodes = nodes
return inst.cached
}
// This function was causing deopts in Chrome.
function builderInjectTextNode(inst, first) {
try {
insertNode(inst.parent, first, inst.index)
first.nodeValue = inst.data
} catch (e) {
// IE erroneously throws error when appending an empty text node
// after a null
}
}
m.startComputation = startComputation
function startComputation() { pendingRequests++ }
m.endComputation = endComputation
function endComputation() {
if (pendingRequests > 1) {
pendingRequests--
} else {
pendingRequests = 0
m.redraw()
}
}
function getSubArrayCount(item) {
if (item.$trusted) {
// fix offset of next element if item was a trusted string w/ more
// than one HTML element. the first clause in the regexp matches
// elements the second clause (after the pipe) matches text nodes
var match = item.match(/<[^\/]|\>\s*[^<]/g)
if (match != null) return match.length
} else if (isArray(item)) {
return item.length
} else {
return 1
}
}
function sortChanges(a, b) {
return a.action - b.action || a.index - b.index
}
function shouldSetAttrDirectly(attr) {
return !/^(list|style|form|type|width|height)$/.test(attr)
}
function trySetAttribute(attr, dataAttr, cachedAttr, node, namespace, tag) {
if (attr === "config" || attr === "key") {
// `config` and `key` aren't real attributes
return
} else if (isFunction(dataAttr) && attr.slice(0, 2) === "on") {
// hook event handlers to the auto-redrawing system
node[attr] = autoredraw(dataAttr, node)
} else if (attr === "style" && dataAttr != null && isObject(dataAttr)) {
// handle `style: {...}`
forOwn(dataAttr, function (value, rule) {
if (cachedAttr == null || cachedAttr[rule] !== value) {
node.style[rule] = value
}
})
for (var rule in cachedAttr) {
if (hasOwn.call(cachedAttr, rule)) {
if (!hasOwn.call(dataAttr, rule)) node.style[rule] = ""
}
}
} else if (namespace != null) {
// handle SVG
if (attr === "href") {
node.setAttributeNS("http://www.w3.org/1999/xlink", "href",
dataAttr)
} else {
node.setAttribute(attr === "className" ? "class" : attr,
dataAttr)
}
} else if (attr in node && shouldSetAttrDirectly(attr)) {
// handle cases that are properties (but ignore cases where we
// should use setAttribute instead):
//
// - list and form are typically used as strings, but are DOM
// element references in js
// - when using CSS selectors (e.g. `m("[style='']")`), style is
// used as a string, but it's an object in js
//
// #348
// don't set the value if not needed, otherwise cursor placement
// breaks in Chrome
if (tag !== "input" || node[attr] !== dataAttr) {
node[attr] = dataAttr
}
} else {
node.setAttribute(attr, dataAttr)
}
}
function trySetSingle(attr, data, cached, node, namespace, tag) {
try {
trySetAttribute(attr, data, cached, node, namespace, tag)
} catch (e) {
// swallow IE's invalid argument errors to mimic HTML's
// fallback-to-doing-nothing-on-invalid-attributes behavior
if (/\bInvalid argument\b/.test(e.message)) throw e
}
}
function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
forOwn(dataAttrs, function (dataAttr, attr) {
var cachedAttr = cachedAttrs[attr]
if (!(attr in cachedAttrs) || (cachedAttr !== dataAttr)) {
cachedAttrs[attr] = dataAttr
trySetSingle(attr, dataAttr, cachedAttr, node, namespace, tag)
} else if (attr === "value" && tag === "input" &&
// #348: dataAttr may not be a string, so use loose
// comparison (i.e. identity not required).
node.value != dataAttr) { // eslint-disable-line eqeqeq
node.value = dataAttr
}
})
return cachedAttrs
}
function clearSingle(node) {
try {
node.parentNode.removeChild(node)
} catch (e) {
/* eslint-disable max-len */
// ignore if this fails due to order of events (see
// http://stackoverflow.com/questions/21926083/failed-to-execute-removechild-on-node)
/* eslint-enable max-len */
}
}
function clear(nodes, cached) {
// If it's empty, there's nothing to clear
if (!nodes.length) return
cached = [].concat(cached)
for (var i = nodes.length - 1; i >= 0; i--) {
var node = nodes[i]
if (node != null && node.parentNode) {
clearSingle(node)
if (cached[i]) unload(cached[i])
}
}
// release memory if nodes is an array. This check should fail if nodes
// is a NodeList (see loop above)
if (nodes.length) nodes.length = 0
}
function unload(cached) {
if (cached.cfgCtx && isFunction(cached.cfgCtx.onunload)) {
cached.cfgCtx.onunload()
cached.cfgCtx.onunload = null
}
if (cached.controllers) {
forEach(cached.controllers, function (controller) {
if (isFunction(controller.onunload)) {
controller.onunload({preventDefault: noop})
}
})
}
if (cached.children) {
if (isArray(cached.children)) {
forEach(cached.children, unload)
} else if (cached.children.tag) {
unload(cached.children)
}
}
}
var insertAdjacentBeforeEnd = (function () {
try {
$document.createRange().createContextualFragment("x")
return function (parent, data) {
parent.appendChild(
$document.createRange().createContextualFragment(data))
}
} catch (e) {
return function (parent, data) {
parent.insertAdjacentHTML("beforeend", data)
}
}
})()
function injectHTML(parent, index, data) {
var nextSibling = parent.childNodes[index]
if (nextSibling) {
if (nextSibling.nodeType !== 1) {
var placeholder = $document.createElement("span")
parent.insertBefore(placeholder, nextSibling || null)
placeholder.insertAdjacentHTML("beforebegin", data)
parent.removeChild(placeholder)
} else {
nextSibling.insertAdjacentHTML("beforebegin", data)
}
} else {
insertAdjacentBeforeEnd(parent, data)
}
var nodes = []
while (parent.childNodes[index] !== nextSibling) {
nodes.push(parent.childNodes[index++])
}
return nodes
}
function autoredraw(callback, object) {
return function (e) {
redrawStrategy("diff")
startComputation()
try {
return callback.call(object, e || event)
} finally {
endFirstComputation()
}
}
}
var documentNode = {
appendChild: function (node) {
if ($document.documentElement &&
$document.documentElement !== node) {
$document.replaceChild(node, $document.documentElement)
} else {
$document.appendChild(node)
}
this.childNodes = $document.childNodes
},
insertBefore: function (node) {
this.appendChild(node)
},
childNodes: []
}
var nodeCache = []
var cellCache = {}
m.render = function (root, cell, forceRecreation) {
if (!root) {
throw new Error("Ensure the DOM element being passed to " +
"m.route/m.mount/m.render exists.")
}
var configs = []
var id = getCellCacheKey(root)
var isDocumentRoot = root === $document
var node
if (isDocumentRoot || root === $document.documentElement) {
node = documentNode
} else {
node = root
}
if (isDocumentRoot && cell.tag !== "html") {
cell = {tag: "html", attrs: {}, children: cell}
}
if (cellCache[id] === undefined) clear(node.childNodes)
if (forceRecreation === true) reset(root)
cellCache[id] = builderBuild(buildContext(
node,
null,
undefined,
undefined,
cell,
cellCache[id],
false,
0,
null,
undefined,
configs
))
forEach(configs, function (config) {
config()
})
}
function getCellCacheKey(element) {
var index = nodeCache.indexOf(element)
return index < 0 ? nodeCache.push(element) - 1 : index
}
m.trust = function (value) {
value = new String(value) // eslint-disable-line no-new-wrappers
value.$trusted = true
return value
}
var roots = []
var components = []
var controllers = []
var computePreRedrawHook = null
var computePostRedrawHook = null
var FRAME_BUDGET = 16 // 60 frames per second = 1 call per 16 ms
var topComponent
function initComponent(component, root, index, isPrevented) {
var isNullComponent = component === null
if (!isPrevented) {
redrawStrategy("all")
startComputation()
roots[index] = root
component = topComponent = component || {controller: noop}
// controllers may call m.mount recursively (via m.route redirects,
// for example). This conditional ensures only the last recursive
// m.mount call is applied. Please don't change the order of this.
var controller = new (component.controller || noop)()
if (component === topComponent) {
controllers[index] = controller
components[index] = component
}
endFirstComputation()
if (isNullComponent) {
removeRootElement(root, index)
}
return controllers[index]
}
if (isNullComponent) {
removeRootElement(root, index)
}
}
m.mount = m.module = mmount
function mmount(root, component) {
if (!root) {
throw new Error("Please ensure the DOM element exists before " +
"rendering a template into it.")
}
var index = roots.indexOf(root)
if (index < 0) index = roots.length
var isPrevented = false
var ev = {
preventDefault: function () {
isPrevented = true
computePreRedrawHook = computePostRedrawHook = null
}
}
forEach(unloaders, function (unloader) {
if (unloader.ctrl != null) {
unloaderHandler(unloader, ev)
unloader.ctrl.onunload = null
}
})
if (isPrevented) {
forEach(unloaders, function (unloader) {
unloader.ctrl.onunload = function (ev) {
unloaderHandler(unloader, ev)
}
})
} else {
unloaders = []
}
if (controllers[index] && isFunction(controllers[index].onunload)) {
controllers[index].onunload(ev)
}
return initComponent(component, root, index, isPrevented)
}
function removeRootElement(root, index) {
roots.splice(index, 1)
controllers.splice(index, 1)
components.splice(index, 1)
reset(root)
nodeCache.splice(getCellCacheKey(root), 1)
}
// lastRedrawId is a positive number if a second redraw is requested before
// the next animation frame, or 0 if it's the first redraw and not an event
// handler
var lastRedrawId = 0
var lastRedrawCallTime = 0
function actuallyPerformRedraw() {
if (lastRedrawId !== 0) $cancelAnimationFrame(lastRedrawId)
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
}
// when setTimeout:
// only reschedule redraw if time between now and previous redraw is bigger
// than a frame, otherwise keep currently scheduled timeout
//
// when rAF:
// always reschedule redraw
var performRedraw = $requestAnimationFrame ===
window.requestAnimationFrame ?
actuallyPerformRedraw :
function () {
if (+new Date() - lastRedrawCallTime > FRAME_BUDGET) {
actuallyPerformRedraw()
}
}
function resetLastRedrawId() {
lastRedrawId = 0
}
function attemptRedraw(force) {
if (lastRedrawId && !force) {
performRedraw()
} else {
redraw()
lastRedrawId = $requestAnimationFrame(resetLastRedrawId,
FRAME_BUDGET)
}
}
function redraw() {
if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
for (var i = 0; i < roots.length; i++) {
var root = roots[i]
var component = components[i]
var controller = controllers[i]
if (controller != null) {
m.render(
root,
component.view ?
component.view(controller, [controller]) :
""
)
}
}
// after rendering within a routed context, we need to scroll back to
// the top, and fetch the document title for history.pushState
if (computePostRedrawHook) {
computePostRedrawHook()
computePostRedrawHook = null
}
lastRedrawId = null
lastRedrawCallTime = new Date()
redrawStrategy("diff")
}
function endFirstComputation() {
if (redrawStrategy() === "none") {
pendingRequests--
redrawStrategy("diff")
} else {
endComputation()
}
}
m.withAttr = function (prop, withAttrCallback, callbackThis) {
return function (e) {
/* eslint-disable no-invalid-this */
e = e || event
var currentTarget = e.currentTarget || this
var targetProp
if (prop in currentTarget) {
targetProp = currentTarget[prop]
} else {
targetProp = currentTarget.getAttribute(prop)
}
withAttrCallback.call(callbackThis || this, targetProp)
/* eslint-enable no-invalid-this */
}
}
// routing
var modes = {
pathname: "",
hash: "#",
search: "?"
}
var redirect = noop
var isDefaultRoute = false
var routeParams, currentRoute
function historyListener() {
var path = $location[mroute.mode]
if (mroute.mode === "pathname") path += $location.search
if (currentRoute !== normalizeRoute(path)) redirect(path)
}
function runHistoryListener(listener) {
window[listener] = historyListener
computePreRedrawHook = setScroll
window[listener]()
}
function getRouteBase() {
return (mroute.mode === "pathname" ? "" : $location.pathname) +
modes[mroute.mode]
}
function windowPushState() {
window.history.pushState(null,
$document.title,
modes[mroute.mode] + currentRoute)
}
function windowReplaceState() {
window.history.replaceState(null,
$document.title,
modes[mroute.mode] + currentRoute)
}
function computeAndLaunchRedirect(replaceHistory) {
if (window.history.pushState) {
computePreRedrawHook = setScroll
computePostRedrawHook = replaceHistory ?
windowReplaceState :
windowPushState
redirect(modes[mroute.mode] + currentRoute)
} else {
$location[mroute.mode] = currentRoute
redirect(modes[mroute.mode] + currentRoute)
}
}
function routeTo(route, params, replaceHistory) {
if (arguments.length < 3 && typeof params !== "object") {
replaceHistory = params
params = null
}
var oldRoute = currentRoute
currentRoute = route
var args = params || {}
var queryIndex = currentRoute.indexOf("?")
var queryString, currentPath
if (queryIndex >= 0) {
var paramsObj = parseQueryString(currentRoute.slice(queryIndex + 1))
forOwn(args, function (value, key) {
paramsObj[key] = args[key]
})
queryString = buildQueryString(paramsObj)
currentPath = currentRoute.slice(0, queryIndex)
} else {
queryString = buildQueryString(params)
currentPath = currentRoute
}
if (queryString) {
var delimiter = currentPath.indexOf("?") === -1 ? "?" : "&"
currentRoute = currentPath + delimiter + queryString
}
return computeAndLaunchRedirect(replaceHistory || oldRoute === route)
}
m.route = mroute
function mroute(root, arg1, arg2, vdom) {
if (arguments.length === 0) {
// m.route()
return currentRoute
} else if (arguments.length === 3 && isString(arg1)) {
// m.route(el, defaultRoute, routes)
redirect = function (source) {
var path = currentRoute = normalizeRoute(source)
if (!routeByValue(root, arg2, path)) {
if (isDefaultRoute) {
throw new Error("Ensure the default route matches " +
"one of the routes defined in m.route")
}
isDefaultRoute = true
mroute(arg1, true)
isDefaultRoute = false
}
}
runHistoryListener(
mroute.mode === "hash" ? "onhashchange" : "onpopstate")
} else if (root.addEventListener || root.attachEvent) {
// config: m.route
root.href = getRouteBase() + vdom.attrs.href
if (root.addEventListener) {
root.removeEventListener("click", routeUnobtrusive)
root.addEventListener("click", routeUnobtrusive)
} else {
root.detachEvent("onclick", routeUnobtrusive)
root.attachEvent("onclick", routeUnobtrusive)
}
} else if (isString(root)) {
// m.route(route, params, shouldReplaceHistoryEntry)
return routeTo.apply(null, arguments)
}
}
mroute.param = function (key) {
if (!routeParams) {
throw new Error("You must call m.route(element, defaultRoute, " +
"routes) before calling mroute.param()")
}
if (key) {
return routeParams[key]
} else {
return routeParams
}
}
mroute.mode = "search"
function normalizeRoute(route) {
return route.slice(modes[mroute.mode].length)
}
function routeByValue(root, router, path) {
var queryStart = path.indexOf("?")
if (queryStart >= 0) {
routeParams = parseQueryString(
path.substr(queryStart + 1, path.length))
path = path.substr(0, queryStart)
} else {
routeParams = {}
}
// Get all routes and check if there's an exact match for the current
// path
var keys = Object.keys(router)
var index = keys.indexOf(path)
if (index >= 0) {
mmount(root, router[keys[index]])
return true
}
for (var route in router) {
if (hasOwn.call(router, route)) {
if (route === path) {
mmount(root, router[route])
return true
}
var matcher = new RegExp("^" +
route.replace(/:[^\/]+?\.{3}/g, "(.*?)")
.replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
if (matcher.test(path)) {
/* eslint-disable no-loop-func */
path.replace(matcher, function () {
var values = []
for (var i = 1, end = arguments.length - 2; i < end;) {
values.push(arguments[i++])
}
var keys = route.match(/:[^\/]+/g) || []
forEach(keys, function (key, i) {
key = key.replace(/:|\./g, "")
routeParams[key] = decodeURIComponent(values[i])
})
})
/* eslint-enable no-loop-func */
mmount(root, router[route])
return true
}
}
}
}
function routeUnobtrusive(e) {
e = e || event
if (e.ctrlKey || e.metaKey || e.which === 2) return
if (e.preventDefault) {
e.preventDefault()
} else {
e.returnValue = false
}
var currentTarget = e.currentTarget || e.srcElement
var args
if (mroute.mode === "pathname" && currentTarget.search) {
args = parseQueryString(currentTarget.search.slice(1))
} else {
args = {}
}
while (currentTarget && currentTarget.nodeName.toUpperCase() !== "A") {
currentTarget = currentTarget.parentNode
}
// clear pendingRequests because we want an immediate route change
pendingRequests = 0
mroute(currentTarget[mroute.mode].slice(modes[mroute.mode].length),
args)
}
function setScroll() {
if (mroute.mode !== "hash" && $location.hash) {
$location.hash = $location.hash
} else {
window.scrollTo(0, 0)
}
}
function buildQueryString(object, prefix) {
var duplicates = {}
var str = []
forOwn(object, function (value, prop) {
var key = prefix ? prefix + "[" + prop + "]" : prop
if (value === null) {
str.push(encodeURIComponent(key))
} else if (isObject(value)) {
str.push(buildQueryString(value, key))
} else if (isArray(value)) {
var keys = []
duplicates[key] = duplicates[key] || {}
/* eslint-disable-line no-loop-func */
forEach(value, function (item) {
if (!duplicates[key][item]) {
duplicates[key][item] = true
keys.push(encodeURIComponent(key) + "=" +
encodeURIComponent(item))
}
})
/* eslint-enable-line no-loop-func */
str.push(keys.join("&"))
} else if (value !== undefined) {
str.push(encodeURIComponent(key) + "=" +
encodeURIComponent(value))
}
})
return str.join("&")
}
function parseQueryString(str) {
if (!str) return {}
if (str[0] === "?") str = str.slice(1)
var pairs = str.split("&")
var params = {}
forEach(pairs, function (string) {
var pair = string.split("=")
var key = decodeURIComponent(pair[0])
var value = pair.length === 2 ? decodeURIComponent(pair[1]) : null
if (params[key] != null) {
if (!isArray(params[key])) params[key] = [params[key]]
params[key].push(value)
} else {
params[key] = value
}
})
return params
}
mroute.buildQueryString = buildQueryString
mroute.parseQueryString = parseQueryString
function reset(root) {
var cacheKey = getCellCacheKey(root)
clear(root.childNodes, cellCache[cacheKey])
cellCache[cacheKey] = undefined
}
// Promiz.mithril.js | Zolmeister | MIT
// a modified version of Promiz.js, which does not conform to Promises/A+
// for two reasons:
//
// 1) `then` callbacks are called synchronously (because setTimeout is too
// slow, and the setImmediate polyfill is too big
// 2) throwing subclasses of Error cause the error to be bubbled up instead
// of triggering rejection (because the spec does not account for the
// important use case of default browser error handling, i.e. message w/
// line number)
var RESOLVING = 1
var REJECTING = 2
var RESOLVED = 3
var REJECTED = 4
function coerce(value, next, error) {
if (isPromise(value)) {
return value.then(function (value) {
coerce(value, next, error)
}, function (e) {
coerce(e, error, error)
})
} else {
return next(value)
}
}
function Deferred(onSuccess, onFailure) { // eslint-disable-line max-statements, max-len
var self = this
var promiseValue
var next = []
var func = push
function set(value) {
promiseValue = value
}
function resolve(deferred) {
deferred.resolve(promiseValue)
}
function reject(deferred) {
deferred.reject(promiseValue)
}
function init(promise) {
if (func !== reject) promise(promiseValue)
return promise
}
function push(value) {
next.push(value)
}
self.resolve = function (value) {
if (func === push) {
fire(RESOLVING, value, self)
}
return self
}
self.reject = function (value) {
if (func === push) {
fire(REJECTING, value, self)
}
return self
}
self.promise = function (value) {
if (arguments.length) coerce(value, set, set)
return func !== reject ? promiseValue : undefined
}
self.promise.then = function (onSuccess, onFailure) {
var deferred = new Deferred(onSuccess, onFailure)
func(deferred)
return init(deferred.promise)
}
self.promise.catch = function (callback) {
return self.promise.then(null, callback)
}
function wrapper(callback, func) {
var p = mdeferred().resolve(callback()).promise
if (func !== reject) p(promiseValue)
return p.then(func)
}
self.promise.finally = function (callback) {
return self.promise.then(function () {
return wrapper(callback, function () {
return promiseValue
})
}, function () {
return wrapper(callback, function () {
throw promiseValue
})
})
}
function run(callback) {
func = callback
forEach(next, callback)
// Clear these (which hold all the extra references)
finish = fire = null // eslint-disable-line no-func-assign
}
function finish(value, state) {
coerce(value, function (value) {
promiseValue = value
run(state === RESOLVED ? resolve : reject)
}, function (value) {
promiseValue = value
run(reject)
})
}
function doThen(value, deferred) {
// count protects against abuse calls from spec checker
var count = 0
try {
return value.then(function (value) {
if (count++) return
fire(RESOLVING, value, deferred)
}, function (value) {
if (count++) return
fire(REJECTING, value, deferred)
})
} catch (e) {
mdeferred.onerror(e)
return fire(REJECTING, e, deferred)
}
}
function notThenable(value, state, deferred) {
try {
if (state === RESOLVING && isFunction(onSuccess)) {
value = onSucc