Created
June 5, 2016 05:59
-
-
Save DylanPiercey/80b307f817bcbdadfe4e4622ee1a239c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
var TEXT_TYPE = 3 | |
var ELEMENT_TYPE = 1 | |
var NODE_KEY = '__set-dom-key__' | |
var NODE_INDEX = '__set-dom-index__' | |
var HTML_ELEMENT = document.createElement('html') | |
var BODY_ELEMENT = document.createElement('body') | |
module.exports = setDOM | |
/** | |
* @description | |
* Updates existing dom to match a new dom. | |
* | |
* @param {HTMLEntity} prev - The html entity to update. | |
* @param {String|HTMLEntity} next - The updated html(entity). | |
*/ | |
function setDOM (prev, next) { | |
// Alias document element with document. | |
if (prev === document) prev = document.documentElement | |
// If a string was provided we will parse it as dom. | |
if (typeof next === 'string') { | |
if (prev === document.documentElement) { | |
HTML_ELEMENT.innerHTML = next | |
next = HTML_ELEMENT | |
} else { | |
BODY_ELEMENT.innerHTML = next | |
next = BODY_ELEMENT.firstChild | |
} | |
} | |
// Update the node. | |
setNode(prev, next) | |
} | |
/** | |
* @private | |
* @description | |
* Updates a specific htmlNode and does whatever it takes to convert it to another one. | |
* | |
* @param {HTMLEntity} prev - The previous HTMLNode. | |
* @param {HTMLEntity} next - The updated HTMLNode. | |
*/ | |
function setNode (prev, next) { | |
// Handle text node update. | |
if (next.nodeType === TEXT_TYPE) { | |
if (prev.nodeType !== TEXT_TYPE) { | |
// we have to replace the node. | |
prev.parentNode.replaceChild(next, prev) | |
} else if (prev.nodeValue !== next.nodeValue) { | |
// If both are text nodes we can update directly. | |
prev.nodeValue = next.nodeValue | |
} | |
return | |
} | |
// Update all children (and subchildren). | |
setChildNodes(prev, next) | |
// Update the elements attributes / tagName. | |
if (prev.nodeName === next.nodeName) { | |
// If we have the same nodename then we can directly update the attributes. | |
setAttributes(prev, next) | |
} else { | |
// Otherwise clone the new node to use as the existing node. | |
var newPrev = next.cloneNode() | |
// Copy over all existing children from the original node. | |
while (prev.firstChild) newPrev.appendChild(prev.firstChild) | |
// Replace the original node with the new one with the right tag. | |
prev.parentNode.replaceChild(newPrev, prev) | |
} | |
} | |
/* | |
* @private | |
* @description | |
* Utility that will update one nodes list of attributes to match another node. | |
* | |
* @param {Attributes} prev - The previous node. | |
* @param {Attributes} next - The updated node. | |
*/ | |
function setAttributes (prev, next) { | |
var i, prevAttr, nextAttr, ns | |
var prevAttrs = prev.attributes | |
var nextAttrs = next.attributes | |
// Remove old attributes. | |
for (i = prevAttrs.length; i--;) { | |
prevAttr = prevAttrs[i] | |
ns = prevAttr.namespaceURI | |
nextAttr = nextAttrs.getNamedItemNS(ns, prevAttr.name) | |
if (!nextAttr) { | |
prevAttrs.removeNamedItemNS(ns, prevAttr.name) | |
} | |
} | |
// Set new attributes. | |
for (i = nextAttrs.length; i--;) { | |
nextAttr = nextAttrs[i] | |
ns = nextAttr.namespaceURI | |
prevAttr = prevAttrs.getNamedItemNS(ns, nextAttr.name) | |
if (!prevAttr) { | |
// Add a new attribute. | |
nextAttrs.removeNamedItemNS(ns, nextAttr.name) | |
prevAttrs.setNamedItemNS(nextAttr) | |
} else if (prevAttr.value !== nextAttr.value) { | |
// Update existing attribute. | |
prevAttr.value = nextAttr.value | |
} | |
} | |
} | |
/* | |
* @private | |
* @description | |
* Utility that will update one nodes list of childNodes to match another node. | |
* | |
* @param {NodeList} prevChildNodes - The previous node. | |
* @param {NodeList} nextChildNodes - The updated node. | |
*/ | |
function setChildNodes (prev, next) { | |
var i, len, prevChild, nextChild | |
var parent = prev | |
// Convert nodelists into a usuable map. | |
var prevChildren = keyNodes(prev) | |
var nextChildren = keyNodes(next) | |
// Remove old nodes. | |
for (i = prevChildren.length; i--;) { | |
prevChild = prevChildren[i] | |
nextChild = nextChildren[prevChild[NODE_KEY]] | |
if (!nextChild || prevChild[NODE_KEY] !== nextChild[NODE_KEY]) { | |
// Remove node from dom. | |
parent.removeChild(prevChild) | |
// Remove node from children map. | |
prevChildren.splice(prevChild[NODE_INDEX], 1) | |
} | |
} | |
// Set new nodes. | |
for (i = 0, len = nextChildren.length; i < len; i++) { | |
nextChild = nextChildren[i] | |
prevChild = prevChildren[nextChild[NODE_KEY]] | |
if (prevChild) { | |
// Update an existing node. | |
setNode(prevChild, nextChild) | |
// Check if the node has moved in the tree. | |
if (prevChildren[i] === prevChild) continue | |
// Reposition node. | |
parent.insertBefore(prevChild, prevChildren[i]) | |
// Update childnode's position in the children map. | |
prevChildren.splice(prevChild[NODE_INDEX], 1) | |
prevChildren.splice(i, 0, prevChild) | |
} else { | |
// Append the new node. | |
parent.appendChild(nextChild) | |
} | |
} | |
} | |
/** | |
* @private | |
* @description | |
* Converts an elements childNodes into a keyed map. | |
* This is used for diffing while keeping elements with 'data-key' or 'id' if possible. | |
* | |
* @param {HTMLEntity} parent - The parent element with childNodes to convert. | |
* @return {Array} | |
*/ | |
function keyNodes (parent) { | |
var result = [] | |
var el = parent.firstChild | |
var i = 0 | |
var key | |
while (el) { | |
result.push(el) | |
key = getKey(el) | |
el[NODE_KEY] = key || i | |
el[NODE_INDEX] = i | |
if (key) result[key] = el | |
el = el.nextSibling | |
i++ | |
} | |
return result | |
} | |
/** | |
* @private | |
* @description | |
* Utility to try to pull a key out of an element. | |
* (Uses 'id' if possible and falls back to 'data-key') | |
* | |
* @param {HTMLEntity} node - The node to get the key for. | |
* @return {String} | |
*/ | |
function getKey (node) { | |
if (node.nodeType !== ELEMENT_TYPE) return | |
return node.id || node.getAttribute('data-key') | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment