216 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			216 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
| // REPEAT binding for Knockout http://knockoutjs.com/
 | |
| // (c) Michael Best
 | |
| // License: MIT (http://www.opensource.org/licenses/mit-license.php)
 | |
| // Version 2.1.0
 | |
| 
 | |
| (function(factory) {
 | |
|     if (typeof define === 'function' && define.amd) {
 | |
|         // [1] AMD anonymous module
 | |
|         define(['knockout'], factory);
 | |
|     } else if (typeof exports === 'object') {
 | |
|         // [2] commonJS
 | |
|         factory(require('knockout'));
 | |
|     } else {
 | |
|         // [3] No module loader (plain <script> tag) - put directly in global namespace
 | |
|         factory(window.ko);
 | |
|     }
 | |
| })(function(ko) {
 | |
| 
 | |
| if (!ko.virtualElements)
 | |
|     throw Error('Repeat requires at least Knockout 2.1');
 | |
| 
 | |
| var ko_bindingFlags = ko.bindingFlags || {};
 | |
| var ko_unwrap = ko.utils.unwrapObservable;
 | |
| 
 | |
| var koProtoName = '__ko_proto__';
 | |
| 
 | |
| if (ko.version >= "3.0.0") {
 | |
|     // In Knockout 3.0.0, use the node preprocessor to replace a node with a repeat binding with a virtual element
 | |
|     var provider = ko.bindingProvider.instance, previousPreprocessFn = provider.preprocessNode;
 | |
|     provider.preprocessNode = function(node) {
 | |
|         var newNodes, nodeBinding;
 | |
|         if (!previousPreprocessFn || !(newNodes = previousPreprocessFn.call(this, node))) {
 | |
|             if (node.nodeType === 1 && (nodeBinding = node.getAttribute('data-bind'))) {
 | |
|                 if (/^\s*repeat\s*:/.test(nodeBinding)) {
 | |
|                     var leadingComment = node.ownerDocument.createComment('ko ' + nodeBinding),
 | |
|                         trailingComment = node.ownerDocument.createComment('/ko');
 | |
|                     node.parentNode.insertBefore(leadingComment, node);
 | |
|                     node.parentNode.insertBefore(trailingComment, node.nextSibling);
 | |
|                     node.removeAttribute('data-bind');
 | |
|                     newNodes = [leadingComment, node, trailingComment];
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return newNodes;
 | |
|     };
 | |
| }
 | |
| 
 | |
| ko.virtualElements.allowedBindings.repeat = true;
 | |
| ko.bindingHandlers.repeat = {
 | |
|     flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual,
 | |
|     init: function(element, valueAccessor, allBindingsAccessor, xxx, bindingContext) {
 | |
| 
 | |
|         // Read and set fixed options--these options cannot be changed
 | |
|         var repeatParam = ko_unwrap(valueAccessor());
 | |
|         if (repeatParam && typeof repeatParam == 'object' && !('length' in repeatParam)) {
 | |
|             var repeatIndex = repeatParam.index,
 | |
|                 repeatData = repeatParam.item,
 | |
|                 repeatStep = repeatParam.step,
 | |
|                 repeatReversed = repeatParam.reverse,
 | |
|                 repeatBind = repeatParam.bind,
 | |
|                 repeatInit = repeatParam.init,
 | |
|                 repeatUpdate = repeatParam.update;
 | |
|         }
 | |
|         // Set default values for options that need it
 | |
|         repeatIndex = repeatIndex || '$index';
 | |
|         repeatData = repeatData || ko.bindingHandlers.repeat.itemName || '$item';
 | |
|         repeatStep = repeatStep || 1;
 | |
|         repeatReversed = repeatReversed || false;
 | |
| 
 | |
|         var parent = element.parentNode, placeholder;
 | |
|         if (element.nodeType == 8) {    // virtual element
 | |
|             // Extract the "children" and find the single element node
 | |
|             var childNodes = ko.utils.arrayFilter(ko.virtualElements.childNodes(element), function(node) { return node.nodeType == 1;});
 | |
|             if (childNodes.length !== 1) {
 | |
|                 throw Error("Repeat binding requires a single element to repeat");
 | |
|             }
 | |
|             ko.virtualElements.emptyNode(element);
 | |
| 
 | |
|             // The placeholder is the closing comment normally, or the opening comment if reversed
 | |
|             placeholder = repeatReversed ? element : element.nextSibling;
 | |
|             // The element to repeat is the contained element
 | |
|             element = childNodes[0];
 | |
|         } else {    // regular element
 | |
|             // First clean the element node and remove node's binding
 | |
|             var origBindString = element.getAttribute('data-bind');
 | |
|             ko.cleanNode(element);
 | |
|             element.removeAttribute('data-bind');
 | |
| 
 | |
|             // Original element is no longer needed: delete it and create a placeholder comment
 | |
|             placeholder = element.ownerDocument.createComment('ko_repeatplaceholder ' + origBindString);
 | |
|             parent.replaceChild(placeholder, element);
 | |
|         }
 | |
| 
 | |
|         // extract and remove a data-repeat-bind attribute, if present
 | |
|         if (!repeatBind) {
 | |
|             repeatBind = element.getAttribute('data-repeat-bind');
 | |
|             if (repeatBind) {
 | |
|                 element.removeAttribute('data-repeat-bind');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Make a copy of the element node to be copied for each repetition
 | |
|         var cleanNode = element.cloneNode(true);
 | |
|         if (typeof repeatBind == "string") {
 | |
|             cleanNode.setAttribute('data-bind', repeatBind);
 | |
|             repeatBind = null;
 | |
|         }
 | |
| 
 | |
|         // Set up persistent data
 | |
|         var lastRepeatCount = 0,
 | |
|             notificationObservable = ko.observable(),
 | |
|             repeatArray, arrayObservable;
 | |
| 
 | |
|         if (repeatInit) {
 | |
|             repeatInit(parent);
 | |
|         }
 | |
| 
 | |
|         var subscribable = ko.computed(function() {
 | |
|             function makeArrayItemAccessor(index) {
 | |
|                 var f = function(newValue) {
 | |
|                     var item = repeatArray[index];
 | |
|                     // Reading the value of the item
 | |
|                     if (!arguments.length) {
 | |
|                         notificationObservable();   // for dependency tracking
 | |
|                         return ko_unwrap(item);
 | |
|                     }
 | |
|                     // Writing a value to the item
 | |
|                     if (ko.isObservable(item)) {
 | |
|                         item(newValue);
 | |
|                     } else if (arrayObservable && arrayObservable.splice) {
 | |
|                         arrayObservable.splice(index, 1, newValue);
 | |
|                     } else {
 | |
|                         repeatArray[index] = newValue;
 | |
|                     }
 | |
|                     return this;
 | |
|                 };
 | |
|                 // Pretend that our accessor function is an observable
 | |
|                 f[koProtoName] = ko.observable;
 | |
|                 return f;
 | |
|             }
 | |
| 
 | |
|             function makeBinding(item, index, context) {
 | |
|                 return repeatArray
 | |
|                     ? function() { return repeatBind.call(bindingContext.$data, item, index, context); }
 | |
|                     : function() { return repeatBind.call(bindingContext.$data, index, context); }
 | |
|             }
 | |
| 
 | |
|             // Read and set up variable options--these options can change and will update the binding
 | |
|             var paramObservable = valueAccessor(), repeatParam = ko_unwrap(paramObservable), repeatCount = 0;
 | |
|             if (repeatParam && typeof repeatParam == 'object') {
 | |
|                 if ('length' in repeatParam) {
 | |
|                     repeatArray = repeatParam;
 | |
|                     repeatCount = repeatArray.length;
 | |
|                 } else {
 | |
|                     if ('foreach' in repeatParam) {
 | |
|                         repeatArray = ko_unwrap(paramObservable = repeatParam.foreach);
 | |
|                         if (repeatArray && typeof repeatArray == 'object' && 'length' in repeatArray) {
 | |
|                             repeatCount = repeatArray.length || 0;
 | |
|                         } else {
 | |
|                             repeatCount = repeatArray || 0;
 | |
|                             repeatArray = null;
 | |
|                         }
 | |
|                     }
 | |
|                     // If a count value is provided (>0), always output that number of items
 | |
|                     if ('count' in repeatParam)
 | |
|                         repeatCount = ko_unwrap(repeatParam.count) || repeatCount;
 | |
|                     // If a limit is provided, don't output more than the limit
 | |
|                     if ('limit' in repeatParam)
 | |
|                         repeatCount = Math.min(repeatCount, ko_unwrap(repeatParam.limit)) || repeatCount;
 | |
|                 }
 | |
|                 arrayObservable = repeatArray && ko.isObservable(paramObservable) ? paramObservable : null;
 | |
|             } else {
 | |
|                 repeatCount = repeatParam || 0;
 | |
|             }
 | |
| 
 | |
|             // Remove nodes from end if array is shorter
 | |
|             for (; lastRepeatCount > repeatCount; lastRepeatCount-=repeatStep) {
 | |
|                 ko.removeNode(repeatReversed ? placeholder.nextSibling : placeholder.previousSibling);
 | |
|             }
 | |
| 
 | |
|             // Notify existing nodes of change
 | |
|             notificationObservable.notifySubscribers();
 | |
| 
 | |
|             // Add nodes to end if array is longer (also initially populates nodes)
 | |
|             for (; lastRepeatCount < repeatCount; lastRepeatCount+=repeatStep) {
 | |
|                 // Clone node and add to document
 | |
|                 var newNode = cleanNode.cloneNode(true);
 | |
|                 parent.insertBefore(newNode, repeatReversed ? placeholder.nextSibling : placeholder);
 | |
|                 newNode.setAttribute('data-repeat-index', lastRepeatCount);
 | |
| 
 | |
|                 // Apply bindings to inserted node
 | |
|                 if (repeatArray && repeatData == '$data') {
 | |
|                     var newContext = bindingContext.createChildContext(makeArrayItemAccessor(lastRepeatCount));
 | |
|                 } else {
 | |
|                     var newContext = bindingContext.extend();
 | |
|                     if (repeatArray)
 | |
|                         newContext[repeatData] = makeArrayItemAccessor(lastRepeatCount);
 | |
|                 }
 | |
|                 newContext[repeatIndex] = lastRepeatCount;
 | |
|                 if (repeatBind) {
 | |
|                     var result = ko.applyBindingsToNode(newNode, makeBinding(newContext[repeatData], lastRepeatCount, newContext), newContext, true),
 | |
|                         shouldBindDescendants = result && result.shouldBindDescendants;
 | |
|                 }
 | |
|                 if (!repeatBind || (result && shouldBindDescendants !== false)) {
 | |
|                     ko.applyBindings(newContext, newNode);
 | |
|                 }
 | |
|             }
 | |
|             if (repeatUpdate) {
 | |
|                 repeatUpdate(parent);
 | |
|             }
 | |
|         }, null, {disposeWhenNodeIsRemoved: placeholder});
 | |
| 
 | |
|         return { controlsDescendantBindings: true, subscribable: subscribable };
 | |
|     }
 | |
| };
 | |
| }); |