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 };
|
|
}
|
|
};
|
|
}); |