todomvc
Version:
> Helping you select an MV\* framework
366 lines (325 loc) • 11.2 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/util/library", "can/view/elements", "can/view", "can/view/node_lists"], function(can, elements,view,nodeLists){
// ## live.js
//
// The live module provides live binding for computes
// and can.List.
//
// Currently, it's API is designed for `can/view/render`, but
// it could easily be used for other purposes.
// ### Helper methods
//
// #### setup
//
// `setup(HTMLElement, bind(data), unbind(data)) -> data`
//
// Calls bind right away, but will call unbind
// if the element is "destroyed" (removed from the DOM).
var setup = function(el, bind, unbind){
var teardown = function(){
unbind(data)
can.unbind.call(el,'removed', teardown);
return true
},
data = {
// returns true if no parent
teardownCheck: function(parent){
return parent ? false : teardown();
}
}
can.bind.call(el,'removed', teardown);
bind(data)
return data;
},
// #### listen
// Calls setup, but presets bind and unbind to
// operate on a compute
listen = function(el, compute, change){
return setup(el, function(){
compute.bind("change", change);
},function(data){
compute.unbind("change", change);
if(data.nodeList){
nodeLists.unregister( data.nodeList );
}
});
},
// #### getAttributeParts
// Breaks up a string like foo='bar' into ["foo","'bar'""]
getAttributeParts = function(newVal){
return (newVal|| "").replace(/['"]/g, '').split('=')
}
// #### insertElementsAfter
// Appends elements after the last item in oldElements.
insertElementsAfter = function(oldElements, newFrag){
var last = oldElements[oldElements.length - 1];
// Insert it in the `document` or `documentFragment`
if( last.nextSibling ){
can.insertBefore(last.parentNode, newFrag, last.nextSibling)
} else {
can.appendChild(last.parentNode, newFrag);
}
};
var live = {
nodeLists : nodeLists,
/**
* Used
* @param {Object} el
* @param {can.compute} list a compute that returns a number or a list
* @param {Object} func
* @param {Object} context
* @param {Object} parentNode
*/
list: function(el, compute, func, context, parentNode){
// A mapping of the index to an array
// of elements that represent the item.
// Each array is registered so child or parent
// live structures can update the elements
var nodesMap = [],
// called when an item is added
add = function(ev, items, index){
// check that the placeholder textNode still has a parent.
// it's possible someone removed the contents of
// this element without removing the parent
if(data.teardownCheck(text.parentNode)){
return
}
// Collect new html and mappings
var frag = document.createDocumentFragment(),
newMappings = [];
can.each(items, function(item, key){
var itemHTML = func.call(context, item, (key + index)),
itemFrag = can.view.frag(itemHTML, parentNode);
newMappings.push(can.makeArray(itemFrag.childNodes));
frag.appendChild(itemFrag);
})
// Inserting at the end of the list
if(!nodesMap[index]){
insertElementsAfter(
index == 0 ?
[text] :
nodesMap[index-1], frag)
} else {
var el = nodesMap[index][0];
can.insertBefore(el.parentNode, frag, el);
}
// register each item
can.each(newMappings,function(nodeList){
nodeLists.register(nodeList)
});
[].splice.apply(nodesMap, [index, 0].concat(newMappings));
},
// Remove can be called during teardown or when items are
// removed from the element.
remove = function(ev, items, index, duringTeardown){
// If this is because an element was removed, we should
// check to make sure the live elements are still in the page.
// If we did this during a teardown, it would cause an infinite loop.
if(!duringTeardown && data.teardownCheck(text.parentNode)){
return
}
var removedMappings = nodesMap.splice(index, items.length),
itemsToRemove = [];
can.each(removedMappings,function(nodeList){
// add items that we will remove all at once
[].push.apply(itemsToRemove, nodeList)
// Update any parent lists to remove these items
nodeLists.replace(nodeList,[]);
// unregister the list
nodeLists.unregister(nodeList);
});
can.remove( can.$(itemsToRemove) );
},
parentNode = elements.getParentNode(el, parentNode),
text = document.createTextNode(""),
list;
teardownList = function(){
// there might be no list right away, and the list might be a plain
// array
list && list.unbind && list.unbind("add", add).unbind("remove", remove);
// use remove to clean stuff up for us
remove({},{length: nodesMap.length},0, true);
}
updateList = function(ev, newList, oldList){
teardownList();
// make an empty list if the compute returns null or undefined
list = newList || [];
// list might be a plain array
list.bind && list.bind("add", add).bind("remove", remove);
add({}, list, 0);
}
insertElementsAfter([el],text);
can.remove( can.$(el) );
// Setup binding and teardown to add and remove events
var data = setup(parentNode, function(){
can.isFunction(compute) && compute.bind("change",updateList)
},function(){
can.isFunction(compute) && compute.unbind("change",updateList)
teardownList()
})
updateList({},can.isFunction(compute) ? compute() : compute)
},
html: function(el, compute, parentNode){
var parentNode = elements.getParentNode(el, parentNode),
data = listen(parentNode, compute, function(ev, newVal, oldVal){
var attached = nodes[0].parentNode;
// update the nodes in the DOM with the new rendered value
if( attached ) {
makeAndPut(newVal);
}
data.teardownCheck(nodes[0].parentNode);
});
var nodes,
makeAndPut = function(val){
// create the fragment, but don't hook it up
// we need to insert it into the document first
var frag = can.view.frag(val, parentNode),
// keep a reference to each node
newNodes = can.makeArray(frag.childNodes);
// Insert it in the `document` or `documentFragment`
insertElementsAfter(nodes || [el], frag)
// nodes hasn't been set yet
if( !nodes ) {
can.remove( can.$(el) );
nodes = newNodes;
// set the teardown nodeList
data.nodeList = nodes;
nodeLists.register(nodes);
} else {
// Update node Array's to point to new nodes
// and then remove the old nodes.
// It has to be in this order for Mootools
// and IE because somehow, after an element
// is removed from the DOM, it loses its
// expando values.
var nodesToRemove = can.makeArray(nodes);
nodeLists.replace(nodes,newNodes);
can.remove( can.$(nodesToRemove) );
}
};
makeAndPut(compute(), [el]);
},
text: function(el, compute, parentNode){
var parent = elements.getParentNode(el, parentNode);
// setup listening right away so we don't have to re-calculate value
var data = listen( parent, compute, function(ev, newVal, oldVal){
// Sometimes this is 'unknown' in IE and will throw an exception if it is
if ( typeof node.nodeValue != 'unknown' ) {
node.nodeValue = ""+newVal;
}
data.teardownCheck(node.parentNode);
});
var node = document.createTextNode(compute());
if ( el.parentNode !== parent ) {
parent = el.parentNode;
parent.insertBefore(node, el);
parent.removeChild(el);
} else {
parent.insertBefore(node, el);
parent.removeChild(el);
}
},
attributes: function(el, compute, currentValue){
var setAttrs = function(newVal){
var parts = getAttributeParts(newVal),
newAttrName = parts.shift();
// Remove if we have a change and used to have an `attrName`.
if((newAttrName != attrName) && attrName){
elements.removeAttr(el,attrName);
}
// Set if we have a new `attrName`.
if(newAttrName){
elements.setAttr(el, newAttrName, parts.join('='));
attrName = newAttrName;
}
}
listen(el, compute, function(ev, newVal){
setAttrs(newVal)
})
// current value has been set
if(arguments.length >= 3) {
var attrName = getAttributeParts(currentValue)[0]
} else {
setAttrs(compute())
}
},
attributePlaceholder: '__!!__',
attributeReplace: /__!!__/g,
attribute: function(el, attributeName, compute){
listen(el, compute, function(ev, newVal){
elements.setAttr( el, attributeName, hook.render() );
})
var wrapped = can.$(el),
hooks;
// Get the list of hookups or create one for this element.
// Hooks is a map of attribute names to hookup `data`s.
// Each hookup data has:
// `render` - A `function` to render the value of the attribute.
// `funcs` - A list of hookup `function`s on that attribute.
// `batchNum` - The last event `batchNum`, used for performance.
hooks = can.data(wrapped,'hooks');
if ( ! hooks ) {
can.data(wrapped, 'hooks', hooks = {});
}
// Get the attribute value.
var attr = elements.getAttr(el, attributeName ),
// Split the attribute value by the template.
// Only split out the first __!!__ so if we have multiple hookups in the same attribute,
// they will be put in the right spot on first render
parts = attr.split(live.attributePlaceholder),
goodParts = [],
hook;
goodParts.push(parts.shift(),
parts.join(live.attributePlaceholder));
// If we already had a hookup for this attribute...
if(hooks[attributeName]) {
// Just add to that attribute's list of `function`s.
hooks[attributeName].computes.push(compute);
} else {
// Create the hookup data.
hooks[attributeName] = {
render: function() {
var i =0,
// attr doesn't have a value in IE
newAttr = attr
? attr.replace(live.attributeReplace, function() {
return elements.contentText( hook.computes[i++]() );
})
: elements.contentText( hook.computes[i++]() );
return newAttr;
},
computes: [compute],
batchNum : undefined
};
}
// Save the hook for slightly faster performance.
hook = hooks[attributeName];
// Insert the value in parts.
goodParts.splice(1,0,compute());
// Set the attribute.
elements.setAttr(el, attributeName, goodParts.join("") );
},
specialAttribute: function(el, attributeName, compute){
listen(el, compute, function(ev, newVal){
elements.setAttr( el, attributeName, getValue( newVal ) );
});
elements.setAttr(el, attributeName, getValue( compute() ) );
}
}
var newLine = /(\r|\n)+/g;
var getValue = function(val){
val = val.replace(elements.attrReg,"").replace(newLine,"");
// check if starts and ends with " or '
return /^["'].*["']$/.test(val) ? val.substr(1, val.length-2) : val
}
can.view.live = live;
return live;
});