all repos — litestore @ 13dbd572c839c98a665dea47d80201065a7ebdcd

A minimalist nosql document store.

Migrating to Mithril 0.2.0; Refactoring widgets (untested).
h3rald h3rald@h3rald.com
Sat, 09 May 2015 21:27:19 +0200
commit

13dbd572c839c98a665dea47d80201065a7ebdcd

parent

b3dae462cc3601faa78f84b779a6f40814b35bdf

M admin/index.htmladmin/index.html

@@ -31,6 +31,7 @@ <script src="js/vendor/ace/ext-searchbox.js"> </script>

<script src="js/vendor/ace/ext-settings_menu.js"> </script> <script src="js/vendor/ace/ext-statusbar.js"> </script> <script src="js/utils.js"> </script> + <script src="js/components/widgets.js"> </script> <script src="js/models.js"> </script> <script src="js/components/navbar.js"> </script> <script src="js/components/uploader.js"> </script>
M admin/js/components/doclist.jsadmin/js/components/doclist.js

@@ -5,40 +5,51 @@ var u = app.utils;

app.doclist = {}; - /* - * - id - * - tags - * - content - */ - app.doclist.panel = function(item){ - var obj = {}; - var path = (item.id.match(/\.html?$/)) ? "/html/" : "/document/view/"; - obj.title = m("a", {href: path+item.id, config: m.route}, [item.id]); - obj.content = m("div", [ - m("p", [item.content]), - m("p", item.tags.map(function(tag){ - return u.taglink(tag); - })) - ] - ); - return m(".row.search-result", m(".col-md-12", [u.panel(obj)])); + // Subcomponent + app.doclist.panel = { + + controller: function(args){ + return { + id: args.id, + tags: args.tags, + content: args.content + }; + }, + + view: function(ctrl){ + var obj = {}; + var path = (ctrl.id.match(/\.html?$/)) ? "/html/" : "/document/view/"; + obj.title = m("a", {href: path+ctrl.id, config: m.route}, [ctrl.id]); + obj.content = m("div", [ + m("p", [ctrl.content]), + m("p", ctrl.tags.map(function(tag){ + return u.taglink(tag); + })) + ]); + return m(".row.search-result", m(".col-md-12", [u.panel(obj)])); + } + }; + + app.doclist.controller = function(args){ + return { + items: args.items, + title: args.title, + subtitle: args.subtitle, + querydata: args.querydata + }; }; - /* - * - items (id, tags, content) - * - title - * - subtitle - * - querydata (total, limit, offset, baseurl) - */ - app.doclist.view = function(obj){ - var results = m(".row", [m(".col-md-12", obj.items.map(app.doclist.panel))]); + app.doclist.view = function(ctrl){ + var results = m(".row", [m(".col-md-12", ctrl.items.map(function(item){ + return m.component(app.doclist.panel, item); + }))]); return m("section", [ - m(".row", [obj.title]), - m(".row", [obj.subtitle]), - m(".row.text-center", [u.paginator(obj.querydata)]), + m(".row", [ctrl.title]), + m(".row", [ctrl.subtitle]), + m(".row.text-center", [u.paginator(ctrl.querydata)]), results, - m(".row.text-center", [u.paginator(obj.querydata)]) + m(".row.text-center", [u.paginator(ctrl.querydata)]) ]); };
A admin/js/components/widgets.js

@@ -0,0 +1,238 @@

+(function(){ + 'use strict'; + var app = window.LS || (window.LS = {}); + + app.widgets = {}; + + // Panel + app.widgets.panel = function(obj){ + var panel = { + controller: function(args){ + return { + title: args.title, + footer: args.footer, + content: args.content, + }; + }, + view: function(ctrl){ + var title = ""; + var footer = ""; + if (ctrl.title){ + title = m(".panel-heading", [ + m("h2.panel-title", [ctrl.title]) + ]); + } + if (ctrl.footer){ + footer = m(".panel-footer", ctrl.footer); + } + return m(".panel.panel-default", [ + title, + m(".panel-body", [ + ctrl.content + ]), + footer + ]); + } + }; + return m.component(panel, obj); + }; + + // Paginator + app.widgets.paginator = function(obj) { + var paginator = { + controller: function(args){ + var max_page = Math.min(14, Math.ceil(args.total/args.limit)-1); + var c_page = Math.ceil(args.offset/args.limit); + return { + baseurl: args.baseurl, + total: args.total, + limit: args.limit, + offset: args.offset, + max_page: max_page, + c_page: c_page + }; + }, + view: function(ctrl){ + var page = function(n, sign, disabled){ + var klass; + if (disabled) { + klass = "disabled"; + } else { + klass = (n === ctrl.c_page) ? "active" : "inactive"; + } + var first = (n === 0); + var last = (n == ctrl.max_page); + var offset = ctrl.limit * n; + sign = sign || n+1; + return m("li", {class: klass}, + [m("a", { + href: ctrl.baseurl +(n+1), // assuming 10 elements per page //+"/"+obj.limit, + config: m.route + }, [m.trust(sign)] + )] + ); + }; + + var pages = []; + var prev; + var next; + for (var i=0; i<=ctrl.max_page; i++){ + var p; + switch(i){ + case ctrl.c_page-1: + prev = page(i, "&laquo;"); + break; + case ctrl.c_page+1: + next = page(i, "&raquo;"); + break; + } + if (ctrl.c_page === 0){ + prev = page(0, "&laquo;", true); + } + if (c_page === ctrl.max_page){ + next = page(ctrl.max_page, "&raquo;", true); + } + pages.push(page(i)); + } + pages.unshift(prev); + pages.push(next); + return m("nav", [m("ul.pagination", pages)]); + } + }; + return m.component(paginator, obj); + }; + + // Dropdown + app.widgets.dropdown = function(obj) { + var dropdown = { + controller: function(args){ + return { + icon: args.icon, + active: args.active, + title: args.title, + links: args.links + }; + }, + view: function(ctrl){ + var el = "li.dropdown"; + var icon = (ctrl.icon) ? m("i.fa."+ctrl.icon) : ""; + if (ctrl.active.length > 0) { + el += "."+ctrl.active; + } + return m(el, [ + m("a.dropdown-toggle[href='#'][data-toggle='dropdown'][role='button'][aria-expanded='false']", + [icon, m("span", " "+ctrl.title+" "), m("span.caret")]), + m("ul.dropdown-menu[role='menu']", + ctrl.links.map(function(e){ + return m("li", + [m("a", {href: e.path, config: m.route}, m.trust(e.title))]);})) + ]); + } + }; + return m.component(dropdown, obj); + }; + + // TagLink + app.widgets.taglink = function(obj){ + var taglink = { + controller: function(args) { + return { + color: /^\$/.test(args.tag) ? "warning" : "primary", + tag: args.tag + }; + }, + view: function(ctrl) { + return m("span.tag-label.label.label-"+ctrl.color, + [m("i.fa.fa-tag"), " ", m("a", {href: "/tags/"+ctrl.tag, config:m.route}, ctrl.tag)]); + } + }; + return m.component(taglink, obj); + }; + + // DocLink (warning: API change!) + app.widgets.doclink = function(obj) { + var doclink = { + controller: function(args){ + return {id: args.id}; + }, + view: function(ctrl) { + return m("a", {href: "/document/view/"+ctrl.id, config: m.route}, id); + } + }; + return m.component(doclink, obj); + }; + + // TagButton (warning: API change!) + app.widgets.tagbutton = function(obj) { + var tagbutton = { + controller: function(args){ + return { + tag: args.tag, + n: args.n + }; + }, + view: function(ctrl) { + return m("a", {href: "/tags/"+ctrl.tag, config:m.route}, + [m("i.fa.fa-tag"), " "+ctrl.tag+" ", m("span.badge", ctrl.n)]); + } + }; + return m.component(tagbutton, obj); + }; + + // Toolbar + app.widgets.toolbar = function(obj) { + var toolbar = { + controller: function(args){ + return {link: args.links}; + }, + view: function(ctrl){ + return m("nav.toolbar.btn-group[role='group'][aria-label='...'].pull-right", + ctrl.links.map(function(l){ + return m("a.btn.btn-default", + {onclick:l.action, config: l.config}, + [m("i.fa.fa-"+l.icon), " "+l.title]); + }) + ); + } + }; + return m.component(toolbar, obj); + }; + + // Modal + app.widgets.modal = function(obj) { + var modal = { + controller: function(args){ + return { + id: args.id, + content: args.content, + title: args.title, + action: args.action, + actionText: args.actionText + }; + }, + view: function(ctrl){ + return m(".modal.fade", + {id: ctrl.id, tabindex: "-1", role: "dialog"}, + [ + m(".modal-dialog", [ + m(".modal-content", [ + m(".modal-header", [ + m("button", {type: "button", class: "close", "data-dismiss": "modal"}, + [m.trust("&times;")]), + m("h4.modal-title", ctrl.title) + ]), + m(".modal-body", [ctrl.content]), + m(".modal-footer", [ + m("button.btn.btn-default[data-dismiss='modal']", "Close"), + m("button.btn.btn-primary[data-dismiss='modal']", {onclick: ctrl.action}, ctrl.actionText) + ]) + ]) + ]) + ] + ); + } + }; + return m.component(modal, obj); + }; + +}());
M admin/js/models.jsadmin/js/models.js

@@ -109,4 +109,4 @@ data: ops

}); }); }; -}());+}());
M admin/js/modules/search.jsadmin/js/modules/search.js

@@ -19,7 +19,7 @@ vm.execTime = 0;

Doc.search(vm.query, vm.offset, vm.limit).then(function(result){ vm.result(result); vm.total = result.total; - vm.execTime = (result["execution-time"]*1000).toFixed(0); + vm.execTime = (result.execution_time*1000).toFixed(0); }, vm.flashError); }; app.search.main = function(){

@@ -31,9 +31,9 @@ obj.subtitle = m("p.col-md-12", [m("strong", result.total), " results ("+vm.execTime+" ms)"]);

obj.items = result.results; obj.items.forEach(function(item){ item.content = m.trust(item.highlight) }); obj.querydata = vm; - return app.doclist.view(obj); + return m.component(app.doclist, obj); }; u.layout(app.search); -}())+}());
M admin/js/modules/tags.jsadmin/js/modules/tags.js

@@ -17,7 +17,7 @@ vm.total= 0;

vm.execTime = 0; vm.docs = Doc.getByTag(vm.id, vm.offset, vm.limit).then(function(docs){ vm.total = docs.total; - vm.execTime = (docs["execution-time"]*1000).toFixed(0); + vm.execTime = (docs["execution_time"]*1000).toFixed(0); return docs; }, vm.flashError); };

@@ -32,10 +32,10 @@ obj.items = docs.results;

obj.items.forEach(function(item){ item.content = m("ul", [ m("li", [m("strong", "Created: "), u.date(item.created)]), - m("li", [m("strong", "Modified: "), u.date(item.modified) || "n/a"]), + m("li", [m("strong", "Modified: "), u.date(item.modified)]), ]); }); - return app.doclist.view(obj); + return m.component(app.doclist, obj); }; u.layout(app.tags);
M admin/js/utils.jsadmin/js/utils.js

@@ -101,23 +101,35 @@ };

}; u.panel = function(obj){ - var title = ""; - var footer = ""; - if (obj.title){ - title = m(".panel-heading", [ - m("h2.panel-title", [obj.title]) - ]); - } - if (obj.footer){ - footer = m(".panel-footer", obj.footer); - } - return m(".panel.panel-default", [ - title, - m(".panel-body", [ - obj.content - ]), - footer - ]); + var panel = { + controller: function(args){ + return { + title: args.title, + footer: args.footer, + content: args.content, + }; + }, + view: function(ctrl){ + var title = ""; + var footer = ""; + if (ctrl.title){ + title = m(".panel-heading", [ + m("h2.panel-title", [ctrl.title]) + ]); + } + if (ctrl.footer){ + footer = m(".panel-footer", ctrl.footer); + } + return m(".panel.panel-default", [ + title, + m(".panel-body", [ + ctrl.content + ]), + footer + ]); + } + }; + return m.component(panel, obj); }; /**

@@ -207,11 +219,7 @@ return m("a", {href: "/document/view/"+id, config: m.route}, id);

}; u.date = function(date) { - if (date === ""){ - return ""; - } else { - return new Date(Date.parse(date)).toUTCString(); - } + return (date) ? new Date(Date.parse(date)).toUTCString() : "n/a"; }; u.toolbar = function(obj){
M admin/js/vendor/mithril.jsadmin/js/vendor/mithril.js

@@ -3,6 +3,7 @@ var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function";

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 @@ *

*/ 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,8 +49,6 @@ var pair = attrParser.exec(match[3]);

cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) } } - if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); - var children = hasAttrs ? args.slice(2) : args.slice(1); if (children.length === 1 && type.call(children[0]) === ARRAY) {

@@ -58,14 +57,18 @@ }

else { cell.children = children } - + for (var attrName in attrs) { - if (attrName === classAttrName) { - var className = cell.attrs[attrName] - cell.attrs[attrName] = (className && attrs[attrName] ? className + " " : className || "") + 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) {

@@ -94,7 +97,7 @@

//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 + //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);

@@ -130,7 +133,7 @@ //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 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;

@@ -236,15 +239,37 @@ cached.nodes = nodes

} } 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 || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.retain !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { + 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;

@@ -252,6 +277,7 @@ var node, isNew = cached.nodes.length === 0;

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);

@@ -264,9 +290,22 @@ build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) :

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 {

@@ -274,11 +313,15 @@ 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 + } if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null) } //schedule configs to be called. They are called after `build` finishes running if (typeof data.attrs["config"] === FUNCTION) { - var context = cached.configContext = cached.configContext || {retain: (m.redraw.strategy() == "diff") || undefined}; + var context = cached.configContext = cached.configContext || {}; // bind var callback = function(data, args) {

@@ -399,6 +442,11 @@ 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)

@@ -456,7 +504,7 @@ };

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;

@@ -499,42 +547,75 @@

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) }

@@ -543,13 +624,18 @@ else {

redraw(); lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET) } + redrawing = false }; m.redraw.strategy = m.prop(); - var blank = function() {return ""} function redraw() { + 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()) + 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

@@ -586,7 +672,7 @@ };

//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;

@@ -596,7 +682,10 @@ var root = arguments[0], defaultRoute = arguments[1], router = arguments[2];

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";

@@ -607,7 +696,7 @@ if (currentRoute != normalizeRoute(path)) {

redirect(path) } }; - computePostRedrawHook = setScroll; + computePreRedrawHook = setScroll; window[listener]() } //config: m.route

@@ -615,7 +704,8 @@ 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; + 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)

@@ -625,7 +715,7 @@ 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];

@@ -640,9 +730,9 @@

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) }

@@ -674,13 +764,13 @@ // an exact match for the current path

var keys = Object.keys(router); var index = keys.indexOf(path); if(index !== -1){ - m.module(root, router[keys [index]]); + 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 }

@@ -691,7 +781,7 @@ path.replace(matcher, function() {

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 }

@@ -734,6 +824,8 @@ }

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("=");

@@ -1022,9 +1114,10 @@ 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) {

@@ -1063,4 +1156,4 @@ return m

})(typeof window != "undefined" ? window : {}); if (typeof module != "undefined" && module !== null && module.exports) module.exports = m; -else if (typeof define === "function" && define.amd) define(function() {return m}); +else if (typeof define === "function" && define.amd) define(function() {return m});