simulacra
Version:
Data-binding function for the DOM.
166 lines (132 loc) • 4.25 kB
JavaScript
var keyMap = require('./key_map')
var isBoundToParentKey = keyMap.isBoundToParent
var markerKey = keyMap.marker
var matchedNodeKey = keyMap.matchedNode
var templateKey = keyMap.template
var isMarkerLastKey = keyMap.isMarkerLast
// A fixed constant for `NodeFilter.SHOW_ALL`.
var showAll = 0xFFFFFFFF
// Option to use comment nodes as markers.
processNodes.useCommentNode = false
// Avoiding duplication of compatibility hack.
processNodes.acceptNode = acceptNode
module.exports = processNodes
/**
* Internal function to remove bound nodes and replace them with markers.
*
* @param {*} [scope]
* @param {Node} node
* @param {Object} def
* @return {Node}
*/
function processNodes (scope, node, def) {
var document = scope ? scope.document : window.document
var key, branch, result, mirrorNode, parent, marker, indices
var i, j, treeWalker, orderedKeys
result = def[templateKey]
if (!result) {
node = node.cloneNode(true)
indices = []
matchNodes(scope, node, def)
orderedKeys = Object.keys(def).sort(function (a, b) {
var nodeA = def[a][0][matchedNodeKey]
var nodeB = def[b][0][matchedNodeKey]
return nodeA.index - nodeB.index
})
for (i = 0; i < orderedKeys.length; i++) {
key = orderedKeys[i]
branch = def[key]
if (branch[isBoundToParentKey]) continue
result = branch[0][matchedNodeKey]
indices.push(result.index)
mirrorNode = result.node
parent = mirrorNode.parentNode
// This value is memoized so that `appendChild` can be used instead of
// `insertBefore`, which is a performance optimization.
if (mirrorNode.nextElementSibling === null)
branch[isMarkerLastKey] = true
if (processNodes.useCommentNode) {
marker = parent.insertBefore(
document.createComment(' end "' + key + '" '), mirrorNode)
parent.insertBefore(
document.createComment(' begin "' + key + '" '), marker)
}
else marker = parent.insertBefore(
document.createTextNode(''), mirrorNode)
branch[markerKey] = marker
parent.removeChild(mirrorNode)
}
Object.defineProperty(def, templateKey, {
value: {
node: node.cloneNode(true),
indices: indices
}
})
}
else {
node = result.node.cloneNode(true)
indices = result.indices
i = 0
j = 0
treeWalker = document.createTreeWalker(
node, showAll, acceptNode, false)
for (key in def) {
branch = def[key]
if (branch[isBoundToParentKey]) continue
while (treeWalker.nextNode()) {
if (i === indices[j]) {
branch[markerKey] = treeWalker.currentNode
i++
break
}
i++
}
j++
}
}
return node
}
/**
* Internal function to find and set matching DOM nodes on cloned nodes.
*
* @param {*} [scope]
* @param {Node} node
* @param {Object} def
*/
function matchNodes (scope, node, def) {
var document = scope ? scope.document : window.document
var treeWalker = document.createTreeWalker(
node, showAll, acceptNode, false)
var nodes = []
var i, j, key, currentNode, childWalker
var nodeIndex = 0
// This offset is a bit tricky, it's used to determine the index of the
// marker in the processed node, which depends on whether comment nodes
// are used and the count of child nodes.
var offset = processNodes.useCommentNode ? 1 : 0
for (key in def) nodes.push(def[key][0])
while (treeWalker.nextNode() && nodes.length) {
for (i = 0, j = nodes.length; i < j; i++) {
currentNode = nodes[i]
if (treeWalker.currentNode.isEqualNode(currentNode)) {
Object.defineProperty(currentNode, matchedNodeKey, {
value: {
index: nodeIndex + offset,
node: treeWalker.currentNode
}
})
if (processNodes.useCommentNode) offset++
childWalker = document.createTreeWalker(
currentNode, showAll, acceptNode, false)
while (childWalker.nextNode()) offset--
nodes.splice(i, 1)
break
}
}
nodeIndex++
}
}
// A crazy Internet Explorer workaround.
function acceptNode () { return 1 }
acceptNode.acceptNode = acceptNode