knockout-repeat.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. // REPEAT binding for Knockout http://knockoutjs.com/
  2. // (c) Michael Best
  3. // License: MIT (http://www.opensource.org/licenses/mit-license.php)
  4. // Version 2.1.0
  5. (function(factory) {
  6. if (typeof define === 'function' && define.amd) {
  7. // [1] AMD anonymous module
  8. define(['knockout'], factory);
  9. } else if (typeof exports === 'object') {
  10. // [2] commonJS
  11. factory(require('knockout'));
  12. } else {
  13. // [3] No module loader (plain <script> tag) - put directly in global namespace
  14. factory(window.ko);
  15. }
  16. })(function(ko) {
  17. if (!ko.virtualElements)
  18. throw Error('Repeat requires at least Knockout 2.1');
  19. var ko_bindingFlags = ko.bindingFlags || {};
  20. var ko_unwrap = ko.utils.unwrapObservable;
  21. var koProtoName = '__ko_proto__';
  22. if (ko.version >= "3.0.0") {
  23. // In Knockout 3.0.0, use the node preprocessor to replace a node with a repeat binding with a virtual element
  24. var provider = ko.bindingProvider.instance, previousPreprocessFn = provider.preprocessNode;
  25. provider.preprocessNode = function(node) {
  26. var newNodes, nodeBinding;
  27. if (!previousPreprocessFn || !(newNodes = previousPreprocessFn.call(this, node))) {
  28. if (node.nodeType === 1 && (nodeBinding = node.getAttribute('data-bind'))) {
  29. if (/^\s*repeat\s*:/.test(nodeBinding)) {
  30. var leadingComment = node.ownerDocument.createComment('ko ' + nodeBinding),
  31. trailingComment = node.ownerDocument.createComment('/ko');
  32. node.parentNode.insertBefore(leadingComment, node);
  33. node.parentNode.insertBefore(trailingComment, node.nextSibling);
  34. node.removeAttribute('data-bind');
  35. newNodes = [leadingComment, node, trailingComment];
  36. }
  37. }
  38. }
  39. return newNodes;
  40. };
  41. }
  42. ko.virtualElements.allowedBindings.repeat = true;
  43. ko.bindingHandlers.repeat = {
  44. flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual,
  45. init: function(element, valueAccessor, allBindingsAccessor, xxx, bindingContext) {
  46. // Read and set fixed options--these options cannot be changed
  47. var repeatParam = ko_unwrap(valueAccessor());
  48. if (repeatParam && typeof repeatParam == 'object' && !('length' in repeatParam)) {
  49. var repeatIndex = repeatParam.index,
  50. repeatData = repeatParam.item,
  51. repeatStep = repeatParam.step,
  52. repeatReversed = repeatParam.reverse,
  53. repeatBind = repeatParam.bind,
  54. repeatInit = repeatParam.init,
  55. repeatUpdate = repeatParam.update;
  56. }
  57. // Set default values for options that need it
  58. repeatIndex = repeatIndex || '$index';
  59. repeatData = repeatData || ko.bindingHandlers.repeat.itemName || '$item';
  60. repeatStep = repeatStep || 1;
  61. repeatReversed = repeatReversed || false;
  62. var parent = element.parentNode, placeholder;
  63. if (element.nodeType == 8) { // virtual element
  64. // Extract the "children" and find the single element node
  65. var childNodes = ko.utils.arrayFilter(ko.virtualElements.childNodes(element), function(node) { return node.nodeType == 1;});
  66. if (childNodes.length !== 1) {
  67. throw Error("Repeat binding requires a single element to repeat");
  68. }
  69. ko.virtualElements.emptyNode(element);
  70. // The placeholder is the closing comment normally, or the opening comment if reversed
  71. placeholder = repeatReversed ? element : element.nextSibling;
  72. // The element to repeat is the contained element
  73. element = childNodes[0];
  74. } else { // regular element
  75. // First clean the element node and remove node's binding
  76. var origBindString = element.getAttribute('data-bind');
  77. ko.cleanNode(element);
  78. element.removeAttribute('data-bind');
  79. // Original element is no longer needed: delete it and create a placeholder comment
  80. placeholder = element.ownerDocument.createComment('ko_repeatplaceholder ' + origBindString);
  81. parent.replaceChild(placeholder, element);
  82. }
  83. // extract and remove a data-repeat-bind attribute, if present
  84. if (!repeatBind) {
  85. repeatBind = element.getAttribute('data-repeat-bind');
  86. if (repeatBind) {
  87. element.removeAttribute('data-repeat-bind');
  88. }
  89. }
  90. // Make a copy of the element node to be copied for each repetition
  91. var cleanNode = element.cloneNode(true);
  92. if (typeof repeatBind == "string") {
  93. cleanNode.setAttribute('data-bind', repeatBind);
  94. repeatBind = null;
  95. }
  96. // Set up persistent data
  97. var lastRepeatCount = 0,
  98. notificationObservable = ko.observable(),
  99. repeatArray, arrayObservable;
  100. if (repeatInit) {
  101. repeatInit(parent);
  102. }
  103. var subscribable = ko.computed(function() {
  104. function makeArrayItemAccessor(index) {
  105. var f = function(newValue) {
  106. var item = repeatArray[index];
  107. // Reading the value of the item
  108. if (!arguments.length) {
  109. notificationObservable(); // for dependency tracking
  110. return ko_unwrap(item);
  111. }
  112. // Writing a value to the item
  113. if (ko.isObservable(item)) {
  114. item(newValue);
  115. } else if (arrayObservable && arrayObservable.splice) {
  116. arrayObservable.splice(index, 1, newValue);
  117. } else {
  118. repeatArray[index] = newValue;
  119. }
  120. return this;
  121. };
  122. // Pretend that our accessor function is an observable
  123. f[koProtoName] = ko.observable;
  124. return f;
  125. }
  126. function makeBinding(item, index, context) {
  127. return repeatArray
  128. ? function() { return repeatBind.call(bindingContext.$data, item, index, context); }
  129. : function() { return repeatBind.call(bindingContext.$data, index, context); }
  130. }
  131. // Read and set up variable options--these options can change and will update the binding
  132. var paramObservable = valueAccessor(), repeatParam = ko_unwrap(paramObservable), repeatCount = 0;
  133. if (repeatParam && typeof repeatParam == 'object') {
  134. if ('length' in repeatParam) {
  135. repeatArray = repeatParam;
  136. repeatCount = repeatArray.length;
  137. } else {
  138. if ('foreach' in repeatParam) {
  139. repeatArray = ko_unwrap(paramObservable = repeatParam.foreach);
  140. if (repeatArray && typeof repeatArray == 'object' && 'length' in repeatArray) {
  141. repeatCount = repeatArray.length || 0;
  142. } else {
  143. repeatCount = repeatArray || 0;
  144. repeatArray = null;
  145. }
  146. }
  147. // If a count value is provided (>0), always output that number of items
  148. if ('count' in repeatParam)
  149. repeatCount = ko_unwrap(repeatParam.count) || repeatCount;
  150. // If a limit is provided, don't output more than the limit
  151. if ('limit' in repeatParam)
  152. repeatCount = Math.min(repeatCount, ko_unwrap(repeatParam.limit)) || repeatCount;
  153. }
  154. arrayObservable = repeatArray && ko.isObservable(paramObservable) ? paramObservable : null;
  155. } else {
  156. repeatCount = repeatParam || 0;
  157. }
  158. // Remove nodes from end if array is shorter
  159. for (; lastRepeatCount > repeatCount; lastRepeatCount-=repeatStep) {
  160. ko.removeNode(repeatReversed ? placeholder.nextSibling : placeholder.previousSibling);
  161. }
  162. // Notify existing nodes of change
  163. notificationObservable.notifySubscribers();
  164. // Add nodes to end if array is longer (also initially populates nodes)
  165. for (; lastRepeatCount < repeatCount; lastRepeatCount+=repeatStep) {
  166. // Clone node and add to document
  167. var newNode = cleanNode.cloneNode(true);
  168. parent.insertBefore(newNode, repeatReversed ? placeholder.nextSibling : placeholder);
  169. newNode.setAttribute('data-repeat-index', lastRepeatCount);
  170. // Apply bindings to inserted node
  171. if (repeatArray && repeatData == '$data') {
  172. var newContext = bindingContext.createChildContext(makeArrayItemAccessor(lastRepeatCount));
  173. } else {
  174. var newContext = bindingContext.extend();
  175. if (repeatArray)
  176. newContext[repeatData] = makeArrayItemAccessor(lastRepeatCount);
  177. }
  178. newContext[repeatIndex] = lastRepeatCount;
  179. if (repeatBind) {
  180. var result = ko.applyBindingsToNode(newNode, makeBinding(newContext[repeatData], lastRepeatCount, newContext), newContext, true),
  181. shouldBindDescendants = result && result.shouldBindDescendants;
  182. }
  183. if (!repeatBind || (result && shouldBindDescendants !== false)) {
  184. ko.applyBindings(newContext, newNode);
  185. }
  186. }
  187. if (repeatUpdate) {
  188. repeatUpdate(parent);
  189. }
  190. }, null, {disposeWhenNodeIsRemoved: placeholder});
  191. return { controlsDescendantBindings: true, subscribable: subscribable };
  192. }
  193. };
  194. });