Skip to content

Instantly share code, notes, and snippets.

@rafaelcardoso
Created July 8, 2016 15:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rafaelcardoso/411278b2b6be634ffc90ee23984c2087 to your computer and use it in GitHub Desktop.
Save rafaelcardoso/411278b2b6be634ffc90ee23984c2087 to your computer and use it in GitHub Desktop.
mithril source code without routing and ajax features for performance testing purpose
/* global Promise */
;(function (global, factory) { // eslint-disable-line
"use strict"
/* eslint-disable no-undef */
var m = factory(global)
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 */
})(typeof window !== "undefined" ? window : this, function (global, undefined) { // eslint-disable-line
"use strict"
m.version = function () {
return "v0.2.5"
}
var hasOwn = {}.hasOwnProperty
var type = {}.toString
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() {}
var voidElements = {
AREA: 1,
BASE: 1,
BR: 1,
COL: 1,
COMMAND: 1,
EMBED: 1,
HR: 1,
IMG: 1,
INPUT: 1,
KEYGEN: 1,
LINK: 1,
META: 1,
PARAM: 1,
SOURCE: 1,
TRACK: 1,
WBR: 1
}
// caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame
// self invoking function needed because of the way mocks work
function initialize(mock) {
$document = mock.document
$location = mock.location
$cancelAnimationFrame = mock.cancelAnimationFrame || mock.clearTimeout
$requestAnimationFrame = mock.requestAnimationFrame || mock.setTimeout
}
// testing API
m.deps = function (mock) {
initialize(global = mock || window)
return global
}
m.deps(global)
/**
* @typedef {String} Tag
* A string that looks like -> div.classname#id[param=one][param2=two]
* Which describes a DOM node
*/
function parseTagAttrs(cell, tag) {
var classes = []
/* eslint-disable max-len */
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
/* eslint-enable max-len */
var match
while ((match = parser.exec(tag))) {
if (match[1] === "" && match[2]) {
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 attrValue = match[6]
if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1")
cell.attrs[match[4]] = attrValue || true
}
}
return classes
}
function getVirtualChildren(args, hasAttrs) {
var children = hasAttrs ? args.slice(1) : args
if (children.length === 1 && isArray(children[0])) {
return children[0]
} else {
return children
}
}
function assignAttrs(target, attrs, classes) {
var classAttr = "class" in attrs ? "class" : "className"
for (var attrName in attrs) {
if (hasOwn.call(attrs, attrName)) {
if (attrName === classAttr &&
attrs[attrName] != null &&
attrs[attrName] !== "") {
classes.push(attrs[attrName])
// create key in correct iteration order
target[attrName] = ""
} else {
target[attrName] = attrs[attrName]
}
}
}
if (classes.length) target[classAttr] = classes.join(" ")
}
/**
*
* @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) {
var args = []
for (var i = 1, length = arguments.length; i < length; i++) {
args[i - 1] = arguments[i]
}
if (isObject(tag)) return parameterize(tag, args)
if (!isString(tag)) {
throw new Error("selector in m(selector, attrs, children) should " +
"be a string")
}
var hasAttrs = pairs != null && isObject(pairs) &&
!("tag" in pairs || "view" in pairs || "subtree" in pairs)
var attrs = hasAttrs ? pairs : {}
var cell = {
tag: "div",
attrs: {},
children: getVirtualChildren(args, hasAttrs)
}
assignAttrs(cell.attrs, attrs, parseTagAttrs(cell, tag))
return cell
}
function forEach(list, f) {
for (var i = 0; i < list.length && !f(list[i], i++);) {
// function called in condition
}
}
function forKeys(list, f) {
forEach(list, function (attrs, i) {
return (attrs = attrs && attrs.attrs) &&
attrs.key != null &&
f(attrs, i)
})
}
// 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 (behavior depends on
// version)
try {
if (typeof data !== "boolean" &&
data != null &&
data.toString() != null) return data
} catch (e) {
// silently ignore errors
}
return ""
}
// This function was causing deopts in Chrome.
function injectTextNode(parentElement, first, index, data) {
try {
insertNode(parentElement, first, index)
first.nodeValue = data
} catch (e) {
// IE erroneously throws error when appending an empty text node
// after a null
}
}
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 and flatten until there are no more
// nested arrays at that index
i--
}
}
return list
}
function insertNode(parentElement, node, index) {
parentElement.insertBefore(node,
parentElement.childNodes[index] || null)
}
var DELETION = 1
var INSERTION = 2
var MOVE = 3
function handleKeysDiffer(data, existing, cached, parentElement) {
forKeys(data, function (key, i) {
existing[key = key.key] = existing[key] ? {
action: MOVE,
index: i,
from: existing[key].index,
element: cached.nodes[existing[key].index] ||
$document.createElement("div")
} : {action: INSERTION, index: i}
})
var actions = []
for (var prop in existing) {
if (hasOwn.call(existing, prop)) {
actions.push(existing[prop])
}
}
var changes = actions.sort(sortChanges)
var newCached = new Array(cached.length)
newCached.nodes = cached.nodes.slice()
forEach(changes, function (change) {
var index = change.index
if (change.action === DELETION) {
clear(cached[index].nodes, cached[index])
newCached.splice(index, 1)
}
if (change.action === INSERTION) {
var dummy = $document.createElement("div")
dummy.key = data[index].attrs.key
insertNode(parentElement, dummy, index)
newCached.splice(index, 0, {
attrs: {key: data[index].attrs.key},
nodes: [dummy]
})
newCached.nodes[index] = dummy
}
if (change.action === MOVE) {
var changeElement = change.element
var maybeChanged = parentElement.childNodes[index]
if (maybeChanged !== changeElement && changeElement !== null) {
parentElement.insertBefore(changeElement,
maybeChanged || null)
}
newCached[index] = cached[change.from]
newCached.nodes[index] = changeElement
}
})
return newCached
}
function diffKeys(data, cached, existing, parentElement) {
var keysDiffer = data.length !== cached.length
if (!keysDiffer) {
forKeys(data, function (attrs, i) {
var cachedCell = cached[i]
return keysDiffer = cachedCell &&
cachedCell.attrs &&
cachedCell.attrs.key !== attrs.key
})
}
if (keysDiffer) {
return handleKeysDiffer(data, existing, cached, parentElement)
} else {
return cached
}
}
function diffArray(data, cached, nodes) {
// diff the array itself
// update the list of DOM nodes by collecting the nodes from each item
forEach(data, function (_, i) {
if (cached[i] != null) nodes.push.apply(nodes, cached[i].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(cached.nodes, function (node, i) {
if (node.parentNode != null && nodes.indexOf(node) < 0) {
clear([node], [cached[i]])
}
})
if (data.length < cached.length) cached.length = data.length
cached.nodes = nodes
}
function buildArrayKeys(data) {
var guid = 0
forKeys(data, function () {
forEach(data, function (attrs) {
if ((attrs = attrs && attrs.attrs) && attrs.key == null) {
attrs.key = "__mithril__" + guid++
}
})
return 1
})
}
function isDifferentEnough(data, cached, dataAttrKeys) {
if (data.tag !== cached.tag) return true
if (dataAttrKeys.sort().join() !==
Object.keys(cached.attrs).sort().join()) {
return true
}
if (data.attrs.id !== cached.attrs.id) {
return true
}
if (data.attrs.key !== cached.attrs.key) {
return true
}
if (m.redraw.strategy() === "all") {
return !cached.configContext || cached.configContext.retain !== true
}
if (m.redraw.strategy() === "diff") {
return cached.configContext && cached.configContext.retain === false
}
return false
}
function maybeRecreateObject(data, cached, dataAttrKeys) {
// if an element is different enough from the one in cache, recreate it
if (isDifferentEnough(data, cached, dataAttrKeys)) {
if (cached.nodes.length) clear(cached.nodes)
if (cached.configContext &&
isFunction(cached.configContext.onunload)) {
cached.configContext.onunload()
}
if (cached.controllers) {
forEach(cached.controllers, function (controller) {
if (controller.onunload) {
controller.onunload({preventDefault: noop})
}
})
}
}
}
function getObjectNamespace(data, namespace) {
if (data.attrs.xmlns) return data.attrs.xmlns
if (data.tag === "svg") return "http://www.w3.org/2000/svg"
if (data.tag === "math") return "http://www.w3.org/1998/Math/MathML"
return namespace
}
var pendingRequests = 0
m.startComputation = function () { pendingRequests++ }
m.endComputation = function () {
if (pendingRequests > 1) {
pendingRequests--
} else {
pendingRequests = 0
m.redraw()
}
}
function unloadCachedControllers(cached, views, controllers) {
if (controllers.length) {
cached.views = views
cached.controllers = controllers
forEach(controllers, function (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 scheduleConfigsToBeCalled(configs, data, node, isNew, cached) {
// schedule configs to be called. They are called after `build` finishes
// running
if (isFunction(data.attrs.config)) {
var context = cached.configContext = cached.configContext || {}
// bind
configs.push(function () {
return data.attrs.config.call(data, node, !isNew, context,
cached)
})
}
}
function buildUpdatedNode(
cached,
data,
editable,
hasKeys,
namespace,
views,
configs,
controllers
) {
var node = cached.nodes[0]
if (hasKeys) {
setAttributes(node, data.tag, data.attrs, cached.attrs, namespace)
}
cached.children = build(
node,
data.tag,
undefined,
undefined,
data.children,
cached.children,
false,
0,
data.attrs.contenteditable ? node : editable,
namespace,
configs
)
cached.nodes.intact = true
if (controllers.length) {
cached.views = views
cached.controllers = controllers
}
return node
}
function handleNonexistentNodes(data, parentElement, index) {
var nodes
if (data.$trusted) {
nodes = injectHTML(parentElement, index, data)
} else {
nodes = [$document.createTextNode(data)]
if (!(parentElement.nodeName in voidElements)) {
insertNode(parentElement, nodes[0], index)
}
}
var cached
if (typeof data === "string" ||
typeof data === "number" ||
typeof data === "boolean") {
cached = new data.constructor(data)
} else {
cached = data
}
cached.nodes = nodes
return cached
}
function reattachNodes(
data,
cached,
parentElement,
editable,
index,
parentTag
) {
var nodes = cached.nodes
if (!editable || editable !== $document.activeElement) {
if (data.$trusted) {
clear(nodes, cached)
nodes = injectHTML(parentElement, index, data)
} else if (parentTag === "textarea") {
// <textarea> uses `value` instead of `nodeValue`.
parentElement.value = data
} else if (editable) {
// contenteditable nodes use `innerHTML` instead of `nodeValue`.
editable.innerHTML = data
} else {
// was a trusted string
if (nodes[0].nodeType === 1 || nodes.length > 1 ||
(nodes[0].nodeValue.trim &&
!nodes[0].nodeValue.trim())) {
clear(cached.nodes, cached)
nodes = [$document.createTextNode(data)]
}
injectTextNode(parentElement, nodes[0], index, data)
}
}
cached = new data.constructor(data)
cached.nodes = nodes
return cached
}
function handleTextNode(
cached,
data,
index,
parentElement,
shouldReattach,
editable,
parentTag
) {
if (!cached.nodes.length) {
return handleNonexistentNodes(data, parentElement, index)
} else if (cached.valueOf() !== data.valueOf() || shouldReattach) {
return reattachNodes(data, cached, parentElement, editable, index,
parentTag)
} else {
return (cached.nodes.intact = true, cached)
}
}
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
}
return 1
}
function buildArray(
data,
cached,
parentElement,
index,
parentTag,
shouldReattach,
editable,
namespace,
configs
) {
data = flatten(data)
var nodes = []
var intact = cached.length === 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(cached, function (attrs, i) {
shouldMaintainIdentities = true
existing[cached[i].attrs.key] = {action: DELETION, index: i}
})
buildArrayKeys(data)
if (shouldMaintainIdentities) {
cached = diffKeys(data, cached, existing, parentElement)
}
// end key algorithm
var cacheCount = 0
// faster explicitly written
for (var i = 0, len = data.length; i < len; i++) {
// diff each item in the array
var item = build(
parentElement,
parentTag,
cached,
index,
data[i],
cached[cacheCount],
shouldReattach,
index + subArrayCount || subArrayCount,
editable,
namespace,
configs)
if (item !== undefined) {
intact = intact && item.nodes.intact
subArrayCount += getSubArrayCount(item)
cached[cacheCount++] = item
}
}
if (!intact) diffArray(data, cached, nodes)
return cached
}
function makeCache(data, cached, index, parentIndex, parentCache) {
if (cached != null) {
if (type.call(cached) === type.call(data)) return cached
if (parentCache && parentCache.nodes) {
var offset = index - parentIndex
var end = offset + (isArray(data) ? data : cached.nodes).length
clear(
parentCache.nodes.slice(offset, end),
parentCache.slice(offset, end))
} else if (cached.nodes) {
clear(cached.nodes, cached)
}
}
cached = new 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 (cached.tag) cached = {}
cached.nodes = []
return cached
}
function constructNode(data, namespace) {
if (data.attrs.is) {
if (namespace == null) {
return $document.createElement(data.tag, data.attrs.is)
} else {
return $document.createElementNS(namespace, data.tag,
data.attrs.is)
}
} else if (namespace == null) {
return $document.createElement(data.tag)
} else {
return $document.createElementNS(namespace, data.tag)
}
}
function constructAttrs(data, node, namespace, hasKeys) {
if (hasKeys) {
return setAttributes(node, data.tag, data.attrs, {}, namespace)
} else {
return data.attrs
}
}
function constructChildren(
data,
node,
cached,
editable,
namespace,
configs
) {
if (data.children != null && data.children.length > 0) {
return build(
node,
data.tag,
undefined,
undefined,
data.children,
cached.children,
true,
0,
data.attrs.contenteditable ? node : editable,
namespace,
configs)
} else {
return data.children
}
}
function reconstructCached(
data,
attrs,
children,
node,
namespace,
views,
controllers
) {
var cached = {
tag: data.tag,
attrs: attrs,
children: children,
nodes: [node]
}
unloadCachedControllers(cached, views, controllers)
if (cached.children && !cached.children.nodes) {
cached.children.nodes = []
}
return cached
}
function getController(views, view, cachedControllers, controller) {
var controllerIndex
if (m.redraw.strategy() === "diff" && views) {
controllerIndex = views.indexOf(view)
} else {
controllerIndex = -1
}
if (controllerIndex > -1) {
return cachedControllers[controllerIndex]
} else if (isFunction(controller)) {
return new controller()
} else {
return {}
}
}
var unloaders = []
function updateLists(views, controllers, view, controller) {
if (controller.onunload != null &&
unloaders.map(function (u) { return u.handler })
.indexOf(controller.onunload) < 0) {
unloaders.push({
controller: controller,
handler: controller.onunload
})
}
views.push(view)
controllers.push(controller)
}
var forcing = false
function checkView(
data,
view,
cached,
cachedControllers,
controllers,
views
) {
var controller = getController(
cached.views,
view,
cachedControllers,
data.controller)
var key = data && data.attrs && data.attrs.key
if (pendingRequests === 0 ||
forcing ||
cachedControllers &&
cachedControllers.indexOf(controller) > -1) {
data = data.view(controller)
} else {
data = {tag: "placeholder"}
}
if (data.subtree === "retain") return data
data.attrs = data.attrs || {}
data.attrs.key = key
updateLists(views, controllers, view, controller)
return data
}
function markViews(data, cached, views, controllers) {
var cachedControllers = cached && cached.controllers
while (data.view != null) {
data = checkView(
data,
data.view.$original || data.view,
cached,
cachedControllers,
controllers,
views)
}
return data
}
function buildObject( // eslint-disable-line max-statements
data,
cached,
editable,
parentElement,
index,
shouldReattach,
namespace,
configs
) {
var views = []
var controllers = []
data = markViews(data, cached, views, controllers)
if (data.subtree === "retain") return cached
if (!data.tag && controllers.length) {
throw new Error("Component template must return a virtual " +
"element, not an array, string, etc.")
}
data.attrs = data.attrs || {}
cached.attrs = cached.attrs || {}
var dataAttrKeys = Object.keys(data.attrs)
var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
maybeRecreateObject(data, cached, dataAttrKeys)
if (!isString(data.tag)) return
var isNew = cached.nodes.length === 0
namespace = getObjectNamespace(data, namespace)
var node
if (isNew) {
node = constructNode(data, namespace)
// set attributes first, then create children
var attrs = constructAttrs(data, node, namespace, hasKeys)
// add the node to its parent before attaching children to it
insertNode(parentElement, node, index)
var children = constructChildren(data, node, cached, editable,
namespace, configs)
cached = reconstructCached(
data,
attrs,
children,
node,
namespace,
views,
controllers)
} else {
node = buildUpdatedNode(
cached,
data,
editable,
hasKeys,
namespace,
views,
configs,
controllers)
}
// edge case: setting value on <select> doesn't work before children
// exist, so set it again after children have been created/updated
if (data.tag === "select" && "value" in data.attrs) {
setAttributes(node, data.tag, {value: data.attrs.value}, {},
namespace)
}
if (!isNew && shouldReattach === true && node != null) {
insertNode(parentElement, node, index)
}
// The configs are called after `build` finishes running
scheduleConfigsToBeCalled(configs, data, node, isNew, cached)
return cached
}
function build(
parentElement,
parentTag,
parentCache,
parentIndex,
data,
cached,
shouldReattach,
index,
editable,
namespace,
configs
) {
/*
* `build` is a recursive function that 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 `configContext` 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
*
* `parentElement` is a DOM element used for W3C DOM API calls
* `parentTag` is only used for handling a corner case for textarea
* values
* `parentCache` is used to remove nodes in some multi-node cases
* `parentIndex` 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
* `shouldReattach` 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
* `namespace` indicates the closest HTML namespace as it cascades down
* from an ancestor
* `configs` 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
*/
data = dataToString(data)
if (data.subtree === "retain") return cached
cached = makeCache(data, cached, index, parentIndex, parentCache)
if (isArray(data)) {
return buildArray(
data,
cached,
parentElement,
index,
parentTag,
shouldReattach,
editable,
namespace,
configs)
} else if (data != null && isObject(data)) {
return buildObject(
data,
cached,
editable,
parentElement,
index,
shouldReattach,
namespace,
configs)
} else if (!isFunction(data)) {
return handleTextNode(
cached,
data,
index,
parentElement,
shouldReattach,
editable,
parentTag)
} else {
return cached
}
}
function sortChanges(a, b) {
return a.action - b.action || a.index - b.index
}
function copyStyleAttrs(node, dataAttr, cachedAttr) {
if (cachedAttr === dataAttr) {
node.style = ""
cachedAttr = {}
}
for (var rule in dataAttr) {
if (hasOwn.call(dataAttr, rule)) {
if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) {
node.style[rule] = dataAttr[rule]
}
}
}
for (rule in cachedAttr) {
if (hasOwn.call(cachedAttr, rule)) {
if (!hasOwn.call(dataAttr, rule)) node.style[rule] = ""
}
}
}
var shouldUseSetAttribute = {
list: 1,
style: 1,
form: 1,
type: 1,
width: 1,
height: 1
}
function setSingleAttr(
node,
attrName,
dataAttr,
cachedAttr,
tag,
namespace
) {
if (attrName === "config" || attrName === "key") {
// `config` isn't a real attribute, so ignore it
return true
} else if (isFunction(dataAttr) && attrName.slice(0, 2) === "on") {
// hook event handlers to the auto-redrawing system
node[attrName] = autoredraw(dataAttr, node)
} else if (attrName === "style" && dataAttr != null &&
isObject(dataAttr)) {
// handle `style: {...}`
copyStyleAttrs(node, dataAttr, cachedAttr)
} else if (namespace != null) {
// handle SVG
if (attrName === "href") {
node.setAttributeNS("http://www.w3.org/1999/xlink",
"href", dataAttr)
} else {
node.setAttribute(
attrName === "className" ? "class" : attrName,
dataAttr)
}
} else if (attrName in node && !shouldUseSetAttribute[attrName]) {
// 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
try {
if (tag !== "input" || node[attrName] !== dataAttr) {
node[attrName] = dataAttr
}
} catch (e) {
node.setAttribute(attrName, dataAttr)
}
} else node.setAttribute(attrName, dataAttr)
}
function trySetAttr(
node,
attrName,
dataAttr,
cachedAttr,
cachedAttrs,
tag,
namespace
) {
if (!(attrName in cachedAttrs) ||
(cachedAttr !== dataAttr) ||
typeof dataAttr === "object" ||
($document.activeElement === node)) {
cachedAttrs[attrName] = dataAttr
try {
return setSingleAttr(
node,
attrName,
dataAttr,
cachedAttr,
tag,
namespace)
} catch (e) {
// swallow IE's invalid argument errors to mimic HTML's
// fallback-to-doing-nothing-on-invalid-attributes behavior
if (e.message.indexOf("Invalid argument") < 0) throw e
}
} else if (attrName === "value" && tag === "input" &&
node.value !== dataAttr) {
// #348 dataAttr may not be a string, so use loose comparison
node.value = dataAttr
}
}
function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
for (var attrName in dataAttrs) {
if (hasOwn.call(dataAttrs, attrName)) {
if (trySetAttr(
node,
attrName,
dataAttrs[attrName],
cachedAttrs[attrName],
cachedAttrs,
tag,
namespace)) {
continue
}
}
}
return cachedAttrs
}
function clear(nodes, cached) {
for (var i = nodes.length - 1; i > -1; i--) {
if (nodes[i] && nodes[i].parentNode) {
try {
nodes[i].parentNode.removeChild(nodes[i])
} 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 */
}
cached = [].concat(cached)
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.configContext && isFunction(cached.configContext.onunload)) {
cached.configContext.onunload()
cached.configContext.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)
}
}
function appendTextFragment(parentElement, data) {
try {
parentElement.appendChild(
$document.createRange().createContextualFragment(data))
} catch (e) {
parentElement.insertAdjacentHTML("beforeend", data)
replaceScriptNodes(parentElement)
}
}
// Replace script tags inside given DOM element with executable ones.
// Will also check children recursively and replace any found script
// tags in same manner.
function replaceScriptNodes(node) {
if (node.tagName === "SCRIPT") {
node.parentNode.replaceChild(buildExecutableNode(node), node)
} else {
var children = node.childNodes
if (children && children.length) {
for (var i = 0; i < children.length; i++) {
replaceScriptNodes(children[i])
}
}
}
return node
}
// Replace script element with one whose contents are executable.
function buildExecutableNode(node){
var scriptEl = document.createElement("script")
var attrs = node.attributes
for (var i = 0; i < attrs.length; i++) {
scriptEl.setAttribute(attrs[i].name, attrs[i].value)
}
scriptEl.text = node.innerHTML
return scriptEl
}
function injectHTML(parentElement, index, data) {
var nextSibling = parentElement.childNodes[index]
if (nextSibling) {
var isElement = nextSibling.nodeType !== 1
var placeholder = $document.createElement("span")
if (isElement) {
parentElement.insertBefore(placeholder, nextSibling || null)
placeholder.insertAdjacentHTML("beforebegin", data)
parentElement.removeChild(placeholder)
} else {
nextSibling.insertAdjacentHTML("beforebegin", data)
}
} else {
appendTextFragment(parentElement, data)
}
var nodes = []
while (parentElement.childNodes[index] !== nextSibling) {
nodes.push(parentElement.childNodes[index])
index++
}
return nodes
}
function autoredraw(callback, object) {
return function (e) {
e = e || event
m.redraw.strategy("diff")
m.startComputation()
try {
return callback.call(object, e)
} finally {
endFirstComputation()
}
}
}
var html
var documentNode = {
appendChild: function (node) {
if (html === undefined) html = $document.createElement("html")
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.mount/m.render is not undefined.")
}
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] = build(
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
}
function gettersetter(store) {
function prop() {
if (arguments.length) store = arguments[0]
return store
}
prop.toJSON = function () {
return store
}
return prop
}
m.prop = function (store) {
if ((store != null && (isObject(store) || isFunction(store)) ||
((typeof Promise !== "undefined") &&
(store instanceof Promise))) &&
isFunction(store.then)) {
return propify(store)
}
return gettersetter(store)
}
var roots = []
var components = []
var controllers = []
var lastRedrawId = null
var lastRedrawCallTime = 0
var computePreRedrawHook = null
var computePostRedrawHook = null
var topComponent
var FRAME_BUDGET = 16 // 60 frames per second = 1 call per 16 ms
function parameterize(component, args) {
function controller() {
/* eslint-disable no-invalid-this */
return (component.controller || noop).apply(this, args) || this
/* eslint-enable no-invalid-this */
}
if (component.controller) {
controller.prototype = component.controller.prototype
}
function view(ctrl) {
var currentArgs = [ctrl].concat(args)
for (var i = 1; i < arguments.length; i++) {
currentArgs.push(arguments[i])
}
return component.view.apply(component, currentArgs)
}
view.$original = component.view
var output = {controller: controller, view: view}
if (args[0] && args[0].key != null) output.attrs = {key: args[0].key}
return output
}
m.component = function (component) {
var args = new Array(arguments.length - 1)
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i]
}
return parameterize(component, args)
}
function checkPrevented(component, root, index, isPrevented) {
if (!isPrevented) {
m.redraw.strategy("all")
m.startComputation()
roots[index] = root
var currentComponent
if (component) {
currentComponent = topComponent = component
} else {
currentComponent = topComponent = component = {controller: noop}
}
var controller = new (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
if (currentComponent === topComponent) {
controllers[index] = controller
components[index] = component
}
endFirstComputation()
if (component === null) {
removeRootElement(root, index)
}
return controllers[index]
} else {
if (component == null) {
removeRootElement(root, index)
}
}
}
m.mount = m.module = function (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 event = {
preventDefault: function () {
isPrevented = true
computePreRedrawHook = computePostRedrawHook = null
}
}
forEach(unloaders, function (unloader) {
unloader.handler.call(unloader.controller, event)
unloader.controller.onunload = null
})
if (isPrevented) {
forEach(unloaders, function (unloader) {
unloader.controller.onunload = unloader.handler
})
} else {
unloaders = []
}
if (controllers[index] && isFunction(controllers[index].onunload)) {
controllers[index].onunload(event)
}
return checkPrevented(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)
unloaders = []
}
var redrawing = false
m.redraw = function (force) {
if (redrawing) return
redrawing = true
if (force) forcing = true
try {
// lastRedrawId is a positive number if a second redraw is requested
// before the next animation frame
// lastRedrawId is null if it's the first redraw and not an event
// handler
if (lastRedrawId && !force) {
// 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
if ($requestAnimationFrame === global.requestAnimationFrame ||
new Date() - lastRedrawCallTime > FRAME_BUDGET) {
if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId)
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
}
} else {
redraw()
lastRedrawId = $requestAnimationFrame(function () {
lastRedrawId = null
}, FRAME_BUDGET)
}
} finally {
redrawing = forcing = false
}
}
m.redraw.strategy = m.prop()
function redraw() {
if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
forEach(roots, function (root, i) {
var component = components[i]
if (controllers[i]) {
var args = [controllers[i]]
m.render(root,
component.view ? component.view(controllers[i], args) : "")
}
})
// 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()
m.redraw.strategy("diff")
}
function endFirstComputation() {
if (m.redraw.strategy() === "none") {
pendingRequests--
m.redraw.strategy("diff")
} else {
m.endComputation()
}
}
m.withAttr = function (prop, withAttrCallback, callbackThis) {
return function (e) {
e = e || window.event
/* eslint-disable no-invalid-this */
var currentTarget = e.currentTarget || this
var _this = callbackThis || this
/* eslint-enable no-invalid-this */
var target = prop in currentTarget ?
currentTarget[prop] :
currentTarget.getAttribute(prop)
withAttrCallback.call(_this, target)
}
}
// routing
function reset(root) {
var cacheKey = getCellCacheKey(root)
clear(root.childNodes, cellCache[cacheKey])
cellCache[cacheKey] = undefined
}
m.deferred = function () {
var deferred = new Deferred()
deferred.promise = propify(deferred.promise)
return deferred
}
function propify(promise, initialValue) {
var prop = m.prop(initialValue)
promise.then(prop)
prop.then = function (resolve, reject) {
return propify(promise.then(resolve, reject), initialValue)
}
prop["catch"] = prop.then.bind(null, null)
return prop
}
// 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 Deferred(onSuccess, onFailure) {
var self = this
var state = 0
var promiseValue = 0
var next = []
self.promise = {}
self.resolve = function (value) {
if (!state) {
promiseValue = value
state = RESOLVING
fire()
}
return self
}
self.reject = function (value) {
if (!state) {
promiseValue = value
state = REJECTING
fire()
}
return self
}
self.promise.then = function (onSuccess, onFailure) {
var deferred = new Deferred(onSuccess, onFailure)
if (state === RESOLVED) {
deferred.resolve(promiseValue)
} else if (state === REJECTED) {
deferred.reject(promiseValue)
} else {
next.push(deferred)
}
return deferred.promise
}
function finish(type) {
state = type || REJECTED
next.map(function (deferred) {
if (state === RESOLVED) {
deferred.resolve(promiseValue)
} else {
deferred.reject(promiseValue)
}
})
}
function thennable(then, success, failure, notThennable) {
if (((promiseValue != null && isObject(promiseValue)) ||
isFunction(promiseValue)) && isFunction(then)) {
try {
// count protects against abuse calls from spec checker
var count = 0
then.call(promiseValue, function (value) {
if (count++) return
promiseValue = value
success()
}, function (value) {
if (count++) return
promiseValue = value
failure()
})
} catch (e) {
m.deferred.onerror(e)
promiseValue = e
failure()
}
} else {
notThennable()
}
}
function fire() {
// check if it's a thenable
var then
try {
then = promiseValue && promiseValue.then
} catch (e) {
m.deferred.onerror(e)
promiseValue = e
state = REJECTING
return fire()
}
if (state === REJECTING) {
m.deferred.onerror(promiseValue)
}
thennable(then, function () {
state = RESOLVING
fire()
}, function () {
state = REJECTING
fire()
}, function () {
try {
if (state === RESOLVING && isFunction(onSuccess)) {
promiseValue = onSuccess(promiseValue)
} else if (state === REJECTING && isFunction(onFailure)) {
promiseValue = onFailure(promiseValue)
state = RESOLVING
}
} catch (e) {
m.deferred.onerror(e)
promiseValue = e
return finish()
}
if (promiseValue === self) {
promiseValue = TypeError()
finish()
} else {
thennable(then, function () {
finish(RESOLVED)
}, finish, function () {
finish(state === RESOLVING && RESOLVED)
})
}
})
}
}
m.deferred.onerror = function (e) {
if (type.call(e) === "[object Error]" &&
!/ Error/.test(e.constructor.toString())) {
pendingRequests = 0
throw e
}
}
m.sync = function (args) {
var deferred = m.deferred()
var outstanding = args.length
var results = []
var method = "resolve"
function synchronizer(pos, resolved) {
return function (value) {
results[pos] = value
if (!resolved) method = "reject"
if (--outstanding === 0) {
deferred.promise(results)
deferred[method](results)
}
return value
}
}
if (args.length > 0) {
forEach(args, function (arg, i) {
arg.then(synchronizer(i, true), synchronizer(i, false))
})
} else {
deferred.resolve([])
}
return deferred.promise
}
return m
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment