can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
406 lines (380 loc) • 12.7 kB
JavaScript
// # can/view/node_lists/node_list.js
//
// `can.view.nodeLists` are used to make sure "directly nested" live-binding
// sections update content correctly.
//
// Consider the following template:
//
// ```
// <div>
// {{#if items.length}}
// Items:
// {{#items}}
// <label></label>
// {{/items}}
// {{/if}}
// </div>
// ```
//
// The `{{#if}}` and `{{#items}}` seconds are "directly nested" because
// they share the same `<div>` parent element.
//
// If `{{#items}}` changes the DOM by adding more `<labels>`,
// `{{#if}}` needs to know about the `<labels>` to remove them
// if `{{#if}}` is re-rendered. `{{#if}}` would be re-rendered, for example, if
// all items were removed.
steal('can/util', 'can/view/elements.js', function (can) {
// ## Helpers
// Some browsers don't allow expando properties on HTMLTextNodes
// so let's try to assign a custom property, an 'expando' property.
// We use this boolean to determine how we are going to hold on
// to HTMLTextNode within a nodeList. More about this in the 'id'
// function.
var canExpando = true;
try {
document.createTextNode('')._ = 0;
} catch (ex) {
canExpando = false;
}
// A mapping of element ids to nodeList id allowing us to quickly find an element
// that needs to be replaced when updated.
var nodeMap = {},
// A mapping of ids to text nodes, this map will be used in the
// case of the browser not supporting expando properties.
textNodeMap = {},
// The name of the expando property; the value returned
// given a nodeMap key.
expando = 'ejs_' + Math.random(),
// The id used as the key in our nodeMap, this integer
// will be preceded by 'element_' or 'obj_' depending on whether
// the element has a nodeName.
_id = 0,
// ## nodeLists.id
// Given a template node, create an id on the node as a expando
// property, or if the node is an HTMLTextNode and the browser
// doesn't support expando properties store the id with a
// reference to the text node in an internal collection then return
// the lookup id.
id = function (node, localMap) {
var _textNodeMap = localMap || textNodeMap;
var id = readId(node,_textNodeMap);
if(id) {
return id;
} else {
// If the browser supports expando properties or the node
// provided is not an HTMLTextNode, we don't need to work
// with the internal textNodeMap and we can place the property
// on the node.
if (canExpando || node.nodeType !== 3) {
++_id;
return node[expando] = (node.nodeName ? 'element_' : 'obj_') + _id;
} else {
// If we didn't find the node, we need to register it and return
// the id used.
++_id;
// If we didn't find the node, we need to register it and return
// the id used.
//
// We have to store the node itself because of the browser's lack
// of support for expando properties (i.e. we can't use a look-up
// table and store the id on the node as a custom property).
_textNodeMap['text_' + _id] = node;
return 'text_' + _id;
}
}
},
readId = function(node,textNodeMap){
if (canExpando || node.nodeType !== 3) {
return node[expando];
} else {
// The nodeList has a specific collection for HTMLTextNodes for
// (older) browsers that do not support expando properties.
for (var textNodeID in textNodeMap) {
if (textNodeMap[textNodeID] === node) {
return textNodeID;
}
}
}
},
splice = [].splice,
push = [].push,
// ## nodeLists.itemsInChildListTree
// Given a nodeList return the number of child items in the provided
// list and any child lists.
itemsInChildListTree = function(list){
var count = 0;
for(var i = 0, len = list.length ; i < len; i++){
var item = list[i];
// If the item is an HTMLElement then increment the count by 1.
if(item.nodeType) {
count++;
} else {
// If the item is not an HTMLElement it is a list, so
// increment the count by the number of items in the child
// list.
count += itemsInChildListTree(item);
}
}
return count;
},
replacementMap = function(replacements, idMap){
var map = {};
for(var i = 0, len = replacements.length; i < len; i++){
var node = nodeLists.first(replacements[i]);
map[id(node, idMap)] = replacements[i];
}
return map;
},
addUnfoundAsDeepChildren = function(list, rMap, foundIds){
for(var repId in rMap) {
if(!foundIds[repId]) {
list.newDeepChildren.push(rMap[repId]);
}
}
};
// ## Registering & Updating
//
// To keep all live-bound sections knowing which elements they are managing,
// all live-bound elments are registered and updated when they change.
//
// For example, the above template, when rendered with data like:
//
// data = new can.Map({
// items: ["first","second"]
// })
//
// This will first render the following content:
//
// <div>
// <span data-view-id='5'/>
// </div>
//
// When the `5` callback is called, this will register the `<span>` like:
//
// var ifsNodes = [<span 5>]
// nodeLists.register(ifsNodes);
//
// And then render `{{if}}`'s contents and update `ifsNodes` with it:
//
// nodeLists.update( ifsNodes, [<"\nItems:\n">, <span data-view-id="6">] );
//
// Next, hookup `6` is called which will regsiter the `<span>` like:
//
// var eachsNodes = [<span 6>];
// nodeLists.register(eachsNodes);
//
// And then it will render `{{#each}}`'s content and update `eachsNodes` with it:
//
// nodeLists.update(eachsNodes, [<label>,<label>]);
//
// As `nodeLists` knows that `eachsNodes` is inside `ifsNodes`, it also updates
// `ifsNodes`'s nodes to look like:
//
// [<"\nItems:\n">,<label>,<label>]
//
// Now, if all items were removed, `{{#if}}` would be able to remove
// all the `<label>` elements.
//
// When you regsiter a nodeList, you can also provide a callback to know when
// that nodeList has been replaced by a parent nodeList. This is
// useful for tearing down live-binding.
var nodeLists = {
id: id,
// ## nodeLists.update
// Updates a nodeList with new items, i.e. when values for the template have changed.
update: function (nodeList, newNodes) {
// Unregister all childNodeLists.
var oldNodes = nodeLists.unregisterChildren(nodeList);
newNodes = can.makeArray(newNodes);
var oldListLength = nodeList.length;
// Replace oldNodeLists's contents.
splice.apply(nodeList, [
0,
oldListLength
].concat(newNodes));
// Replacements are nodes that have replaced the original element this is on.
// We can't simply insert elements because stache does children before parents.
if(nodeList.replacements){
nodeLists.nestReplacements(nodeList);
nodeList.deepChildren = nodeList.newDeepChildren;
nodeList.newDeepChildren = [];
} else {
nodeLists.nestList(nodeList);
}
return oldNodes;
},
// Goes through each node in the list. [el1, el2, el3, ...]
// Finds the nodeList for that node in repacements. el1's nodeList might look like [el1, [el2]].
// Replaces that element and any other elements in the node list with the
// nodelist itself. resulting in [ [el1, [el2]], el3, ...]
// If a replacement is not found, it was improperly added, so we add it as a deepChild.
nestReplacements: function(list){
var index = 0,
// temporary id map that is limited to this call
idMap = {},
// replacements are in reverse order in the DOM
rMap = replacementMap(list.replacements, idMap),
rCount = list.replacements.length,
foundIds = {};
while(index < list.length && rCount) {
var node = list[index],
nodeId = readId(node, idMap),
replacement = rMap[nodeId];
if( replacement ) {
list.splice( index, itemsInChildListTree(replacement), replacement );
foundIds[nodeId] = true;
rCount--;
}
index++;
}
// Only do this if
if(rCount) {
addUnfoundAsDeepChildren(list, rMap, foundIds );
}
list.replacements = [];
},
// ## nodeLists.nestList
// If a given list does not exist in the nodeMap then create an lookup
// id for it in the nodeMap and assign the list to it.
// If the the provided does happen to exist in the nodeMap update the
// elements in the list.
// @param {Array.<HTMLElement>} nodeList The nodeList being nested.
nestList: function(list){
var index = 0;
while(index < list.length) {
var node = list[index],
childNodeList = nodeMap[id(node)];
if(childNodeList) {
if(childNodeList !== list) {
list.splice( index, itemsInChildListTree(childNodeList), childNodeList );
}
} else {
// Indicate the new nodes belong to this list.
nodeMap[id(node)] = list;
}
index++;
}
},
// ## nodeLists.last
// Return the last HTMLElement in a nodeList, if the last
// element is a nodeList, returns the last HTMLElement of
// the child list, etc.
last: function(nodeList){
var last = nodeList[nodeList.length - 1];
// If the last node in the list is not an HTMLElement
// it is a nodeList so call `last` again.
if(last.nodeType) {
return last;
} else {
return nodeLists.last(last);
}
},
// ## nodeLists.first
// Return the first HTMLElement in a nodeList, if the first
// element is a nodeList, returns the first HTMLElement of
// the child list, etc.
first: function(nodeList) {
var first = nodeList[0];
// If the first node in the list is not an HTMLElement
// it is a nodeList so call `first` again.
if(first.nodeType) {
return first;
} else {
return nodeLists.first(first);
}
},
flatten: function(nodeList){
var items = [];
for(var i = 0 ; i < nodeList.length; i++) {
var item = nodeList[i];
if(item.nodeType) {
items.push(item);
} else {
items.push.apply(items, nodeLists.flatten(item));
}
}
return items;
},
// ## nodeLists.register
// Registers a nodeList and returns the nodeList passed to register
register: function (nodeList, unregistered, parent, directlyNested) {
// If a unregistered callback has been provided assign it to the nodeList
// as a property to be called when the nodeList is unregistred.
can.cid(nodeList);
nodeList.unregistered = unregistered;
nodeList.parentList = parent;
nodeList.nesting = parent && typeof parent.nesting !== 'undefined' ? parent.nesting + 1 : 0;
if(parent) {
nodeList.deepChildren = [];
nodeList.newDeepChildren = [];
nodeList.replacements = [];
if(parent !== true) {
if(directlyNested) {
parent.replacements.push(nodeList);
}
else {
parent.newDeepChildren.push(nodeList);
}
}
}
else {
nodeLists.nestList(nodeList);
}
return nodeList;
},
// ## nodeLists.unregisterChildren
// Unregister all childen within the provided list and return the
// unregistred nodes.
// @param {Array.<HTMLElement>} nodeList The child list to unregister.
unregisterChildren: function(nodeList){
var nodes = [];
// For each node in the nodeList we want to compute it's id
// and delete it from the nodeList's internal map.
can.each(nodeList, function (node) {
// If the node does not have a nodeType it is an array of
// nodes.
if(node.nodeType) {
if(!nodeList.replacements) {
delete nodeMap[id(node)];
}
nodes.push(node);
} else {
// Recursively unregister each of the child lists in
// the nodeList.
push.apply(nodes, nodeLists.unregister(node, true));
}
});
can.each(nodeList.deepChildren, function(nodeList){
nodeLists.unregister(nodeList, true);
});
return nodes;
},
// ## nodeLists.unregister
// Unregister's a nodeList and returns the unregistered nodes.
// Call if the nodeList is no longer being updated. This will
// also unregister all child nodeLists.
unregister: function (nodeList, isChild) {
var nodes = nodeLists.unregisterChildren(nodeList, true);
// If an 'unregisted' function was provided during registration, remove
// it from the list, and call the function provided.
if (nodeList.unregistered) {
var unregisteredCallback = nodeList.unregistered;
nodeList.replacements = nodeList.unregistered = null;
if(!isChild) {
var deepChildren = nodeList.parentList && nodeList.parentList.deepChildren;
if(deepChildren) {
var index = deepChildren.indexOf(nodeList);
if(index !== -1) {
deepChildren.splice(index,1);
}
}
}
unregisteredCallback();
}
return nodes;
},
nodeMap: nodeMap
};
can.view.nodeLists = nodeLists;
return nodeLists;
});