// jshint newcap: false /* global require, module, document, Element */ 'use strict'; var VNode = require('./vnode'); var is = require('./is'); function isUndef(s) { return s === undefined; } function emptyNodeAt(elm) { return VNode(elm.tagName, {}, [], undefined, elm); } var emptyNode = VNode('', {}, [], undefined, undefined); var insertedVnodeQueue; function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } function createKeyToOldIdx(children, beginIdx, endIdx) { var i, map = {}, key; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (!isUndef(key)) map[key] = i; } return map; } function createRmCb(parentElm, childElm, listeners) { return function() { if (--listeners === 0) parentElm.removeChild(childElm); }; } var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; function init(modules) { var i, j, cbs = {}; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } } function createElm(vnode) { var i, data = vnode.data; if (!isUndef(data)) { if (!isUndef(i = data.hook) && !isUndef(i = i.init)) i(vnode); if (!isUndef(i = data.vnode)) vnode = i; } var elm, children = vnode.children, sel = vnode.sel; if (!isUndef(sel)) { // Parse selector var hashIdx = sel.indexOf('#'); var dotIdx = sel.indexOf('.', hashIdx); var hash = hashIdx > 0 ? hashIdx : sel.length; var dot = dotIdx > 0 ? dotIdx : sel.length; var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; elm = vnode.elm = !isUndef(data) && !isUndef(i = data.ns) ? document.createElementNS(i, tag) : document.createElement(tag); if (hash < dot) elm.id = sel.slice(hash + 1, dot); if (dotIdx > 0) elm.className = sel.slice(dot+1).replace(/\./g, ' '); if (is.array(children)) { for (i = 0; i < children.length; ++i) { elm.appendChild(createElm(children[i])); } } else if (is.primitive(vnode.text)) { elm.appendChild(document.createTextNode(vnode.text)); } for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); i = vnode.data.hook; // Reuse variable if (!isUndef(i)) { if (i.create) i.create(emptyNode, vnode); if (i.insert) insertedVnodeQueue.push(vnode); } } else { elm = vnode.elm = document.createTextNode(vnode.text); } return vnode.elm; } function addVnodes(parentElm, before, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { parentElm.insertBefore(createElm(vnodes[startIdx]), before); } } function invokeDestroyHook(vnode) { var i = vnode.data, j; if (!isUndef(i)) { if (!isUndef(i = i.hook) && !isUndef(i = i.destroy)) i(vnode); for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); if (!isUndef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]); } } } } function removeVnodes(parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { var i, listeners, rm, ch = vnodes[startIdx]; if (!isUndef(ch)) { invokeDestroyHook(ch); listeners = cbs.remove.length + 1; rm = createRmCb(parentElm, ch.elm, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); if (!isUndef(i = ch.data) && !isUndef(i = i.hook) && !isUndef(i = i.remove)) { i(ch, rm); } else { rm(); } } } } function updateChildren(parentElm, oldCh, newCh) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode); parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode); parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { // New element parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode); oldCh[idxInOld] = undefined; parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } function patchVnode(oldVnode, vnode) { var i, hook; if (!isUndef(i = vnode.data) && !isUndef(hook = i.hook) && !isUndef(i = hook.prepatch)) { i(oldVnode, vnode); } if (!isUndef(i = oldVnode.data) && !isUndef(i = i.vnode)) oldVnode = i; if (!isUndef(i = vnode.data) && !isUndef(i = i.vnode)) vnode = i; var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; if (oldVnode === vnode) return; if (!isUndef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (!isUndef(i) && !isUndef(i = i.update)) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (!isUndef(oldCh) && !isUndef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch); } else if (!isUndef(ch)) { addVnodes(elm, null, ch, 0, ch.length - 1); } else if (!isUndef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } } else if (oldVnode.text !== vnode.text) { elm.textContent = vnode.text; } if (!isUndef(hook) && !isUndef(i = hook.postpatch)) { i(oldVnode, vnode); } return vnode; } return function(oldVnode, vnode) { var i; insertedVnodeQueue = []; if (oldVnode instanceof Element) { oldVnode = emptyNodeAt(oldVnode); } for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); patchVnode(oldVnode, vnode); for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } insertedVnodeQueue = undefined; for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; }; } module.exports = {init: init};