From 96bcc81022a0a58dcf1aa31c6b5b0943eb81f3ee Mon Sep 17 00:00:00 2001 From: impinball Date: Tue, 3 Nov 2015 00:53:15 -0500 Subject: [PATCH] Lint Mithril main This changes enough things to merit a new patch release. It changed a few implementation details in the process, but it's at least much cleaner. Be ready for every other currently outstanding PR for this file to have merge conflicts. --- .eslintignore | 3 - Gruntfile.js | 4 +- mithril.js | 2785 +++++++++++++++++++++++++--------------- mithril.min.js | 4 +- mithril.min.js.map | 2 +- test-deps/mock.js | 15 +- test/mithril.mount.js | 6 + test/mithril.redraw.js | 4 + 8 files changed, 1780 insertions(+), 1043 deletions(-) diff --git a/.eslintignore b/.eslintignore index ff583c3f..a6ff032f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,3 @@ mithril.closure-compiler-externs.js # This is merely a dependency for the documentation. docs/layout/lib - -# TODO: These are temporary, and need to be eventually enabled. -mithril.js diff --git a/Gruntfile.js b/Gruntfile.js index 39f8fe60..a22e6198 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -88,9 +88,7 @@ module.exports = function (grunt) { // eslint-disable-line "!archive/**", "!deploy/**", "!mithril.closure-compiler-externs.js", - "!docs/layout/lib/**", - // TODO(impinball): Finish this. - "!mithril.js" + "!docs/layout/lib/**" ] }, diff --git a/mithril.js b/mithril.js index 58760547..864565f4 100644 --- a/mithril.js +++ b/mithril.js @@ -1,236 +1,386 @@ -var m = (function app(window, undefined) { - "use strict"; - var VERSION = "v0.2.1"; +(function (global, factory) { + "use strict" + /* eslint-disable no-undef */ + var m = factory(window) + if (typeof module === "object" && module != null && module.exports) { + module.exports = m + } else if (typeof define === "function" && define.amd) { + define(function () { return m }) + } else { + global.m = m + } + /* eslint-enable no-undef */ +})(this, function (window, undefined) { // eslint-disable-line + "use strict" + + var VERSION = "v0.2.1" + + // Save these two. + var type = {}.toString + var hasOwn = {}.hasOwnProperty + function isFunction(object) { - return typeof object === "function"; + return typeof object === "function" } + function isObject(object) { - return type.call(object) === "[object Object]"; + return type.call(object) === "[object Object]" } + function isString(object) { - return type.call(object) === "[object String]"; + return type.call(object) === "[object String]" } + var isArray = Array.isArray || function (object) { - return type.call(object) === "[object Array]"; - }; - 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 () {}; + return type.call(object) === "[object Array]" + } + + function noop() {} + + function forEach(list, f) { + for (var i = 0; i < list.length && !f(list[i], i++);) { + // empty + } + } + + function forOwn(obj, f) { + for (var prop in obj) if (hasOwn.call(obj, prop)) { + if (f(obj[prop], prop)) break + } + } + + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g + var attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ + var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ // eslint-disable-line max-len // caching commonly used variables - var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; + var $document, $location, $requestAnimationFrame, $cancelAnimationFrame // self invoking function needed because of the way mocks work function initialize(window) { - $document = window.document; - $location = window.location; - $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; - $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; + $document = window.document + $location = window.location + $cancelAnimationFrame = window.cancelAnimationFrame || + window.clearTimeout + $requestAnimationFrame = window.requestAnimationFrame || + window.setTimeout } - initialize(window); + initialize(window) - m.version = function() { - return VERSION; - }; + // testing API + m.deps = function (mock) { + initialize(window = mock || window) + return window + } + + m.version = function () { + return VERSION + } /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ + * @typedef {String} Tag + * A string that looks like -> div.classname#id[param=one][param2=two] + * Which describes a DOM node + */ + + function checkForAttrs(pairs) { + return pairs != null && + isObject(pairs) && + !("tag" in pairs || "view" in pairs || "subtree" in pairs) + } + + function parseSelector(tag, cell) { + var classes = [] + var match + while ((match = parser.exec(tag)) != null) { + if (match[1] === "" && match[2]) { + cell.tag = match[2] + } else if (match[1] === "#") { + cell.attrs.id = match[2] + } else if (match[1] === ".") { + classes.push(match[2]) + } else if (match[3][0] === "[") { + var pair = attrParser.exec(match[3]) + cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true) + } + } + + return classes + } + + function getChildrenFromList(hasAttrs, args) { + var children = hasAttrs ? args.slice(1) : args + if (children.length === 1 && isArray(children[0])) { + return children[0] + } else { + return children + } + } + + function assignAttrs(cell, attrs, classAttr, classes) { + forOwn(attrs, function (value, attr) { + if (attr === classAttr && + attrs[attr] != null && + attrs[attr] !== "") { + classes.push(attrs[attr]) + + // create key in correct iteration order + cell.attrs[attr] = "" + } else { + cell.attrs[attr] = attrs[attr] + } + }) + + if (classes.length) { + cell.attrs[classAttr] = classes.join(" ") + } + } /** - * - * @param {Tag} The DOM node tag - * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs - * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) - * - */ + * @param {Tag} The DOM node tag + * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs + * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, + * or splat (optional) + */ function m(tag, pairs) { for (var args = [], i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } - if (isObject(tag)) return parameterize(tag, args); - var hasAttrs = pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs); - var attrs = hasAttrs ? pairs : {}; - var classAttrName = "class" in attrs ? "class" : "className"; - var cell = {tag: "div", attrs: {}}; - var match, classes = []; - if (!isString(tag)) throw new Error("selector in m(selector, attrs, children) should be a string"); - while ((match = parser.exec(tag)) != null) { - if (match[1] === "" && match[2]) cell.tag = match[2]; - else if (match[1] === "#") cell.attrs.id = match[2]; - else if (match[1] === ".") classes.push(match[2]); - else if (match[3][0] === "[") { - var pair = attrParser.exec(match[3]); - cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true); - } + args[i - 1] = arguments[i] } - var children = hasAttrs ? args.slice(1) : args; - if (children.length === 1 && isArray(children[0])) { - cell.children = children[0]; - } - else { - cell.children = children; + if (isObject(tag)) return parameterize(tag, args) + var hasAttrs = checkForAttrs(pairs) + var attrs = hasAttrs ? pairs : {} + var classAttr = "class" in attrs ? "class" : "className" + var cell = {tag: "div", attrs: {}} + + if (!isString(tag)) { + throw new Error("selector in m(selector, attrs, children) should " + + "be a string") } - for (var attrName in attrs) { - 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]; - } - } - if (classes.length) cell.attrs[classAttrName] = classes.join(" "); + var classes = parseSelector(tag, cell) + cell.children = getChildrenFromList(hasAttrs, args) - return cell; - } - function forEach(list, f) { - for (var i = 0; i < list.length && !f(list[i], i++);) {} + assignAttrs(cell, attrs, classAttr, classes) + + return cell } + function forKeys(list, f) { forEach(list, function (attrs, i) { - return (attrs = attrs && attrs.attrs) && attrs.key != null && f(attrs, i); - }); + attrs = attrs && attrs.attrs + return attrs && attrs.key != null && f(attrs, i) + }) } + // This function was causing deopts in Chrome. function dataToString(data) { - //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version) + // data.toString() might throw or return null if data is the return + // value of Console.log in some versions of Firefox try { - if (data == null || data.toString() == null) return ""; + if (data != null && data.toString() != null) { + return data + } } catch (e) { - return ""; + // Swallow all errors here. } - return data; + + return "" } + // This function was causing deopts in Chrome. - function injectTextNode(parentElement, first, index, data) { + function injectTextNode(parent, first, index, data) { try { - insertNode(parentElement, first, index); - first.nodeValue = data; - } catch (e) {} //IE erroneously throws error when appending an empty text node after a null + insertNode(parent, first, index) + first.nodeValue = data + } catch (e) { + // IE erroneously throws error when appending an empty text node + // after a null + } } function flatten(list) { - //recursively flatten array + // recursively flatten array for (var i = 0; i < list.length; i++) { if (isArray(list[i])) { - list = list.concat.apply([], list); - //check current index again and flatten until there are no more nested arrays at that index - i--; + list = list.concat.apply([], list) + // check current index again while there is an array at this + // index. + i-- } } - return list; + + return list } - function insertNode(parentElement, node, index) { - parentElement.insertBefore(node, parentElement.childNodes[index] || null); + function insertNode(parent, node, index) { + parent.insertBefore(node, parent.childNodes[index] || null) } - var DELETION = 1, INSERTION = 2, MOVE = 3; + var DELETION = 1 + var INSERTION = 2 + var MOVE = 3 - function handleKeysDiffer(data, existing, cached, parentElement) { + function handleKeysDiffer(data, existing, cached, parent) { forKeys(data, function (key, i) { - existing[key = key.key] = existing[key] ? { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || $document.createElement("div") - } : {action: INSERTION, index: i}; - }); - var actions = []; - for (var prop in existing) actions.push(existing[prop]); - var changes = actions.sort(sortChanges), newCached = new Array(cached.length); - newCached.nodes = cached.nodes.slice(); + key = key.key + if (existing[key]) { + existing[key] = { + action: MOVE, + index: i, + from: existing[key].index, + element: cached.nodes[existing[key].index] || + $document.createElement("div") + } + } else { + existing[key] = {action: INSERTION, index: i} + } + }) + + var actions = [] + + forOwn(existing, function (value) { + actions.push(value) + }) + + var changes = actions.sort(sortChanges) + var newCached = new Array(cached.length) + newCached.nodes = cached.nodes.slice() forEach(changes, function (change) { - var index = change.index; - if (change.action === DELETION) { - clear(cached[index].nodes, cached[index]); - newCached.splice(index, 1); - } - if (change.action === INSERTION) { - var dummy = $document.createElement("div"); - dummy.key = data[index].attrs.key; - insertNode(parentElement, dummy, index); + var index = change.index + + switch (change.action) { + case DELETION: + clear(cached[index].nodes, cached[index]) + newCached.splice(index, 1) + break + + case INSERTION: + var dummy = $document.createElement("div") + dummy.key = data[index].attrs.key + insertNode(parent, dummy, index) newCached.splice(index, 0, { attrs: {key: data[index].attrs.key}, nodes: [dummy] - }); - newCached.nodes[index] = dummy; - } + }) + newCached.nodes[index] = dummy + break - if (change.action === MOVE) { - var changeElement = change.element; - var maybeChanged = parentElement.childNodes[index]; + case MOVE: + var changeElement = change.element + var maybeChanged = parent.childNodes[index] if (maybeChanged !== changeElement && changeElement !== null) { - parentElement.insertBefore(changeElement, maybeChanged || null); + parent.insertBefore(changeElement, maybeChanged || null) } - newCached[index] = cached[change.from]; - newCached.nodes[index] = changeElement; + newCached[index] = cached[change.from] + newCached.nodes[index] = changeElement } - }); + }) - return newCached; + return newCached } function diffKeys(data, cached, existing, parentElement) { - var keysDiffer = data.length !== cached.length; + var keysDiffer = data.length !== cached.length + if (!keysDiffer) { forKeys(data, function (attrs, i) { - var cachedCell = cached[i]; - return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key; - }); + var cachedCell = cached[i] + return keysDiffer = cachedCell && + cachedCell.attrs && + cachedCell.attrs.key !== attrs.key + }) } - return keysDiffer ? handleKeysDiffer(data, existing, cached, parentElement) : cached; + if (keysDiffer) { + return handleKeysDiffer(data, existing, cached, parentElement) + } else { + return cached + } } + // diffs the array itself function diffArray(data, cached, nodes) { - //diff the array itself - - //update the list of DOM nodes by collecting the nodes from each item + // update the list of DOM nodes by collecting the nodes from each item forEach(data, function (_, i) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes); + if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) }) - //remove items from the end of the array if the new array is shorter than the old one. if errors ever happen here, the issue is most likely - //a bug in the construction of the `cached` data structure somewhere earlier in the program + + // remove items from the end of the array if the new array is shorter + // than the old one. if errors ever happen here, the issue is most + // likely a bug in the construction of the `cached` data structure + // somewhere earlier in the program forEach(cached.nodes, function (node, i) { - if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]); + if (node.parentNode != null && nodes.indexOf(node) < 0) { + clear([node], [cached[i]]) + } }) - if (data.length < cached.length) cached.length = data.length; - cached.nodes = nodes; + + if (data.length < cached.length) cached.length = data.length + + cached.nodes = nodes } function buildArrayKeys(data) { - var guid = 0; + var guid = 0 forKeys(data, function () { forEach(data, function (attrs) { - if ((attrs = attrs && attrs.attrs) && attrs.key == null) attrs.key = "__mithril__" + guid++; + attrs = attrs && attrs.attrs + if (attrs && attrs.key == null) { + attrs.key = "__mithril__" + guid++ + } }) - return 1; - }); + return true + }) + } + + // shallow array compare, sorts + function arraySortCompare(a, b) { + a.sort() + b.sort() + var len = a.length + if (len !== b.length) return false + for (var i = 0; i < len; i++) { + if (a[i] !== b[i]) return false + } + return true + } + + function elemIsDifferentEnough(data, cached, dataAttrKeys) { + if (data.tag !== cached.tag) return true + if (!arraySortCompare(dataAttrKeys, Object.keys(cached.attrs))) { + return true + } + + if (data.attrs.id !== cached.attrs.id) return true + if (data.attrs.key !== cached.attrs.key) return true + + if (m.redraw.strategy() === "all") { + return !(cached.configContext && + cached.configContext.retain === true) + } else if (m.redraw.strategy() === "diff") { + return cached.configContext && + cached.configContext.retain === false + } } function maybeRecreateObject(data, cached, dataAttrKeys) { - //if an element is different enough from the one in cache, recreate it - 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 && isFunction(cached.configContext.onunload)) cached.configContext.onunload(); + // if an element is different enough from the one in cache, recreate it + if (elemIsDifferentEnough(data, cached, dataAttrKeys)) { + if (cached.nodes.length) clear(cached.nodes) + if (cached.configContext && + isFunction(cached.configContext.onunload)) { + cached.configContext.onunload() + } + if (cached.controllers) { forEach(cached.controllers, function (controller) { - if (controller.unload) controller.onunload({preventDefault: noop}); - }); + if (controller.unload) { + controller.onunload({preventDefault: noop}) + } + }) } } } @@ -239,1176 +389,1751 @@ var m = (function app(window, undefined) { return data.attrs.xmlns ? data.attrs.xmlns : data.tag === "svg" ? "http://www.w3.org/2000/svg" : data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" : - namespace; + namespace + } + + var pendingRequests = 0 + m.startComputation = function () { pendingRequests++ } + m.endComputation = function () { + if (pendingRequests > 1) { + pendingRequests-- + } else { + pendingRequests = 0 + m.redraw() + } } function unloadCachedControllers(cached, views, controllers) { if (controllers.length) { - cached.views = views; - cached.controllers = controllers; + cached.views = views + cached.controllers = controllers forEach(controllers, function (controller) { - if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old; - if (pendingRequests && controller.onunload) { - var onunload = controller.onunload; - controller.onunload = noop; - controller.onunload.$old = onunload; + if (controller.onunload && controller.onunload.$old) { + controller.onunload = controller.onunload.$old } - }); + + if (pendingRequests && controller.onunload) { + var onunload = controller.onunload + controller.onunload = noop + controller.onunload.$old = onunload + } + }) } } function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { - //schedule configs to be called. They are called after `build` - //finishes running - if (isFunction(data.attrs.config)) { - var context = cached.configContext = cached.configContext || {}; + // schedule configs to be called. They are called after `build` finishes + // running + var config = data.attrs.config + if (isFunction(config)) { + var context = cached.configContext = cached.configContext || {} - //bind - configs.push(function() { - return data.attrs.config.call(data, node, !isNew, context, cached); - }); + // bind + configs.push(function () { + return config.call(data, node, !isNew, context, cached) + }) } } - function buildUpdatedNode(cached, data, editable, hasKeys, namespace, views, configs, controllers) { - var node = cached.nodes[0]; - if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace); - cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs); - cached.nodes.intact = true; + function buildUpdatedNode( + cached, + data, + editable, + hasKeys, + namespace, + views, + configs, + controllers + ) { + var node = cached.nodes[0] + if (hasKeys) { + setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) + } + + cached.children = build(node, data.tag, undefined, undefined, + data.children, cached.children, false, 0, + data.attrs.contenteditable ? node : editable, namespace, configs) + + cached.nodes.intact = true if (controllers.length) { - cached.views = views; - cached.controllers = controllers; + cached.views = views + cached.controllers = controllers } - return node; + return node } - function handleNonexistentNodes(data, parentElement, index) { - var nodes; + function handleNonexistentNodes(data, parent, index) { + var nodes if (data.$trusted) { - nodes = injectHTML(parentElement, index, data); - } - else { - nodes = [$document.createTextNode(data)]; - if (!parentElement.nodeName.match(voidElements)) insertNode(parentElement, nodes[0], index); + nodes = injectHTML(parent, index, data) + } else { + nodes = [$document.createTextNode(data)] + if (!voidElements.test(parent.nodeName)) { + insertNode(parent, nodes[0], index) + } } - var cached = typeof data === "string" || typeof data === "number" || typeof data === "boolean" ? new data.constructor(data) : data; - cached.nodes = nodes; - return cached; + var cached + + if (typeof data === "string" || + typeof data === "number" || + typeof data === "boolean") { + cached = new data.constructor(data) + } else { + cached = data + } + + cached.nodes = nodes + + return cached } - function reattachNodes(data, cached, parentElement, editable, index, parentTag) { - var nodes = cached.nodes; + function reattachNodes(data, + cached, + parentElement, + editable, + index, + parentTag + ) { + var nodes = cached.nodes if (!editable || editable !== $document.activeElement) { if (data.$trusted) { - clear(nodes, cached); - nodes = injectHTML(parentElement, index, data); - } - //corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work - //we need to update the value property of the parent textarea or the innerHTML of the contenteditable element instead - else if (parentTag === "textarea") { - parentElement.value = data; - } - else if (editable) { - editable.innerHTML = data; - } - else { - //was a trusted string + clear(nodes, cached) + nodes = injectHTML(parentElement, index, data) + } else if (parentTag === "textarea") { + //