/* global define */ (function () { 'use strict'; /** * @module bff/patch-dom */ function moduleFactory() { function makeLevMat(xSize, ySize) { var i, levMat = new Array(xSize + 1); for (i = 0; i <= xSize; ++i) { levMat[i] = new Array(ySize + 1); levMat[i][0] = i; } for (i = 0; i <= ySize; ++i) { levMat[0][i] = i; } return levMat; } var preallocLevMatSizeX = 63; var preallocLevMatSizeY = 63; var preallocLevMat = makeLevMat(preallocLevMatSizeX, preallocLevMatSizeY); function areProbablyTheSame(target, source) { if (!source) { return false; } return target.nodeName === source.nodeName && ((target.attributes && target.getAttribute('data-id')) === (source.attributes && source.getAttribute('data-id'))); } function namedNodeMapToObject(namedNodeMap) { var obj = {}; for (var i = 0, n = namedNodeMap.length; i < n; ++i) { var node = namedNodeMap[i]; obj[node.name] = node.value; } return obj; } function patchElementNode(target, source) { // Ref: http://quirksmode.org/dom/core/#attributes var targetAttrObj = namedNodeMapToObject(target.attributes); var sourceAttrArr = source.attributes; var i, n, sourceAttr, targetAttr, attrName; // Special cases if (target.nodeName === 'INPUT') { target.value = source.value || ''; } if (source.checked !== undefined) { target.checked = source.checked; } if (source.selected !== undefined) { target.selected = source.selected; } for (i = 0, n = sourceAttrArr.length; i < n; ++i) { sourceAttr = sourceAttrArr[i]; attrName = sourceAttr.name; targetAttr = targetAttrObj[attrName]; delete targetAttrObj[sourceAttr.name]; if (targetAttr && targetAttr.value === sourceAttr.value) { continue; } target.setAttribute(attrName, sourceAttr.value); } for (attrName in targetAttrObj) { target.removeAttribute(attrName); } } function patchTextNode(target, source) { var sourceValue = source.nodeValue; if (target.nodeValue === sourceValue) { return; } target.nodeValue = sourceValue; } function patchNode(target, source) { switch (target.nodeType) { case Node.ELEMENT_NODE: patchElementNode(target, source); break; case Node.TEXT_NODE: patchTextNode(target, source); break; } } function shouldIgnoreNode(node) { return !!node.hasAttribute && node.hasAttribute('patch-ignore'); } function patchRecursive(target, source, ignoreSubtreeOf) { var targetParent = target.parentNode; var childrenToPatch = []; // Patch the current node if (areProbablyTheSame(target, source)) { patchNode(target, source); } else { if (source) { targetParent.replaceChild(source, target); } else { targetParent.removeChild(target); } return; } if (ignoreSubtreeOf && Array.prototype.indexOf.call(ignoreSubtreeOf, target) !== -1) { return; } // Diff subtree using Levenshtein distance algorithm var targetChildren = target.childNodes; var sourceChildren = source.childNodes; var i, n, targetPos, sourcePos, targetChild, sourceChild; var nTargetChildren = targetChildren.length; var nSourceChildren = sourceChildren.length; var nLeadingSameTypeChildren = 0; var nIgnoredTargetChildren = 0; var nTargetChildrenToIgnore = 0; var allChildrenMatchSoFar = true; for (i = 0; i < nTargetChildren; ++i) { if (shouldIgnoreNode(targetChildren[i])) { nTargetChildrenToIgnore++; } else if (allChildrenMatchSoFar) { if (areProbablyTheSame(targetChildren[i + nTargetChildrenToIgnore], sourceChildren[i])) { childrenToPatch.push(targetChildren[i + nTargetChildrenToIgnore]); childrenToPatch.push(sourceChildren[i]); nLeadingSameTypeChildren++; } else { allChildrenMatchSoFar = false; } } } if (nTargetChildren - nTargetChildrenToIgnore === 0 && nSourceChildren === 0) { return; } var levMatSizeX = nTargetChildren - nLeadingSameTypeChildren; var levMatSizeY = nSourceChildren - nLeadingSameTypeChildren; var levMat; if (preallocLevMatSizeX < levMatSizeX || preallocLevMatSizeY < levMatSizeY) { // The preallocated matrix is too small. if (preallocLevMatSizeX <= levMatSizeX && preallocLevMatSizeY <= levMatSizeY) { // The needed matrix is bigger or equal to the preallocated one i all dimensions, so let's grow the // preallocated one. preallocLevMatSizeX = levMatSizeX; preallocLevMatSizeY = levMatSizeY; preallocLevMat = makeLevMat(preallocLevMatSizeX, preallocLevMatSizeY); levMat = preallocLevMat; } else { // The needed matrix is larger than the preallocated one in some, but not all dimensions. This // should be quite an edge case, so just use a temporary matrix for this operation. levMat = makeLevMat(levMatSizeX, levMatSizeY); } } else { // The needed matrix fits inside the preallocated one, so just use that one. This should be the most // common case. levMat = preallocLevMat; } for (targetPos = 1; targetPos + nIgnoredTargetChildren <= nTargetChildren - nLeadingSameTypeChildren; targetPos++) { targetChild = targetChildren[targetPos + nIgnoredTargetChildren + nLeadingSameTypeChildren - 1]; if (shouldIgnoreNode(targetChild)) { nIgnoredTargetChildren++; targetPos--; continue; } for (sourcePos = 1; sourcePos <= nSourceChildren - nLeadingSameTypeChildren; ++sourcePos) { if (areProbablyTheSame(targetChild, sourceChildren[sourcePos + nLeadingSameTypeChildren - 1])) { levMat[targetPos][sourcePos] = levMat[targetPos - 1][sourcePos - 1]; } else { levMat[targetPos][sourcePos] = 1 + Math.min( levMat[targetPos - 1][sourcePos - 1], levMat[targetPos][sourcePos - 1], levMat[targetPos - 1][sourcePos]); } } } targetPos = nTargetChildren - nLeadingSameTypeChildren - nTargetChildrenToIgnore; sourcePos = nSourceChildren - nLeadingSameTypeChildren; while (targetPos > 0 || sourcePos > 0) { targetChild = targetChildren[targetPos + nLeadingSameTypeChildren + nTargetChildrenToIgnore - 1]; if (targetChild && shouldIgnoreNode(targetChild)) { nTargetChildrenToIgnore--; continue; } var substitution = targetPos > 0 && sourcePos > 0 ? levMat[targetPos - 1][sourcePos - 1] : Infinity; var insertion = sourcePos > 0 ? levMat[targetPos][sourcePos - 1] : Infinity; var deletion = targetPos > 0 ? levMat[targetPos - 1][sourcePos] : Infinity; sourceChild = sourceChildren[sourcePos + nLeadingSameTypeChildren - 1]; if (substitution <= insertion && substitution <= deletion) { if (substitution < levMat[targetPos][sourcePos]) { // Substitute target.replaceChild(sourceChild, targetChild); } else { // Add to patch list childrenToPatch.push(targetChild); childrenToPatch.push(sourceChild); } targetPos--; sourcePos--; } else if (insertion <= deletion) { // Insert target.insertBefore(sourceChild, targetChild ? targetChild.nextSibling : null); sourcePos--; } else { // Delete target.removeChild(targetChild); targetPos--; } } for (i = 0, n = childrenToPatch.length; i < n; i += 2) { patchRecursive(childrenToPatch[i], childrenToPatch[i + 1], ignoreSubtreeOf); } } /** * Patches the target element and its child elements such that it will be identical to the source element and its child structure. It achieves this by recursively _patching_, _removing_ or _adding_ elements in the target element hierarchy. The overall logic of the algorithm goes as follows: * * If the target and source elements have differing node type types (e.g. a `<div>` and a `<span>` tag) the target element is replaced by the source element. * * Otherwise, if the target and source elements are of the same type (e.g. two `<div>` tags), the attributes of the target element will be replaced by those of the target element. Then the target and source elements' children lists are compared using a version of the Levenshtein algorithm. This results in the children of the target element being either patched (by calling `patchDom` recursively) or removed. Child elements only present in the source child list will also be added to the target child list at their respective positions. * * If any encountered target elements has a `patch-ignore` attribute, that node and its children will not be patched. * * @alias module:bff/patch-dom * @arg {HTMLElement} target - The element (hierarchy) to be patched. Will be identical to the source element (hierarchy) after the function call completes. * @arg {HTMLElement} source - The element (hierarchy) that the target (hierarchy) will be transformed into. * @arg {Object} [options] - Options that will be recursively passed down to all patchDom calls. Currently only one option is implemented: * * _ignoreSubtreeOf_: A CSS selector string that identifies any elements, whose subtrees will not be patched. */ function patchDom(target, source, options) { options = options || {}; if (RUNTIME_CHECKS) { if (!(target instanceof HTMLElement)) { throw '"target" argument must be an HTMLElement'; } else if (!(source instanceof HTMLElement)) { throw '"source" argument must be an HTMLElement'; } else if (arguments.length > 2 && typeof options !== 'object') { throw '"options" argument must be an object'; } else if ('ignoreSubtreeOf' in options && typeof options.ignoreSubtreeOf !== 'string') { throw 'ignoreSubtreeOf option must be a valid CSS selector string'; } else if (target === source) { throw 'Target and source are the same, which makes no sense!'; } } var ignoreSubtreeOf = options.ignoreSubtreeOf && target.querySelectorAll(options.ignoreSubtreeOf); patchRecursive(target, source, ignoreSubtreeOf); return target; } return patchDom; } // Expose, based on environment if (typeof define === 'function' && define.amd) { // AMD define(moduleFactory); } else if (typeof exports === 'object') { // Node, CommonJS-like module.exports = moduleFactory(); } else { // Browser globals var bff = window.bff = window.bff || {}; bff.patchDom = moduleFactory(); } }());