simulacra
Version:
Data-binding function for the DOM.
1,277 lines (1,002 loc) • 37.6 kB
JavaScript
/*!
* Simulacra.js
* Version 2.2.0
* MIT License
* http://simulacra.js.org/
*/
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
var processNodes = require('./process_nodes')
var keyMap = require('./key_map')
var markerKey = keyMap.marker
var metaKey = keyMap.meta
var isMarkerLastKey = keyMap.isMarkerLast
var hasDefinitionKey = keyMap.hasDefinition
var isBoundToParentKey = keyMap.isBoundToParent
var replaceAttributeKey = keyMap.replaceAttribute
var retainElementKey = keyMap.retainElement
var memoizedObjectKey = keyMap.memoizedObject
// Fixed constant for text node type.
var TEXT_NODE = 3
// Element tag names for elements that should update data on change.
var updateTags = [ 'INPUT', 'TEXTAREA' ]
module.exports = bindKeys
/**
* Define getters & setters. This function is the internal entry point to a lot
* of functionality.
*
* @param {*} [scope]
* @param {Object} obj
* @param {Object} def
* @param {Node} parentNode - This is not the same as
* `Node.prototype.parentNode`, this is the internal parent node if the key
* was bound to its parent.
* @param {Array} path
*/
function bindKeys (scope, obj, def, parentNode, path) {
var meta, key
if (typeof obj !== 'object' || obj === null)
throw new TypeError(
'Invalid type of value "' + obj + '", object expected.')
Object.defineProperty(obj, memoizedObjectKey, {
value: {},
configurable: true
})
Object.defineProperty(obj, metaKey, {
value: {},
configurable: true
})
meta = obj[metaKey]
for (key in def) {
meta[key] = {
keyPath: {
key: key,
root: path.root,
target: obj
},
activeNodes: [],
previousValues: [],
// Assign the current marker relevant to this object. This is in case of
// arrays of objects.
currentMarker: def[key][markerKey],
valueIsArray: null
}
bindKey(scope, obj, def, key, parentNode)
}
}
// This is an internal function that's used for defining the getters and
// setters.
function bindKey (scope, obj, def, key, parentNode) {
var memoizedObject = obj[memoizedObjectKey]
var meta = obj[metaKey][key]
var branch = def[key]
var change = !branch[hasDefinitionKey] && branch[1]
var definition = branch[hasDefinitionKey] && branch[1]
var isMarkerLast = branch[isMarkerLastKey]
// Temporary keys.
var keyPath = meta.keyPath
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
var valueIsArray = meta.valueIsArray
// For initialization, call this once.
if (branch[isBoundToParentKey]) parentSetter(obj[key])
else setter(obj[key])
Object.defineProperty(obj, key, {
get: getter,
set: branch[isBoundToParentKey] ? parentSetter : setter,
enumerable: true,
configurable: true
})
function getter () { return memoizedObject[key] }
// Special case for binding same node as parent.
function parentSetter (x) {
var previousValue = memoizedObject[key]
var returnValue
// Optimistically set the memoized value, so it persists even if an error
// occurs after this point.
memoizedObject[key] = x
// Check for no-op.
if (x === previousValue) return x
// Need to qualify this check for non-empty value.
if (definition && x !== null && x !== void 0)
bindKeys(scope, x, definition, parentNode, keyPath)
else if (change) {
returnValue = change(parentNode, x,
previousValue === void 0 ? null : previousValue, keyPath)
if (returnValue !== void 0)
changeValue(parentNode, returnValue, branch[replaceAttributeKey])
}
return x
}
function setter (x) {
var value, marker, currentNode
var a, b, i, j
// Optimistically set the memoized value, so it persists even if an error
// occurs after this point.
memoizedObject[key] = x
valueIsArray = meta.valueIsArray = Array.isArray(x)
value = valueIsArray ? x : [ x ]
for (i = 0, j = Math.max(previousValues.length, value.length);
i < j; i++) {
a = value[i]
b = previousValues[i]
currentNode = !a || a !== b ?
replaceNode(scope, obj, def, key, a, b, i) : null
marker = meta.currentMarker
if (currentNode)
if (isMarkerLast) {
marker.parentNode.appendChild(currentNode)
marker.parentNode.appendChild(marker)
}
else marker.parentNode.insertBefore(currentNode,
getNextNode(i + 1, activeNodes) || marker)
}
// Reset length to current values, implicitly deleting indices and
// allowing for garbage collection.
if (value.length !== previousValues.length)
previousValues.length = activeNodes.length = value.length
// Assign array mutator methods if we get an array.
if (valueIsArray) {
// Some mutators such as `sort`, `reverse`, `fill`, `copyWithin` are
// not present here. That is because they trigger the array index
// setter functions by assigning on them internally.
// These mutators may alter length.
value.pop = patchPop(obj, def, key)
value.push = patchPush(scope, obj, def, key)
value.shift = patchShift(obj, def, key)
value.unshift = patchUnshift(scope, obj, def, key)
value.splice = patchSplice(scope, obj, def, key)
// Handle array index assignment.
for (i = 0, j = value.length; i < j; i++)
defineIndex(scope, obj, def, key, value, i)
}
return x
}
}
// Used for assigning object properties on the indices of arrays passed in.
function defineIndex (scope, obj, def, key, array, i) {
var meta = obj[metaKey][key]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
var branch = def[key]
var isMarkerLast = branch[isMarkerLastKey]
var value = array[i]
Object.defineProperty(array, i, {
get: function () { return value },
set: function (x) {
var a, b, marker, currentNode
value = x
a = array[i]
b = previousValues[i]
if (a !== b) currentNode = replaceNode(scope, obj, def, key, a, b, i)
marker = meta.currentMarker
if (currentNode)
if (isMarkerLast) {
marker.parentNode.appendChild(currentNode)
marker.parentNode.appendChild(marker)
}
else marker.parentNode.insertBefore(
currentNode, getNextNode(i + 1, activeNodes) || marker)
},
enumerable: true,
configurable: true
})
}
// Internal function to remove a node.
function removeNode (obj, def, key, value, previousValue, i) {
var meta = obj[metaKey][key]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
var valueIsArray = meta.valueIsArray
var keyPath = meta.keyPath
var marker = meta.currentMarker
var branch = def[key]
var change = !branch[hasDefinitionKey] && branch[1]
var definition = branch[hasDefinitionKey] && branch[1]
var mount = branch[2]
var activeNode = activeNodes[i]
var returnValue
delete previousValues[i]
if (activeNode) {
delete activeNodes[i]
if (valueIsArray) keyPath.index = i
else delete keyPath.index
if (change)
returnValue = change(activeNode, null, previousValue, keyPath)
else if (definition && mount) {
keyPath.target = previousValue
returnValue = mount(activeNode, null, previousValue, keyPath)
}
// If a change or mount function returns the retain element symbol,
// skip removing the element from the DOM.
if (returnValue !== retainElementKey)
marker.parentNode.removeChild(activeNode)
}
}
// The return value of this function is a Node to be added, otherwise null.
function replaceNode (scope, obj, def, key, value, previousValue, i) {
var meta = obj[metaKey][key]
var branch = def[key]
var change = !branch[hasDefinitionKey] && branch[1]
var definition = branch[hasDefinitionKey] && branch[1]
var node = branch[0]
var mount = branch[2]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
var valueIsArray = meta.valueIsArray
var keyPath = meta.keyPath
var activeNode = activeNodes[i]
var currentNode = node
var returnValue
// Cast values to null if undefined.
if (value === void 0) value = null
if (previousValue === void 0) previousValue = null
// If value is null, just remove the Node.
if (value === null) {
removeNode(obj, def, key, null, previousValue, i)
return null
}
if (valueIsArray) keyPath.index = i
else delete keyPath.index
previousValues[i] = value
if (definition) {
if (activeNode) removeNode(obj, def, key, value, previousValue, i)
currentNode = processNodes(scope, node, definition)
keyPath.target = valueIsArray ? value[i] : value
bindKeys(scope, value, definition, currentNode, keyPath)
if (mount) {
keyPath.target = value
mount(currentNode, value, null, keyPath)
}
}
else {
currentNode = activeNode || node.cloneNode(true)
if (change) {
returnValue = change(currentNode, value, previousValue, keyPath)
if (returnValue !== void 0)
changeValue(currentNode, returnValue, branch[replaceAttributeKey])
}
else {
// Add default update behavior. Note that this event does not get
// removed, since it is assumed that it will be garbage collected.
if (previousValue === null &&
~updateTags.indexOf(currentNode.tagName))
currentNode.addEventListener(
'input', updateChange(branch[replaceAttributeKey], keyPath, key))
changeValue(currentNode, value, branch[replaceAttributeKey])
}
// Do not actually add an element to the DOM if it's only a change
// between non-empty values.
if (activeNode) return null
}
activeNodes[i] = currentNode
return currentNode
}
// Default behavior when a return value is given for a change function.
function changeValue (node, value, attribute) {
var firstChild
switch (attribute) {
case 'textContent':
firstChild = node.firstChild
if (firstChild && !firstChild.nextSibling &&
firstChild.nodeType === TEXT_NODE)
firstChild.textContent = value
else node.textContent = value
break
case 'checked':
node.checked = Boolean(value)
break
case 'value':
// Prevent some misbehavior in certain browsers when setting a value to
// itself, i.e. text caret not in the correct position.
if (node.value !== value) node.value = value
break
default:
break
}
}
// Find next node in a potentially sparse array.
function getNextNode (index, activeNodes) {
var i, j, nextNode
for (i = index, j = activeNodes.length; i < j; i++)
if (activeNodes[i]) {
nextNode = activeNodes[i]
break
}
return nextNode
}
// Internal event listener to update data on input change.
function updateChange (targetKey, path, key) {
var target = path.target
var index = path.index
var replaceKey = key
if (typeof index === 'number') {
target = target[key]
replaceKey = index
}
return function handleChange (event) {
target[replaceKey] = event.target[targetKey]
}
}
// INTERNAL ARRAY MUTATOR METHODS
// ==============================
// Note that the native implementations of these methods do not trigger setter
// functions on array indices.
function patchPop (obj, def, key) {
var meta = obj[metaKey][key]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
return function pop () {
var i = this.length - 1
var previousValue = previousValues[i]
var value = Array.prototype.pop.call(this)
removeNode(obj, def, key, null, previousValue, i)
previousValues.length = activeNodes.length = this.length
return value
}
}
function patchPush (scope, obj, def, key) {
var meta = obj[metaKey][key]
var branch = def[key]
var isMarkerLast = branch[isMarkerLastKey]
return function push () {
var i = this.length
var j = i + arguments.length
var marker, currentNode
// Passing arguments to apply is fine.
var value = Array.prototype.push.apply(this, arguments)
for (j = i + arguments.length; i < j; i++) {
currentNode = replaceNode(scope, obj, def, key, this[i], null, i)
marker = meta.currentMarker
if (currentNode)
if (isMarkerLast) {
marker.parentNode.appendChild(currentNode)
marker.parentNode.appendChild(marker)
}
else marker.parentNode.insertBefore(currentNode, marker)
defineIndex(scope, obj, def, key, this, i)
}
return value
}
}
function patchShift (obj, def, key) {
var meta = obj[metaKey][key]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
return function shift () {
removeNode(obj, def, key, null, previousValues[0], 0)
Array.prototype.shift.call(previousValues)
Array.prototype.shift.call(activeNodes)
return Array.prototype.shift.call(this)
}
}
function patchUnshift (scope, obj, def, key) {
return function unshift () {
var i, j
for (i = this.length, j = i + arguments.length; i < j; i++)
defineIndex(scope, obj, def, key, this, i)
return Array.prototype.unshift.apply(this, arguments)
}
}
function patchSplice (scope, obj, def, key) {
var meta = obj[metaKey][key]
var activeNodes = meta.activeNodes
var previousValues = meta.previousValues
var branch = def[key]
var isMarkerLast = branch[isMarkerLastKey]
return function splice (start, count) {
var insert = []
var i, j, k, value, marker, currentNode, shouldAppend
for (i = start, j = start + count; i < j; i++)
removeNode(obj, def, key, null, previousValues[i], i)
for (i = 2, j = arguments.length; i < j; i++)
insert.push(arguments[i])
// Passing arguments to apply is fine.
Array.prototype.splice.apply(previousValues, arguments)
// In this case, avoid setting new values.
Array.prototype.splice.apply(activeNodes,
[ start, count ].concat(Array(insert.length)))
value = Array.prototype.splice.apply(this, arguments)
shouldAppend = start - count >= this.length - 1
for (i = start, j = start + insert.length; i < j; i++) {
currentNode =
replaceNode(scope, obj, def, key, insert[i - start], null, i)
marker = meta.currentMarker
if (currentNode)
if (isMarkerLast && shouldAppend) {
marker.parentNode.appendChild(currentNode)
marker.parentNode.appendChild(marker)
}
else marker.parentNode.insertBefore(currentNode,
getNextNode(start + insert.length, activeNodes) || marker)
}
k = insert.length - count
if (k < 0)
previousValues.length = activeNodes.length = this.length
else if (k > 0)
for (i = this.length - k, j = this.length; i < j; i++)
defineIndex(scope, obj, def, key, this, i)
return value
}
}
},{"./key_map":5,"./process_nodes":6}],2:[function(require,module,exports){
module.exports = featureCheck
/**
* Check if capabilities are available, or throw an error.
*
* @param {*} globalScope
*/
function featureCheck (globalScope, features) {
var i, j, k, l, feature, path
for (i = 0, j = features.length; i < j; i++) {
path = features[i]
if (typeof path[0] === 'string') {
feature = globalScope
for (k = 0, l = path.length; k < l; k++) {
if (!(path[k] in feature)) throw new Error('Missing ' +
path.slice(0, k + 1).join('.') + ' feature which is required.')
feature = feature[path[k]]
}
}
else {
feature = path[0]
for (k = 1, l = path.length; k < l; k++) {
if (k > 1) feature = feature[path[k]]
if (typeof feature === 'undefined') throw new Error('Missing ' +
path[0].name + path.slice(1, k + 1).join('.') +
' feature which is required.')
}
}
}
}
},{}],3:[function(require,module,exports){
var keyMap = require('./key_map')
var retainElement = keyMap.retainElement
var hasMutationObserver = typeof MutationObserver !== 'undefined'
var hasDocument = typeof document !== 'undefined'
module.exports = {
bindEvents: bindEvents,
animate: animate
}
function makeEventListener (fn, path) {
return function eventListener (event) {
return fn(event, path)
}
}
function ignoreEvent (event) {
event.stopPropagation()
event.preventDefault()
}
function bindEvents (events, useCapture) {
var listeners = {}
if (useCapture === void 0) useCapture = false
return function (node, value, previousValue, path) {
var key
if (value === null)
for (key in events) {
// The point of removing event listeners here is not manual memory
// management, but to ensure that after the value has been unset, it
// no longer triggers events.
node.removeEventListener(key, listeners[key], useCapture)
// Add a capturing event listener to make future events effectively
// ignored.
node.addEventListener(key, ignoreEvent, true)
}
else if (previousValue === null)
for (key in events) {
listeners[key] = makeEventListener(events[key], path)
node.addEventListener(key, listeners[key], useCapture)
}
}
}
function animate (insertClass, mutateClass, removeClass, retainTime) {
return function (node, value, previousValue) {
var observer
if (!('classList' in node)) return void 0
if (value === null) {
if (insertClass) node.classList.remove(insertClass)
if (removeClass) node.classList.add(removeClass)
if (retainTime) {
setTimeout(function () {
node.parentNode.removeChild(node)
}, retainTime)
return retainElement
}
}
else if (value !== null && previousValue !== null && mutateClass) {
if (node.classList.contains(mutateClass)) {
node.classList.remove(mutateClass)
// Hack to trigger reflow.
void node.offsetWidth
}
node.classList.add(mutateClass)
}
else if (previousValue === null && insertClass)
// Trigger class addition after the element is inserted.
if (hasMutationObserver && hasDocument &&
!document.documentElement.contains(node)) {
observer = new MutationObserver(function (mutations) {
var i, j, k, l, mutation
for (i = 0, j = mutations.length; i < j; i++) {
mutation = mutations[i]
for (k = 0, l = mutation.addedNodes.length; k < l; k++)
if (mutation.addedNodes[k] === node) {
// Hack to trigger reflow.
void node.offsetWidth
node.classList.add(insertClass)
observer.disconnect()
}
}
})
observer.observe(document.documentElement, {
childList: true, subtree: true
})
}
else node.classList.add(insertClass)
return void 0
}
}
},{"./key_map":5}],4:[function(require,module,exports){
var processNodes = require('./process_nodes')
var bindKeys = require('./bind_keys')
var keyMap = require('./key_map')
var helpers = require('./helpers')
var rehydrate = require('./rehydrate')
var featureCheck = require('./feature_check')
var helper
var isArray = Array.isArray
var hasDefinitionKey = keyMap.hasDefinition
var replaceAttributeKey = keyMap.replaceAttribute
var isBoundToParentKey = keyMap.isBoundToParent
var isProcessedKey = keyMap.isProcessed
var markerKey = keyMap.marker
// Element tag names which should have value replaced.
var replaceValue = [ 'INPUT', 'PROGRESS' ]
// Input types which use the "checked" attribute.
var replaceChecked = [ 'checkbox', 'radio' ]
// A list of features to check for upon instantiation.
var features = [
// ECMAScript features.
[ Object, 'defineProperty' ],
// DOM features. Missing `contains` since apparently it is not on
// the Node.prototype in Internet Explorer.
[ 'document', 'createTreeWalker' ],
[ 'Node', 'prototype', 'cloneNode' ],
[ 'Node', 'prototype', 'normalize' ],
[ 'Node', 'prototype', 'insertBefore' ],
[ 'Node', 'prototype', 'isEqualNode' ],
[ 'Node', 'prototype', 'removeChild' ]
]
// Symbol for retaining an element instead of removing it.
Object.defineProperty(simulacra, 'retainElement', {
enumerable: true, value: keyMap.retainElement
})
// Option to use comment nodes as markers.
Object.defineProperty(simulacra, 'useCommentNode', {
get: function () { return processNodes.useCommentNode },
set: function (value) { processNodes.useCommentNode = value },
enumerable: true
})
// Assign helpers on the main export.
for (helper in helpers) simulacra[helper] = helpers[helper]
module.exports = simulacra
/**
* Bind an object to the DOM.
*
* @param {Object} obj
* @param {Object} def
* @param {Node} [matchNode]
* @return {Node}
*/
function simulacra (obj, def, matchNode) {
var document = this ? this.document : window.document
var Node = this ? this.Node : window.Node
var node, query
// Before continuing, check if required features are present.
featureCheck(this || window, features)
if (obj === null || typeof obj !== 'object' || isArray(obj))
throw new TypeError('First argument must be a singular object.')
if (!isArray(def))
throw new TypeError('Second argument must be an array.')
if (typeof def[0] === 'string') {
query = def[0]
def[0] = document.querySelector(query)
if (!def[0]) throw new Error(
'Top-level Node "' + query + '" could not be found in the document.')
}
else if (!(def[0] instanceof Node)) throw new TypeError(
'The first position of the top-level must be either a Node or a CSS ' +
'selector string.')
if (!def[isProcessedKey]) {
// Auto-detect template tag.
if ('content' in def[0]) def[0] = def[0].content
def[0] = def[0].cloneNode(true)
cleanNode(this, def[0])
ensureNodes(def[0], def[1])
setProperties(def)
}
node = processNodes(this, def[0], def[1])
bindKeys(this, obj, def[1], node, { root: obj })
if (matchNode) {
rehydrate(this, obj, def[1], node, matchNode)
return matchNode
}
return node
}
/**
* Internal function to mutate string selectors into Nodes and validate that
* they are allowed.
*
* @param {Element} parentNode
* @param {Object} def
*/
function ensureNodes (parentNode, def) {
var adjacentNodes = []
var i, j, key, query, branch, boundNode, matchedNodes
var adjacentNode, adjacentKey
if (typeof def !== 'object') throw new TypeError(
'The second position must be an object.')
for (key in def) {
branch = def[key]
// Change function or definition object bound to parent.
if (typeof branch === 'function' || (typeof branch === 'object' &&
branch !== null && !Array.isArray(branch)))
def[key] = branch = [ parentNode, branch ]
// Cast CSS selector string to array.
else if (typeof branch === 'string') def[key] = branch = [ branch ]
else if (!Array.isArray(branch))
throw new TypeError('The binding on key "' + key + '" is invalid.')
// Dereference CSS selector string to actual DOM element.
if (typeof branch[0] === 'string') {
query = branch[0]
// Match all nodes for the selector, pick the first and remove the rest.
matchedNodes = parentNode.querySelectorAll(query)
if (!matchedNodes.length) throw new Error(
'An element for selector "' + query + '" was not found.')
for (i = 1, j = matchedNodes.length; i < j; i++)
matchedNodes[i].parentNode.removeChild(matchedNodes[i])
branch[0] = matchedNodes[0]
}
else if (!branch[0])
throw new TypeError(
'The first position on key "' + key +
'" must be a CSS selector string.')
// Auto-detect template tag.
if ('content' in branch[0]) branch[0] = branch[0].content
boundNode = branch[0]
if (typeof branch[1] === 'object' && branch[1] !== null) {
Object.defineProperty(branch, hasDefinitionKey, { value: true })
if (branch[2] && typeof branch[2] !== 'function')
throw new TypeError('The third position on key "' + key +
'" must be a function.')
}
else if (branch[1] && typeof branch[1] !== 'function')
throw new TypeError('The second position on key "' + key +
'" must be an object or a function.')
// Special case for binding to parent node.
if (parentNode === boundNode) {
Object.defineProperty(branch, isBoundToParentKey, { value: true })
if (branch[hasDefinitionKey]) ensureNodes(boundNode, branch[1])
else if (typeof branch[1] === 'function')
setReplaceAttribute(branch, boundNode)
else console.warn( // eslint-disable-line
'A change function was not defined on the key "' + key + '".')
setProperties(branch)
continue
}
adjacentNodes.push([ key, boundNode ])
if (!parentNode.contains(boundNode))
throw new Error('The bound DOM element must be either ' +
'contained in or equal to the element in its parent binding.')
if (branch[hasDefinitionKey]) {
ensureNodes(boundNode, branch[1])
setProperties(branch)
continue
}
setReplaceAttribute(branch, boundNode)
setProperties(branch)
}
// Need to loop again to invalidate containment in adjacent nodes, after the
// adjacent nodes are found.
for (key in def) {
boundNode = def[key][0]
for (i = 0, j = adjacentNodes.length; i < j; i++) {
adjacentKey = adjacentNodes[i][0]
adjacentNode = adjacentNodes[i][1]
if (adjacentNode.contains(boundNode) && adjacentKey !== key)
throw new Error(
'The element for key "' + key + '" is contained in the ' +
'element for the adjacent key "' + adjacentKey + '".')
}
}
setProperties(def)
}
// Internal function to strip empty text nodes.
function cleanNode (scope, node) {
// A constant for showing text nodes.
var showText = 0x00000004
var document = scope ? scope.document : window.document
var treeWalker = document.createTreeWalker(
node, showText, processNodes.acceptNode, false)
var textNode
while (treeWalker.nextNode()) {
textNode = treeWalker.currentNode
textNode.textContent = textNode.textContent.trim()
}
node.normalize()
}
function setReplaceAttribute (branch, boundNode) {
Object.defineProperty(branch, replaceAttributeKey, {
value: ~replaceValue.indexOf(boundNode.nodeName) ?
~replaceChecked.indexOf(boundNode.type) ?
'checked' : 'value' : 'textContent'
})
}
function setProperties (obj) {
Object.defineProperty(obj, isProcessedKey, { value: true })
Object.defineProperty(obj, markerKey, { value: null, writable: true })
}
},{"./bind_keys":1,"./feature_check":2,"./helpers":3,"./key_map":5,"./process_nodes":6,"./rehydrate":7}],5:[function(require,module,exports){
var keys = [
// Internal flag when a definition is used instead of a change function.
'hasDefinition',
// Internal flag that is set when a change function is bound to its
// parent object.
'isBoundToParent',
// Boolean flag to check whether a Node has already been processed.
'isProcessed',
// This boolean flag is used for a DOM performance optimization,
// `appendChild` is faster than `insertBefore`.
'isMarkerLast',
// A marker is a superfluous node (empty text or comment) used as a reference
// position for the DOM API.
'marker',
// Generic key for storing meta information.
'meta',
// This keeps the previously assigned values of keys on objects. It is set on
// a bound object and valued by a memoized object that contains the same
// keys as the bound object.
'memoizedObject',
// Internally used to match cloned nodes.
'matchedNode',
// Internally used to indicate what attribute to set.
'replaceAttribute',
// This is a publicly exposed symbol used for indicating that an element
// should be retained in the DOM tree after its value is unset.
'retainElement',
// Used for mapping a DOM Node to its preprocessed template.
'template'
]
var keyMap = {}
var hasSymbol = typeof Symbol === 'function'
var i, j
for (i = 0, j = keys.length; i < j; i++)
keyMap[keys[i]] = hasSymbol ?
Symbol(keys[i]) : '__' + keys[i] + '__'
module.exports = keyMap
},{}],6:[function(require,module,exports){
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]
if (nodeA && nodeB)
return nodeA.index - nodeB.index
return 0
})
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
},{"./key_map":5}],7:[function(require,module,exports){
var processNodes = require('./process_nodes')
var bindKeys = require('./bind_keys')
var keyMap = require('./key_map')
var hasDefinitionKey = keyMap.hasDefinition
var isBoundToParentKey = keyMap.isBoundToParent
var markerKey = keyMap.marker
var metaKey = keyMap.meta
var acceptNode = processNodes.acceptNode
// A fixed constant for `NodeFilter.SHOW_ELEMENT`.
var whatToShow = 0x00000001
// Fixed constant for comment node type.
var COMMENT_NODE = 8
module.exports = rehydrate
/**
* Rehydration of existing DOM nodes by recursively checking equality.
*
* @param {*} scope
* @param {Object} obj
* @param {Object} def
* @param {Node} node
* @param {Node} matchNode
*/
function rehydrate (scope, obj, def, node, matchNode) {
var document = scope ? scope.document : window.document
var key, branch, x, value, change, definition, mount, keyPath
var meta, valueIsArray, activeNodes, index, treeWalker, currentNode
for (key in def) {
branch = def[key]
meta = obj[metaKey][key]
change = !branch[hasDefinitionKey] && branch[1]
definition = branch[hasDefinitionKey] && branch[1]
mount = branch[2]
keyPath = meta.keyPath
if (branch[isBoundToParentKey]) {
x = obj[key]
if (definition && x !== null && x !== void 0)
bindKeys(scope, x, definition, matchNode, keyPath)
else if (change)
change(matchNode, x, null, keyPath)
continue
}
activeNodes = meta.activeNodes
if (!activeNodes.length) continue
valueIsArray = meta.valueIsArray
x = valueIsArray ? obj[key] : [ obj[key] ]
index = 0
treeWalker = document.createTreeWalker(
matchNode, whatToShow, acceptNode, false)
while (index < activeNodes.length && treeWalker.nextNode()) {
currentNode = activeNodes[index]
if (treeWalker.currentNode.isEqualNode(currentNode)) {
activeNodes.splice(index, 1, treeWalker.currentNode)
value = x[index]
if (valueIsArray) keyPath.index = index
else delete keyPath.index
if (definition) {
rehydrate(scope, value, definition,
currentNode, treeWalker.currentNode)
if (mount) {
keyPath.target = value
mount(treeWalker.currentNode, value, null, keyPath)
}
}
else if (change)
change(treeWalker.currentNode, value, null, keyPath)
index++
}
}
if (index !== activeNodes.length) throw new Error(
'Matching nodes could not be found on key "' + key + '", expected ' +
activeNodes.length + ', found ' + index + '.')
// Rehydrate marker node.
currentNode = treeWalker.currentNode
// Try to re-use comment node.
if (processNodes.useCommentNode &&
currentNode.nextSibling !== null &&
currentNode.nextSibling.nodeType === COMMENT_NODE)
branch[markerKey] = currentNode.nextSibling
else branch[markerKey] = currentNode.parentNode.insertBefore(
document.createTextNode(''), currentNode.nextSibling)
}
}
},{"./bind_keys":1,"./key_map":5,"./process_nodes":6}],8:[function(require,module,exports){
window.simulacra = require('../lib/index')
},{"../lib/index":4}]},{},[8]);