From 56b39461f16e2b8db1f536c4d090d52901840557 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Sat, 12 Oct 2024 19:56:01 -0700 Subject: [PATCH] Update Mithril from v0.2 to v2 in example Signed-off-by: Claudia Meadows --- examples/mithril/index.html | 11 +- examples/mithril/js/app.js | 15 +- examples/mithril/js/controllers/todo.js | 61 +- examples/mithril/js/models/todo.js | 7 +- examples/mithril/js/views/footer-view.js | 59 +- examples/mithril/js/views/input-view.js | 39 + examples/mithril/js/views/main-view.js | 101 +- examples/mithril/js/views/todo-view.js | 66 + .../mithril/node_modules/mithril/mithril.js | 2737 ++++++++++------- .../node_modules/todomvc-app-css/index.css | 97 +- .../node_modules/todomvc-common/base.js | 2 +- examples/mithril/package.json | 2 +- 12 files changed, 1946 insertions(+), 1251 deletions(-) create mode 100644 examples/mithril/js/views/input-view.js create mode 100644 examples/mithril/js/views/todo-view.js diff --git a/examples/mithril/index.html b/examples/mithril/index.html index 8f51b345b3..c4f5fd0d9c 100644 --- a/examples/mithril/index.html +++ b/examples/mithril/index.html @@ -10,8 +10,11 @@
@@ -19,8 +22,10 @@ - + + + diff --git a/examples/mithril/js/app.js b/examples/mithril/js/app.js index 5a606b018c..6381410f03 100644 --- a/examples/mithril/js/app.js +++ b/examples/mithril/js/app.js @@ -1,12 +1,21 @@ 'use strict'; /*global m */ var app = app || {}; +var ctrl = new app.Controller(); app.ENTER_KEY = 13; app.ESC_KEY = 27; -m.route.mode = 'hash'; +m.route.prefix = '#!'; m.route(document.querySelector('.todoapp'), '/', { - '/': app, - '/:filter': app + '/': { + render: function () { + return m(app.MainView, {ctrl: ctrl}); + } + }, + '/:filter': { + render: function () { + return m(app.MainView, {ctrl: ctrl}); + } + } }); diff --git a/examples/mithril/js/controllers/todo.js b/examples/mithril/js/controllers/todo.js index bde8b64f6a..2ba18fe979 100644 --- a/examples/mithril/js/controllers/todo.js +++ b/examples/mithril/js/controllers/todo.js @@ -2,7 +2,7 @@ /*global m */ var app = app || {}; -app.controller = function () { +app.Controller = function () { // Todo collection this.list = app.storage.get(); @@ -13,65 +13,67 @@ app.controller = function () { }); // Temp title placeholder - this.title = m.prop(''); + this.title = ''; // Todo list filter - this.filter = m.prop(m.route.param('filter') || ''); + this.filter = function () { + return m.route.param('filter') || ''; + }; this.add = function () { - var title = this.title().trim(); + var title = this.title.trim(); if (title) { this.list.push(new app.Todo({title: title})); app.storage.put(this.list); } - this.title(''); + this.title = ''; }; this.isVisible = function (todo) { switch (this.filter()) { case 'active': - return !todo.completed(); + return !todo.completed; case 'completed': - return todo.completed(); + return todo.completed; default: return true; } }; this.complete = function (todo) { - if (todo.completed()) { - todo.completed(false); + if (todo.completed) { + todo.completed = false; } else { - todo.completed(true); + todo.completed = true; } app.storage.put(this.list); }; this.edit = function (todo) { - todo.previousTitle = todo.title(); - todo.editing(true); + todo.previousTitle = todo.title; + todo.editing = true; }; this.doneEditing = function (todo, index) { - if (!todo.editing()) { + if (!todo.editing) { return; } - todo.editing(false); - todo.title(todo.title().trim()); - if (!todo.title()) { + todo.editing = false; + todo.title = todo.title.trim(); + if (!todo.title) { this.list.splice(index, 1); } app.storage.put(this.list); }; this.cancelEditing = function (todo) { - todo.title(todo.previousTitle); - todo.editing(false); + todo.title = todo.previousTitle; + todo.editing = false; }; this.clearTitle = function () { - this.title(''); + this.title = ''; }; this.remove = function (key) { @@ -81,7 +83,7 @@ app.controller = function () { this.clearCompleted = function () { for (var i = this.list.length - 1; i >= 0; i--) { - if (this.list[i].completed()) { + if (this.list[i].completed) { this.list.splice(i, 1); } } @@ -89,28 +91,17 @@ app.controller = function () { }; this.amountCompleted = function () { - var amount = 0; - for (var i = 0; i < this.list.length; i++) { - if (this.list[i].completed()) { - amount++; - } - } - return amount; + return this.list.filter(function (todo) { return todo.completed; }).length; }; this.allCompleted = function () { - for (var i = 0; i < this.list.length; i++) { - if (!this.list[i].completed()) { - return false; - } - } - return true; + return this.list.every(function (todo) { return todo.completed; }); }; - this.completeAll = function () { + this.toggleAll = function () { var allCompleted = this.allCompleted(); for (var i = 0; i < this.list.length; i++) { - this.list[i].completed(!allCompleted); + this.list[i].completed = !allCompleted; } app.storage.put(this.list); }; diff --git a/examples/mithril/js/models/todo.js b/examples/mithril/js/models/todo.js index 8ca15a6241..032a285cb4 100644 --- a/examples/mithril/js/models/todo.js +++ b/examples/mithril/js/models/todo.js @@ -1,5 +1,4 @@ 'use strict'; -/*global m */ var app = app || {}; var uniqueId = (function () { @@ -11,8 +10,8 @@ var uniqueId = (function () { // 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.title = data.title; + this.completed = data.completed || false; + this.editing = data.editing || false; this.key = uniqueId(); }; diff --git a/examples/mithril/js/views/footer-view.js b/examples/mithril/js/views/footer-view.js index b0640c33fb..13035f6712 100644 --- a/examples/mithril/js/views/footer-view.js +++ b/examples/mithril/js/views/footer-view.js @@ -2,35 +2,38 @@ /*global m */ var app = app || {}; -app.footer = function (ctrl) { - var amountCompleted = ctrl.amountCompleted(); - var amountActive = ctrl.list.length - amountCompleted; +app.FooterView = { + view: function (vnode) { + var amountCompleted = vnode.attrs.amountCompleted; + var amountActive = vnode.attrs.list.length - amountCompleted; - return m('footer.footer', [ - m('span.todo-count', [ - m('strong', amountActive), ' item' + (amountActive !== 1 ? 's' : '') + ' left' - ]), - m('ul.filters', [ - m('li', [ - m('a[href=/]', { - config: m.route, - class: ctrl.filter() === '' ? 'selected' : '' - }, 'All') + return m('footer.footer', [ + m('span.todo-count', [ + m('strong', amountActive), ' item', amountActive !== 1 ? 's' : '', ' left' ]), - m('li', [ - m('a[href=/active]', { - config: m.route, - class: ctrl.filter() === 'active' ? 'selected' : '' - }, 'Active') + m('ul.filters', [ + m('li', [ + m(m.route.Link, { + href: '/', + class: vnode.attrs.filter === '' ? 'selected' : '' + }, 'All') + ]), + m('li', [ + m(m.route.Link, { + href: '/active', + class: vnode.attrs.filter === 'active' ? 'selected' : '' + }, 'Active') + ]), + m('li', [ + m(m.route.Link, { + href: '/completed', + class: vnode.attrs.filter === 'completed' ? 'selected' : '' + }, 'Completed') + ]) ]), - m('li', [ - m('a[href=/completed]', { - config: m.route, - class: ctrl.filter() === 'completed' ? 'selected' : '' - }, 'Completed') - ]) - ]), ctrl.amountCompleted() === 0 ? '' : m('button.clear-completed', { - onclick: ctrl.clearCompleted.bind(ctrl) - }, 'Clear completed') - ]); + amountCompleted === 0 ? '' : m('button.clear-completed', { + onclick: vnode.attrs.clearCompleted + }, 'Clear completed') + ]); + } }; diff --git a/examples/mithril/js/views/input-view.js b/examples/mithril/js/views/input-view.js new file mode 100644 index 0000000000..55bbac1356 --- /dev/null +++ b/examples/mithril/js/views/input-view.js @@ -0,0 +1,39 @@ +'use strict'; +/*global m */ +var app = app || {}; + +app.InputView = function () { + var focused = false; + + return { + view: function (vnode) { + var ctrl = vnode.attrs.ctrl; + + return m('input.new-todo[placeholder="What needs to be done?"]', { + onkeyup: function (e) { + if (e.keyCode === app.ENTER_KEY) { + ctrl.add(); + } else if (e.keyCode === app.ESC_KEY) { + ctrl.clearTitle(); + } + }, + value: ctrl.title, + oninput: function (e) { + ctrl.title = e.target.value; + }, + oncreate: function (vnode) { + if (!focused) { + vnode.dom.focus(); + focused = true; + } + }, + onupdate: function (vnode) { + if (!focused) { + vnode.dom.focus(); + focused = true; + } + } + }); + } + }; +}; diff --git a/examples/mithril/js/views/main-view.js b/examples/mithril/js/views/main-view.js index d2130f8243..8cd5cc7c65 100644 --- a/examples/mithril/js/views/main-view.js +++ b/examples/mithril/js/views/main-view.js @@ -2,38 +2,13 @@ /*global m */ var app = app || {}; -// View utility -app.watchInput = function (onenter, onescape) { - return function (e) { - m.redraw.strategy('none'); - if (e.keyCode === app.ENTER_KEY) { - onenter(); - m.redraw.strategy('diff'); - } else if (e.keyCode === app.ESC_KEY) { - onescape(); - m.redraw.strategy('diff'); - } - }; -}; - -app.view = (function () { - var focused = false; - - return function (ctrl) { +app.MainView = { + view: function (vnode) { + var ctrl = vnode.attrs.ctrl; return [ m('header.header', [ - m('h1', 'todos'), m('input.new-todo[placeholder="What needs to be done?"]', { - onkeyup: app.watchInput(ctrl.add.bind(ctrl), - ctrl.clearTitle.bind(ctrl)), - value: ctrl.title(), - oninput: m.withAttr('value', ctrl.title), - config: function (element) { - if (!focused) { - element.focus(); - focused = true; - } - } - }) + m('h1', 'todos'), + m(app.InputView, {ctrl: ctrl}), ]), m('section.main', { style: { @@ -41,54 +16,36 @@ app.view = (function () { } }, [ m('input#toggle-all.toggle-all[type=checkbox]', { - onclick: ctrl.completeAll.bind(ctrl), + onclick: function () { + ctrl.toggleAll(); + }, checked: ctrl.allCompleted() }), m('label', { for: 'toggle-all' }), m('ul.todo-list', [ - ctrl.list.filter(ctrl.isVisible.bind(ctrl)).map(function (task, index) { - return m('li', { class: (function () { - var classes = ''; - classes += task.completed() ? 'completed' : ''; - classes += task.editing() ? ' editing' : ''; - return classes; - })(), - key: task.key - }, [ - m('.view', [ - m('input.toggle[type=checkbox]', { - onclick: m.withAttr('checked', ctrl.complete.bind(ctrl, task)), - checked: task.completed() - }), - m('label', { - ondblclick: ctrl.edit.bind(ctrl, task) - }, task.title()), - m('button.destroy', { - onclick: ctrl.remove.bind(ctrl, index) - }) - ]), m('input.edit', { - value: task.title(), - onkeyup: app.watchInput( - ctrl.doneEditing.bind(ctrl, task, index), - ctrl.cancelEditing.bind(ctrl, task) - ), - oninput: m.withAttr('value', function (value) { - m.redraw.strategy('none'); - task.title(value); - }), - config: function (element) { - if (task.editing()) { - element.focus(); - } - }, - onblur: ctrl.doneEditing.bind(ctrl, task, index) - }) - ]); - }) + ctrl.list + .filter(function (task) { + return ctrl.isVisible(task); + }) + .map(function (task, index) { + return m(app.TodoView, { + ctrl: ctrl, + task: task, + index: index + }); + }) ]) - ]), ctrl.list.length === 0 ? '' : app.footer(ctrl) + ]), + ctrl.list.length === 0 ? '' : m(app.FooterView, { + amountCompleted: ctrl.amountCompleted(), + list: ctrl.list, + filter: ctrl.filter(), + clearCompleted: function () { + ctrl.clearCompleted(); + } + }) ]; } -})(); +}; diff --git a/examples/mithril/js/views/todo-view.js b/examples/mithril/js/views/todo-view.js new file mode 100644 index 0000000000..47be32037a --- /dev/null +++ b/examples/mithril/js/views/todo-view.js @@ -0,0 +1,66 @@ +'use strict'; +/*global m */ +var app = app || {}; + +app.TodoView = { + view: function (vnode) { + var ctrl = vnode.attrs.ctrl; + var task = vnode.attrs.task; + var index = vnode.attrs.index; + + return m('li', { + class: ( + (task.completed ? 'completed' : '') + + (task.editing ? ' editing' : '') + + '' + ), + key: task.key + }, [ + m('.view', [ + m('input.toggle[type=checkbox]', { + onclick: function () { + ctrl.complete(task); + }, + checked: task.completed + }), + m('label', { + ondblclick: function () { + ctrl.edit(task); + } + }, task.title), + m('button.destroy', { + onclick: function () { + ctrl.remove(index); + } + }) + ]), + m('input.edit', { + value: task.title, + onkeyup: function (e) { + if (e.keyCode === app.ENTER_KEY) { + ctrl.doneEditing(task, index); + } else if (e.keyCode === app.ESC_KEY) { + ctrl.cancelEditing(task); + } + }, + oninput: function (e) { + e.redraw = false; + task.title = e.target.value; + }, + oncreate: function (vnode) { + if (task.editing) { + vnode.dom.focus(); + } + }, + onupdate: function (vnode) { + if (task.editing) { + vnode.dom.focus(); + } + }, + onblur: function () { + ctrl.doneEditing(task, index); + } + }) + ]); + } +}; diff --git a/examples/mithril/node_modules/mithril/mithril.js b/examples/mithril/node_modules/mithril/mithril.js index 707d09aa70..4fd695556d 100644 --- a/examples/mithril/node_modules/mithril/mithril.js +++ b/examples/mithril/node_modules/mithril/mithril.js @@ -1,1159 +1,1768 @@ -var m = (function app(window, undefined) { - 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; - - // 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; - } - - initialize(window); - - - /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ - - /** - * - * @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() { - var args = [].slice.call(arguments); - 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: {}}; - var match, classes = []; - if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") - while (match = parser.exec(args[0])) { - 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) +;(function() { +"use strict" +function Vnode(tag, key, attrs0, children, text, dom) { + return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined} +} +Vnode.normalize = function(node) { + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (node == null || typeof node === "boolean") return null + if (typeof node === "object") return node + return Vnode("#", undefined, undefined, String(node), undefined, undefined) +} +Vnode.normalizeChildren = function(input) { + var children = [] + if (input.length) { + var isKeyed = input[0] != null && input[0].key != null + // Note: this is a *very* perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // it, noticeably so. + for (var i = 1; i < input.length; i++) { + if ((input[i] != null && input[i].key != null) !== isKeyed) { + throw new TypeError( + isKeyed && (input[i] != null || typeof input[i] === "boolean") + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." + : "In fragments, vnodes must either all have keys or none have keys." + ) } } - - var children = hasAttrs ? args.slice(2) : args.slice(1); - if (children.length === 1 && type.call(children[0]) === ARRAY) { - cell.children = children[0] + for (var i = 0; i < input.length; i++) { + children[i] = Vnode.normalize(input[i]) } - else { - cell.children = children - } - - 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] + } + return children +} +// Call via `hyperscriptVnode0.apply(startOffset, arguments)` +// +// The reason I do it this way, forwarding the arguments and passing the start +// offset in `this`, is so I don't have to create a temporary array in a +// performance-critical path. +// +// In native ES6, I'd instead add a final `...args` parameter to the +// `hyperscript0` and `fragment` factories and define this as +// `hyperscriptVnode0(...args)`, since modern engines do optimize that away. But +// ES5 (what Mithril.js requires thanks to IE support) doesn't give me that luxury, +// and engines aren't nearly intelligent enough to do either of these: +// +// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to +// another function only to be indexed. +// 2. Elide an `arguments` allocation when it's passed to any function other +// than `Function.prototype.apply` or `Reflect.apply`. +// +// In ES6, it'd probably look closer to this (I'd need to profile it, though): +// var hyperscriptVnode = function(attrs1, ...children0) { +// if (attrs1 == null || typeof attrs1 === "object" && attrs1.tag == null && !Array.isArray(attrs1)) { +// if (children0.length === 1 && Array.isArray(children0[0])) children0 = children0[0] +// } else { +// children0 = children0.length === 0 && Array.isArray(attrs1) ? attrs1 : [attrs1, ...children0] +// attrs1 = undefined +// } +// +// if (attrs1 == null) attrs1 = {} +// return Vnode("", attrs1.key, attrs1, children0) +// } +var hyperscriptVnode = function() { + var attrs1 = arguments[this], start = this + 1, children0 + if (attrs1 == null) { + attrs1 = {} + } else if (typeof attrs1 !== "object" || attrs1.tag != null || Array.isArray(attrs1)) { + attrs1 = {} + start = this + } + if (arguments.length === start + 1) { + children0 = arguments[start] + if (!Array.isArray(children0)) children0 = [children0] + } else { + children0 = [] + while (start < arguments.length) children0.push(arguments[start++]) + } + return Vnode("", attrs1.key, attrs1, children0) +} +// This exists so I'm1 only saving it once. +var hasOwn = {}.hasOwnProperty +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g +var selectorCache = {} +function isEmpty(object) { + for (var key in object) if (hasOwn.call(object, key)) return false + return true +} +function compileSelector(selector) { + var match, tag = "div", classes = [], attrs = {} + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") tag = value + else if (type === "#") attrs.id = value + else if (type === ".") classes.push(value) + else if (match[3][0] === "[") { + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") + if (match[4] === "class") classes.push(attrValue) + else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true + } + } + if (classes.length > 0) attrs.className = classes.join(" ") + return selectorCache[selector] = {tag: tag, attrs: attrs} +} +function execSelector(state, vnode) { + var attrs = vnode.attrs + var hasClass = hasOwn.call(attrs, "class") + var className = hasClass ? attrs.class : attrs.className + vnode.tag = state.tag + if (!isEmpty(state.attrs)) { + var newAttrs = {} + for (var key in attrs) { + if (hasOwn.call(attrs, key)) newAttrs[key] = attrs[key] + } + attrs = newAttrs + } + for (var key in state.attrs) { + if (hasOwn.call(state.attrs, key) && key !== "className" && !hasOwn.call(attrs, key)){ + attrs[key] = state.attrs[key] + } + } + if (className != null || state.attrs.className != null) attrs.className = + className != null + ? state.attrs.className != null + ? String(state.attrs.className) + " " + String(className) + : className + : state.attrs.className != null + ? state.attrs.className + : null + if (hasClass) attrs.class = null + vnode.attrs = attrs + return vnode +} +function hyperscript(selector) { + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { + throw Error("The selector must be either a string or a component."); + } + var vnode = hyperscriptVnode.apply(1, arguments) + if (typeof selector === "string") { + vnode.children = Vnode.normalizeChildren(vnode.children) + if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) + } + vnode.tag = selector + return vnode +} +hyperscript.trust = function(html) { + if (html == null) html = "" + return Vnode("<", undefined, undefined, html, undefined, undefined) +} +hyperscript.fragment = function() { + var vnode2 = hyperscriptVnode.apply(0, arguments) + vnode2.tag = "[" + vnode2.children = Vnode.normalizeChildren(vnode2.children) + return vnode2 +} +var delayedRemoval0 = new WeakMap +function *domFor1(vnode4, object = {}) { + // To avoid unintended mangling of the internal bundler, + // parameter destructuring is0 not used here. + var dom = vnode4.dom + var domSize0 = vnode4.domSize + var generation0 = object.generation + if (dom != null) do { + var nextSibling = dom.nextSibling + if (delayedRemoval0.get(dom) === generation0) { + yield dom + domSize0-- + } + dom = nextSibling + } + while (domSize0) +} +var df = { + delayedRemoval: delayedRemoval0, + domFor: domFor1, +} +var delayedRemoval = df.delayedRemoval +var domFor0 = df.domFor +var _11 = function() { + var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + } + var currentRedraw + var currentRender + function getDocument(dom) { + return dom.ownerDocument; + } + function getNameSpace(vnode3) { + return vnode3.attrs && vnode3.attrs.xmlns || nameSpace[vnode3.tag] + } + //sanity check to discourage people from doing `vnode3.state = ...` + function checkState(vnode3, original) { + if (vnode3.state !== original) throw new Error("'vnode.state' must not be modified.") + } + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode3` is the first argument in + //all lifecycle methods. + function callHook(vnode3) { + var original = vnode3.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode3, original) + } + } + // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when + // inside an iframe. Catch and swallow this error, and heavy-handidly return null. + function activeElement(dom) { + try { + return getDocument(dom).activeElement + } catch (e) { + return null + } + } + //create + function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { + for (var i = start; i < end; i++) { + var vnode3 = vnodes[i] + if (vnode3 != null) { + createNode(parent, vnode3, hooks, ns, nextSibling) } } - if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); - - return cell - } - function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { - //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` - //the diff algorithm can be summarized as this: - //1 - compare `data` and `cached` - //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is - //3 - recursively apply this algorithm for every array and for the children of every virtual element - - //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: - //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element - //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` - //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) - //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node - - //`parentElement` is a DOM element used for W3C DOM API calls - //`parentTag` is only used for handling a corner case for textarea values - //`parentCache` is used to remove nodes in some multi-node cases - //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable - //`data` and `cached` are, respectively, the new and old nodes being diffed - //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) - //`editable` is a flag that indicates whether an ancestor is contenteditable - //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor - //`configs` is a list of config functions to run after the topmost `build` call finishes running - - //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings - //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} - //- it simplifies diffing code - //data.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) { - if (cached != null) { - if (parentCache && parentCache.nodes) { - var offset = index - parentIndex; - var end = offset + (dataType === ARRAY ? data : cached.nodes).length; - clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) - } - else if (cached.nodes) clear(cached.nodes, cached) + } + function createNode(parent, vnode3, hooks, ns, nextSibling) { + var tag = vnode3.tag + if (typeof tag === "string") { + vnode3.state = {} + if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) + switch (tag) { + case "#": createText(parent, vnode3, nextSibling); break + case "<": createHTML(parent, vnode3, ns, nextSibling); break + case "[": createFragment(parent, vnode3, hooks, ns, nextSibling); break + default: createElement(parent, vnode3, hooks, ns, nextSibling) } - cached = new data.constructor; - if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) - cached.nodes = [] - } - - if (dataType === ARRAY) { - //recursively flatten array - for (var i = 0, len = data.length; i < len; i++) { - 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 - } + } + else createComponent(parent, vnode3, hooks, ns, nextSibling) + } + function createText(parent, vnode3, nextSibling) { + vnode3.dom = getDocument(parent).createTextNode(vnode3.children) + insertDOM(parent, vnode3.dom, nextSibling) + } + var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} + function createHTML(parent, vnode3, ns, nextSibling) { + var match0 = vnode3.children.match(/^\s*?<(\w+)/im) || [] + // not using the proper parent makes the child element(s) vanish. + // var div = document.createElement("div") + // div.innerHTML = "ij" + // console.log(div.innerHTML) + // --> "ij", no in sight. + var temp = getDocument(parent).createElement(possibleParents[match0[1]] || "div") + if (ns === "http://www.w3.org/2000/svg") { + temp.innerHTML = "" + vnode3.children + "" + temp = temp.firstChild + } else { + temp.innerHTML = vnode3.children + } + vnode3.dom = temp.firstChild + vnode3.domSize = temp.childNodes.length + // Capture nodes to remove, so we don't confuse them. + var fragment = getDocument(parent).createDocumentFragment() + var child + while (child = temp.firstChild) { + fragment.appendChild(child) + } + insertDOM(parent, fragment, nextSibling) + } + function createFragment(parent, vnode3, hooks, ns, nextSibling) { + var fragment = getDocument(parent).createDocumentFragment() + if (vnode3.children != null) { + var children2 = vnode3.children + createNodes(fragment, children2, 0, children2.length, hooks, null, ns) + } + vnode3.dom = fragment.firstChild + vnode3.domSize = fragment.childNodes.length + insertDOM(parent, fragment, nextSibling) + } + function createElement(parent, vnode3, hooks, ns, nextSibling) { + var tag = vnode3.tag + var attrs2 = vnode3.attrs + var is = attrs2 && attrs2.is + ns = getNameSpace(vnode3) || ns + var element = ns ? + is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : + is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) + vnode3.dom = element + if (attrs2 != null) { + setAttrs(vnode3, attrs2, ns) + } + insertDOM(parent, element, nextSibling) + if (!maybeSetContentEditable(vnode3)) { + if (vnode3.children != null) { + var children2 = vnode3.children + createNodes(element, children2, 0, children2.length, hooks, null, ns) + if (vnode3.tag === "select" && attrs2 != null) setLateSelectAttrs(vnode3, attrs2) } - - var nodes = [], intact = cached.length === data.length, subArrayCount = 0; - - //keys algorithm: sort elements without recreating them if keys are present - //1) create a map of all existing keys, and mark all for deletion - //2) add new keys to map and mark them for addition - //3) if key exists in new list, change action from deletion to a move - //4) for each key, handle its corresponding action as marked in previous steps - var DELETION = 1, INSERTION = 2 , MOVE = 3; - 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} + } + } + function initComponent(vnode3, hooks) { + var sentinel + if (typeof vnode3.tag.view === "function") { + vnode3.state = Object.create(vnode3.tag) + sentinel = vnode3.state.view + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + } else { + vnode3.state = void 0 + sentinel = vnode3.tag + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + vnode3.state = (vnode3.tag.prototype != null && typeof vnode3.tag.prototype.view === "function") ? new vnode3.tag(vnode3) : vnode3.tag(vnode3) + } + initLifecycle(vnode3.state, vnode3, hooks) + if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) + vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) + if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") + sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode3, hooks, ns, nextSibling) { + initComponent(vnode3, hooks) + if (vnode3.instance != null) { + createNode(parent, vnode3.instance, hooks, ns, nextSibling) + vnode3.dom = vnode3.instance.dom + vnode3.domSize = vnode3.dom != null ? vnode3.instance.domSize : 0 + } + else { + vnode3.domSize = 0 + } + } + //update + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last `render0()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render0()` call. + * @param {Function[]} hooks - an accumulator of post-render0 hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next DOM node if we're dealing with a + * fragment that is not the last item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. discuss DOM node operations. + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed based on the first non-null node + // of each list. + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + // ## Diffing + // + // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 + // may be good for context on longest increasing subsequence-based logic for moving nodes. + // + // In order to diff keyed lists, one has to + // + // 1) match0 nodes in both lists, per key, and update them accordingly + // 2) create the nodes present in the new list, but absent in the old one + // 3) remove the nodes present in the old list, but absent in the new one + // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. + // + // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate + // over the new list and for each new vnode3, find the corresponding vnode3 in the old list using + // the map. + // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new + // and must be created. + // For the removals, we actually remove the nodes that have been updated from the old list. + // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. + // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) + // algorithm. + // + // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going + // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices + // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would + // match0 the above lists, for example). + // + // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We + // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. + // + // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually + // the longest increasing subsequence *of old nodes still present in the new list*). + // + // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation + // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, + // the `LIS` and a temporary one to create the LIS). + // + // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of + // the LIS and can be updated without moving them. + // + // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with + // the exception of the last node if the list is fully reversed). + // + // ## Finding the next sibling. + // + // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. + // When the list is being traversed top-down, at any index, the DOM nodes up to the previous + // vnode3 reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. + // + // In the other scenarios (swaps, upwards traversal, map-based diff), + // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the + // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node + // as the next sibling (cached in the `nextSibling` variable). + // ## DOM node moves + // + // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo). We move + // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` + // variable rather than fetching it using `getNextSibling()`. + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + if (old === vnodes || old == null && vnodes == null) return + else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) + else { + var isOldKeyed = old[0] != null && old[0].key != null + var isKeyed0 = vnodes[0] != null && vnodes[0].key != null + var start = 0, oldStart = 0 + if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ + if (!isKeyed0) while (start < vnodes.length && vnodes[start] == null) start++ + if (isOldKeyed !== isKeyed0) { + removeNodes(parent, old, oldStart, old.length) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else if (!isKeyed0) { + // Don't index past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(parent, o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) } - } - - 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 (old.length > commonLength) removeNodes(parent, old, start, old.length) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else { + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling + // bottom-up + while (oldEnd >= oldStart && end >= start) { + oe = old[oldEnd] + ve = vnodes[end] + if (oe.key !== ve.key) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- } - } - - if (shouldMaintainIdentities) { - var keysDiffer = false - if (data.length != cached.length) keysDiffer = true - else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { - if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { - keysDiffer = true - break - } + // top-down + while (oldEnd >= oldStart && end >= start) { + o = old[oldStart] + v = vnodes[start] + if (o.key !== v.key) break + oldStart++, start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) } - - if (keysDiffer) { - for (var i = 0, len = data.length; i < len; i++) { - if (data[i] && data[i].attrs) { - if (data[i].attrs.key != null) { - var key = data[i].attrs.key; - if (!existing[key]) existing[key] = {action: INSERTION, index: i}; - else existing[key] = { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || $document.createElement("div") - } - } + // swaps and list reversals + while (oldEnd >= oldStart && end >= start) { + if (start === end) break + if (o.key !== ve.key || oe.key !== v.key) break + topSibling = getNextSibling(old, oldStart, nextSibling) + moveDOM(parent, oe, topSibling) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) + if (++start <= --end) moveDOM(parent, o, nextSibling) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldStart++; oldEnd-- + oe = old[oldEnd] + ve = vnodes[end] + o = old[oldStart] + v = vnodes[start] + } + // bottom up once again + while (oldEnd >= oldStart && end >= start) { + if (oe.key !== ve.key) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + oe = old[oldEnd] + ve = vnodes[end] + } + if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) + else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul + var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices + for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 + for (i = end; i >= start; i--) { + if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) + ve = vnodes[i] + var oldIndex = map[ve.key] + if (oldIndex != null) { + pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered + oldIndices[i-start] = oldIndex + oe = old[oldIndex] + old[oldIndex] = null + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + matched++ } } - 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) { - clear(cached[change.index].nodes, cached[change.index]); - newCached.splice(change.index, 1) - } - if (change.action === INSERTION) { - var dummy = $document.createElement("div"); - 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) { - if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { - parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) + nextSibling = originalNextSibling + if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) + if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + if (pos === -1) { + // the indices of the indices of the items that are part of the + // longest increasing subsequence in the oldIndices list + lisIndices = makeLisIndices(oldIndices) + li = lisIndices.length - 1 + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + else { + if (lisIndices[li] === i - start) li-- + else moveDOM(parent, v, nextSibling) + } + if (v.dom != null) nextSibling = vnodes[i].dom + } + } else { + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = vnodes[i].dom } - newCached[change.index] = cached[change.from] - newCached.nodes[change.index] = change.element } } - cached = newCached; } } - //end key algorithm - - for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { - //diff each item in the array - var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); - if (item === undefined) continue; - if (!item.nodes.intact) intact = false; - if (item.$trusted) { - //fix offset of next element if item was a trusted string w/ more than one html element - //the first clause in the regexp matches elements - //the second clause (after the pipe) matches text nodes - subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length - } - else subArrayCount += type.call(item) === ARRAY ? item.length : 1; - cached[cacheCount++] = item - } - if (!intact) { - //diff the array itself - - //update the list of DOM nodes by collecting the nodes from each item - for (var i = 0, len = data.length; i < len; i++) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) + } + } + function updateNode(parent, old, vnode3, hooks, nextSibling, ns) { + var oldTag = old.tag, tag = vnode3.tag + if (oldTag === tag) { + vnode3.state = old.state + vnode3.events = old.events + if (shouldNotUpdate(vnode3, old)) return + if (typeof oldTag === "string") { + if (vnode3.attrs != null) { + updateLifecycle(vnode3.attrs, vnode3, hooks) } - //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 - for (var i = 0, node; node = cached.nodes[i]; i++) { - if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) + switch (oldTag) { + case "#": updateText(old, vnode3); break + case "<": updateHTML(parent, old, vnode3, ns, nextSibling); break + case "[": updateFragment(parent, old, vnode3, hooks, nextSibling, ns); break + default: updateElement(old, vnode3, hooks, ns) } - if (data.length < cached.length) cached.length = data.length; - cached.nodes = nodes } + else updateComponent(parent, old, vnode3, hooks, nextSibling, ns) + } + else { + removeNode(parent, old) + createNode(parent, vnode3, hooks, ns, nextSibling) + } + } + function updateText(old, vnode3) { + if (old.children.toString() !== vnode3.children.toString()) { + old.dom.nodeValue = vnode3.children + } + vnode3.dom = old.dom + } + function updateHTML(parent, old, vnode3, ns, nextSibling) { + if (old.children !== vnode3.children) { + removeDOM(parent, old, undefined) + createHTML(parent, vnode3, ns, nextSibling) } - 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 + else { + vnode3.dom = old.dom + vnode3.domSize = old.domSize + } + } + function updateFragment(parent, old, vnode3, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode3.children, hooks, nextSibling, ns) + var domSize = 0, children2 = vnode3.children + vnode3.dom = null + if (children2 != null) { + for (var i = 0; i < children2.length; i++) { + var child = children2[i] + if (child != null && child.dom != null) { + if (vnode3.dom == null) vnode3.dom = child.dom + domSize += child.domSize || 1 } - 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.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 (domSize !== 1) vnode3.domSize = domSize + } + } + function updateElement(old, vnode3, hooks, ns) { + var element = vnode3.dom = old.dom + ns = getNameSpace(vnode3) || ns + updateAttrs(vnode3, old.attrs, vnode3.attrs, ns) + if (!maybeSetContentEditable(vnode3)) { + updateNodes(element, old.children, vnode3.children, hooks, null, ns) + } + } + function updateComponent(parent, old, vnode3, hooks, nextSibling, ns) { + vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) + if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") + updateLifecycle(vnode3.state, vnode3, hooks) + if (vnode3.attrs != null) updateLifecycle(vnode3.attrs, vnode3, hooks) + if (vnode3.instance != null) { + if (old.instance == null) createNode(parent, vnode3.instance, hooks, ns, nextSibling) + else updateNode(parent, old.instance, vnode3.instance, hooks, nextSibling, ns) + vnode3.dom = vnode3.instance.dom + vnode3.domSize = vnode3.instance.domSize + } + else if (old.instance != null) { + removeNode(parent, old.instance) + vnode3.dom = undefined + vnode3.domSize = 0 + } + else { + vnode3.dom = old.dom + vnode3.domSize = old.domSize + } + } + function getKeyMap(vnodes, start, end) { + var map = Object.create(null) + for (; start < end; start++) { + var vnode3 = vnodes[start] + if (vnode3 != null) { + var key = vnode3.key + if (key != null) map[key] = start } - if (type.call(data.tag) != STRING) return; - - 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); - cached = { - tag: data.tag, - //set attributes first, then create children - attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, - children: data.children != null && data.children.length > 0 ? - 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