Commit 224134ae authored by Sam Saccone's avatar Sam Saccone

Merge pull request #1448 from nordfjord/master

fix checkbox bug in the mithril TodoMVC example
parents aa29541c 4464e459
...@@ -2,9 +2,17 @@ ...@@ -2,9 +2,17 @@
/*global m */ /*global m */
var app = app || {}; var app = app || {};
var uniqueId = (function () {
var count = 0;
return function () {
return ++count;
};
}());
// Todo Model // Todo Model
app.Todo = function (data) { app.Todo = function (data) {
this.title = m.prop(data.title); this.title = m.prop(data.title);
this.completed = m.prop(data.completed || false); this.completed = m.prop(data.completed || false);
this.editing = m.prop(data.editing || false); this.editing = m.prop(data.editing || false);
this.key = uniqueId();
}; };
...@@ -51,7 +51,8 @@ app.view = (function () { ...@@ -51,7 +51,8 @@ app.view = (function () {
classes += task.completed() ? 'completed' : ''; classes += task.completed() ? 'completed' : '';
classes += task.editing() ? ' editing' : ''; classes += task.editing() ? ' editing' : '';
return classes; return classes;
})() })(),
key: task.key
}, [ }, [
m('.view', [ m('.view', [
m('input.toggle[type=checkbox]', { m('input.toggle[type=checkbox]', {
......
...@@ -3,6 +3,7 @@ var m = (function app(window, undefined) { ...@@ -3,6 +3,7 @@ var m = (function app(window, undefined) {
var type = {}.toString; var type = {}.toString;
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
var noop = function() {}
// caching commonly used variables // caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
...@@ -33,7 +34,7 @@ var m = (function app(window, undefined) { ...@@ -33,7 +34,7 @@ var m = (function app(window, undefined) {
*/ */
function m() { function m() {
var args = [].slice.call(arguments); var args = [].slice.call(arguments);
var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]); var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]);
var attrs = hasAttrs ? args[1] : {}; var attrs = hasAttrs ? args[1] : {};
var classAttrName = "class" in attrs ? "class" : "className"; var classAttrName = "class" in attrs ? "class" : "className";
var cell = {tag: "div", attrs: {}}; var cell = {tag: "div", attrs: {}};
...@@ -48,23 +49,26 @@ var m = (function app(window, undefined) { ...@@ -48,23 +49,26 @@ var m = (function app(window, undefined) {
cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
} }
} }
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
var children = hasAttrs ? args[2] : args[1]; var children = hasAttrs ? args.slice(2) : args.slice(1);
if (type.call(children) === ARRAY) { if (children.length === 1 && type.call(children[0]) === ARRAY) {
cell.children = children cell.children = children[0]
} }
else { else {
cell.children = hasAttrs ? args.slice(2) : args.slice(1) cell.children = children
} }
for (var attrName in attrs) { for (var attrName in attrs) {
if (attrName === classAttrName) { if (attrs.hasOwnProperty(attrName)) {
if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]; if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") {
classes.push(attrs[attrName])
cell.attrs[attrName] = "" //create key in correct iteration order
} }
else cell.attrs[attrName] = attrs[attrName] else cell.attrs[attrName] = attrs[attrName]
} }
}
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
return cell return cell
} }
function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
...@@ -93,8 +97,8 @@ var m = (function app(window, undefined) { ...@@ -93,8 +97,8 @@ var m = (function app(window, undefined) {
//there's logic that relies on the assumption that null and undefined data are equivalent to empty strings //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")} //- 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 //- it simplifies diffing code
//data.toString() is null if data is the return value of Console.log in Firefox //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
if (data == null || data.toString() == null) data = ""; try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""}
if (data.subtree === "retain") return cached; if (data.subtree === "retain") return cached;
var cachedType = type.call(cached), dataType = type.call(data); var cachedType = type.call(cached), dataType = type.call(data);
if (cached == null || cachedType !== dataType) { if (cached == null || cachedType !== dataType) {
...@@ -117,6 +121,7 @@ var m = (function app(window, undefined) { ...@@ -117,6 +121,7 @@ var m = (function app(window, undefined) {
if (type.call(data[i]) === ARRAY) { if (type.call(data[i]) === ARRAY) {
data = data.concat.apply([], data); data = data.concat.apply([], data);
i-- //check current index again and flatten until there are no more nested arrays at that index i-- //check current index again and flatten until there are no more nested arrays at that index
len = data.length
} }
} }
...@@ -127,18 +132,26 @@ var m = (function app(window, undefined) { ...@@ -127,18 +132,26 @@ var m = (function app(window, undefined) {
//2) add new keys to map and mark them for addition //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 //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 //4) for each key, handle its corresponding action as marked in previous steps
//5) copy unkeyed items into their respective gaps
var DELETION = 1, INSERTION = 2 , MOVE = 3; var DELETION = 1, INSERTION = 2 , MOVE = 3;
var existing = {}, unkeyed = [], shouldMaintainIdentities = false; var existing = {}, shouldMaintainIdentities = false;
for (var i = 0; i < cached.length; i++) { for (var i = 0; i < cached.length; i++) {
if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
shouldMaintainIdentities = true; shouldMaintainIdentities = true;
existing[cached[i].attrs.key] = {action: DELETION, index: i} existing[cached[i].attrs.key] = {action: DELETION, index: i}
} }
} }
if (shouldMaintainIdentities) {
if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
var guid = 0
for (var i = 0, len = data.length; i < len; i++) {
if (data[i] && data[i].attrs && data[i].attrs.key != null) {
for (var j = 0, len = data.length; j < len; j++) {
if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++
}
break
}
}
if (shouldMaintainIdentities) {
var keysDiffer = false var keysDiffer = false
if (data.length != cached.length) keysDiffer = true if (data.length != cached.length) keysDiffer = true
else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
...@@ -161,13 +174,13 @@ var m = (function app(window, undefined) { ...@@ -161,13 +174,13 @@ var m = (function app(window, undefined) {
element: cached.nodes[existing[key].index] || $document.createElement("div") element: cached.nodes[existing[key].index] || $document.createElement("div")
} }
} }
else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
} }
} }
var actions = [] var actions = []
for (var prop in existing) actions.push(existing[prop]) for (var prop in existing) actions.push(existing[prop])
var changes = actions.sort(sortChanges); var changes = actions.sort(sortChanges);
var newCached = new Array(cached.length) var newCached = new Array(cached.length)
newCached.nodes = cached.nodes.slice()
for (var i = 0, change; change = changes[i]; i++) { for (var i = 0, change; change = changes[i]; i++) {
if (change.action === DELETION) { if (change.action === DELETION) {
...@@ -179,6 +192,7 @@ var m = (function app(window, undefined) { ...@@ -179,6 +192,7 @@ var m = (function app(window, undefined) {
dummy.key = data[change.index].attrs.key; dummy.key = data[change.index].attrs.key;
parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
newCached.nodes[change.index] = dummy
} }
if (change.action === MOVE) { if (change.action === MOVE) {
...@@ -186,16 +200,10 @@ var m = (function app(window, undefined) { ...@@ -186,16 +200,10 @@ var m = (function app(window, undefined) {
parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
} }
newCached[change.index] = cached[change.from] newCached[change.index] = cached[change.from]
newCached.nodes[change.index] = change.element
} }
} }
for (var i = 0, len = unkeyed.length; i < len; i++) {
var change = unkeyed[i];
parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null);
newCached[change.index] = cached[change.index]
}
cached = newCached; cached = newCached;
cached.nodes = new Array(parentElement.childNodes.length);
for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
} }
} }
//end key algorithm //end key algorithm
...@@ -209,7 +217,7 @@ var m = (function app(window, undefined) { ...@@ -209,7 +217,7 @@ var m = (function app(window, undefined) {
//fix offset of next element if item was a trusted string w/ more than one html element //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 first clause in the regexp matches elements
//the second clause (after the pipe) matches text nodes //the second clause (after the pipe) matches text nodes
subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || []).length subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length
} }
else subArrayCount += type.call(item) === ARRAY ? item.length : 1; else subArrayCount += type.call(item) === ARRAY ? item.length : 1;
cached[cacheCount++] = item cached[cacheCount++] = item
...@@ -231,15 +239,37 @@ var m = (function app(window, undefined) { ...@@ -231,15 +239,37 @@ var m = (function app(window, undefined) {
} }
} }
else if (data != null && dataType === OBJECT) { else if (data != null && dataType === OBJECT) {
var views = [], controllers = []
while (data.view) {
var view = data.view.$original || data.view
var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1
var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop)
var key = data && data.attrs && data.attrs.key
data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"}
if (data.subtree === "retain") return cached;
if (key) {
if (!data.attrs) data.attrs = {}
data.attrs.key = key
}
if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload})
views.push(view)
controllers.push(controller)
}
if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.")
if (!data.attrs) data.attrs = {}; if (!data.attrs) data.attrs = {};
if (!cached.attrs) cached.attrs = {}; if (!cached.attrs) cached.attrs = {};
var dataAttrKeys = Object.keys(data.attrs) var dataAttrKeys = Object.keys(data.attrs)
var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
//if an element is different enough from the one in cache, recreate it //if an element is different enough from the one in cache, recreate it
if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) { if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) {
if (cached.nodes.length) clear(cached.nodes); if (cached.nodes.length) clear(cached.nodes);
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload()
if (cached.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop})
}
}
} }
if (type.call(data.tag) != STRING) return; if (type.call(data.tag) != STRING) return;
...@@ -247,6 +277,7 @@ var m = (function app(window, undefined) { ...@@ -247,6 +277,7 @@ var m = (function app(window, undefined) {
if (data.attrs.xmlns) namespace = data.attrs.xmlns; if (data.attrs.xmlns) namespace = data.attrs.xmlns;
else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg";
else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML";
if (isNew) { if (isNew) {
if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is);
else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag);
...@@ -259,9 +290,22 @@ var m = (function app(window, undefined) { ...@@ -259,9 +290,22 @@ var m = (function app(window, undefined) {
data.children, data.children,
nodes: [node] nodes: [node]
}; };
if (controllers.length) {
cached.views = views
cached.controllers = controllers
for (var i = 0, controller; controller = controllers[i]; i++) {
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
}
}
}
if (cached.children && !cached.children.nodes) cached.children.nodes = []; 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 //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" && data.attrs.value) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace); if (data.tag === "select" && "value" in data.attrs) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
parentElement.insertBefore(node, parentElement.childNodes[index] || null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
} }
else { else {
...@@ -269,6 +313,10 @@ var m = (function app(window, undefined) { ...@@ -269,6 +313,10 @@ var m = (function app(window, undefined) {
if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace); 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.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs);
cached.nodes.intact = true; cached.nodes.intact = true;
if (controllers.length) {
cached.views = views
cached.controllers = controllers
}
if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null) if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
} }
//schedule configs to be called. They are called after `build` finishes running //schedule configs to be called. They are called after `build` finishes running
...@@ -284,7 +332,7 @@ var m = (function app(window, undefined) { ...@@ -284,7 +332,7 @@ var m = (function app(window, undefined) {
configs.push(callback(data, [node, !isNew, context, cached])) configs.push(callback(data, [node, !isNew, context, cached]))
} }
} }
else if (typeof dataType != FUNCTION) { else if (typeof data != FUNCTION) {
//handle text nodes //handle text nodes
var nodes; var nodes;
if (cached.nodes.length === 0) { if (cached.nodes.length === 0) {
...@@ -360,7 +408,7 @@ var m = (function app(window, undefined) { ...@@ -360,7 +408,7 @@ var m = (function app(window, undefined) {
//handle cases that are properties (but ignore cases where we should use setAttribute instead) //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 //- 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 //- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type")) { else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type" || attrName === "width" || attrName === "height")) {
//#348 don't set the value if not needed otherwise cursor placement breaks in Chrome //#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr
} }
...@@ -390,7 +438,15 @@ var m = (function app(window, undefined) { ...@@ -390,7 +438,15 @@ var m = (function app(window, undefined) {
if (nodes.length != 0) nodes.length = 0 if (nodes.length != 0) nodes.length = 0
} }
function unload(cached) { function unload(cached) {
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload(); if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) {
cached.configContext.onunload();
cached.configContext.onunload = null
}
if (cached.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop});
}
}
if (cached.children) { if (cached.children) {
if (type.call(cached.children) === ARRAY) { if (type.call(cached.children) === ARRAY) {
for (var i = 0, child; child = cached.children[i]; i++) unload(child) for (var i = 0, child; child = cached.children[i]; i++) unload(child)
...@@ -448,7 +504,7 @@ var m = (function app(window, undefined) { ...@@ -448,7 +504,7 @@ var m = (function app(window, undefined) {
var nodeCache = [], cellCache = {}; var nodeCache = [], cellCache = {};
m.render = function(root, cell, forceRecreation) { m.render = function(root, cell, forceRecreation) {
var configs = []; var configs = [];
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it."); if (!root) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
var id = getCellCacheKey(root); var id = getCellCacheKey(root);
var isDocumentRoot = root === $document; var isDocumentRoot = root === $document;
var node = isDocumentRoot || root === $document.documentElement ? documentNode : root; var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
...@@ -491,42 +547,75 @@ var m = (function app(window, undefined) { ...@@ -491,42 +547,75 @@ var m = (function app(window, undefined) {
return gettersetter(store) return gettersetter(store)
}; };
var roots = [], modules = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePostRedrawHook = null, prevented = false, topModule; var roots = [], components = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePreRedrawHook = null, computePostRedrawHook = null, prevented = false, topComponent, unloaders = [];
var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms
m.module = function(root, module) { function parameterize(component, args) {
var controller = function() {
return (component.controller || noop).apply(this, args) || this
}
var view = function(ctrl) {
if (arguments.length > 1) args = args.concat([].slice.call(arguments, 1))
return component.view.apply(component, args ? [ctrl].concat(args) : [ctrl])
}
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) {
return parameterize(component, [].slice.call(arguments, 1))
}
m.mount = m.module = function(root, component) {
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it."); if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
var index = roots.indexOf(root); var index = roots.indexOf(root);
if (index < 0) index = roots.length; if (index < 0) index = roots.length;
var isPrevented = false; var isPrevented = false;
var event = {preventDefault: function() {
isPrevented = true;
computePreRedrawHook = computePostRedrawHook = null;
}};
for (var i = 0, unloader; unloader = unloaders[i]; i++) {
unloader.handler.call(unloader.controller, event)
unloader.controller.onunload = null
}
if (isPrevented) {
for (var i = 0, unloader; unloader = unloaders[i]; i++) unloader.controller.onunload = unloader.handler
}
else unloaders = []
if (controllers[index] && typeof controllers[index].onunload === FUNCTION) { if (controllers[index] && typeof controllers[index].onunload === FUNCTION) {
var event = {
preventDefault: function() {isPrevented = true}
};
controllers[index].onunload(event) controllers[index].onunload(event)
} }
if (!isPrevented) { if (!isPrevented) {
m.redraw.strategy("all"); m.redraw.strategy("all");
m.startComputation(); m.startComputation();
roots[index] = root; roots[index] = root;
var currentModule = topModule = module = module || {}; if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2))
var controller = new (module.controller || function() {}); var currentComponent = topComponent = component = component || {controller: function() {}};
//controllers may call m.module recursively (via m.route redirects, for example) var constructor = component.controller || noop
//this conditional ensures only the last recursive m.module call is applied var controller = new constructor;
if (currentModule === topModule) { //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; controllers[index] = controller;
modules[index] = module components[index] = component
} }
endFirstComputation(); endFirstComputation();
return controllers[index] return controllers[index]
} }
}; };
var redrawing = false
m.redraw = function(force) { m.redraw = function(force) {
if (redrawing) return
redrawing = true
//lastRedrawId is a positive number if a second redraw is requested before the next animation frame //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 //lastRedrawID is null if it's the first redraw and not an event handler
if (lastRedrawId && force !== true) { if (lastRedrawId && force !== true) {
//when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout //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 //when rAF: always reschedule redraw
if (new Date - lastRedrawCallTime > FRAME_BUDGET || $requestAnimationFrame === window.requestAnimationFrame) { if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) {
if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId); if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId);
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET) lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
} }
...@@ -535,14 +624,18 @@ var m = (function app(window, undefined) { ...@@ -535,14 +624,18 @@ var m = (function app(window, undefined) {
redraw(); redraw();
lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET) lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
} }
redrawing = false
}; };
m.redraw.strategy = m.prop(); m.redraw.strategy = m.prop();
var blank = function() {return ""}
function redraw() { function redraw() {
var forceRedraw = m.redraw.strategy() === "all"; if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
for (var i = 0, root; root = roots[i]; i++) { for (var i = 0, root; root = roots[i]; i++) {
if (controllers[i]) { if (controllers[i]) {
m.render(root, modules[i].view ? modules[i].view(controllers[i]) : blank(), forceRedraw) var args = components[i].controller && components[i].controller.$$args ? [controllers[i]].concat(components[i].controller.$$args) : [controllers[i]]
m.render(root, components[i].view ? components[i].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 //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState
...@@ -579,7 +672,7 @@ var m = (function app(window, undefined) { ...@@ -579,7 +672,7 @@ var m = (function app(window, undefined) {
//routing //routing
var modes = {pathname: "", hash: "#", search: "?"}; var modes = {pathname: "", hash: "#", search: "?"};
var redirect = function() {}, routeParams, currentRoute; var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
m.route = function() { m.route = function() {
//m.route() //m.route()
if (arguments.length === 0) return currentRoute; if (arguments.length === 0) return currentRoute;
...@@ -589,7 +682,10 @@ var m = (function app(window, undefined) { ...@@ -589,7 +682,10 @@ var m = (function app(window, undefined) {
redirect = function(source) { redirect = function(source) {
var path = currentRoute = normalizeRoute(source); var path = currentRoute = normalizeRoute(source);
if (!routeByValue(root, router, path)) { if (!routeByValue(root, router, path)) {
if (isDefaultRoute) throw new Error("Ensure the default route matches one of the routes defined in m.route")
isDefaultRoute = true
m.route(defaultRoute, true) m.route(defaultRoute, true)
isDefaultRoute = false
} }
}; };
var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate"; var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
...@@ -600,19 +696,26 @@ var m = (function app(window, undefined) { ...@@ -600,19 +696,26 @@ var m = (function app(window, undefined) {
redirect(path) redirect(path)
} }
}; };
computePostRedrawHook = setScroll; computePreRedrawHook = setScroll;
window[listener]() window[listener]()
} }
//config: m.route //config: m.route
else if (arguments[0].addEventListener) { else if (arguments[0].addEventListener || arguments[0].attachEvent) {
var element = arguments[0]; var element = arguments[0];
var isInitialized = arguments[1]; var isInitialized = arguments[1];
var context = arguments[2]; var context = arguments[2];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href; var vdom = arguments[3];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + vdom.attrs.href;
if (element.addEventListener) {
element.removeEventListener("click", routeUnobtrusive); element.removeEventListener("click", routeUnobtrusive);
element.addEventListener("click", routeUnobtrusive) element.addEventListener("click", routeUnobtrusive)
} }
//m.route(route, params) else {
element.detachEvent("onclick", routeUnobtrusive);
element.attachEvent("onclick", routeUnobtrusive)
}
}
//m.route(route, params, shouldReplaceHistoryEntry)
else if (type.call(arguments[0]) === STRING) { else if (type.call(arguments[0]) === STRING) {
var oldRoute = currentRoute; var oldRoute = currentRoute;
currentRoute = arguments[0]; currentRoute = arguments[0];
...@@ -627,13 +730,16 @@ var m = (function app(window, undefined) { ...@@ -627,13 +730,16 @@ var m = (function app(window, undefined) {
var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0]; var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0];
if (window.history.pushState) { if (window.history.pushState) {
computePreRedrawHook = setScroll
computePostRedrawHook = function() { computePostRedrawHook = function() {
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute); window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
setScroll()
}; };
redirect(modes[m.route.mode] + currentRoute) redirect(modes[m.route.mode] + currentRoute)
} }
else $location[m.route.mode] = currentRoute else {
$location[m.route.mode] = currentRoute
redirect(modes[m.route.mode] + currentRoute)
}
} }
}; };
m.route.param = function(key) { m.route.param = function(key) {
...@@ -653,9 +759,18 @@ var m = (function app(window, undefined) { ...@@ -653,9 +759,18 @@ var m = (function app(window, undefined) {
path = path.substr(0, queryStart) path = path.substr(0, queryStart)
} }
// 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 !== -1){
m.mount(root, router[keys [index]]);
return true;
}
for (var route in router) { for (var route in router) {
if (route === path) { if (route === path) {
m.module(root, router[route]); m.mount(root, router[route]);
return true return true
} }
...@@ -666,7 +781,7 @@ var m = (function app(window, undefined) { ...@@ -666,7 +781,7 @@ var m = (function app(window, undefined) {
var keys = route.match(/:[^\/]+/g) || []; var keys = route.match(/:[^\/]+/g) || [];
var values = [].slice.call(arguments, 1, -2); var values = [].slice.call(arguments, 1, -2);
for (var i = 0, len = keys.length; i < len; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) for (var i = 0, len = keys.length; i < len; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
m.module(root, router[route]) m.mount(root, router[route])
}); });
return true return true
} }
...@@ -677,8 +792,9 @@ var m = (function app(window, undefined) { ...@@ -677,8 +792,9 @@ var m = (function app(window, undefined) {
if (e.ctrlKey || e.metaKey || e.which === 2) return; if (e.ctrlKey || e.metaKey || e.which === 2) return;
if (e.preventDefault) e.preventDefault(); if (e.preventDefault) e.preventDefault();
else e.returnValue = false; else e.returnValue = false;
var currentTarget = e.currentTarget || this; var currentTarget = e.currentTarget || e.srcElement;
var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {}; var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
while (currentTarget && currentTarget.nodeName.toUpperCase() != "A") currentTarget = currentTarget.parentNode
m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args) m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
} }
function setScroll() { function setScroll() {
...@@ -686,28 +802,46 @@ var m = (function app(window, undefined) { ...@@ -686,28 +802,46 @@ var m = (function app(window, undefined) {
else window.scrollTo(0, 0) else window.scrollTo(0, 0)
} }
function buildQueryString(object, prefix) { function buildQueryString(object, prefix) {
var str = []; var duplicates = {}
for(var prop in object) { var str = []
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop]; for (var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop
var value = object[prop]
var valueType = type.call(value) var valueType = type.call(value)
var pair = value != null && (valueType === OBJECT) ? var pair = (value === null) ? encodeURIComponent(key) :
buildQueryString(value, key) : valueType === OBJECT ? buildQueryString(value, key) :
valueType === ARRAY ? valueType === ARRAY ? value.reduce(function(memo, item) {
value.map(function(item) {return encodeURIComponent(key + "[]") + "=" + encodeURIComponent(item)}).join("&") : if (!duplicates[key]) duplicates[key] = {}
if (!duplicates[key][item]) {
duplicates[key][item] = true
return memo.concat(encodeURIComponent(key) + "=" + encodeURIComponent(item))
}
return memo
}, []).join("&") :
encodeURIComponent(key) + "=" + encodeURIComponent(value) encodeURIComponent(key) + "=" + encodeURIComponent(value)
str.push(pair) if (value !== undefined) str.push(pair)
} }
return str.join("&") return str.join("&")
} }
function parseQueryString(str) { function parseQueryString(str) {
if (str.charAt(0) === "?") str = str.substring(1);
var pairs = str.split("&"), params = {}; var pairs = str.split("&"), params = {};
for (var i = 0, len = pairs.length; i < len; i++) { for (var i = 0, len = pairs.length; i < len; i++) {
var pair = pairs[i].split("="); var pair = pairs[i].split("=");
params[decodeURIComponent(pair[0])] = pair[1] ? decodeURIComponent(pair[1]) : "" var key = decodeURIComponent(pair[0])
var value = pair.length == 2 ? decodeURIComponent(pair[1]) : null
if (params[key] != null) {
if (type.call(params[key]) !== ARRAY) params[key] = [params[key]]
params[key].push(value)
}
else params[key] = value
} }
return params return params
} }
m.route.buildQueryString = buildQueryString
m.route.parseQueryString = parseQueryString
function reset(root) { function reset(root) {
var cacheKey = getCellCacheKey(root); var cacheKey = getCellCacheKey(root);
clear(root.childNodes, cellCache[cacheKey]); clear(root.childNodes, cellCache[cacheKey]);
...@@ -719,11 +853,11 @@ var m = (function app(window, undefined) { ...@@ -719,11 +853,11 @@ var m = (function app(window, undefined) {
deferred.promise = propify(deferred.promise); deferred.promise = propify(deferred.promise);
return deferred return deferred
}; };
function propify(promise) { function propify(promise, initialValue) {
var prop = m.prop(); var prop = m.prop(initialValue);
promise.then(prop); promise.then(prop);
prop.then = function(resolve, reject) { prop.then = function(resolve, reject) {
return propify(promise.then(resolve, reject)) return propify(promise.then(resolve, reject), initialValue)
}; };
return prop return prop
} }
...@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) { ...@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) {
m.request = function(xhrOptions) { m.request = function(xhrOptions) {
if (xhrOptions.background !== true) m.startComputation(); if (xhrOptions.background !== true) m.startComputation();
var deferred = m.deferred(); var deferred = new Deferred();
var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp"; var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify; var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse; var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse;
var extract = xhrOptions.extract || function(xhr) { var extract = isJSONP ? function(jsonp) {return jsonp.responseText} : xhrOptions.extract || function(xhr) {
return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText
}; };
xhrOptions.method = (xhrOptions.method || 'GET').toUpperCase();
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data); xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize); xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
xhrOptions.onload = xhrOptions.onerror = function(e) { xhrOptions.onload = xhrOptions.onerror = function(e) {
try { try {
e = e || event; e = e || event;
var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity; var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
var response = unwrap(deserialize(extract(e.target, xhrOptions))); var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target);
if (e.type === "load") { if (e.type === "load") {
if (type.call(response) === ARRAY && xhrOptions.type) { if (type.call(response) === ARRAY && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i]) for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
...@@ -1005,7 +1140,7 @@ var m = (function app(window, undefined) { ...@@ -1005,7 +1140,7 @@ var m = (function app(window, undefined) {
if (xhrOptions.background !== true) m.endComputation() if (xhrOptions.background !== true) m.endComputation()
}; };
ajax(xhrOptions); ajax(xhrOptions);
deferred.promise(xhrOptions.initialValue); deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
return deferred.promise return deferred.promise
}; };
......
...@@ -114,7 +114,12 @@ ...@@ -114,7 +114,12 @@
})({}); })({});
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
} }
/* jshint ignore:end */ /* jshint ignore:end */
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
xhr.onload = function (e) { xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText); var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) { if (parsedResponse instanceof Array) {
var count = parsedResponse.length var count = parsedResponse.length;
if (count !== 0) { if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues'; issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline'; document.getElementById('issue-count').style.display = 'inline';
......
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"mithril": "^0.1.20",
"todomvc-common": "^1.0.1", "todomvc-common": "^1.0.1",
"todomvc-app-css": "^1.0.1" "todomvc-app-css": "^1.0.1",
"mithril": "~0.2.0"
} }
} }
...@@ -30,6 +30,8 @@ function TestOperations(page) { ...@@ -30,6 +30,8 @@ function TestOperations(page) {
this.assertClearCompleteButtonIsHidden = function () { this.assertClearCompleteButtonIsHidden = function () {
page.tryGetClearCompleteButton().then(function (element) { page.tryGetClearCompleteButton().then(function (element) {
testIsHidden(element, 'clear completed items button'); testIsHidden(element, 'clear completed items button');
}, function (_error) {
assert(_error.code === 7, 'error accessing clear completed items button, error: ' + _error.message);
}); });
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment