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 @@
/*global m */
var app = app || {};
var uniqueId = (function () {
var count = 0;
return function () {
return ++count;
};
}());
// Todo Model
app.Todo = function (data) {
this.title = m.prop(data.title);
this.completed = m.prop(data.completed || false);
this.editing = m.prop(data.editing || false);
this.key = uniqueId();
};
......@@ -51,7 +51,8 @@ app.view = (function () {
classes += task.completed() ? 'completed' : '';
classes += task.editing() ? ' editing' : '';
return classes;
})()
})(),
key: task.key
}, [
m('.view', [
m('input.toggle[type=checkbox]', {
......
......@@ -3,6 +3,7 @@ var m = (function app(window, undefined) {
var type = {}.toString;
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
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
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
......@@ -33,7 +34,7 @@ var m = (function app(window, undefined) {
*/
function m() {
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 classAttrName = "class" in attrs ? "class" : "className";
var cell = {tag: "div", attrs: {}};
......@@ -48,23 +49,26 @@ var m = (function app(window, undefined) {
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];
if (type.call(children) === ARRAY) {
cell.children = children
var children = hasAttrs ? args.slice(2) : args.slice(1);
if (children.length === 1 && type.call(children[0]) === ARRAY) {
cell.children = children[0]
}
else {
cell.children = hasAttrs ? args.slice(2) : args.slice(1)
cell.children = children
}
for (var attrName in attrs) {
if (attrName === classAttrName) {
if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName];
if (attrs.hasOwnProperty(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
}
function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
......@@ -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
//- 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.toString() is null if data is the return value of Console.log in Firefox
if (data == null || data.toString() == null) data = "";
//data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""}
if (data.subtree === "retain") return cached;
var cachedType = type.call(cached), dataType = type.call(data);
if (cached == null || cachedType !== dataType) {
......@@ -117,6 +121,7 @@ var m = (function app(window, undefined) {
if (type.call(data[i]) === ARRAY) {
data = data.concat.apply([], data);
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) {
//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
//5) copy unkeyed items into their respective gaps
var DELETION = 1, INSERTION = 2 , MOVE = 3;
var existing = {}, unkeyed = [], shouldMaintainIdentities = false;
var existing = {}, shouldMaintainIdentities = false;
for (var i = 0; i < cached.length; i++) {
if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
shouldMaintainIdentities = true;
existing[cached[i].attrs.key] = {action: DELETION, index: i}
}
}
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) {
if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
var keysDiffer = false
if (data.length != cached.length) keysDiffer = true
else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
......@@ -161,13 +174,13 @@ var m = (function app(window, undefined) {
element: cached.nodes[existing[key].index] || $document.createElement("div")
}
}
else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
}
}
var actions = []
for (var prop in existing) actions.push(existing[prop])
var changes = actions.sort(sortChanges);
var newCached = new Array(cached.length)
newCached.nodes = cached.nodes.slice()
for (var i = 0, change; change = changes[i]; i++) {
if (change.action === DELETION) {
......@@ -179,6 +192,7 @@ var m = (function app(window, undefined) {
dummy.key = data[change.index].attrs.key;
parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
newCached.nodes[change.index] = dummy
}
if (change.action === MOVE) {
......@@ -186,16 +200,10 @@ var m = (function app(window, undefined) {
parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
}
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.nodes = new Array(parentElement.childNodes.length);
for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
}
}
//end key algorithm
......@@ -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
//the first clause in the regexp matches elements
//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;
cached[cacheCount++] = item
......@@ -231,15 +239,37 @@ var m = (function app(window, undefined) {
}
}
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 (!cached.attrs) cached.attrs = {};
var dataAttrKeys = Object.keys(data.attrs)
var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
//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.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;
......@@ -247,6 +277,7 @@ var m = (function app(window, undefined) {
if (data.attrs.xmlns) namespace = data.attrs.xmlns;
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";
if (isNew) {
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);
......@@ -259,9 +290,22 @@ var m = (function app(window, undefined) {
data.children,
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 = [];
//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)
}
else {
......@@ -269,6 +313,10 @@ var m = (function app(window, undefined) {
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
}
if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
}
//schedule configs to be called. They are called after `build` finishes running
......@@ -284,7 +332,7 @@ var m = (function app(window, undefined) {
configs.push(callback(data, [node, !isNew, context, cached]))
}
}
else if (typeof dataType != FUNCTION) {
else if (typeof data != FUNCTION) {
//handle text nodes
var nodes;
if (cached.nodes.length === 0) {
......@@ -360,7 +408,7 @@ var m = (function app(window, undefined) {
//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
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
if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr
}
......@@ -390,7 +438,15 @@ var m = (function app(window, undefined) {
if (nodes.length != 0) nodes.length = 0
}
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 (type.call(cached.children) === ARRAY) {
for (var i = 0, child; child = cached.children[i]; i++) unload(child)
......@@ -448,7 +504,7 @@ var m = (function app(window, undefined) {
var nodeCache = [], cellCache = {};
m.render = function(root, cell, forceRecreation) {
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 isDocumentRoot = root === $document;
var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
......@@ -491,42 +547,75 @@ var m = (function app(window, undefined) {
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
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.");
var index = roots.indexOf(root);
if (index < 0) index = roots.length;
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) {
var event = {
preventDefault: function() {isPrevented = true}
};
controllers[index].onunload(event)
}
if (!isPrevented) {
m.redraw.strategy("all");
m.startComputation();
roots[index] = root;
var currentModule = topModule = module = module || {};
var controller = new (module.controller || function() {});
//controllers may call m.module recursively (via m.route redirects, for example)
//this conditional ensures only the last recursive m.module call is applied
if (currentModule === topModule) {
if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2))
var currentComponent = topComponent = component = component || {controller: function() {}};
var constructor = component.controller || noop
var controller = new constructor;
//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;
modules[index] = module
components[index] = component
}
endFirstComputation();
return controllers[index]
}
};
var redrawing = false
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 null if it's the first redraw and not an event handler
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 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);
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
}
......@@ -535,14 +624,18 @@ var m = (function app(window, undefined) {
redraw();
lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
}
redrawing = false
};
m.redraw.strategy = m.prop();
var blank = function() {return ""}
function redraw() {
var forceRedraw = m.redraw.strategy() === "all";
if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
for (var i = 0, root; root = roots[i]; 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
......@@ -579,7 +672,7 @@ var m = (function app(window, undefined) {
//routing
var modes = {pathname: "", hash: "#", search: "?"};
var redirect = function() {}, routeParams, currentRoute;
var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
m.route = function() {
//m.route()
if (arguments.length === 0) return currentRoute;
......@@ -589,7 +682,10 @@ var m = (function app(window, undefined) {
redirect = function(source) {
var path = currentRoute = normalizeRoute(source);
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)
isDefaultRoute = false
}
};
var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
......@@ -600,19 +696,26 @@ var m = (function app(window, undefined) {
redirect(path)
}
};
computePostRedrawHook = setScroll;
computePreRedrawHook = setScroll;
window[listener]()
}
//config: m.route
else if (arguments[0].addEventListener) {
else if (arguments[0].addEventListener || arguments[0].attachEvent) {
var element = arguments[0];
var isInitialized = arguments[1];
var context = arguments[2];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href;
element.removeEventListener("click", routeUnobtrusive);
element.addEventListener("click", routeUnobtrusive)
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.addEventListener("click", routeUnobtrusive)
}
else {
element.detachEvent("onclick", routeUnobtrusive);
element.attachEvent("onclick", routeUnobtrusive)
}
}
//m.route(route, params)
//m.route(route, params, shouldReplaceHistoryEntry)
else if (type.call(arguments[0]) === STRING) {
var oldRoute = currentRoute;
currentRoute = arguments[0];
......@@ -627,13 +730,16 @@ var m = (function app(window, undefined) {
var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0];
if (window.history.pushState) {
computePreRedrawHook = setScroll
computePostRedrawHook = function() {
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
setScroll()
};
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) {
......@@ -653,9 +759,18 @@ var m = (function app(window, undefined) {
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) {
if (route === path) {
m.module(root, router[route]);
m.mount(root, router[route]);
return true
}
......@@ -666,7 +781,7 @@ var m = (function app(window, undefined) {
var keys = route.match(/:[^\/]+/g) || [];
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])
m.module(root, router[route])
m.mount(root, router[route])
});
return true
}
......@@ -677,8 +792,9 @@ var m = (function app(window, undefined) {
if (e.ctrlKey || e.metaKey || e.which === 2) return;
if (e.preventDefault) e.preventDefault();
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)) : {};
while (currentTarget && currentTarget.nodeName.toUpperCase() != "A") currentTarget = currentTarget.parentNode
m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
}
function setScroll() {
......@@ -686,28 +802,46 @@ var m = (function app(window, undefined) {
else window.scrollTo(0, 0)
}
function buildQueryString(object, prefix) {
var str = [];
for(var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop];
var duplicates = {}
var str = []
for (var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop
var value = object[prop]
var valueType = type.call(value)
var pair = value != null && (valueType === OBJECT) ?
buildQueryString(value, key) :
valueType === ARRAY ?
value.map(function(item) {return encodeURIComponent(key + "[]") + "=" + encodeURIComponent(item)}).join("&") :
encodeURIComponent(key) + "=" + encodeURIComponent(value)
str.push(pair)
var pair = (value === null) ? encodeURIComponent(key) :
valueType === OBJECT ? buildQueryString(value, key) :
valueType === ARRAY ? value.reduce(function(memo, item) {
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)
if (value !== undefined) str.push(pair)
}
return str.join("&")
}
function parseQueryString(str) {
if (str.charAt(0) === "?") str = str.substring(1);
var pairs = str.split("&"), params = {};
for (var i = 0, len = pairs.length; i < len; i++) {
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
}
m.route.buildQueryString = buildQueryString
m.route.parseQueryString = parseQueryString
function reset(root) {
var cacheKey = getCellCacheKey(root);
clear(root.childNodes, cellCache[cacheKey]);
......@@ -719,11 +853,11 @@ var m = (function app(window, undefined) {
deferred.promise = propify(deferred.promise);
return deferred
};
function propify(promise) {
var prop = m.prop();
function propify(promise, initialValue) {
var prop = m.prop(initialValue);
promise.then(prop);
prop.then = function(resolve, reject) {
return propify(promise.then(resolve, reject))
return propify(promise.then(resolve, reject), initialValue)
};
return prop
}
......@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) {
m.request = function(xhrOptions) {
if (xhrOptions.background !== true) m.startComputation();
var deferred = m.deferred();
var deferred = new Deferred();
var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
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
};
xhrOptions.method = (xhrOptions.method || 'GET').toUpperCase();
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
xhrOptions.onload = xhrOptions.onerror = function(e) {
try {
e = e || event;
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 (type.call(response) === ARRAY && xhrOptions.type) {
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) {
if (xhrOptions.background !== true) m.endComputation()
};
ajax(xhrOptions);
deferred.promise(xhrOptions.initialValue);
deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
return deferred.promise
};
......
......@@ -114,7 +114,12 @@
})({});
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 */
......@@ -228,7 +233,7 @@
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
......
{
"private": true,
"dependencies": {
"mithril": "^0.1.20",
"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) {
this.assertClearCompleteButtonIsHidden = function () {
page.tryGetClearCompleteButton().then(function (element) {
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