fanforce-plugin-factory 0.38.2 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,16 @@
1
- .btn-primary { background:#48739c; @include ff-gradient(#6a93bb, #48739c); border:1px solid #324e69; @include box-shadow(inset 1px 1px 0 #98badc); color:#ffffff; text-shadow:-1px -1px 1px #2d4963;
2
- &:hover, &:active, &:focus, &.disabled, &[disabled] { background:#3f6890; @include ff-gradient(#5c85ad, #3f6890); text-decoration:none; }
3
- }
1
+ body { padding:0; font-size:12px;
2
+ .btn-primary { background:#48739c; @include ff-gradient(#6a93bb, #48739c); border:1px solid #324e69; @include box-shadow(inset 1px 1px 0 #98badc); color:#ffffff; text-shadow:-1px -1px 1px #2d4963;
3
+ &:hover, &:active, &:focus, &.disabled, &[disabled] { background:#3f6890; @include ff-gradient(#5c85ad, #3f6890); text-decoration:none; }
4
+ }
4
5
 
5
- textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input {
6
- border: 1px solid #CCCCCC;
7
- @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.075) inset);
8
- }
6
+ textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input {
7
+ border: 1px solid #CCCCCC;
8
+ @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.075) inset);
9
+ }
9
10
 
10
- select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input {
11
- @include border-radius(4px);
12
- }
11
+ select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input {
12
+ @include border-radius(4px);
13
+ }
13
14
 
14
- .btn { padding: 4px 12px;
15
- }
15
+ }
16
+ .btn { padding: 2px 10px; color: #666666; border-radius: 3px; font-size: 11.05px; background-color: #F5F5F5; @include ff-gradient(#FFFFFF, #E6E6E6); border-color: #CCCCCC #CCCCCC #B3B3B3; @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 1px 2px rgba(0, 0, 0, 0.05)); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); }
@@ -1,15 +1,17 @@
1
- .module-add-initiative-footer { position:absolute; bottom:0; left:0; padding:5px 0; text-align:left; width:100%; margin:0px; overflow:hidden; background: #f2f5f8; @include box-shadow(#ffffff 0 1px 0 inset, #d1d1d1 0 -1px 2px);
2
- &.disabled { pointer-events: none;
1
+ .module-add-initiative-footer { position:absolute; bottom:0; left:0; padding:0 3px; @include box-sizing(border-box); text-align:left; width:100%; margin:0px; overflow:hidden;
2
+ &.disabled .footer { pointer-events: none;
3
3
  label { @include opacity(.5); }
4
4
  .input-wrapper { @include opacity(.5); cursor: not-allowed; }
5
- button { @include opacity(.5); }
5
+ button { @include opacity(.5); background: #c9d5e1; border-color: #a6b4c1; text-shadow: none; }
6
6
  }
7
- label { font-weight:100; color:#666666; text-shadow:1px 1px 0 #ffffff; margin-left:20px; }
8
- .input-wrapper { cursor: text; position:relative; display: inline-block; color: #555555; background: rgba(255, 255, 255, 0.75); @include border-radius(4px); border: 1px solid #CCCCCC; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.075) inset);
9
- .prefix { pointer-events: none; font-size: 12px; font-weight: normal; line-height: 1.5; height:28px; vertical-align: middle; padding: 5px 0 0 8px; display:inline-block; }
10
- input { font-size: 12px; font-weight: normal; background: transparent; padding-left: 0px; border:none; height:28px; @include box-shadow(none); min-width:250px; width:auto; display:inline-block; vertical-align: middle; }
11
- }
12
- button { margin-top: 0; margin-left: 2px;
13
- i { margin-right: 5px; }
7
+ .footer { @include box-shadow(#ffffff 0 1px 0 inset); padding: 5px 0; border-top: 1px solid #e1e1e1;
8
+ label { font-weight:100; color:#666666; text-shadow:1px 1px 0 #ffffff; margin-left:20px; }
9
+ .input-wrapper { cursor: text; position:relative; display: inline-block; color: #555555; background: rgba(255, 255, 255, 0.75); @include border-radius(4px); border: 1px solid #CCCCCC; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.075) inset);
10
+ .prefix { pointer-events: none; font-size: 12px; font-weight: normal; line-height: 1.5; height:28px; vertical-align: middle; padding: 5px 0 0 8px; display:inline-block; }
11
+ input { font-size: 12px; font-weight: normal; background: transparent; padding-left: 0px; border:none; height:28px; @include box-shadow(none); min-width:250px; width:auto; display:inline-block; vertical-align: middle; }
12
+ }
13
+ button { margin-top: 0; margin-left: 2px; padding-top: 5px; padding-bottom: 5px;
14
+ i { margin-right: 5px; }
15
+ }
14
16
  }
15
17
  }
@@ -1,7 +1,7 @@
1
1
  $module_path: '/assets/common/module-initiative-searcher';
2
2
  /////////////////////////////////////////////////////////////////////////////////////////
3
3
 
4
- .module-initiative-searcher { position:relative; width:100%; padding:20px 23px 15px; overflow:hidden;
4
+ .module-initiative-searcher { position:relative; width:100%; padding:20px 20px 15px; overflow:hidden;
5
5
  &.supreme { @include ff-box(); }
6
6
  .header { padding:0 0 5px; max-width:900px;
7
7
  h2 { color:#505a62; text-shadow:#ffffff 1px 1px 0; margin-top:0; padding-bottom:5px; }
@@ -1,83 +1 @@
1
- @import 'variables';
2
- //////////////////////////////////////////////////////////////////////////////////////////////////
3
- // COMMON TAGS
4
- //////////////////////////////////////////////////////////////////////////////////////////////////
5
- html { }
6
- body { font-family:$font-family; font-size:$font-size-normal; line-height:$line-height; }
7
-
8
- h1, .h1 { font-size: 30px; font-weight:bold; margin:10px 0; line-height:1em; }
9
- h2, .h2 { font-size:20px; font-weight:bold; margin:8px 0; line-height:1em; }
10
- h3, .h3 { font-size:17px; font-weight:bold; margin:6px 0; line-height:1em; }
11
- h4, .h4 { font-size:14px; font-weight:bold; margin:4px 0; line-height:1em; }
12
- h5, .h5 { font-size:$font-size-large; font-weight:bold; margin:2px 0; line-height:1em; }
13
- h6, .h6 { font-size:$font-size-normal; font-weight:bold; margin:2px 0; line-height:1em; }
14
- h7, .h7 { font-size:$font-size-small; font-weight:bold; margin:2px 0; line-height:1em; }
15
-
16
- p, .p { line-height:$line-height; margin-bottom:10px; }
17
- a { text-decoration:none; color:$link-color;
18
- &:hover { text-decoration:underline; }
19
- }
20
- strong, .strong { font-weight:bold; }
21
- emphasis, .emphasis { font-style:italic; }
22
-
23
-
24
- ul { list-style-type: square; list-style-position:outside; padding:0; margin-left:0; line-height:$line-height;
25
- li { margin-top:5px; margin-left:1em; }
26
- }
27
-
28
- ol { list-style-type:decimal; list-style-position:outside; padding:0; margin-left:0; line-height:$line-height;
29
- li { margin-top:5px; margin-left:1.5em; }
30
- }
31
-
32
- pre { font-family:"Courier New", Courier, monospace, sans-serif; color: #555; line-height: 1.6em; }
33
-
34
- //////////////////////////////////////////////////////////////////////////////////////////////////
35
- // HELPER CLASSES
36
- //////////////////////////////////////////////////////////////////////////////////////////////////
37
-
38
- .hide { display:none !important; }
39
-
40
- input[type='text'], input[type="password"], textarea { @include border-radius(5px); @include box-shadow(inset #dddddd 1px 1px 0); border:1px solid #aaaaaa; padding-top:0; padding-bottom:0; padding-left:6px; font-family:$font-family; font-size:$font-size-normal; height:27px; line-height:27px; }
41
- textarea { padding:4px 0 0 5px; line-height:1.3em; height:auto; }
42
- .form-row { clear:both; margin-bottom:10px; position:relative; }
43
- .form-buttons { }
44
-
45
- label.fLabel { color:#999999; font-family:$font-family; line-height:23px; margin:3px 0 0 8px; }
46
-
47
-
48
- //////////////////////////////////////////////////////////////////////////////////////////////////
49
- // BASE BUTTON STYLES
50
- //////////////////////////////////////////////////////////////////////////////////////////////////
51
- button,input[type=button],input[type=submit],input[type=reset],.button{ position:relative; @include border-radius(3px); @include box-shadow(inset 1px 1px 0 #ffffff); @include user-select(none); background:#e7e7e7; @include ff-gradient(#fafafa, #e7e7e7); border:1px solid #b0b5bb; color:#1e659b; cursor:pointer; cursor:hand; display:inline-block; font-size:12px; padding:3px 8px 3px 8px; line-height:15px; text-align:center; text-decoration:none; text-shadow:1px 1px 0 #ffffff;
52
- &:hover, &.hover{ background:#d7d7d7; @include ff-gradient(#eeeeee, #d7d7d7); text-decoration:none; }
53
- &:active,&.active{ @include box-shadow(inset 1px 1px 1px #c9cacb); text-shadow:none; }
54
- &:disabled,&.disabled{ opacity:0.5; cursor:default; text-shadow:none; pointer-events:none; }
55
- img { vertical-align:middle; }
56
-
57
- /* large */
58
- &.super{ font-size:13px; font-weight:bold; padding:5px 10px 5px 10px; }
59
-
60
- /* large */
61
- &.large{ font-size:12px; font-weight:bold; }
62
-
63
- /* small */
64
- &.small{ font-size:11px; padding:1px 5px 1px 5px; }
65
-
66
- /* toolbar */
67
- &.toolbar { font-size:11px; padding:0 3px 0 3px; background:#e4e4e4; @include ff-gradient(#f7f7f7, #e4e4e4); border:1px solid #a0a0a0; color:#444444; text-shadow:1px 1px 0 #ffffff;
68
- &:hover,&.hover { background:#dbdde0; @include ff-gradient(#f1f2f3, #dbdde0); text-decoration:none; }
69
- }
70
-
71
- /* primary */
72
- &.primary { background:#48739c; @include ff-gradient(#6a93bb, #48739c); border:1px solid #324e69; @include box-shadow(inset 1px 1px 0 #98badc); color:#ffffff; text-shadow:-1px -1px 1px #2d4963;
73
- &:hover,&.hover { background:#3f6890; @include ff-gradient(#5c85ad, #3f6890); text-decoration:none; }
74
- &:disabled,&.disabled{ @include ff-gradient(#e7e7e7, #e7e7e7); border:1px solid #b0b5bb; @include box-shadow(none); text-shadow:none; color:#999999; }
75
- }
76
-
77
- /* secondary */
78
- &.secondary { background:#d5e7f6; @include ff-gradient(#eaf3fb, #c3dcf1); border:1px solid #a3aab0; color:#4777a4; text-shadow:#ffffff 1px 1px 0;
79
- &:hover,&.hover { background:#b9d3e9; @include ff-gradient(#e1ebf5, #b9d3e9); text-decoration:none; }
80
- }
81
- }
82
-
83
1
 
@@ -4,7 +4,7 @@
4
4
  //////////////////////////////////////////////////////////////////////////////////////////////////
5
5
  $link-color: #1861a8;
6
6
  $action-color: #ffb400;
7
- $true-fan-color: #72c14b;
7
+ $true-fan-color: #72c14b;
8
8
  $error-color: #e70000;
9
9
 
10
10
  $font-family: 'Helvetica', Arial, sans-serif;
@@ -21,7 +21,7 @@
21
21
  // BASE STYLES (AS FEW AS POSSIBLE)
22
22
  //////////////////////////////////////////////////////////////////////////////////////////////////
23
23
 
24
- body { overflow:hidden;
24
+ body { overflow:hidden; background: #F4F8FC;
25
25
  .hide { display:none !important; }
26
26
  .dropdown-menu { @include border-radius(4px); border:1px solid #aec7dd; @include box-shadow(1px 1px 3px rgba(0, 0, 0, 0.2), #ffffff 1px 1px 0 inset); background:#f7fafd; }
27
27
  }
@@ -21,7 +21,7 @@
21
21
  // BASE STYLES (AS FEW AS POSSIBLE)
22
22
  //////////////////////////////////////////////////////////////////////////////////////////////////
23
23
 
24
- body { overflow:hidden;
24
+ body { overflow:hidden; background: #F4F8FC;
25
25
  .hide { display:none !important; }
26
26
  .dropdown-menu { @include border-radius(4px); border:1px solid #aec7dd; @include box-shadow(1px 1px 3px rgba(0, 0, 0, 0.2), #ffffff 1px 1px 0 inset); background:#f7fafd; }
27
27
  }
@@ -15,13 +15,44 @@
15
15
  // FRAMEWORK: COMMON FILES
16
16
  //////////////////////////////////////////////////////////////////////////////////////////////////
17
17
  @import "common/mixins";
18
- @import "common/tags";
18
+ @import "common/variables";
19
+
20
+ //////////////////////////////////////////////////////////////////////////////////////////////////
21
+ // COMMON TAGS
22
+ //////////////////////////////////////////////////////////////////////////////////////////////////
23
+
24
+ h1, .h1 { font-size: 30px; font-weight:bold; margin:10px 0; line-height:1em; }
25
+ h2, .h2 { font-size: 20px; font-weight:bold; margin:8px 0; line-height:1em; }
26
+ h3, .h3 { font-size: 17px; font-weight:bold; margin:6px 0; line-height:1em; }
27
+ h4, .h4 { font-size: 14px; font-weight:bold; margin:4px 0; line-height:1em; }
28
+ h5, .h5 { font-size: $font-size-large; font-weight: bold; margin:2px 0; line-height:1em; }
29
+ h6, .h6 { font-size: $font-size-normal; font-weight: bold; margin:2px 0; line-height:1em; }
30
+ h7, .h7 { font-size: $font-size-small; font-weight: bold; margin:2px 0; line-height:1em; }
31
+
32
+ p, .p { line-height:$line-height; margin-bottom:10px; }
33
+ a { text-decoration:none; color:$link-color;
34
+ &:hover { text-decoration:underline; }
35
+ }
36
+ strong, .strong { font-weight:bold; }
37
+ emphasis, .emphasis { font-style:italic; }
38
+
39
+ ul { list-style-type: square; list-style-position:outside; padding:0; margin-left:0; line-height:$line-height;
40
+ li { margin-top:5px; margin-left:1em; }
41
+ }
42
+
43
+ ol { list-style-type:decimal; list-style-position:outside; padding:0; margin-left:0; line-height:$line-height;
44
+ li { margin-top:5px; margin-left:1.5em; }
45
+ }
46
+
47
+ pre { font-family:"Courier New", Courier, monospace, sans-serif; color: #555; line-height: 1.6em; }
48
+
49
+ .hide { display:none !important; }
19
50
 
20
51
  //////////////////////////////////////////////////////////////////////////////////////////////////
21
52
  // FRAMEWORK: MODULE ACTIVATION
22
53
  //////////////////////////////////////////////////////////////////////////////////////////////////
23
54
  html { height:100%; }
24
- body { height: 100%; background-color:#292a2e; @include ff-gradient(#26262a,#2b2d31); text-align:center; margin:0;
55
+ body { height: 100%; background-color:#292a2e; @include ff-gradient(#26262a,#2b2d31); text-align:center; margin:0; font-family:$font-family; font-size:$font-size-normal; line-height:$line-height;
25
56
  $content-width:content-width('engage');
26
57
  @include sticky-footer(40px, ".lyt-root", ".lyt-root-footer", ".lyt-footer");
27
58
 
@@ -18,7 +18,7 @@
18
18
  // BASE STYLES (AS FEW AS POSSIBLE)
19
19
  //////////////////////////////////////////////////////////////////////////////////////////////////
20
20
 
21
- body { overflow:hidden;
21
+ body { overflow:hidden; background: #F4F8FC;
22
22
  .hide { display:none !important; }
23
23
  .dropdown-menu { @include border-radius(4px); border:1px solid #aec7dd; @include box-shadow(1px 1px 3px rgba(0, 0, 0, 0.2), #ffffff 1px 1px 0 inset); background:#f7fafd; }
24
24
  }
@@ -0,0 +1,6 @@
1
+ .chosen-container { font-size: 12px; }
2
+ .chosen-container-single {
3
+ .chosen-default, .chosen-single { line-height: 20px; height:22px;
4
+ div b { background-position: 0 0; }
5
+ }
6
+ }
@@ -430,3 +430,5 @@ $chosen-img-path: '/assets/vendors/chosen';
430
430
  }
431
431
  }
432
432
  /* @end */
433
+
434
+ @import 'chosen_override';
@@ -1,4 +1,4 @@
1
- // Knockout JavaScript library v2.3.0
1
+ // Knockout JavaScript library v3.1.0
2
2
  // (c) Steven Sanderson - http://knockoutjs.com/
3
3
  // License: MIT (http://www.opensource.org/licenses/mit-license.php)
4
4
 
@@ -44,17 +44,35 @@
44
44
  ko.exportProperty = function(owner, publicName, object) {
45
45
  owner[publicName] = object;
46
46
  };
47
- ko.version = "2.3.0";
47
+ ko.version = "3.1.0";
48
48
 
49
49
  ko.exportSymbol('version', ko.version);
50
50
  ko.utils = (function () {
51
- var objectForEach = function(obj, action) {
51
+ function objectForEach(obj, action) {
52
52
  for (var prop in obj) {
53
53
  if (obj.hasOwnProperty(prop)) {
54
54
  action(prop, obj[prop]);
55
55
  }
56
56
  }
57
- };
57
+ }
58
+
59
+ function extend(target, source) {
60
+ if (source) {
61
+ for(var prop in source) {
62
+ if(source.hasOwnProperty(prop)) {
63
+ target[prop] = source[prop];
64
+ }
65
+ }
66
+ }
67
+ return target;
68
+ }
69
+
70
+ function setPrototypeOf(obj, proto) {
71
+ obj.__proto__ = proto;
72
+ return obj;
73
+ }
74
+
75
+ var canSetPrototype = ({ __proto__: [] } instanceof Array);
58
76
 
59
77
  // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
60
78
  var knownEvents = {}, knownEventTypesByEventName = {};
@@ -98,7 +116,7 @@
98
116
 
99
117
  arrayForEach: function (array, action) {
100
118
  for (var i = 0, j = array.length; i < j; i++)
101
- action(array[i]);
119
+ action(array[i], i);
102
120
  },
103
121
 
104
122
  arrayIndexOf: function (array, item) {
@@ -112,15 +130,19 @@
112
130
 
113
131
  arrayFirst: function (array, predicate, predicateOwner) {
114
132
  for (var i = 0, j = array.length; i < j; i++)
115
- if (predicate.call(predicateOwner, array[i]))
133
+ if (predicate.call(predicateOwner, array[i], i))
116
134
  return array[i];
117
135
  return null;
118
136
  },
119
137
 
120
138
  arrayRemoveItem: function (array, itemToRemove) {
121
139
  var index = ko.utils.arrayIndexOf(array, itemToRemove);
122
- if (index >= 0)
140
+ if (index > 0) {
123
141
  array.splice(index, 1);
142
+ }
143
+ else if (index === 0) {
144
+ array.shift();
145
+ }
124
146
  },
125
147
 
126
148
  arrayGetDistinctValues: function (array) {
@@ -137,7 +159,7 @@
137
159
  array = array || [];
138
160
  var result = [];
139
161
  for (var i = 0, j = array.length; i < j; i++)
140
- result.push(mapping(array[i]));
162
+ result.push(mapping(array[i], i));
141
163
  return result;
142
164
  },
143
165
 
@@ -145,7 +167,7 @@
145
167
  array = array || [];
146
168
  var result = [];
147
169
  for (var i = 0, j = array.length; i < j; i++)
148
- if (predicate(array[i]))
170
+ if (predicate(array[i], i))
149
171
  result.push(array[i]);
150
172
  return result;
151
173
  },
@@ -160,7 +182,7 @@
160
182
  },
161
183
 
162
184
  addOrRemoveItem: function(array, value, included) {
163
- var existingEntryIndex = array.indexOf ? array.indexOf(value) : ko.utils.arrayIndexOf(array, value);
185
+ var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.peekObservable(array), value);
164
186
  if (existingEntryIndex < 0) {
165
187
  if (included)
166
188
  array.push(value);
@@ -170,19 +192,28 @@
170
192
  }
171
193
  },
172
194
 
173
- extend: function (target, source) {
174
- if (source) {
175
- for(var prop in source) {
176
- if(source.hasOwnProperty(prop)) {
177
- target[prop] = source[prop];
178
- }
195
+ canSetPrototype: canSetPrototype,
196
+
197
+ extend: extend,
198
+
199
+ setPrototypeOf: setPrototypeOf,
200
+
201
+ setPrototypeOfOrExtend: canSetPrototype ? setPrototypeOf : extend,
202
+
203
+ objectForEach: objectForEach,
204
+
205
+ objectMap: function(source, mapping) {
206
+ if (!source)
207
+ return source;
208
+ var target = {};
209
+ for (var prop in source) {
210
+ if (source.hasOwnProperty(prop)) {
211
+ target[prop] = mapping(source[prop], prop, source);
179
212
  }
180
213
  }
181
214
  return target;
182
215
  },
183
216
 
184
- objectForEach: objectForEach,
185
-
186
217
  emptyDomNode: function (domNode) {
187
218
  while (domNode.firstChild) {
188
219
  ko.removeNode(domNode.firstChild);
@@ -230,6 +261,45 @@
230
261
  }
231
262
  },
232
263
 
264
+ fixUpContinuousNodeArray: function(continuousNodeArray, parentNode) {
265
+ // Before acting on a set of nodes that were previously outputted by a template function, we have to reconcile
266
+ // them against what is in the DOM right now. It may be that some of the nodes have already been removed, or that
267
+ // new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
268
+ // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
269
+ // So, this function translates the old "map" output array into its best guess of the set of current DOM nodes.
270
+ //
271
+ // Rules:
272
+ // [A] Any leading nodes that have been removed should be ignored
273
+ // These most likely correspond to memoization nodes that were already removed during binding
274
+ // See https://github.com/SteveSanderson/knockout/pull/440
275
+ // [B] We want to output a continuous series of nodes. So, ignore any nodes that have already been removed,
276
+ // and include any nodes that have been inserted among the previous collection
277
+
278
+ if (continuousNodeArray.length) {
279
+ // The parent node can be a virtual element; so get the real parent node
280
+ parentNode = (parentNode.nodeType === 8 && parentNode.parentNode) || parentNode;
281
+
282
+ // Rule [A]
283
+ while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode)
284
+ continuousNodeArray.shift();
285
+
286
+ // Rule [B]
287
+ if (continuousNodeArray.length > 1) {
288
+ var current = continuousNodeArray[0], last = continuousNodeArray[continuousNodeArray.length - 1];
289
+ // Replace with the actual new continuous node set
290
+ continuousNodeArray.length = 0;
291
+ while (current !== last) {
292
+ continuousNodeArray.push(current);
293
+ current = current.nextSibling;
294
+ if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
295
+ return;
296
+ }
297
+ continuousNodeArray.push(last);
298
+ }
299
+ }
300
+ return continuousNodeArray;
301
+ },
302
+
233
303
  setOptionNodeSelectionState: function (optionNode, isSelected) {
234
304
  // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
235
305
  if (ieVersion < 7)
@@ -264,18 +334,22 @@
264
334
  },
265
335
 
266
336
  domNodeIsContainedBy: function (node, containedByNode) {
337
+ if (node === containedByNode)
338
+ return true;
339
+ if (node.nodeType === 11)
340
+ return false; // Fixes issue #1162 - can't use node.contains for document fragments on IE8
341
+ if (containedByNode.contains)
342
+ return containedByNode.contains(node.nodeType === 3 ? node.parentNode : node);
267
343
  if (containedByNode.compareDocumentPosition)
268
344
  return (containedByNode.compareDocumentPosition(node) & 16) == 16;
269
- while (node != null) {
270
- if (node == containedByNode)
271
- return true;
345
+ while (node && node != containedByNode) {
272
346
  node = node.parentNode;
273
347
  }
274
- return false;
348
+ return !!node;
275
349
  },
276
350
 
277
351
  domNodeIsAttachedToDocument: function (node) {
278
- return ko.utils.domNodeIsContainedBy(node, node.ownerDocument);
352
+ return ko.utils.domNodeIsContainedBy(node, node.ownerDocument.documentElement);
279
353
  },
280
354
 
281
355
  anyDomNodeIsAttachedToDocument: function(nodes) {
@@ -291,21 +365,7 @@
291
365
 
292
366
  registerEventHandler: function (element, eventType, handler) {
293
367
  var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
294
- if (!mustUseAttachEvent && typeof jQuery != "undefined") {
295
- if (isClickOnCheckableElement(element, eventType)) {
296
- // For click events on checkboxes, jQuery interferes with the event handling in an awkward way:
297
- // it toggles the element checked state *after* the click event handlers run, whereas native
298
- // click events toggle the checked state *before* the event handler.
299
- // Fix this by intecepting the handler and applying the correct checkedness before it runs.
300
- var originalHandler = handler;
301
- handler = function(event, eventData) {
302
- var jQuerySuppliedCheckedState = this.checked;
303
- if (eventData)
304
- this.checked = eventData.checkedStateBeforeEvent !== true;
305
- originalHandler.call(this, event);
306
- this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied
307
- };
308
- }
368
+ if (!mustUseAttachEvent && jQuery) {
309
369
  jQuery(element)['bind'](eventType, handler);
310
370
  } else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
311
371
  element.addEventListener(eventType, handler, false);
@@ -327,13 +387,14 @@
327
387
  if (!(element && element.nodeType))
328
388
  throw new Error("element must be a DOM node when calling triggerEvent");
329
389
 
330
- if (typeof jQuery != "undefined") {
331
- var eventData = [];
332
- if (isClickOnCheckableElement(element, eventType)) {
333
- // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler
334
- eventData.push({ checkedStateBeforeEvent: element.checked });
335
- }
336
- jQuery(element)['trigger'](eventType, eventData);
390
+ // For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the
391
+ // event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.)
392
+ // IE doesn't change the checked state when you trigger the click event using "fireEvent".
393
+ // In both cases, we'll use the click method instead.
394
+ var useClickWorkaround = isClickOnCheckableElement(element, eventType);
395
+
396
+ if (jQuery && !useClickWorkaround) {
397
+ jQuery(element)['trigger'](eventType);
337
398
  } else if (typeof document.createEvent == "function") {
338
399
  if (typeof element.dispatchEvent == "function") {
339
400
  var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
@@ -343,15 +404,13 @@
343
404
  }
344
405
  else
345
406
  throw new Error("The supplied element doesn't support dispatchEvent");
407
+ } else if (useClickWorkaround && element.click) {
408
+ element.click();
346
409
  } else if (typeof element.fireEvent != "undefined") {
347
- // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
348
- // so to make it consistent, we'll do it manually here
349
- if (isClickOnCheckableElement(element, eventType))
350
- element.checked = element.checked !== true;
351
410
  element.fireEvent("on" + eventType);
352
- }
353
- else
411
+ } else {
354
412
  throw new Error("Browser doesn't support triggering events");
413
+ }
355
414
  },
356
415
 
357
416
  unwrapObservable: function (value) {
@@ -383,7 +442,7 @@
383
442
  // we'll clear everything and create a single text node.
384
443
  var innerTextNode = ko.virtualElements.firstChild(element);
385
444
  if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
386
- ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
445
+ ko.virtualElements.setDomNodeChildren(element, [element.ownerDocument.createTextNode(value)]);
387
446
  } else {
388
447
  innerTextNode.data = value;
389
448
  }
@@ -560,31 +619,33 @@
560
619
  var uniqueId = 0;
561
620
  var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
562
621
  var dataStore = {};
622
+
623
+ function getAll(node, createIfNotFound) {
624
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
625
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
626
+ if (!hasExistingDataStore) {
627
+ if (!createIfNotFound)
628
+ return undefined;
629
+ dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
630
+ dataStore[dataStoreKey] = {};
631
+ }
632
+ return dataStore[dataStoreKey];
633
+ }
634
+
563
635
  return {
564
636
  get: function (node, key) {
565
- var allDataForNode = ko.utils.domData.getAll(node, false);
637
+ var allDataForNode = getAll(node, false);
566
638
  return allDataForNode === undefined ? undefined : allDataForNode[key];
567
639
  },
568
640
  set: function (node, key, value) {
569
641
  if (value === undefined) {
570
642
  // Make sure we don't actually create a new domData key if we are actually deleting a value
571
- if (ko.utils.domData.getAll(node, false) === undefined)
643
+ if (getAll(node, false) === undefined)
572
644
  return;
573
645
  }
574
- var allDataForNode = ko.utils.domData.getAll(node, true);
646
+ var allDataForNode = getAll(node, true);
575
647
  allDataForNode[key] = value;
576
648
  },
577
- getAll: function (node, createIfNotFound) {
578
- var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
579
- var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
580
- if (!hasExistingDataStore) {
581
- if (!createIfNotFound)
582
- return undefined;
583
- dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
584
- dataStore[dataStoreKey] = {};
585
- }
586
- return dataStore[dataStoreKey];
587
- },
588
649
  clear: function (node) {
589
650
  var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
590
651
  if (dataStoreKey) {
@@ -593,15 +654,19 @@
593
654
  return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
594
655
  }
595
656
  return false;
657
+ },
658
+
659
+ nextKey: function () {
660
+ return (uniqueId++) + dataStoreKeyExpandoPropertyName;
596
661
  }
597
- }
662
+ };
598
663
  })();
599
664
 
600
665
  ko.exportSymbol('utils.domData', ko.utils.domData);
601
666
  ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully
602
667
 
603
668
  ko.utils.domNodeDisposal = new (function () {
604
- var domDataKey = "__ko_domNodeDisposal__" + (new Date).getTime();
669
+ var domDataKey = ko.utils.domData.nextKey();
605
670
  var cleanableNodeTypes = { 1: true, 8: true, 9: true }; // Element, Comment, Document
606
671
  var cleanableNodeTypesWithDescendants = { 1: true, 9: true }; // Element, Document
607
672
 
@@ -626,16 +691,13 @@
626
691
  callbacks[i](node);
627
692
  }
628
693
 
629
- // Also erase the DOM data
694
+ // Erase the DOM data
630
695
  ko.utils.domData.clear(node);
631
696
 
632
- // Special support for jQuery here because it's so commonly used.
633
- // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
634
- // so notify it to tear down any resources associated with the node & descendants here.
635
- if ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function"))
636
- jQuery['cleanData']([node]);
697
+ // Perform cleanup needed by external libraries (currently only jQuery, but can be extended)
698
+ ko.utils.domNodeDisposal["cleanExternalData"](node);
637
699
 
638
- // Also clear any immediate-child comment nodes, as these wouldn't have been found by
700
+ // Clear any immediate-child comment nodes, as these wouldn't have been found by
639
701
  // node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements)
640
702
  if (cleanableNodeTypesWithDescendants[node.nodeType])
641
703
  cleanImmediateCommentTypeChildren(node);
@@ -687,6 +749,14 @@
687
749
  ko.cleanNode(node);
688
750
  if (node.parentNode)
689
751
  node.parentNode.removeChild(node);
752
+ },
753
+
754
+ "cleanExternalData" : function (node) {
755
+ // Special support for jQuery here because it's so commonly used.
756
+ // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
757
+ // so notify it to tear down any resources associated with the node & descendants here.
758
+ if (jQuery && (typeof jQuery['cleanData'] == "function"))
759
+ jQuery['cleanData']([node]);
690
760
  }
691
761
  }
692
762
  })();
@@ -760,7 +830,7 @@
760
830
  }
761
831
 
762
832
  ko.utils.parseHtmlFragment = function(html) {
763
- return typeof jQuery != 'undefined' ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
833
+ return jQuery ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
764
834
  : simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases.
765
835
  };
766
836
 
@@ -777,7 +847,7 @@
777
847
  // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
778
848
  // for example <tr> elements which are not normally allowed to exist on their own.
779
849
  // If you've referenced jQuery we'll use that rather than duplicating its code.
780
- if (typeof jQuery != 'undefined') {
850
+ if (jQuery) {
781
851
  jQuery(node)['html'](html);
782
852
  } else {
783
853
  // ... otherwise, use KO's own parsing logic.
@@ -883,21 +953,62 @@
883
953
  });
884
954
  },
885
955
 
956
+ 'rateLimit': function(target, options) {
957
+ var timeout, method, limitFunction;
958
+
959
+ if (typeof options == 'number') {
960
+ timeout = options;
961
+ } else {
962
+ timeout = options['timeout'];
963
+ method = options['method'];
964
+ }
965
+
966
+ limitFunction = method == 'notifyWhenChangesStop' ? debounce : throttle;
967
+ target.limit(function(callback) {
968
+ return limitFunction(callback, timeout);
969
+ });
970
+ },
971
+
886
972
  'notify': function(target, notifyWhen) {
887
- target["equalityComparer"] = notifyWhen == "always"
888
- ? function() { return false } // Treat all values as not equal
889
- : ko.observable["fn"]["equalityComparer"];
890
- return target;
973
+ target["equalityComparer"] = notifyWhen == "always" ?
974
+ null : // null equalityComparer means to always notify
975
+ valuesArePrimitiveAndEqual;
891
976
  }
892
977
  };
893
978
 
979
+ var primitiveTypes = { 'undefined':1, 'boolean':1, 'number':1, 'string':1 };
980
+ function valuesArePrimitiveAndEqual(a, b) {
981
+ var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
982
+ return oldValueIsPrimitive ? (a === b) : false;
983
+ }
984
+
985
+ function throttle(callback, timeout) {
986
+ var timeoutInstance;
987
+ return function () {
988
+ if (!timeoutInstance) {
989
+ timeoutInstance = setTimeout(function() {
990
+ timeoutInstance = undefined;
991
+ callback();
992
+ }, timeout);
993
+ }
994
+ };
995
+ }
996
+
997
+ function debounce(callback, timeout) {
998
+ var timeoutInstance;
999
+ return function () {
1000
+ clearTimeout(timeoutInstance);
1001
+ timeoutInstance = setTimeout(callback, timeout);
1002
+ };
1003
+ }
1004
+
894
1005
  function applyExtenders(requestedExtenders) {
895
1006
  var target = this;
896
1007
  if (requestedExtenders) {
897
1008
  ko.utils.objectForEach(requestedExtenders, function(key, value) {
898
1009
  var extenderHandler = ko.extenders[key];
899
1010
  if (typeof extenderHandler == 'function') {
900
- target = extenderHandler(target, value);
1011
+ target = extenderHandler(target, value) || target;
901
1012
  }
902
1013
  });
903
1014
  }
@@ -910,6 +1021,7 @@
910
1021
  this.target = target;
911
1022
  this.callback = callback;
912
1023
  this.disposeCallback = disposeCallback;
1024
+ this.isDisposed = false;
913
1025
  ko.exportProperty(this, 'dispose', this.dispose);
914
1026
  };
915
1027
  ko.subscription.prototype.dispose = function () {
@@ -918,45 +1030,98 @@
918
1030
  };
919
1031
 
920
1032
  ko.subscribable = function () {
1033
+ ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']);
921
1034
  this._subscriptions = {};
922
-
923
- ko.utils.extend(this, ko.subscribable['fn']);
924
- ko.exportProperty(this, 'subscribe', this.subscribe);
925
- ko.exportProperty(this, 'extend', this.extend);
926
- ko.exportProperty(this, 'getSubscriptionsCount', this.getSubscriptionsCount);
927
1035
  }
928
1036
 
929
1037
  var defaultEvent = "change";
930
1038
 
931
- ko.subscribable['fn'] = {
1039
+ var ko_subscribable_fn = {
932
1040
  subscribe: function (callback, callbackTarget, event) {
1041
+ var self = this;
1042
+
933
1043
  event = event || defaultEvent;
934
1044
  var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
935
1045
 
936
- var subscription = new ko.subscription(this, boundCallback, function () {
937
- ko.utils.arrayRemoveItem(this._subscriptions[event], subscription);
938
- }.bind(this));
1046
+ var subscription = new ko.subscription(self, boundCallback, function () {
1047
+ ko.utils.arrayRemoveItem(self._subscriptions[event], subscription);
1048
+ });
1049
+
1050
+ // This will force a computed with deferEvaluation to evaluate before any subscriptions
1051
+ // are registered.
1052
+ if (self.peek) {
1053
+ self.peek();
1054
+ }
939
1055
 
940
- if (!this._subscriptions[event])
941
- this._subscriptions[event] = [];
942
- this._subscriptions[event].push(subscription);
1056
+ if (!self._subscriptions[event])
1057
+ self._subscriptions[event] = [];
1058
+ self._subscriptions[event].push(subscription);
943
1059
  return subscription;
944
1060
  },
945
1061
 
946
1062
  "notifySubscribers": function (valueToNotify, event) {
947
1063
  event = event || defaultEvent;
948
- if (this._subscriptions[event]) {
949
- ko.dependencyDetection.ignore(function() {
950
- ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
1064
+ if (this.hasSubscriptionsForEvent(event)) {
1065
+ try {
1066
+ ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined)
1067
+ for (var a = this._subscriptions[event].slice(0), i = 0, subscription; subscription = a[i]; ++i) {
951
1068
  // In case a subscription was disposed during the arrayForEach cycle, check
952
1069
  // for isDisposed on each subscription before invoking its callback
953
- if (subscription && (subscription.isDisposed !== true))
1070
+ if (!subscription.isDisposed)
954
1071
  subscription.callback(valueToNotify);
955
- });
956
- }, this);
1072
+ }
1073
+ } finally {
1074
+ ko.dependencyDetection.end(); // End suppressing dependency detection
1075
+ }
957
1076
  }
958
1077
  },
959
1078
 
1079
+ limit: function(limitFunction) {
1080
+ var self = this, selfIsObservable = ko.isObservable(self),
1081
+ isPending, previousValue, pendingValue, beforeChange = 'beforeChange';
1082
+
1083
+ if (!self._origNotifySubscribers) {
1084
+ self._origNotifySubscribers = self["notifySubscribers"];
1085
+ self["notifySubscribers"] = function(value, event) {
1086
+ if (!event || event === defaultEvent) {
1087
+ self._rateLimitedChange(value);
1088
+ } else if (event === beforeChange) {
1089
+ self._rateLimitedBeforeChange(value);
1090
+ } else {
1091
+ self._origNotifySubscribers(value, event);
1092
+ }
1093
+ };
1094
+ }
1095
+
1096
+ var finish = limitFunction(function() {
1097
+ // If an observable provided a reference to itself, access it to get the latest value.
1098
+ // This allows computed observables to delay calculating their value until needed.
1099
+ if (selfIsObservable && pendingValue === self) {
1100
+ pendingValue = self();
1101
+ }
1102
+ isPending = false;
1103
+ if (self.isDifferent(previousValue, pendingValue)) {
1104
+ self._origNotifySubscribers(previousValue = pendingValue);
1105
+ }
1106
+ });
1107
+
1108
+ self._rateLimitedChange = function(value) {
1109
+ isPending = true;
1110
+ pendingValue = value;
1111
+ finish();
1112
+ };
1113
+ self._rateLimitedBeforeChange = function(value) {
1114
+ if (!isPending) {
1115
+ previousValue = value;
1116
+ self._origNotifySubscribers(value, beforeChange);
1117
+ }
1118
+ };
1119
+ },
1120
+
1121
+ hasSubscriptionsForEvent: function(event) {
1122
+ return this._subscriptions[event] && this._subscriptions[event].length;
1123
+ },
1124
+
960
1125
  getSubscriptionsCount: function () {
961
1126
  var total = 0;
962
1127
  ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) {
@@ -965,9 +1130,26 @@
965
1130
  return total;
966
1131
  },
967
1132
 
1133
+ isDifferent: function(oldValue, newValue) {
1134
+ return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue);
1135
+ },
1136
+
968
1137
  extend: applyExtenders
969
1138
  };
970
1139
 
1140
+ ko.exportProperty(ko_subscribable_fn, 'subscribe', ko_subscribable_fn.subscribe);
1141
+ ko.exportProperty(ko_subscribable_fn, 'extend', ko_subscribable_fn.extend);
1142
+ ko.exportProperty(ko_subscribable_fn, 'getSubscriptionsCount', ko_subscribable_fn.getSubscriptionsCount);
1143
+
1144
+ // For browsers that support proto assignment, we overwrite the prototype of each
1145
+ // observable instance. Since observables are functions, we need Function.prototype
1146
+ // to still be in the prototype chain.
1147
+ if (ko.utils.canSetPrototype) {
1148
+ ko.utils.setPrototypeOf(ko_subscribable_fn, Function.prototype);
1149
+ }
1150
+
1151
+ ko.subscribable['fn'] = ko_subscribable_fn;
1152
+
971
1153
 
972
1154
  ko.isSubscribable = function (instance) {
973
1155
  return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
@@ -976,42 +1158,67 @@
976
1158
  ko.exportSymbol('subscribable', ko.subscribable);
977
1159
  ko.exportSymbol('isSubscribable', ko.isSubscribable);
978
1160
 
979
- ko.dependencyDetection = (function () {
980
- var _frames = [];
1161
+ ko.computedContext = ko.dependencyDetection = (function () {
1162
+ var outerFrames = [],
1163
+ currentFrame,
1164
+ lastId = 0;
1165
+
1166
+ // Return a unique ID that can be assigned to an observable for dependency tracking.
1167
+ // Theoretically, you could eventually overflow the number storage size, resulting
1168
+ // in duplicate IDs. But in JavaScript, the largest exact integral value is 2^53
1169
+ // or 9,007,199,254,740,992. If you created 1,000,000 IDs per second, it would
1170
+ // take over 285 years to reach that number.
1171
+ // Reference http://blog.vjeux.com/2010/javascript/javascript-max_int-number-limits.html
1172
+ function getId() {
1173
+ return ++lastId;
1174
+ }
1175
+
1176
+ function begin(options) {
1177
+ outerFrames.push(currentFrame);
1178
+ currentFrame = options;
1179
+ }
1180
+
1181
+ function end() {
1182
+ currentFrame = outerFrames.pop();
1183
+ }
981
1184
 
982
1185
  return {
983
- begin: function (callback) {
984
- _frames.push({ callback: callback, distinctDependencies:[] });
985
- },
1186
+ begin: begin,
986
1187
 
987
- end: function () {
988
- _frames.pop();
989
- },
1188
+ end: end,
990
1189
 
991
1190
  registerDependency: function (subscribable) {
992
- if (!ko.isSubscribable(subscribable))
993
- throw new Error("Only subscribable things can act as dependencies");
994
- if (_frames.length > 0) {
995
- var topFrame = _frames[_frames.length - 1];
996
- if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
997
- return;
998
- topFrame.distinctDependencies.push(subscribable);
999
- topFrame.callback(subscribable);
1191
+ if (currentFrame) {
1192
+ if (!ko.isSubscribable(subscribable))
1193
+ throw new Error("Only subscribable things can act as dependencies");
1194
+ currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId()));
1000
1195
  }
1001
1196
  },
1002
1197
 
1003
- ignore: function(callback, callbackTarget, callbackArgs) {
1198
+ ignore: function (callback, callbackTarget, callbackArgs) {
1004
1199
  try {
1005
- _frames.push(null);
1200
+ begin();
1006
1201
  return callback.apply(callbackTarget, callbackArgs || []);
1007
1202
  } finally {
1008
- _frames.pop();
1203
+ end();
1009
1204
  }
1205
+ },
1206
+
1207
+ getDependenciesCount: function () {
1208
+ if (currentFrame)
1209
+ return currentFrame.computed.getDependenciesCount();
1210
+ },
1211
+
1212
+ isInitial: function() {
1213
+ if (currentFrame)
1214
+ return currentFrame.isInitial;
1010
1215
  }
1011
1216
  };
1012
1217
  })();
1013
- var primitiveTypes = { 'undefined':true, 'boolean':true, 'number':true, 'string':true };
1014
1218
 
1219
+ ko.exportSymbol('computedContext', ko.computedContext);
1220
+ ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount);
1221
+ ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial);
1015
1222
  ko.observable = function (initialValue) {
1016
1223
  var _latestValue = initialValue;
1017
1224
 
@@ -1020,7 +1227,7 @@
1020
1227
  // Write
1021
1228
 
1022
1229
  // Ignore writes if the value hasn't changed
1023
- if ((!observable['equalityComparer']) || !observable['equalityComparer'](_latestValue, arguments[0])) {
1230
+ if (observable.isDifferent(_latestValue, arguments[0])) {
1024
1231
  observable.valueWillMutate();
1025
1232
  _latestValue = arguments[0];
1026
1233
  if (DEBUG) observable._latestValue = _latestValue;
@@ -1034,12 +1241,13 @@
1034
1241
  return _latestValue;
1035
1242
  }
1036
1243
  }
1037
- if (DEBUG) observable._latestValue = _latestValue;
1038
1244
  ko.subscribable.call(observable);
1245
+ ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']);
1246
+
1247
+ if (DEBUG) observable._latestValue = _latestValue;
1039
1248
  observable.peek = function() { return _latestValue };
1040
1249
  observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
1041
1250
  observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
1042
- ko.utils.extend(observable, ko.observable['fn']);
1043
1251
 
1044
1252
  ko.exportProperty(observable, 'peek', observable.peek);
1045
1253
  ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
@@ -1049,15 +1257,18 @@
1049
1257
  }
1050
1258
 
1051
1259
  ko.observable['fn'] = {
1052
- "equalityComparer": function valuesArePrimitiveAndEqual(a, b) {
1053
- var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
1054
- return oldValueIsPrimitive ? (a === b) : false;
1055
- }
1260
+ "equalityComparer": valuesArePrimitiveAndEqual
1056
1261
  };
1057
1262
 
1058
1263
  var protoProperty = ko.observable.protoProperty = "__ko_proto__";
1059
1264
  ko.observable['fn'][protoProperty] = ko.observable;
1060
1265
 
1266
+ // Note that for browsers that don't support proto assignment, the
1267
+ // inheritance chain is created manually in the ko.observable constructor
1268
+ if (ko.utils.canSetPrototype) {
1269
+ ko.utils.setPrototypeOf(ko.observable['fn'], ko.subscribable['fn']);
1270
+ }
1271
+
1061
1272
  ko.hasPrototype = function(instance, prototype) {
1062
1273
  if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
1063
1274
  if (instance[protoProperty] === prototype) return true;
@@ -1089,15 +1300,15 @@
1089
1300
  throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
1090
1301
 
1091
1302
  var result = ko.observable(initialValues);
1092
- ko.utils.extend(result, ko.observableArray['fn']);
1093
- return result;
1303
+ ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
1304
+ return result.extend({'trackArrayChanges':true});
1094
1305
  };
1095
1306
 
1096
1307
  ko.observableArray['fn'] = {
1097
1308
  'remove': function (valueOrPredicate) {
1098
1309
  var underlyingArray = this.peek();
1099
1310
  var removedValues = [];
1100
- var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1311
+ var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1101
1312
  for (var i = 0; i < underlyingArray.length; i++) {
1102
1313
  var value = underlyingArray[i];
1103
1314
  if (predicate(value)) {
@@ -1135,7 +1346,7 @@
1135
1346
 
1136
1347
  'destroy': function (valueOrPredicate) {
1137
1348
  var underlyingArray = this.peek();
1138
- var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1349
+ var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1139
1350
  this.valueWillMutate();
1140
1351
  for (var i = underlyingArray.length - 1; i >= 0; i--) {
1141
1352
  var value = underlyingArray[i];
@@ -1182,6 +1393,7 @@
1182
1393
  // (for consistency with mutating regular observables)
1183
1394
  var underlyingArray = this.peek();
1184
1395
  this.valueWillMutate();
1396
+ this.cacheDiffForKnownOperation(underlyingArray, methodName, arguments);
1185
1397
  var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
1186
1398
  this.valueHasMutated();
1187
1399
  return methodCallResult;
@@ -1196,11 +1408,144 @@
1196
1408
  };
1197
1409
  });
1198
1410
 
1411
+ // Note that for browsers that don't support proto assignment, the
1412
+ // inheritance chain is created manually in the ko.observableArray constructor
1413
+ if (ko.utils.canSetPrototype) {
1414
+ ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']);
1415
+ }
1416
+
1199
1417
  ko.exportSymbol('observableArray', ko.observableArray);
1200
- ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
1418
+ var arrayChangeEventName = 'arrayChange';
1419
+ ko.extenders['trackArrayChanges'] = function(target) {
1420
+ // Only modify the target observable once
1421
+ if (target.cacheDiffForKnownOperation) {
1422
+ return;
1423
+ }
1424
+ var trackingChanges = false,
1425
+ cachedDiff = null,
1426
+ pendingNotifications = 0,
1427
+ underlyingSubscribeFunction = target.subscribe;
1428
+
1429
+ // Intercept "subscribe" calls, and for array change events, ensure change tracking is enabled
1430
+ target.subscribe = target['subscribe'] = function(callback, callbackTarget, event) {
1431
+ if (event === arrayChangeEventName) {
1432
+ trackChanges();
1433
+ }
1434
+ return underlyingSubscribeFunction.apply(this, arguments);
1435
+ };
1436
+
1437
+ function trackChanges() {
1438
+ // Calling 'trackChanges' multiple times is the same as calling it once
1439
+ if (trackingChanges) {
1440
+ return;
1441
+ }
1442
+
1443
+ trackingChanges = true;
1444
+
1445
+ // Intercept "notifySubscribers" to track how many times it was called.
1446
+ var underlyingNotifySubscribersFunction = target['notifySubscribers'];
1447
+ target['notifySubscribers'] = function(valueToNotify, event) {
1448
+ if (!event || event === defaultEvent) {
1449
+ ++pendingNotifications;
1450
+ }
1451
+ return underlyingNotifySubscribersFunction.apply(this, arguments);
1452
+ };
1453
+
1454
+ // Each time the array changes value, capture a clone so that on the next
1455
+ // change it's possible to produce a diff
1456
+ var previousContents = [].concat(target.peek() || []);
1457
+ cachedDiff = null;
1458
+ target.subscribe(function(currentContents) {
1459
+ // Make a copy of the current contents and ensure it's an array
1460
+ currentContents = [].concat(currentContents || []);
1461
+
1462
+ // Compute the diff and issue notifications, but only if someone is listening
1463
+ if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
1464
+ var changes = getChanges(previousContents, currentContents);
1465
+ if (changes.length) {
1466
+ target['notifySubscribers'](changes, arrayChangeEventName);
1467
+ }
1468
+ }
1469
+
1470
+ // Eliminate references to the old, removed items, so they can be GCed
1471
+ previousContents = currentContents;
1472
+ cachedDiff = null;
1473
+ pendingNotifications = 0;
1474
+ });
1475
+ }
1476
+
1477
+ function getChanges(previousContents, currentContents) {
1478
+ // We try to re-use cached diffs.
1479
+ // The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
1480
+ // plugin, which without this check would not be compatible with arrayChange notifications. Normally,
1481
+ // notifications are issued immediately so we wouldn't be queueing up more than one.
1482
+ if (!cachedDiff || pendingNotifications > 1) {
1483
+ cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true });
1484
+ }
1485
+
1486
+ return cachedDiff;
1487
+ }
1488
+
1489
+ target.cacheDiffForKnownOperation = function(rawArray, operationName, args) {
1490
+ // Only run if we're currently tracking changes for this observable array
1491
+ // and there aren't any pending deferred notifications.
1492
+ if (!trackingChanges || pendingNotifications) {
1493
+ return;
1494
+ }
1495
+ var diff = [],
1496
+ arrayLength = rawArray.length,
1497
+ argsLength = args.length,
1498
+ offset = 0;
1499
+
1500
+ function pushDiff(status, value, index) {
1501
+ return diff[diff.length] = { 'status': status, 'value': value, 'index': index };
1502
+ }
1503
+ switch (operationName) {
1504
+ case 'push':
1505
+ offset = arrayLength;
1506
+ case 'unshift':
1507
+ for (var index = 0; index < argsLength; index++) {
1508
+ pushDiff('added', args[index], offset + index);
1509
+ }
1510
+ break;
1511
+
1512
+ case 'pop':
1513
+ offset = arrayLength - 1;
1514
+ case 'shift':
1515
+ if (arrayLength) {
1516
+ pushDiff('deleted', rawArray[offset], offset);
1517
+ }
1518
+ break;
1519
+
1520
+ case 'splice':
1521
+ // Negative start index means 'from end of array'. After that we clamp to [0...arrayLength].
1522
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
1523
+ var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
1524
+ endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
1525
+ endAddIndex = startIndex + argsLength - 2,
1526
+ endIndex = Math.max(endDeleteIndex, endAddIndex),
1527
+ additions = [], deletions = [];
1528
+ for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
1529
+ if (index < endDeleteIndex)
1530
+ deletions.push(pushDiff('deleted', rawArray[index], index));
1531
+ if (index < endAddIndex)
1532
+ additions.push(pushDiff('added', args[argsIndex], index));
1533
+ }
1534
+ ko.utils.findMovesInArrayComparison(deletions, additions);
1535
+ break;
1536
+
1537
+ default:
1538
+ return;
1539
+ }
1540
+ cachedDiff = diff;
1541
+ };
1542
+ };
1543
+ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
1201
1544
  var _latestValue,
1202
- _hasBeenEvaluated = false,
1545
+ _needsEvaluation = true,
1203
1546
  _isBeingEvaluated = false,
1547
+ _suppressDisposalUntilDisposeWhenReturnsFalse = false,
1548
+ _isDisposed = false,
1204
1549
  readFunction = evaluatorFunctionOrOptions;
1205
1550
 
1206
1551
  if (readFunction && typeof readFunction == "object") {
@@ -1216,15 +1561,21 @@
1216
1561
  if (typeof readFunction != "function")
1217
1562
  throw new Error("Pass a function that returns the value of the ko.computed");
1218
1563
 
1219
- function addSubscriptionToDependency(subscribable) {
1220
- _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
1564
+ function addSubscriptionToDependency(subscribable, id) {
1565
+ if (!_subscriptionsToDependencies[id]) {
1566
+ _subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync);
1567
+ ++_dependenciesCount;
1568
+ }
1221
1569
  }
1222
1570
 
1223
1571
  function disposeAllSubscriptionsToDependencies() {
1224
- ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
1572
+ _isDisposed = true;
1573
+ ko.utils.objectForEach(_subscriptionsToDependencies, function (id, subscription) {
1225
1574
  subscription.dispose();
1226
1575
  });
1227
- _subscriptionsToDependencies = [];
1576
+ _subscriptionsToDependencies = {};
1577
+ _dependenciesCount = 0;
1578
+ _needsEvaluation = false;
1228
1579
  }
1229
1580
 
1230
1581
  function evaluatePossiblyAsync() {
@@ -1232,8 +1583,11 @@
1232
1583
  if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
1233
1584
  clearTimeout(evaluationTimeoutInstance);
1234
1585
  evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
1235
- } else
1586
+ } else if (dependentObservable._evalRateLimited) {
1587
+ dependentObservable._evalRateLimited();
1588
+ } else {
1236
1589
  evaluateImmediate();
1590
+ }
1237
1591
  }
1238
1592
 
1239
1593
  function evaluateImmediate() {
@@ -1245,49 +1599,83 @@
1245
1599
  return;
1246
1600
  }
1247
1601
 
1248
- // Don't dispose on first evaluation, because the "disposeWhen" callback might
1249
- // e.g., dispose when the associated DOM element isn't in the doc, and it's not
1250
- // going to be in the doc until *after* the first evaluation
1251
- if (_hasBeenEvaluated && disposeWhen()) {
1252
- dispose();
1602
+ // Do not evaluate (and possibly capture new dependencies) if disposed
1603
+ if (_isDisposed) {
1253
1604
  return;
1254
1605
  }
1255
1606
 
1607
+ if (disposeWhen && disposeWhen()) {
1608
+ // See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse
1609
+ if (!_suppressDisposalUntilDisposeWhenReturnsFalse) {
1610
+ dispose();
1611
+ return;
1612
+ }
1613
+ } else {
1614
+ // It just did return false, so we can stop suppressing now
1615
+ _suppressDisposalUntilDisposeWhenReturnsFalse = false;
1616
+ }
1617
+
1256
1618
  _isBeingEvaluated = true;
1257
1619
  try {
1258
1620
  // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal).
1259
1621
  // Then, during evaluation, we cross off any that are in fact still being used.
1260
- var disposalCandidates = ko.utils.arrayMap(_subscriptionsToDependencies, function(item) {return item.target;});
1261
-
1262
- ko.dependencyDetection.begin(function(subscribable) {
1263
- var inOld;
1264
- if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
1265
- disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
1266
- else
1267
- addSubscriptionToDependency(subscribable); // Brand new subscription - add it
1622
+ var disposalCandidates = _subscriptionsToDependencies, disposalCount = _dependenciesCount;
1623
+ ko.dependencyDetection.begin({
1624
+ callback: function(subscribable, id) {
1625
+ if (!_isDisposed) {
1626
+ if (disposalCount && disposalCandidates[id]) {
1627
+ // Don't want to dispose this subscription, as it's still being used
1628
+ _subscriptionsToDependencies[id] = disposalCandidates[id];
1629
+ ++_dependenciesCount;
1630
+ delete disposalCandidates[id];
1631
+ --disposalCount;
1632
+ } else {
1633
+ // Brand new subscription - add it
1634
+ addSubscriptionToDependency(subscribable, id);
1635
+ }
1636
+ }
1637
+ },
1638
+ computed: dependentObservable,
1639
+ isInitial: !_dependenciesCount // If we're evaluating when there are no previous dependencies, it must be the first time
1268
1640
  });
1269
1641
 
1270
- var newValue = readFunction.call(evaluatorFunctionTarget);
1642
+ _subscriptionsToDependencies = {};
1643
+ _dependenciesCount = 0;
1644
+
1645
+ try {
1646
+ var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction();
1647
+
1648
+ } finally {
1649
+ ko.dependencyDetection.end();
1650
+
1651
+ // For each subscription no longer being used, remove it from the active subscriptions list and dispose it
1652
+ if (disposalCount) {
1653
+ ko.utils.objectForEach(disposalCandidates, function(id, toDispose) {
1654
+ toDispose.dispose();
1655
+ });
1656
+ }
1271
1657
 
1272
- // For each subscription no longer being used, remove it from the active subscriptions list and dispose it
1273
- for (var i = disposalCandidates.length - 1; i >= 0; i--) {
1274
- if (disposalCandidates[i])
1275
- _subscriptionsToDependencies.splice(i, 1)[0].dispose();
1658
+ _needsEvaluation = false;
1276
1659
  }
1277
- _hasBeenEvaluated = true;
1278
1660
 
1279
- dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
1661
+ if (dependentObservable.isDifferent(_latestValue, newValue)) {
1662
+ dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
1280
1663
 
1281
- _latestValue = newValue;
1282
- if (DEBUG) dependentObservable._latestValue = _latestValue;
1283
- dependentObservable["notifySubscribers"](_latestValue);
1664
+ _latestValue = newValue;
1665
+ if (DEBUG) dependentObservable._latestValue = _latestValue;
1284
1666
 
1667
+ // If rate-limited, the notification will happen within the limit function. Otherwise,
1668
+ // notify as soon as the value changes. Check specifically for the throttle setting since
1669
+ // it overrides rateLimit.
1670
+ if (!dependentObservable._evalRateLimited || dependentObservable['throttleEvaluation']) {
1671
+ dependentObservable["notifySubscribers"](_latestValue);
1672
+ }
1673
+ }
1285
1674
  } finally {
1286
- ko.dependencyDetection.end();
1287
1675
  _isBeingEvaluated = false;
1288
1676
  }
1289
1677
 
1290
- if (!_subscriptionsToDependencies.length)
1678
+ if (!_dependenciesCount)
1291
1679
  dispose();
1292
1680
  }
1293
1681
 
@@ -1302,7 +1690,7 @@
1302
1690
  return this; // Permits chained assignments
1303
1691
  } else {
1304
1692
  // Reading the value
1305
- if (!_hasBeenEvaluated)
1693
+ if (_needsEvaluation)
1306
1694
  evaluateImmediate();
1307
1695
  ko.dependencyDetection.registerDependency(dependentObservable);
1308
1696
  return _latestValue;
@@ -1310,58 +1698,90 @@
1310
1698
  }
1311
1699
 
1312
1700
  function peek() {
1313
- if (!_hasBeenEvaluated)
1701
+ // Peek won't re-evaluate, except to get the initial value when "deferEvaluation" is set.
1702
+ // That's the only time that both of these conditions will be satisfied.
1703
+ if (_needsEvaluation && !_dependenciesCount)
1314
1704
  evaluateImmediate();
1315
1705
  return _latestValue;
1316
1706
  }
1317
1707
 
1318
1708
  function isActive() {
1319
- return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0;
1709
+ return _needsEvaluation || _dependenciesCount > 0;
1320
1710
  }
1321
1711
 
1322
1712
  // By here, "options" is always non-null
1323
1713
  var writeFunction = options["write"],
1324
1714
  disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
1325
- disposeWhen = options["disposeWhen"] || options.disposeWhen || function() { return false; },
1715
+ disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
1716
+ disposeWhen = disposeWhenOption,
1326
1717
  dispose = disposeAllSubscriptionsToDependencies,
1327
- _subscriptionsToDependencies = [],
1718
+ _subscriptionsToDependencies = {},
1719
+ _dependenciesCount = 0,
1328
1720
  evaluationTimeoutInstance = null;
1329
1721
 
1330
1722
  if (!evaluatorFunctionTarget)
1331
1723
  evaluatorFunctionTarget = options["owner"];
1332
1724
 
1725
+ ko.subscribable.call(dependentObservable);
1726
+ ko.utils.setPrototypeOfOrExtend(dependentObservable, ko.dependentObservable['fn']);
1727
+
1333
1728
  dependentObservable.peek = peek;
1334
- dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; };
1729
+ dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
1335
1730
  dependentObservable.hasWriteFunction = typeof options["write"] === "function";
1336
1731
  dependentObservable.dispose = function () { dispose(); };
1337
1732
  dependentObservable.isActive = isActive;
1338
1733
 
1339
- ko.subscribable.call(dependentObservable);
1340
- ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
1734
+ // Replace the limit function with one that delays evaluation as well.
1735
+ var originalLimit = dependentObservable.limit;
1736
+ dependentObservable.limit = function(limitFunction) {
1737
+ originalLimit.call(dependentObservable, limitFunction);
1738
+ dependentObservable._evalRateLimited = function() {
1739
+ dependentObservable._rateLimitedBeforeChange(_latestValue);
1740
+
1741
+ _needsEvaluation = true; // Mark as dirty
1742
+
1743
+ // Pass the observable to the rate-limit code, which will access it when
1744
+ // it's time to do the notification.
1745
+ dependentObservable._rateLimitedChange(dependentObservable);
1746
+ }
1747
+ };
1341
1748
 
1342
1749
  ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
1343
1750
  ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
1344
1751
  ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive);
1345
1752
  ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
1346
1753
 
1754
+ // Add a "disposeWhen" callback that, on each evaluation, disposes if the node was removed without using ko.removeNode.
1755
+ if (disposeWhenNodeIsRemoved) {
1756
+ // Since this computed is associated with a DOM node, and we don't want to dispose the computed
1757
+ // until the DOM node is *removed* from the document (as opposed to never having been in the document),
1758
+ // we'll prevent disposal until "disposeWhen" first returns false.
1759
+ _suppressDisposalUntilDisposeWhenReturnsFalse = true;
1760
+
1761
+ // Only watch for the node's disposal if the value really is a node. It might not be,
1762
+ // e.g., { disposeWhenNodeIsRemoved: true } can be used to opt into the "only dispose
1763
+ // after first false result" behaviour even if there's no specific node to watch. This
1764
+ // technique is intended for KO's internal use only and shouldn't be documented or used
1765
+ // by application code, as it's likely to change in a future version of KO.
1766
+ if (disposeWhenNodeIsRemoved.nodeType) {
1767
+ disposeWhen = function () {
1768
+ return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || (disposeWhenOption && disposeWhenOption());
1769
+ };
1770
+ }
1771
+ }
1772
+
1347
1773
  // Evaluate, unless deferEvaluation is true
1348
1774
  if (options['deferEvaluation'] !== true)
1349
1775
  evaluateImmediate();
1350
1776
 
1351
- // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values.
1352
- // But skip if isActive is false (there will never be any dependencies to dispose).
1353
- // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
1354
- // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
1355
- if (disposeWhenNodeIsRemoved && isActive()) {
1777
+ // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is
1778
+ // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose).
1779
+ if (disposeWhenNodeIsRemoved && isActive() && disposeWhenNodeIsRemoved.nodeType) {
1356
1780
  dispose = function() {
1357
1781
  ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
1358
1782
  disposeAllSubscriptionsToDependencies();
1359
1783
  };
1360
1784
  ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
1361
- var existingDisposeWhenFunction = disposeWhen;
1362
- disposeWhen = function () {
1363
- return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
1364
- }
1365
1785
  }
1366
1786
 
1367
1787
  return dependentObservable;
@@ -1374,9 +1794,17 @@
1374
1794
  var protoProp = ko.observable.protoProperty; // == "__ko_proto__"
1375
1795
  ko.dependentObservable[protoProp] = ko.observable;
1376
1796
 
1377
- ko.dependentObservable['fn'] = {};
1797
+ ko.dependentObservable['fn'] = {
1798
+ "equalityComparer": valuesArePrimitiveAndEqual
1799
+ };
1378
1800
  ko.dependentObservable['fn'][protoProp] = ko.dependentObservable;
1379
1801
 
1802
+ // Note that for browsers that don't support proto assignment, the
1803
+ // inheritance chain is created manually in the ko.dependentObservable constructor
1804
+ if (ko.utils.canSetPrototype) {
1805
+ ko.utils.setPrototypeOf(ko.dependentObservable['fn'], ko.subscribable['fn']);
1806
+ }
1807
+
1380
1808
  ko.exportSymbol('dependentObservable', ko.dependentObservable);
1381
1809
  ko.exportSymbol('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
1382
1810
  ko.exportSymbol('isComputed', ko.isComputed);
@@ -1498,7 +1926,7 @@
1498
1926
  }
1499
1927
  },
1500
1928
 
1501
- writeValue: function(element, value) {
1929
+ writeValue: function(element, value, allowUnset) {
1502
1930
  switch (ko.utils.tagNameLower(element)) {
1503
1931
  case 'option':
1504
1932
  switch(typeof value) {
@@ -1520,19 +1948,19 @@
1520
1948
  }
1521
1949
  break;
1522
1950
  case 'select':
1523
- if (value === "")
1951
+ if (value === "" || value === null) // A blank string or null value will select the caption
1524
1952
  value = undefined;
1525
- if (value === null || value === undefined)
1526
- element.selectedIndex = -1;
1527
- for (var i = element.options.length - 1; i >= 0; i--) {
1528
- if (ko.selectExtensions.readValue(element.options[i]) == value) {
1529
- element.selectedIndex = i;
1953
+ var selection = -1;
1954
+ for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
1955
+ optionValue = ko.selectExtensions.readValue(element.options[i]);
1956
+ // Include special check to handle selecting a caption with a blank string value
1957
+ if (optionValue == value || (optionValue == "" && value === undefined)) {
1958
+ selection = i;
1530
1959
  break;
1531
1960
  }
1532
1961
  }
1533
- // for drop-down select, ensure first is selected
1534
- if (!(element.size > 1) && element.selectedIndex === -1) {
1535
- element.selectedIndex = 0;
1962
+ if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) {
1963
+ element.selectedIndex = selection;
1536
1964
  }
1537
1965
  break;
1538
1966
  default:
@@ -1549,170 +1977,162 @@
1549
1977
  ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
1550
1978
  ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
1551
1979
  ko.expressionRewriting = (function () {
1552
- var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
1553
1980
  var javaScriptReservedWords = ["true", "false", "null", "undefined"];
1554
1981
 
1555
1982
  // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
1556
1983
  // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
1984
+ // This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911).
1557
1985
  var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
1558
1986
 
1559
- function restoreTokens(string, tokens) {
1560
- var prevValue = null;
1561
- while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
1562
- prevValue = string;
1563
- string = string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) {
1564
- return tokens[tokenIndex];
1565
- });
1566
- }
1567
- return string;
1568
- }
1569
-
1570
1987
  function getWriteableValue(expression) {
1571
- if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0)
1988
+ if (ko.utils.arrayIndexOf(javaScriptReservedWords, expression) >= 0)
1572
1989
  return false;
1573
1990
  var match = expression.match(javaScriptAssignmentTarget);
1574
1991
  return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
1575
1992
  }
1576
1993
 
1577
- function ensureQuoted(key) {
1578
- var trimmedKey = ko.utils.stringTrim(key);
1579
- switch (trimmedKey.length && trimmedKey.charAt(0)) {
1580
- case "'":
1581
- case '"':
1582
- return key;
1583
- default:
1584
- return "'" + trimmedKey + "'";
1994
+ // The following regular expressions will be used to split an object-literal string into tokens
1995
+
1996
+ // These two match strings, either with double quotes or single quotes
1997
+ var stringDouble = '"(?:[^"\\\\]|\\\\.)*"',
1998
+ stringSingle = "'(?:[^'\\\\]|\\\\.)*'",
1999
+ // Matches a regular expression (text enclosed by slashes), but will also match sets of divisions
2000
+ // as a regular expression (this is handled by the parsing loop below).
2001
+ stringRegexp = '/(?:[^/\\\\]|\\\\.)*/\w*',
2002
+ // These characters have special meaning to the parser and must not appear in the middle of a
2003
+ // token, except as part of a string.
2004
+ specials = ',"\'{}()/:[\\]',
2005
+ // Match text (at least two characters) that does not contain any of the above special characters,
2006
+ // although some of the special characters are allowed to start it (all but the colon and comma).
2007
+ // The text can contain spaces, but leading or trailing spaces are skipped.
2008
+ everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
2009
+ // Match any non-space character not matched already. This will match colons and commas, since they're
2010
+ // not matched by "everyThingElse", but will also match any other single character that wasn't already
2011
+ // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
2012
+ oneNotSpace = '[^\\s]',
2013
+
2014
+ // Create the actual regular expression by or-ing the above strings. The order is important.
2015
+ bindingToken = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g'),
2016
+
2017
+ // Match end of previous token to determine whether a slash is a division or regex.
2018
+ divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
2019
+ keywordRegexLookBehind = {'in':1,'return':1,'typeof':1};
2020
+
2021
+ function parseObjectLiteral(objectLiteralString) {
2022
+ // Trim leading and trailing spaces from the string
2023
+ var str = ko.utils.stringTrim(objectLiteralString);
2024
+
2025
+ // Trim braces '{' surrounding the whole object literal
2026
+ if (str.charCodeAt(0) === 123) str = str.slice(1, -1);
2027
+
2028
+ // Split into tokens
2029
+ var result = [], toks = str.match(bindingToken), key, values, depth = 0;
2030
+
2031
+ if (toks) {
2032
+ // Append a comma so that we don't need a separate code block to deal with the last item
2033
+ toks.push(',');
2034
+
2035
+ for (var i = 0, tok; tok = toks[i]; ++i) {
2036
+ var c = tok.charCodeAt(0);
2037
+ // A comma signals the end of a key/value pair if depth is zero
2038
+ if (c === 44) { // ","
2039
+ if (depth <= 0) {
2040
+ if (key)
2041
+ result.push(values ? {key: key, value: values.join('')} : {'unknown': key});
2042
+ key = values = depth = 0;
2043
+ continue;
2044
+ }
2045
+ // Simply skip the colon that separates the name and value
2046
+ } else if (c === 58) { // ":"
2047
+ if (!values)
2048
+ continue;
2049
+ // A set of slashes is initially matched as a regular expression, but could be division
2050
+ } else if (c === 47 && i && tok.length > 1) { // "/"
2051
+ // Look at the end of the previous token to determine if the slash is actually division
2052
+ var match = toks[i-1].match(divisionLookBehind);
2053
+ if (match && !keywordRegexLookBehind[match[0]]) {
2054
+ // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
2055
+ str = str.substr(str.indexOf(tok) + 1);
2056
+ toks = str.match(bindingToken);
2057
+ toks.push(',');
2058
+ i = -1;
2059
+ // Continue with just the slash
2060
+ tok = '/';
2061
+ }
2062
+ // Increment depth for parentheses, braces, and brackets so that interior commas are ignored
2063
+ } else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
2064
+ ++depth;
2065
+ } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
2066
+ --depth;
2067
+ // The key must be a single token; if it's a string, trim the quotes
2068
+ } else if (!key && !values) {
2069
+ key = (c === 34 || c === 39) /* '"', "'" */ ? tok.slice(1, -1) : tok;
2070
+ continue;
2071
+ }
2072
+ if (values)
2073
+ values.push(tok);
2074
+ else
2075
+ values = [tok];
2076
+ }
1585
2077
  }
2078
+ return result;
1586
2079
  }
1587
2080
 
1588
- return {
1589
- bindingRewriteValidators: [],
2081
+ // Two-way bindings include a write function that allow the handler to update the value even if it's not an observable.
2082
+ var twoWayBindings = {};
1590
2083
 
1591
- parseObjectLiteral: function(objectLiteralString) {
1592
- // A full tokeniser+lexer would add too much weight to this library, so here's a simple parser
1593
- // that is sufficient just to split an object literal string into a set of top-level key-value pairs
1594
-
1595
- var str = ko.utils.stringTrim(objectLiteralString);
1596
- if (str.length < 3)
1597
- return [];
1598
- if (str.charAt(0) === "{")// Ignore any braces surrounding the whole object literal
1599
- str = str.substring(1, str.length - 1);
1600
-
1601
- // Pull out any string literals and regex literals
1602
- var tokens = [];
1603
- var tokenStart = null, tokenEndChar;
1604
- for (var position = 0; position < str.length; position++) {
1605
- var c = str.charAt(position);
1606
- if (tokenStart === null) {
1607
- switch (c) {
1608
- case '"':
1609
- case "'":
1610
- case "/":
1611
- tokenStart = position;
1612
- tokenEndChar = c;
1613
- break;
1614
- }
1615
- } else if ((c == tokenEndChar) && (str.charAt(position - 1) !== "\\")) {
1616
- var token = str.substring(tokenStart, position + 1);
1617
- tokens.push(token);
1618
- var replacement = "@ko_token_" + (tokens.length - 1) + "@";
1619
- str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
1620
- position -= (token.length - replacement.length);
1621
- tokenStart = null;
1622
- }
1623
- }
2084
+ function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) {
2085
+ bindingOptions = bindingOptions || {};
1624
2086
 
1625
- // Next pull out balanced paren, brace, and bracket blocks
1626
- tokenStart = null;
1627
- tokenEndChar = null;
1628
- var tokenDepth = 0, tokenStartChar = null;
1629
- for (var position = 0; position < str.length; position++) {
1630
- var c = str.charAt(position);
1631
- if (tokenStart === null) {
1632
- switch (c) {
1633
- case "{": tokenStart = position; tokenStartChar = c;
1634
- tokenEndChar = "}";
1635
- break;
1636
- case "(": tokenStart = position; tokenStartChar = c;
1637
- tokenEndChar = ")";
1638
- break;
1639
- case "[": tokenStart = position; tokenStartChar = c;
1640
- tokenEndChar = "]";
1641
- break;
1642
- }
1643
- }
2087
+ function processKeyValue(key, val) {
2088
+ var writableVal;
2089
+ function callPreprocessHook(obj) {
2090
+ return (obj && obj['preprocess']) ? (val = obj['preprocess'](val, key, processKeyValue)) : true;
2091
+ }
2092
+ if (!callPreprocessHook(ko['getBindingHandler'](key)))
2093
+ return;
1644
2094
 
1645
- if (c === tokenStartChar)
1646
- tokenDepth++;
1647
- else if (c === tokenEndChar) {
1648
- tokenDepth--;
1649
- if (tokenDepth === 0) {
1650
- var token = str.substring(tokenStart, position + 1);
1651
- tokens.push(token);
1652
- var replacement = "@ko_token_" + (tokens.length - 1) + "@";
1653
- str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
1654
- position -= (token.length - replacement.length);
1655
- tokenStart = null;
1656
- }
1657
- }
2095
+ if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
2096
+ // For two-way bindings, provide a write method in case the value
2097
+ // isn't a writable observable.
2098
+ propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
1658
2099
  }
1659
2100
 
1660
- // Now we can safely split on commas to get the key/value pairs
1661
- var result = [];
1662
- var keyValuePairs = str.split(",");
1663
- for (var i = 0, j = keyValuePairs.length; i < j; i++) {
1664
- var pair = keyValuePairs[i];
1665
- var colonPos = pair.indexOf(":");
1666
- if ((colonPos > 0) && (colonPos < pair.length - 1)) {
1667
- var key = pair.substring(0, colonPos);
1668
- var value = pair.substring(colonPos + 1);
1669
- result.push({ 'key': restoreTokens(key, tokens), 'value': restoreTokens(value, tokens) });
1670
- } else {
1671
- result.push({ 'unknown': restoreTokens(pair, tokens) });
1672
- }
2101
+ // Values are wrapped in a function so that each value can be accessed independently
2102
+ if (makeValueAccessors) {
2103
+ val = 'function(){return ' + val + ' }';
1673
2104
  }
1674
- return result;
1675
- },
2105
+ resultStrings.push("'" + key + "':" + val);
2106
+ }
1676
2107
 
1677
- preProcessBindings: function (objectLiteralStringOrKeyValueArray) {
1678
- var keyValueArray = typeof objectLiteralStringOrKeyValueArray === "string"
1679
- ? ko.expressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
1680
- : objectLiteralStringOrKeyValueArray;
1681
- var resultStrings = [], propertyAccessorResultStrings = [];
2108
+ var resultStrings = [],
2109
+ propertyAccessorResultStrings = [],
2110
+ makeValueAccessors = bindingOptions['valueAccessors'],
2111
+ keyValueArray = typeof bindingsStringOrKeyValueArray === "string" ?
2112
+ parseObjectLiteral(bindingsStringOrKeyValueArray) : bindingsStringOrKeyValueArray;
1682
2113
 
1683
- var keyValueEntry;
1684
- for (var i = 0; keyValueEntry = keyValueArray[i]; i++) {
1685
- if (resultStrings.length > 0)
1686
- resultStrings.push(",");
2114
+ ko.utils.arrayForEach(keyValueArray, function(keyValue) {
2115
+ processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value);
2116
+ });
1687
2117
 
1688
- if (keyValueEntry['key']) {
1689
- var quotedKey = ensureQuoted(keyValueEntry['key']), val = keyValueEntry['value'];
1690
- resultStrings.push(quotedKey);
1691
- resultStrings.push(":");
1692
- resultStrings.push(val);
2118
+ if (propertyAccessorResultStrings.length)
2119
+ processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
1693
2120
 
1694
- if (val = getWriteableValue(ko.utils.stringTrim(val))) {
1695
- if (propertyAccessorResultStrings.length > 0)
1696
- propertyAccessorResultStrings.push(", ");
1697
- propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
1698
- }
1699
- } else if (keyValueEntry['unknown']) {
1700
- resultStrings.push(keyValueEntry['unknown']);
1701
- }
1702
- }
2121
+ return resultStrings.join(",");
2122
+ }
1703
2123
 
1704
- var combinedResult = resultStrings.join("");
1705
- if (propertyAccessorResultStrings.length > 0) {
1706
- var allPropertyAccessors = propertyAccessorResultStrings.join("");
1707
- combinedResult = combinedResult + ", '_ko_property_writers' : { " + allPropertyAccessors + " } ";
1708
- }
2124
+ return {
2125
+ bindingRewriteValidators: [],
1709
2126
 
1710
- return combinedResult;
1711
- },
2127
+ twoWayBindings: twoWayBindings,
2128
+
2129
+ parseObjectLiteral: parseObjectLiteral,
2130
+
2131
+ preProcessBindings: preProcessBindings,
1712
2132
 
1713
2133
  keyValueArrayContainsKey: function(keyValueArray, key) {
1714
2134
  for (var i = 0; i < keyValueArray.length; i++)
1715
- if (ko.utils.stringTrim(keyValueArray[i]['key']) == key)
2135
+ if (keyValueArray[i]['key'] == key)
1716
2136
  return true;
1717
2137
  return false;
1718
2138
  },
@@ -1720,15 +2140,15 @@
1720
2140
  // Internal, private KO utility for updating model properties from within bindings
1721
2141
  // property: If the property being updated is (or might be) an observable, pass it here
1722
2142
  // If it turns out to be a writable observable, it will be written to directly
1723
- // allBindingsAccessor: All bindings in the current execution context.
2143
+ // allBindings: An object with a get method to retrieve bindings in the current execution context.
1724
2144
  // This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable
1725
2145
  // key: The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus'
1726
2146
  // value: The value to be written
1727
2147
  // checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if
1728
2148
  // it is !== existing value on that writable observable
1729
- writeValueToProperty: function(property, allBindingsAccessor, key, value, checkIfDifferent) {
2149
+ writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) {
1730
2150
  if (!property || !ko.isObservable(property)) {
1731
- var propWriters = allBindingsAccessor()['_ko_property_writers'];
2151
+ var propWriters = allBindings.get('_ko_property_writers');
1732
2152
  if (propWriters && propWriters[key])
1733
2153
  propWriters[key](value);
1734
2154
  } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
@@ -1743,10 +2163,20 @@
1743
2163
  ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral);
1744
2164
  ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings);
1745
2165
 
2166
+ // Making bindings explicitly declare themselves as "two way" isn't ideal in the long term (it would be better if
2167
+ // all bindings could use an official 'property writer' API without needing to declare that they might). However,
2168
+ // since this is not, and has never been, a public API (_ko_property_writers was never documented), it's acceptable
2169
+ // as an internal implementation detail in the short term.
2170
+ // For those developers who rely on _ko_property_writers in their custom bindings, we expose _twoWayBindings as an
2171
+ // undocumented feature that makes it relatively easy to upgrade to KO 3.0. However, this is still not an official
2172
+ // public API, and we reserve the right to remove it at any time if we create a real public property writers API.
2173
+ ko.exportSymbol('expressionRewriting._twoWayBindings', ko.expressionRewriting.twoWayBindings);
2174
+
1746
2175
  // For backward compatibility, define the following aliases. (Previously, these function names were misleading because
1747
2176
  // they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.)
1748
2177
  ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting);
1749
- ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);(function() {
2178
+ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);
2179
+ (function() {
1750
2180
  // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
1751
2181
  // may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
1752
2182
  // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
@@ -1760,16 +2190,16 @@
1760
2190
  // So, use node.text where available, and node.nodeValue elsewhere
1761
2191
  var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
1762
2192
 
1763
- var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
2193
+ var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;
1764
2194
  var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
1765
2195
  var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
1766
2196
 
1767
2197
  function isStartComment(node) {
1768
- return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
2198
+ return (node.nodeType == 8) && startCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
1769
2199
  }
1770
2200
 
1771
2201
  function isEndComment(node) {
1772
- return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
2202
+ return (node.nodeType == 8) && endCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
1773
2203
  }
1774
2204
 
1775
2205
  function getVirtualChildren(startComment, allowUnbalanced) {
@@ -1896,8 +2326,10 @@
1896
2326
  return node.nextSibling;
1897
2327
  },
1898
2328
 
2329
+ hasBindingValue: isStartComment,
2330
+
1899
2331
  virtualNodeBindingValue: function(node) {
1900
- var regexMatch = isStartComment(node);
2332
+ var regexMatch = (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
1901
2333
  return regexMatch ? regexMatch[1] : null;
1902
2334
  },
1903
2335
 
@@ -1950,7 +2382,7 @@
1950
2382
  'nodeHasBindings': function(node) {
1951
2383
  switch (node.nodeType) {
1952
2384
  case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element
1953
- case 8: return ko.virtualElements.virtualNodeBindingValue(node) != null; // Comment node
2385
+ case 8: return ko.virtualElements.hasBindingValue(node); // Comment node
1954
2386
  default: return false;
1955
2387
  }
1956
2388
  },
@@ -1960,6 +2392,11 @@
1960
2392
  return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
1961
2393
  },
1962
2394
 
2395
+ 'getBindingAccessors': function(node, bindingContext) {
2396
+ var bindingsString = this['getBindingsString'](node, bindingContext);
2397
+ return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, {'valueAccessors':true}) : null;
2398
+ },
2399
+
1963
2400
  // The following function is only used internally by this default provider.
1964
2401
  // It's not part of the interface definition for a general binding provider.
1965
2402
  'getBindingsString': function(node, bindingContext) {
@@ -1972,9 +2409,9 @@
1972
2409
 
1973
2410
  // The following function is only used internally by this default provider.
1974
2411
  // It's not part of the interface definition for a general binding provider.
1975
- 'parseBindingsString': function(bindingsString, bindingContext, node) {
2412
+ 'parseBindingsString': function(bindingsString, bindingContext, node, options) {
1976
2413
  try {
1977
- var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache);
2414
+ var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options);
1978
2415
  return bindingFunction(bindingContext, node);
1979
2416
  } catch (ex) {
1980
2417
  ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message;
@@ -1985,17 +2422,17 @@
1985
2422
 
1986
2423
  ko.bindingProvider['instance'] = new ko.bindingProvider();
1987
2424
 
1988
- function createBindingsStringEvaluatorViaCache(bindingsString, cache) {
1989
- var cacheKey = bindingsString;
2425
+ function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) {
2426
+ var cacheKey = bindingsString + (options && options['valueAccessors'] || '');
1990
2427
  return cache[cacheKey]
1991
- || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString));
2428
+ || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options));
1992
2429
  }
1993
2430
 
1994
- function createBindingsStringEvaluator(bindingsString) {
2431
+ function createBindingsStringEvaluator(bindingsString, options) {
1995
2432
  // Build the source for a function that evaluates "expression"
1996
2433
  // For each scope variable, add an extra level of "with" nesting
1997
2434
  // Example result: with(sc1) { with(sc0) { return (expression) } }
1998
- var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
2435
+ var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
1999
2436
  functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
2000
2437
  return new Function("$context", "$element", functionBody);
2001
2438
  }
@@ -2005,49 +2442,214 @@
2005
2442
  (function () {
2006
2443
  ko.bindingHandlers = {};
2007
2444
 
2008
- ko.bindingContext = function(dataItem, parentBindingContext, dataItemAlias) {
2009
- if (parentBindingContext) {
2010
- ko.utils.extend(this, parentBindingContext); // Inherit $root and any custom properties
2011
- this['$parentContext'] = parentBindingContext;
2012
- this['$parent'] = parentBindingContext['$data'];
2013
- this['$parents'] = (parentBindingContext['$parents'] || []).slice(0);
2014
- this['$parents'].unshift(this['$parent']);
2015
- } else {
2016
- this['$parents'] = [];
2017
- this['$root'] = dataItem;
2018
- // Export 'ko' in the binding context so it will be available in bindings and templates
2019
- // even if 'ko' isn't exported as a global, such as when using an AMD loader.
2020
- // See https://github.com/SteveSanderson/knockout/issues/490
2021
- this['ko'] = ko;
2022
- }
2023
- this['$data'] = dataItem;
2024
- if (dataItemAlias)
2025
- this[dataItemAlias] = dataItem;
2026
- }
2027
- ko.bindingContext.prototype['createChildContext'] = function (dataItem, dataItemAlias) {
2028
- return new ko.bindingContext(dataItem, this, dataItemAlias);
2445
+ // The following element types will not be recursed into during binding. In the future, we
2446
+ // may consider adding <template> to this list, because such elements' contents are always
2447
+ // intended to be bound in a different context from where they appear in the document.
2448
+ var bindingDoesNotRecurseIntoElementTypes = {
2449
+ // Don't want bindings that operate on text nodes to mutate <script> contents,
2450
+ // because it's unexpected and a potential XSS issue
2451
+ 'script': true
2452
+ };
2453
+
2454
+ // Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
2455
+ ko['getBindingHandler'] = function(bindingKey) {
2456
+ return ko.bindingHandlers[bindingKey];
2457
+ };
2458
+
2459
+ // The ko.bindingContext constructor is only called directly to create the root context. For child
2460
+ // contexts, use bindingContext.createChildContext or bindingContext.extend.
2461
+ ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {
2462
+
2463
+ // The binding context object includes static properties for the current, parent, and root view models.
2464
+ // If a view model is actually stored in an observable, the corresponding binding context object, and
2465
+ // any child contexts, must be updated when the view model is changed.
2466
+ function updateContext() {
2467
+ // Most of the time, the context will directly get a view model object, but if a function is given,
2468
+ // we call the function to retrieve the view model. If the function accesses any obsevables or returns
2469
+ // an observable, the dependency is tracked, and those observables can later cause the binding
2470
+ // context to be updated.
2471
+ var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
2472
+ dataItem = ko.utils.unwrapObservable(dataItemOrObservable);
2473
+
2474
+ if (parentContext) {
2475
+ // When a "parent" context is given, register a dependency on the parent context. Thus whenever the
2476
+ // parent context is updated, this context will also be updated.
2477
+ if (parentContext._subscribable)
2478
+ parentContext._subscribable();
2479
+
2480
+ // Copy $root and any custom properties from the parent context
2481
+ ko.utils.extend(self, parentContext);
2482
+
2483
+ // Because the above copy overwrites our own properties, we need to reset them.
2484
+ // During the first execution, "subscribable" isn't set, so don't bother doing the update then.
2485
+ if (subscribable) {
2486
+ self._subscribable = subscribable;
2487
+ }
2488
+ } else {
2489
+ self['$parents'] = [];
2490
+ self['$root'] = dataItem;
2491
+
2492
+ // Export 'ko' in the binding context so it will be available in bindings and templates
2493
+ // even if 'ko' isn't exported as a global, such as when using an AMD loader.
2494
+ // See https://github.com/SteveSanderson/knockout/issues/490
2495
+ self['ko'] = ko;
2496
+ }
2497
+ self['$rawData'] = dataItemOrObservable;
2498
+ self['$data'] = dataItem;
2499
+ if (dataItemAlias)
2500
+ self[dataItemAlias] = dataItem;
2501
+
2502
+ // The extendCallback function is provided when creating a child context or extending a context.
2503
+ // It handles the specific actions needed to finish setting up the binding context. Actions in this
2504
+ // function could also add dependencies to this binding context.
2505
+ if (extendCallback)
2506
+ extendCallback(self, parentContext, dataItem);
2507
+
2508
+ return self['$data'];
2509
+ }
2510
+ function disposeWhen() {
2511
+ return nodes && !ko.utils.anyDomNodeIsAttachedToDocument(nodes);
2512
+ }
2513
+
2514
+ var self = this,
2515
+ isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
2516
+ nodes,
2517
+ subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });
2518
+
2519
+ // At this point, the binding context has been initialized, and the "subscribable" computed observable is
2520
+ // subscribed to any observables that were accessed in the process. If there is nothing to track, the
2521
+ // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
2522
+ // the context object.
2523
+ if (subscribable.isActive()) {
2524
+ self._subscribable = subscribable;
2525
+
2526
+ // Always notify because even if the model ($data) hasn't changed, other context properties might have changed
2527
+ subscribable['equalityComparer'] = null;
2528
+
2529
+ // We need to be able to dispose of this computed observable when it's no longer needed. This would be
2530
+ // easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
2531
+ // we cannot assume that those nodes have any relation to each other. So instead we track any node that
2532
+ // the context is attached to, and dispose the computed when all of those nodes have been cleaned.
2533
+
2534
+ // Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
2535
+ nodes = [];
2536
+ subscribable._addNode = function(node) {
2537
+ nodes.push(node);
2538
+ ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
2539
+ ko.utils.arrayRemoveItem(nodes, node);
2540
+ if (!nodes.length) {
2541
+ subscribable.dispose();
2542
+ self._subscribable = subscribable = undefined;
2543
+ }
2544
+ });
2545
+ };
2546
+ }
2547
+ }
2548
+
2549
+ // Extend the binding context hierarchy with a new view model object. If the parent context is watching
2550
+ // any obsevables, the new child context will automatically get a dependency on the parent context.
2551
+ // But this does not mean that the $data value of the child context will also get updated. If the child
2552
+ // view model also depends on the parent view model, you must provide a function that returns the correct
2553
+ // view model on each update.
2554
+ ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback) {
2555
+ return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
2556
+ // Extend the context hierarchy by setting the appropriate pointers
2557
+ self['$parentContext'] = parentContext;
2558
+ self['$parent'] = parentContext['$data'];
2559
+ self['$parents'] = (parentContext['$parents'] || []).slice(0);
2560
+ self['$parents'].unshift(self['$parent']);
2561
+ if (extendCallback)
2562
+ extendCallback(self);
2563
+ });
2029
2564
  };
2565
+
2566
+ // Extend the binding context with new custom properties. This doesn't change the context hierarchy.
2567
+ // Similarly to "child" contexts, provide a function here to make sure that the correct values are set
2568
+ // when an observable view model is updated.
2030
2569
  ko.bindingContext.prototype['extend'] = function(properties) {
2031
- var clone = ko.utils.extend(new ko.bindingContext(), this);
2032
- return ko.utils.extend(clone, properties);
2570
+ // If the parent context references an observable view model, "_subscribable" will always be the
2571
+ // latest view model object. If not, "_subscribable" isn't set, and we can use the static "$data" value.
2572
+ return new ko.bindingContext(this._subscribable || this['$data'], this, null, function(self, parentContext) {
2573
+ // This "child" context doesn't directly track a parent observable view model,
2574
+ // so we need to manually set the $rawData value to match the parent.
2575
+ self['$rawData'] = parentContext['$rawData'];
2576
+ ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
2577
+ });
2033
2578
  };
2034
2579
 
2580
+ // Returns the valueAccesor function for a binding value
2581
+ function makeValueAccessor(value) {
2582
+ return function() {
2583
+ return value;
2584
+ };
2585
+ }
2586
+
2587
+ // Returns the value of a valueAccessor function
2588
+ function evaluateValueAccessor(valueAccessor) {
2589
+ return valueAccessor();
2590
+ }
2591
+
2592
+ // Given a function that returns bindings, create and return a new object that contains
2593
+ // binding value-accessors functions. Each accessor function calls the original function
2594
+ // so that it always gets the latest value and all dependencies are captured. This is used
2595
+ // by ko.applyBindingsToNode and getBindingsAndMakeAccessors.
2596
+ function makeAccessorsFromFunction(callback) {
2597
+ return ko.utils.objectMap(ko.dependencyDetection.ignore(callback), function(value, key) {
2598
+ return function() {
2599
+ return callback()[key];
2600
+ };
2601
+ });
2602
+ }
2603
+
2604
+ // Given a bindings function or object, create and return a new object that contains
2605
+ // binding value-accessors functions. This is used by ko.applyBindingsToNode.
2606
+ function makeBindingAccessors(bindings, context, node) {
2607
+ if (typeof bindings === 'function') {
2608
+ return makeAccessorsFromFunction(bindings.bind(null, context, node));
2609
+ } else {
2610
+ return ko.utils.objectMap(bindings, makeValueAccessor);
2611
+ }
2612
+ }
2613
+
2614
+ // This function is used if the binding provider doesn't include a getBindingAccessors function.
2615
+ // It must be called with 'this' set to the provider instance.
2616
+ function getBindingsAndMakeAccessors(node, context) {
2617
+ return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context));
2618
+ }
2619
+
2035
2620
  function validateThatBindingIsAllowedForVirtualElements(bindingName) {
2036
2621
  var validator = ko.virtualElements.allowedBindings[bindingName];
2037
2622
  if (!validator)
2038
2623
  throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
2039
2624
  }
2040
2625
 
2041
- function applyBindingsToDescendantsInternal (viewModel, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
2042
- var currentChild, nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
2626
+ function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
2627
+ var currentChild,
2628
+ nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement),
2629
+ provider = ko.bindingProvider['instance'],
2630
+ preprocessNode = provider['preprocessNode'];
2631
+
2632
+ // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
2633
+ // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
2634
+ // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
2635
+ // trigger insertion of <template> contents at that point in the document.
2636
+ if (preprocessNode) {
2637
+ while (currentChild = nextInQueue) {
2638
+ nextInQueue = ko.virtualElements.nextSibling(currentChild);
2639
+ preprocessNode.call(provider, currentChild);
2640
+ }
2641
+ // Reset nextInQueue for the next loop
2642
+ nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
2643
+ }
2644
+
2043
2645
  while (currentChild = nextInQueue) {
2044
2646
  // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
2045
2647
  nextInQueue = ko.virtualElements.nextSibling(currentChild);
2046
- applyBindingsToNodeAndDescendantsInternal(viewModel, currentChild, bindingContextsMayDifferFromDomParentElement);
2648
+ applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild, bindingContextsMayDifferFromDomParentElement);
2047
2649
  }
2048
2650
  }
2049
2651
 
2050
- function applyBindingsToNodeAndDescendantsInternal (viewModel, nodeVerified, bindingContextMayDifferFromDomParentElement) {
2652
+ function applyBindingsToNodeAndDescendantsInternal (bindingContext, nodeVerified, bindingContextMayDifferFromDomParentElement) {
2051
2653
  var shouldBindDescendants = true;
2052
2654
 
2053
2655
  // Perf optimisation: Apply bindings only if...
@@ -2061,137 +2663,218 @@
2061
2663
  var shouldApplyBindings = (isElement && bindingContextMayDifferFromDomParentElement) // Case (1)
2062
2664
  || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
2063
2665
  if (shouldApplyBindings)
2064
- shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, viewModel, bindingContextMayDifferFromDomParentElement).shouldBindDescendants;
2666
+ shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext, bindingContextMayDifferFromDomParentElement)['shouldBindDescendants'];
2065
2667
 
2066
- if (shouldBindDescendants) {
2668
+ if (shouldBindDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
2067
2669
  // We're recursing automatically into (real or virtual) child nodes without changing binding contexts. So,
2068
2670
  // * For children of a *real* element, the binding context is certainly the same as on their DOM .parentNode,
2069
2671
  // hence bindingContextsMayDifferFromDomParentElement is false
2070
2672
  // * For children of a *virtual* element, we can't be sure. Evaluating .parentNode on those children may
2071
2673
  // skip over any number of intermediate virtual elements, any of which might define a custom binding context,
2072
2674
  // hence bindingContextsMayDifferFromDomParentElement is true
2073
- applyBindingsToDescendantsInternal(viewModel, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
2675
+ applyBindingsToDescendantsInternal(bindingContext, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
2074
2676
  }
2075
2677
  }
2076
2678
 
2077
- var boundElementDomDataKey = '__ko_boundElement';
2078
- function applyBindingsToNodeInternal (node, bindings, viewModelOrBindingContext, bindingContextMayDifferFromDomParentElement) {
2079
- // Need to be sure that inits are only run once, and updates never run until all the inits have been run
2080
- var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits
2081
-
2082
- // Each time the dependentObservable is evaluated (after data changes),
2083
- // the binding attribute is reparsed so that it can pick out the correct
2084
- // model properties in the context of the changed data.
2085
- // DOM event callbacks need to be able to access this changed data,
2086
- // so we need a single parsedBindings variable (shared by all callbacks
2087
- // associated with this node's bindings) that all the closures can access.
2088
- var parsedBindings;
2089
- function makeValueAccessor(bindingKey) {
2090
- return function () { return parsedBindings[bindingKey] }
2091
- }
2092
- function parsedBindingsAccessor() {
2093
- return parsedBindings;
2094
- }
2679
+ var boundElementDomDataKey = ko.utils.domData.nextKey();
2680
+
2681
+
2682
+ function topologicalSortBindings(bindings) {
2683
+ // Depth-first sort
2684
+ var result = [], // The list of key/handler pairs that we will return
2685
+ bindingsConsidered = {}, // A temporary record of which bindings are already in 'result'
2686
+ cyclicDependencyStack = []; // Keeps track of a depth-search so that, if there's a cycle, we know which bindings caused it
2687
+ ko.utils.objectForEach(bindings, function pushBinding(bindingKey) {
2688
+ if (!bindingsConsidered[bindingKey]) {
2689
+ var binding = ko['getBindingHandler'](bindingKey);
2690
+ if (binding) {
2691
+ // First add dependencies (if any) of the current binding
2692
+ if (binding['after']) {
2693
+ cyclicDependencyStack.push(bindingKey);
2694
+ ko.utils.arrayForEach(binding['after'], function(bindingDependencyKey) {
2695
+ if (bindings[bindingDependencyKey]) {
2696
+ if (ko.utils.arrayIndexOf(cyclicDependencyStack, bindingDependencyKey) !== -1) {
2697
+ throw Error("Cannot combine the following bindings, because they have a cyclic dependency: " + cyclicDependencyStack.join(", "));
2698
+ } else {
2699
+ pushBinding(bindingDependencyKey);
2700
+ }
2701
+ }
2702
+ });
2703
+ cyclicDependencyStack.length--;
2704
+ }
2705
+ // Next add the current binding
2706
+ result.push({ key: bindingKey, handler: binding });
2707
+ }
2708
+ bindingsConsidered[bindingKey] = true;
2709
+ }
2710
+ });
2095
2711
 
2096
- var bindingHandlerThatControlsDescendantBindings;
2712
+ return result;
2713
+ }
2097
2714
 
2715
+ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
2098
2716
  // Prevent multiple applyBindings calls for the same node, except when a binding value is specified
2099
2717
  var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
2100
- if (!bindings) {
2718
+ if (!sourceBindings) {
2101
2719
  if (alreadyBound) {
2102
2720
  throw Error("You cannot apply bindings multiple times to the same element.");
2103
2721
  }
2104
2722
  ko.utils.domData.set(node, boundElementDomDataKey, true);
2105
2723
  }
2106
2724
 
2107
- ko.dependentObservable(
2108
- function () {
2109
- // Ensure we have a nonnull binding context to work with
2110
- var bindingContextInstance = viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
2111
- ? viewModelOrBindingContext
2112
- : new ko.bindingContext(ko.utils.unwrapObservable(viewModelOrBindingContext));
2113
- var viewModel = bindingContextInstance['$data'];
2114
-
2115
- // Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
2116
- // we can easily recover it just by scanning up the node's ancestors in the DOM
2117
- // (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
2118
- if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
2119
- ko.storedBindingContextForNode(node, bindingContextInstance);
2120
-
2121
- // Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
2122
- var evaluatedBindings = (typeof bindings == "function") ? bindings(bindingContextInstance, node) : bindings;
2123
- parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContextInstance);
2124
-
2125
- if (parsedBindings) {
2126
- // First run all the inits, so bindings can register for notification on changes
2127
- if (initPhase === 0) {
2128
- initPhase = 1;
2129
- ko.utils.objectForEach(parsedBindings, function(bindingKey) {
2130
- var binding = ko.bindingHandlers[bindingKey];
2131
- if (binding && node.nodeType === 8)
2132
- validateThatBindingIsAllowedForVirtualElements(bindingKey);
2133
-
2134
- if (binding && typeof binding["init"] == "function") {
2135
- var handlerInitFn = binding["init"];
2136
- var initResult = handlerInitFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
2137
-
2138
- // If this binding handler claims to control descendant bindings, make a note of this
2139
- if (initResult && initResult['controlsDescendantBindings']) {
2140
- if (bindingHandlerThatControlsDescendantBindings !== undefined)
2141
- throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
2142
- bindingHandlerThatControlsDescendantBindings = bindingKey;
2143
- }
2725
+ // Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
2726
+ // we can easily recover it just by scanning up the node's ancestors in the DOM
2727
+ // (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
2728
+ if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
2729
+ ko.storedBindingContextForNode(node, bindingContext);
2730
+
2731
+ // Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
2732
+ var bindings;
2733
+ if (sourceBindings && typeof sourceBindings !== 'function') {
2734
+ bindings = sourceBindings;
2735
+ } else {
2736
+ var provider = ko.bindingProvider['instance'],
2737
+ getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
2738
+
2739
+ // Get the binding from the provider within a computed observable so that we can update the bindings whenever
2740
+ // the binding context is updated or if the binding provider accesses observables.
2741
+ var bindingsUpdater = ko.dependentObservable(
2742
+ function() {
2743
+ bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
2744
+ // Register a dependency on the binding context to support obsevable view models.
2745
+ if (bindings && bindingContext._subscribable)
2746
+ bindingContext._subscribable();
2747
+ return bindings;
2748
+ },
2749
+ null, { disposeWhenNodeIsRemoved: node }
2750
+ );
2751
+
2752
+ if (!bindings || !bindingsUpdater.isActive())
2753
+ bindingsUpdater = null;
2754
+ }
2755
+
2756
+ var bindingHandlerThatControlsDescendantBindings;
2757
+ if (bindings) {
2758
+ // Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding
2759
+ // context update), just return the value accessor from the binding. Otherwise, return a function that always gets
2760
+ // the latest binding value and registers a dependency on the binding updater.
2761
+ var getValueAccessor = bindingsUpdater
2762
+ ? function(bindingKey) {
2763
+ return function() {
2764
+ return evaluateValueAccessor(bindingsUpdater()[bindingKey]);
2765
+ };
2766
+ } : function(bindingKey) {
2767
+ return bindings[bindingKey];
2768
+ };
2769
+
2770
+ // Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated
2771
+ function allBindings() {
2772
+ return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor);
2773
+ }
2774
+ // The following is the 3.x allBindings API
2775
+ allBindings['get'] = function(key) {
2776
+ return bindings[key] && evaluateValueAccessor(getValueAccessor(key));
2777
+ };
2778
+ allBindings['has'] = function(key) {
2779
+ return key in bindings;
2780
+ };
2781
+
2782
+ // First put the bindings into the right order
2783
+ var orderedBindings = topologicalSortBindings(bindings);
2784
+
2785
+ // Go through the sorted bindings, calling init and update for each
2786
+ ko.utils.arrayForEach(orderedBindings, function(bindingKeyAndHandler) {
2787
+ // Note that topologicalSortBindings has already filtered out any nonexistent binding handlers,
2788
+ // so bindingKeyAndHandler.handler will always be nonnull.
2789
+ var handlerInitFn = bindingKeyAndHandler.handler["init"],
2790
+ handlerUpdateFn = bindingKeyAndHandler.handler["update"],
2791
+ bindingKey = bindingKeyAndHandler.key;
2792
+
2793
+ if (node.nodeType === 8) {
2794
+ validateThatBindingIsAllowedForVirtualElements(bindingKey);
2795
+ }
2796
+
2797
+ try {
2798
+ // Run init, ignoring any dependencies
2799
+ if (typeof handlerInitFn == "function") {
2800
+ ko.dependencyDetection.ignore(function() {
2801
+ var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
2802
+
2803
+ // If this binding handler claims to control descendant bindings, make a note of this
2804
+ if (initResult && initResult['controlsDescendantBindings']) {
2805
+ if (bindingHandlerThatControlsDescendantBindings !== undefined)
2806
+ throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
2807
+ bindingHandlerThatControlsDescendantBindings = bindingKey;
2144
2808
  }
2145
2809
  });
2146
- initPhase = 2;
2147
2810
  }
2148
2811
 
2149
- // ... then run all the updates, which might trigger changes even on the first evaluation
2150
- if (initPhase === 2) {
2151
- ko.utils.objectForEach(parsedBindings, function(bindingKey) {
2152
- var binding = ko.bindingHandlers[bindingKey];
2153
- if (binding && typeof binding["update"] == "function") {
2154
- var handlerUpdateFn = binding["update"];
2155
- handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
2156
- }
2157
- });
2812
+ // Run update in its own computed wrapper
2813
+ if (typeof handlerUpdateFn == "function") {
2814
+ ko.dependentObservable(
2815
+ function() {
2816
+ handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
2817
+ },
2818
+ null,
2819
+ { disposeWhenNodeIsRemoved: node }
2820
+ );
2158
2821
  }
2822
+ } catch (ex) {
2823
+ ex.message = "Unable to process binding \"" + bindingKey + ": " + bindings[bindingKey] + "\"\nMessage: " + ex.message;
2824
+ throw ex;
2159
2825
  }
2160
- },
2161
- null,
2162
- { disposeWhenNodeIsRemoved : node }
2163
- );
2826
+ });
2827
+ }
2164
2828
 
2165
2829
  return {
2166
- shouldBindDescendants: bindingHandlerThatControlsDescendantBindings === undefined
2830
+ 'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
2167
2831
  };
2168
2832
  };
2169
2833
 
2170
- var storedBindingContextDomDataKey = "__ko_bindingContext__";
2834
+ var storedBindingContextDomDataKey = ko.utils.domData.nextKey();
2171
2835
  ko.storedBindingContextForNode = function (node, bindingContext) {
2172
- if (arguments.length == 2)
2836
+ if (arguments.length == 2) {
2173
2837
  ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
2174
- else
2838
+ if (bindingContext._subscribable)
2839
+ bindingContext._subscribable._addNode(node);
2840
+ } else {
2175
2841
  return ko.utils.domData.get(node, storedBindingContextDomDataKey);
2842
+ }
2843
+ }
2844
+
2845
+ function getBindingContext(viewModelOrBindingContext) {
2846
+ return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
2847
+ ? viewModelOrBindingContext
2848
+ : new ko.bindingContext(viewModelOrBindingContext);
2176
2849
  }
2177
2850
 
2178
- ko.applyBindingsToNode = function (node, bindings, viewModel) {
2851
+ ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
2179
2852
  if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
2180
2853
  ko.virtualElements.normaliseVirtualElementDomStructure(node);
2181
- return applyBindingsToNodeInternal(node, bindings, viewModel, true);
2854
+ return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext), true);
2182
2855
  };
2183
2856
 
2184
- ko.applyBindingsToDescendants = function(viewModel, rootNode) {
2857
+ ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
2858
+ var context = getBindingContext(viewModelOrBindingContext);
2859
+ return ko.applyBindingAccessorsToNode(node, makeBindingAccessors(bindings, context, node), context);
2860
+ };
2861
+
2862
+ ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) {
2185
2863
  if (rootNode.nodeType === 1 || rootNode.nodeType === 8)
2186
- applyBindingsToDescendantsInternal(viewModel, rootNode, true);
2864
+ applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
2187
2865
  };
2188
2866
 
2189
- ko.applyBindings = function (viewModel, rootNode) {
2867
+ ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
2868
+ // If jQuery is loaded after Knockout, we won't initially have access to it. So save it here.
2869
+ if (!jQuery && window['jQuery']) {
2870
+ jQuery = window['jQuery'];
2871
+ }
2872
+
2190
2873
  if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
2191
2874
  throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
2192
2875
  rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
2193
2876
 
2194
- applyBindingsToNodeAndDescendantsInternal(viewModel, rootNode, true);
2877
+ applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
2195
2878
  };
2196
2879
 
2197
2880
  // Retrieving binding context from arbitrary nodes
@@ -2215,13 +2898,14 @@
2215
2898
  ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
2216
2899
  ko.exportSymbol('applyBindings', ko.applyBindings);
2217
2900
  ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
2901
+ ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
2218
2902
  ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode);
2219
2903
  ko.exportSymbol('contextFor', ko.contextFor);
2220
2904
  ko.exportSymbol('dataFor', ko.dataFor);
2221
2905
  })();
2222
2906
  var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
2223
2907
  ko.bindingHandlers['attr'] = {
2224
- 'update': function(element, valueAccessor, allBindingsAccessor) {
2908
+ 'update': function(element, valueAccessor, allBindings) {
2225
2909
  var value = ko.utils.unwrapObservable(valueAccessor()) || {};
2226
2910
  ko.utils.objectForEach(value, function(attrName, attrValue) {
2227
2911
  attrValue = ko.utils.unwrapObservable(attrValue);
@@ -2257,50 +2941,108 @@
2257
2941
  });
2258
2942
  }
2259
2943
  };
2260
- ko.bindingHandlers['checked'] = {
2261
- 'init': function (element, valueAccessor, allBindingsAccessor) {
2262
- var updateHandler = function() {
2263
- var valueToWrite;
2264
- if (element.type == "checkbox") {
2265
- valueToWrite = element.checked;
2266
- } else if ((element.type == "radio") && (element.checked)) {
2267
- valueToWrite = element.value;
2268
- } else {
2269
- return; // "checked" binding only responds to checkboxes and selected radio buttons
2270
- }
2944
+ (function() {
2271
2945
 
2272
- var modelValue = valueAccessor(), unwrappedValue = ko.utils.unwrapObservable(modelValue);
2273
- if ((element.type == "checkbox") && (unwrappedValue instanceof Array)) {
2274
- // For checkboxes bound to an array, we add/remove the checkbox value to that array
2275
- // This works for both observable and non-observable arrays
2276
- ko.utils.addOrRemoveItem(modelValue, element.value, element.checked);
2277
- } else {
2278
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
2946
+ ko.bindingHandlers['checked'] = {
2947
+ 'after': ['value', 'attr'],
2948
+ 'init': function (element, valueAccessor, allBindings) {
2949
+ function checkedValue() {
2950
+ return allBindings['has']('checkedValue')
2951
+ ? ko.utils.unwrapObservable(allBindings.get('checkedValue'))
2952
+ : element.value;
2279
2953
  }
2280
- };
2281
- ko.utils.registerEventHandler(element, "click", updateHandler);
2282
2954
 
2283
- // IE 6 won't allow radio buttons to be selected unless they have a name
2284
- if ((element.type == "radio") && !element.name)
2285
- ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
2286
- },
2287
- 'update': function (element, valueAccessor) {
2288
- var value = ko.utils.unwrapObservable(valueAccessor());
2955
+ function updateModel() {
2956
+ // This updates the model value from the view value.
2957
+ // It runs in response to DOM events (click) and changes in checkedValue.
2958
+ var isChecked = element.checked,
2959
+ elemValue = useCheckedValue ? checkedValue() : isChecked;
2289
2960
 
2290
- if (element.type == "checkbox") {
2291
- if (value instanceof Array) {
2292
- // When bound to an array, the checkbox being checked represents its value being present in that array
2293
- element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
2294
- } else {
2295
- // When bound to any other value (not an array), the checkbox being checked represents the value being trueish
2296
- element.checked = value;
2961
+ // When we're first setting up this computed, don't change any model state.
2962
+ if (ko.computedContext.isInitial()) {
2963
+ return;
2964
+ }
2965
+
2966
+ // We can ignore unchecked radio buttons, because some other radio
2967
+ // button will be getting checked, and that one can take care of updating state.
2968
+ if (isRadio && !isChecked) {
2969
+ return;
2970
+ }
2971
+
2972
+ var modelValue = ko.dependencyDetection.ignore(valueAccessor);
2973
+ if (isValueArray) {
2974
+ if (oldElemValue !== elemValue) {
2975
+ // When we're responding to the checkedValue changing, and the element is
2976
+ // currently checked, replace the old elem value with the new elem value
2977
+ // in the model array.
2978
+ if (isChecked) {
2979
+ ko.utils.addOrRemoveItem(modelValue, elemValue, true);
2980
+ ko.utils.addOrRemoveItem(modelValue, oldElemValue, false);
2981
+ }
2982
+
2983
+ oldElemValue = elemValue;
2984
+ } else {
2985
+ // When we're responding to the user having checked/unchecked a checkbox,
2986
+ // add/remove the element value to the model array.
2987
+ ko.utils.addOrRemoveItem(modelValue, elemValue, isChecked);
2988
+ }
2989
+ } else {
2990
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
2991
+ }
2992
+ };
2993
+
2994
+ function updateView() {
2995
+ // This updates the view value from the model value.
2996
+ // It runs in response to changes in the bound (checked) value.
2997
+ var modelValue = ko.utils.unwrapObservable(valueAccessor());
2998
+
2999
+ if (isValueArray) {
3000
+ // When a checkbox is bound to an array, being checked represents its value being present in that array
3001
+ element.checked = ko.utils.arrayIndexOf(modelValue, checkedValue()) >= 0;
3002
+ } else if (isCheckbox) {
3003
+ // When a checkbox is bound to any other value (not an array), being checked represents the value being trueish
3004
+ element.checked = modelValue;
3005
+ } else {
3006
+ // For radio buttons, being checked means that the radio button's value corresponds to the model value
3007
+ element.checked = (checkedValue() === modelValue);
3008
+ }
3009
+ };
3010
+
3011
+ var isCheckbox = element.type == "checkbox",
3012
+ isRadio = element.type == "radio";
3013
+
3014
+ // Only bind to check boxes and radio buttons
3015
+ if (!isCheckbox && !isRadio) {
3016
+ return;
2297
3017
  }
2298
- } else if (element.type == "radio") {
2299
- element.checked = (element.value == value);
3018
+
3019
+ var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array),
3020
+ oldElemValue = isValueArray ? checkedValue() : undefined,
3021
+ useCheckedValue = isRadio || isValueArray;
3022
+
3023
+ // IE 6 won't allow radio buttons to be selected unless they have a name
3024
+ if (isRadio && !element.name)
3025
+ ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
3026
+
3027
+ // Set up two computeds to update the binding:
3028
+
3029
+ // The first responds to changes in the checkedValue value and to element clicks
3030
+ ko.computed(updateModel, null, { disposeWhenNodeIsRemoved: element });
3031
+ ko.utils.registerEventHandler(element, "click", updateModel);
3032
+
3033
+ // The second responds to changes in the model value (the one associated with the checked binding)
3034
+ ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
2300
3035
  }
2301
- }
2302
- };
2303
- var classesWrittenByBindingKey = '__ko__cssValue';
3036
+ };
3037
+ ko.expressionRewriting.twoWayBindings['checked'] = true;
3038
+
3039
+ ko.bindingHandlers['checkedValue'] = {
3040
+ 'update': function (element, valueAccessor) {
3041
+ element.value = ko.utils.unwrapObservable(valueAccessor());
3042
+ }
3043
+ };
3044
+
3045
+ })();var classesWrittenByBindingKey = '__ko__cssValue';
2304
3046
  ko.bindingHandlers['css'] = {
2305
3047
  'update': function (element, valueAccessor) {
2306
3048
  var value = ko.utils.unwrapObservable(valueAccessor());
@@ -2336,19 +3078,19 @@
2336
3078
  // e.g. click:handler instead of the usual full-length event:{click:handler}
2337
3079
  function makeEventHandlerShortcut(eventName) {
2338
3080
  ko.bindingHandlers[eventName] = {
2339
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
3081
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
2340
3082
  var newValueAccessor = function () {
2341
3083
  var result = {};
2342
3084
  result[eventName] = valueAccessor();
2343
3085
  return result;
2344
3086
  };
2345
- return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
3087
+ return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindings, viewModel, bindingContext);
2346
3088
  }
2347
3089
  }
2348
3090
  }
2349
3091
 
2350
3092
  ko.bindingHandlers['event'] = {
2351
- 'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
3093
+ 'init' : function (element, valueAccessor, allBindings, viewModel, bindingContext) {
2352
3094
  var eventsToHandle = valueAccessor() || {};
2353
3095
  ko.utils.objectForEach(eventsToHandle, function(eventName) {
2354
3096
  if (typeof eventName == "string") {
@@ -2357,11 +3099,11 @@
2357
3099
  var handlerFunction = valueAccessor()[eventName];
2358
3100
  if (!handlerFunction)
2359
3101
  return;
2360
- var allBindings = allBindingsAccessor();
2361
3102
 
2362
3103
  try {
2363
3104
  // Take all the event args, and prefix with the viewmodel
2364
3105
  var argsForHandler = ko.utils.makeArray(arguments);
3106
+ viewModel = bindingContext['$data'];
2365
3107
  argsForHandler.unshift(viewModel);
2366
3108
  handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
2367
3109
  } finally {
@@ -2373,7 +3115,7 @@
2373
3115
  }
2374
3116
  }
2375
3117
 
2376
- var bubble = allBindings[eventName + 'Bubble'] !== false;
3118
+ var bubble = allBindings.get(eventName + 'Bubble') !== false;
2377
3119
  if (!bubble) {
2378
3120
  event.cancelBubble = true;
2379
3121
  if (event.stopPropagation)
@@ -2413,11 +3155,11 @@
2413
3155
  };
2414
3156
  };
2415
3157
  },
2416
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
3158
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
2417
3159
  return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
2418
3160
  },
2419
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2420
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
3161
+ 'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
3162
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext);
2421
3163
  }
2422
3164
  };
2423
3165
  ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
@@ -2425,7 +3167,7 @@
2425
3167
  var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
2426
3168
  var hasfocusLastValue = '__ko_hasfocusLastValue';
2427
3169
  ko.bindingHandlers['hasfocus'] = {
2428
- 'init': function(element, valueAccessor, allBindingsAccessor) {
3170
+ 'init': function(element, valueAccessor, allBindings) {
2429
3171
  var handleElementFocusChange = function(isFocused) {
2430
3172
  // Where possible, ignore which event was raised and determine focus state using activeElement,
2431
3173
  // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
@@ -2446,7 +3188,7 @@
2446
3188
  isFocused = (active === element);
2447
3189
  }
2448
3190
  var modelValue = valueAccessor();
2449
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', isFocused, true);
3191
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'hasfocus', isFocused, true);
2450
3192
 
2451
3193
  //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function
2452
3194
  element[hasfocusLastValue] = isFocused;
@@ -2468,8 +3210,10 @@
2468
3210
  }
2469
3211
  }
2470
3212
  };
3213
+ ko.expressionRewriting.twoWayBindings['hasfocus'] = true;
2471
3214
 
2472
3215
  ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
3216
+ ko.expressionRewriting.twoWayBindings['hasFocus'] = true;
2473
3217
  ko.bindingHandlers['html'] = {
2474
3218
  'init': function() {
2475
3219
  // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
@@ -2480,37 +3224,37 @@
2480
3224
  ko.utils.setHtml(element, valueAccessor());
2481
3225
  }
2482
3226
  };
2483
- var withIfDomDataKey = '__ko_withIfBindingData';
2484
3227
  // Makes a binding like with or if
2485
3228
  function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
2486
3229
  ko.bindingHandlers[bindingKey] = {
2487
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2488
- ko.utils.domData.set(element, withIfDomDataKey, {});
2489
- return { 'controlsDescendantBindings': true };
2490
- },
2491
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2492
- var withIfData = ko.utils.domData.get(element, withIfDomDataKey),
2493
- dataValue = ko.utils.unwrapObservable(valueAccessor()),
2494
- shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
2495
- isFirstRender = !withIfData.savedNodes,
2496
- needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate);
2497
-
2498
- if (needsRefresh) {
2499
- if (isFirstRender) {
2500
- withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
2501
- }
3230
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
3231
+ var didDisplayOnLastUpdate,
3232
+ savedNodes;
3233
+ ko.computed(function() {
3234
+ var dataValue = ko.utils.unwrapObservable(valueAccessor()),
3235
+ shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
3236
+ isFirstRender = !savedNodes,
3237
+ needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);
3238
+
3239
+ if (needsRefresh) {
3240
+ // Save a copy of the inner nodes on the initial update, but only if we have dependencies.
3241
+ if (isFirstRender && ko.computedContext.getDependenciesCount()) {
3242
+ savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
3243
+ }
2502
3244
 
2503
- if (shouldDisplay) {
2504
- if (!isFirstRender) {
2505
- ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes));
3245
+ if (shouldDisplay) {
3246
+ if (!isFirstRender) {
3247
+ ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
3248
+ }
3249
+ ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
3250
+ } else {
3251
+ ko.virtualElements.emptyNode(element);
2506
3252
  }
2507
- ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
2508
- } else {
2509
- ko.virtualElements.emptyNode(element);
2510
- }
2511
3253
 
2512
- withIfData.didDisplayOnLastUpdate = shouldDisplay;
2513
- }
3254
+ didDisplayOnLastUpdate = shouldDisplay;
3255
+ }
3256
+ }, null, { disposeWhenNodeIsRemoved: element });
3257
+ return { 'controlsDescendantBindings': true };
2514
3258
  }
2515
3259
  };
2516
3260
  ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
@@ -2525,19 +3269,7 @@
2525
3269
  return bindingContext['createChildContext'](dataValue);
2526
3270
  }
2527
3271
  );
2528
- function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
2529
- if (preferModelValue) {
2530
- if (modelValue !== ko.selectExtensions.readValue(element))
2531
- ko.selectExtensions.writeValue(element, modelValue);
2532
- }
2533
-
2534
- // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
2535
- // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
2536
- // change the model value to match the dropdown.
2537
- if (modelValue !== ko.selectExtensions.readValue(element))
2538
- ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
2539
- };
2540
-
3272
+ var captionPlaceholder = {};
2541
3273
  ko.bindingHandlers['options'] = {
2542
3274
  'init': function(element) {
2543
3275
  if (ko.utils.tagNameLower(element) !== "select")
@@ -2551,24 +3283,24 @@
2551
3283
  // Ensures that the binding processor doesn't try to bind the options
2552
3284
  return { 'controlsDescendantBindings': true };
2553
3285
  },
2554
- 'update': function (element, valueAccessor, allBindingsAccessor) {
3286
+ 'update': function (element, valueAccessor, allBindings) {
3287
+ function selectedOptions() {
3288
+ return ko.utils.arrayFilter(element.options, function (node) { return node.selected; });
3289
+ }
3290
+
2555
3291
  var selectWasPreviouslyEmpty = element.length == 0;
2556
3292
  var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
2557
-
2558
3293
  var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
2559
- var allBindings = allBindingsAccessor();
2560
- var includeDestroyed = allBindings['optionsIncludeDestroyed'];
2561
- var captionPlaceholder = {};
3294
+ var includeDestroyed = allBindings.get('optionsIncludeDestroyed');
3295
+ var arrayToDomNodeChildrenOptions = {};
2562
3296
  var captionValue;
3297
+ var filteredArray;
2563
3298
  var previousSelectedValues;
3299
+
2564
3300
  if (element.multiple) {
2565
- previousSelectedValues = ko.utils.arrayMap(element.selectedOptions || ko.utils.arrayFilter(element.childNodes, function (node) {
2566
- return node.tagName && (ko.utils.tagNameLower(node) === "option") && node.selected;
2567
- }), function (node) {
2568
- return ko.selectExtensions.readValue(node);
2569
- });
2570
- } else if (element.selectedIndex >= 0) {
2571
- previousSelectedValues = [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ];
3301
+ previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
3302
+ } else {
3303
+ previousSelectedValues = element.selectedIndex >= 0 ? [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ] : [];
2572
3304
  }
2573
3305
 
2574
3306
  if (unwrappedArray) {
@@ -2576,13 +3308,13 @@
2576
3308
  unwrappedArray = [unwrappedArray];
2577
3309
 
2578
3310
  // Filter out any entries marked as destroyed
2579
- var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
3311
+ filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
2580
3312
  return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
2581
3313
  });
2582
3314
 
2583
3315
  // If caption is included, add it to the array
2584
- if ('optionsCaption' in allBindings) {
2585
- captionValue = ko.utils.unwrapObservable(allBindings['optionsCaption']);
3316
+ if (allBindings['has']('optionsCaption')) {
3317
+ captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption'));
2586
3318
  // If caption value is null or undefined, don't show a caption
2587
3319
  if (captionValue !== null && captionValue !== undefined) {
2588
3320
  filteredArray.unshift(captionPlaceholder);
@@ -2590,7 +3322,6 @@
2590
3322
  }
2591
3323
  } else {
2592
3324
  // If a falsy value is provided (e.g. null), we'll simply empty the select element
2593
- unwrappedArray = [];
2594
3325
  }
2595
3326
 
2596
3327
  function applyToObject(object, predicate, defaultValue) {
@@ -2607,54 +3338,85 @@
2607
3338
  // The first is when the whole array is being updated directly from this binding handler.
2608
3339
  // The second is when an observable value for a specific array entry is updated.
2609
3340
  // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second.
3341
+ var itemUpdate = false;
2610
3342
  function optionForArrayItem(arrayEntry, index, oldOptions) {
2611
3343
  if (oldOptions.length) {
2612
- previousSelectedValues = oldOptions[0].selected && [ ko.selectExtensions.readValue(oldOptions[0]) ];
3344
+ previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
3345
+ itemUpdate = true;
2613
3346
  }
2614
- var option = document.createElement("option");
3347
+ var option = element.ownerDocument.createElement("option");
2615
3348
  if (arrayEntry === captionPlaceholder) {
2616
- ko.utils.setHtml(option, captionValue);
3349
+ ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
2617
3350
  ko.selectExtensions.writeValue(option, undefined);
2618
3351
  } else {
2619
3352
  // Apply a value to the option element
2620
- var optionValue = applyToObject(arrayEntry, allBindings['optionsValue'], arrayEntry);
3353
+ var optionValue = applyToObject(arrayEntry, allBindings.get('optionsValue'), arrayEntry);
2621
3354
  ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
2622
3355
 
2623
3356
  // Apply some text to the option element
2624
- var optionText = applyToObject(arrayEntry, allBindings['optionsText'], optionValue);
3357
+ var optionText = applyToObject(arrayEntry, allBindings.get('optionsText'), optionValue);
2625
3358
  ko.utils.setTextContent(option, optionText);
2626
3359
  }
2627
3360
  return [option];
2628
3361
  }
2629
3362
 
3363
+ // By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
3364
+ // problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
3365
+ arrayToDomNodeChildrenOptions['beforeRemove'] =
3366
+ function (option) {
3367
+ element.removeChild(option);
3368
+ };
3369
+
2630
3370
  function setSelectionCallback(arrayEntry, newOptions) {
2631
3371
  // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
2632
3372
  // That's why we first added them without selection. Now it's time to set the selection.
2633
- if (previousSelectedValues) {
3373
+ if (previousSelectedValues.length) {
2634
3374
  var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
2635
3375
  ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);
3376
+
3377
+ // If this option was changed from being selected during a single-item update, notify the change
3378
+ if (itemUpdate && !isSelected)
3379
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
2636
3380
  }
2637
3381
  }
2638
3382
 
2639
3383
  var callback = setSelectionCallback;
2640
- if (allBindings['optionsAfterRender']) {
3384
+ if (allBindings['has']('optionsAfterRender')) {
2641
3385
  callback = function(arrayEntry, newOptions) {
2642
3386
  setSelectionCallback(arrayEntry, newOptions);
2643
- ko.dependencyDetection.ignore(allBindings['optionsAfterRender'], null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
3387
+ ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
2644
3388
  }
2645
3389
  }
2646
3390
 
2647
- ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, null, callback);
3391
+ ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
2648
3392
 
2649
- // Clear previousSelectedValues so that future updates to individual objects don't get stale data
2650
- previousSelectedValues = null;
3393
+ ko.dependencyDetection.ignore(function () {
3394
+ if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) {
3395
+ // The model value is authoritative, so make sure its value is the one selected
3396
+ ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
3397
+ } else {
3398
+ // Determine if the selection has changed as a result of updating the options list
3399
+ var selectionChanged;
3400
+ if (element.multiple) {
3401
+ // For a multiple-select box, compare the new selection count to the previous one
3402
+ // But if nothing was selected before, the selection can't have changed
3403
+ selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
3404
+ } else {
3405
+ // For a single-select box, compare the current value to the previous value
3406
+ // But if nothing was selected before or nothing is selected now, just look for a change in selection
3407
+ selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
3408
+ ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
3409
+ : (previousSelectedValues.length || element.selectedIndex >= 0);
3410
+ }
2651
3411
 
2652
- if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
2653
- // Ensure consistency between model value and selected option.
2654
- // If the dropdown is being populated for the first time here (or was otherwise previously empty),
2655
- // the dropdown selection state is meaningless, so we preserve the model value.
2656
- ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.peekObservable(allBindings['value']), /* preferModelValue */ true);
2657
- }
3412
+ // Ensure consistency between model value and selected option.
3413
+ // If the dropdown was changed so that selection is no longer the same,
3414
+ // notify the value or selectedOptions binding.
3415
+ if (selectionChanged) {
3416
+ ko.utils.triggerEvent(element, "change");
3417
+ }
3418
+ }
3419
+ });
2658
3420
 
2659
3421
  // Workaround for IE bug
2660
3422
  ko.utils.ensureSelectElementIsRenderedCorrectly(element);
@@ -2663,16 +3425,17 @@
2663
3425
  element.scrollTop = previousScrollTop;
2664
3426
  }
2665
3427
  };
2666
- ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
3428
+ ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey();
2667
3429
  ko.bindingHandlers['selectedOptions'] = {
2668
- 'init': function (element, valueAccessor, allBindingsAccessor) {
3430
+ 'after': ['options', 'foreach'],
3431
+ 'init': function (element, valueAccessor, allBindings) {
2669
3432
  ko.utils.registerEventHandler(element, "change", function () {
2670
3433
  var value = valueAccessor(), valueToWrite = [];
2671
3434
  ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
2672
3435
  if (node.selected)
2673
3436
  valueToWrite.push(ko.selectExtensions.readValue(node));
2674
3437
  });
2675
- ko.expressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'selectedOptions', valueToWrite);
3438
+ ko.expressionRewriting.writeValueToProperty(value, allBindings, 'selectedOptions', valueToWrite);
2676
3439
  });
2677
3440
  },
2678
3441
  'update': function (element, valueAccessor) {
@@ -2688,6 +3451,7 @@
2688
3451
  }
2689
3452
  }
2690
3453
  };
3454
+ ko.expressionRewriting.twoWayBindings['selectedOptions'] = true;
2691
3455
  ko.bindingHandlers['style'] = {
2692
3456
  'update': function (element, valueAccessor) {
2693
3457
  var value = ko.utils.unwrapObservable(valueAccessor() || {});
@@ -2698,13 +3462,13 @@
2698
3462
  }
2699
3463
  };
2700
3464
  ko.bindingHandlers['submit'] = {
2701
- 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
3465
+ 'init': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
2702
3466
  if (typeof valueAccessor() != "function")
2703
3467
  throw new Error("The value for a submit binding must be a function");
2704
3468
  ko.utils.registerEventHandler(element, "submit", function (event) {
2705
3469
  var handlerReturnValue;
2706
3470
  var value = valueAccessor();
2707
- try { handlerReturnValue = value.call(viewModel, element); }
3471
+ try { handlerReturnValue = value.call(bindingContext['$data'], element); }
2708
3472
  finally {
2709
3473
  if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
2710
3474
  if (event.preventDefault)
@@ -2717,6 +3481,11 @@
2717
3481
  }
2718
3482
  };
2719
3483
  ko.bindingHandlers['text'] = {
3484
+ 'init': function() {
3485
+ // Prevent binding on the dynamically-injected text node (as developers are unlikely to expect that, and it has security implications).
3486
+ // It should also make things faster, as we no longer have to consider whether the text node might be bindable.
3487
+ return { 'controlsDescendantBindings': true };
3488
+ },
2720
3489
  'update': function (element, valueAccessor) {
2721
3490
  ko.utils.setTextContent(element, valueAccessor());
2722
3491
  }
@@ -2732,10 +3501,11 @@
2732
3501
  };
2733
3502
  ko.bindingHandlers['uniqueName'].currentIndex = 0;
2734
3503
  ko.bindingHandlers['value'] = {
2735
- 'init': function (element, valueAccessor, allBindingsAccessor) {
3504
+ 'after': ['options', 'foreach'],
3505
+ 'init': function (element, valueAccessor, allBindings) {
2736
3506
  // Always catch "change" event; possibly other events too if asked
2737
3507
  var eventsToCatch = ["change"];
2738
- var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
3508
+ var requestedEventsToCatch = allBindings.get("valueUpdate");
2739
3509
  var propertyChangedFired = false;
2740
3510
  if (requestedEventsToCatch) {
2741
3511
  if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
@@ -2748,7 +3518,7 @@
2748
3518
  propertyChangedFired = false;
2749
3519
  var modelValue = valueAccessor();
2750
3520
  var elementValue = ko.selectExtensions.readValue(element);
2751
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue);
3521
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'value', elementValue);
2752
3522
  }
2753
3523
 
2754
3524
  // Workaround for https://github.com/SteveSanderson/knockout/issues/122
@@ -2757,6 +3527,7 @@
2757
3527
  && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
2758
3528
  if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
2759
3529
  ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
3530
+ ko.utils.registerEventHandler(element, "focus", function () { propertyChangedFired = false });
2760
3531
  ko.utils.registerEventHandler(element, "blur", function() {
2761
3532
  if (propertyChangedFired) {
2762
3533
  valueUpdateHandler();
@@ -2776,30 +3547,36 @@
2776
3547
  ko.utils.registerEventHandler(element, eventName, handler);
2777
3548
  });
2778
3549
  },
2779
- 'update': function (element, valueAccessor) {
2780
- var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
3550
+ 'update': function (element, valueAccessor, allBindings) {
2781
3551
  var newValue = ko.utils.unwrapObservable(valueAccessor());
2782
3552
  var elementValue = ko.selectExtensions.readValue(element);
2783
3553
  var valueHasChanged = (newValue !== elementValue);
2784
3554
 
2785
3555
  if (valueHasChanged) {
2786
- var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
2787
- applyValueAction();
2788
-
2789
- // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
2790
- // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
2791
- // to apply the value as well.
2792
- var alsoApplyAsynchronously = valueIsSelectOption;
2793
- if (alsoApplyAsynchronously)
2794
- setTimeout(applyValueAction, 0);
3556
+ if (ko.utils.tagNameLower(element) === "select") {
3557
+ var allowUnset = allBindings.get('valueAllowUnset');
3558
+ var applyValueAction = function () {
3559
+ ko.selectExtensions.writeValue(element, newValue, allowUnset);
3560
+ };
3561
+ applyValueAction();
3562
+
3563
+ if (!allowUnset && newValue !== ko.selectExtensions.readValue(element)) {
3564
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
3565
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
3566
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
3567
+ } else {
3568
+ // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
3569
+ // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
3570
+ // to apply the value as well.
3571
+ setTimeout(applyValueAction, 0);
3572
+ }
3573
+ } else {
3574
+ ko.selectExtensions.writeValue(element, newValue);
3575
+ }
2795
3576
  }
2796
-
2797
- // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
2798
- // because you're not allowed to have a model value that disagrees with a visible UI selection.
2799
- if (valueIsSelectOption && (element.length > 0))
2800
- ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
2801
3577
  }
2802
3578
  };
3579
+ ko.expressionRewriting.twoWayBindings['value'] = true;
2803
3580
  ko.bindingHandlers['visible'] = {
2804
3581
  'update': function (element, valueAccessor) {
2805
3582
  var value = ko.utils.unwrapObservable(valueAccessor());
@@ -2909,7 +3686,7 @@
2909
3686
  function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, nodeName, templateEngine) {
2910
3687
  var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue);
2911
3688
  validateDataBindValuesForRewriting(dataBindKeyValueArray);
2912
- var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray);
3689
+ var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray, {'valueAccessors':true});
2913
3690
 
2914
3691
  // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
2915
3692
  // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
@@ -2939,7 +3716,7 @@
2939
3716
  return ko.memoization.memoize(function (domNode, bindingContext) {
2940
3717
  var nodeToBind = domNode.nextSibling;
2941
3718
  if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) {
2942
- ko.applyBindingsToNode(nodeToBind, bindings, bindingContext);
3719
+ ko.applyBindingAccessorsToNode(nodeToBind, bindings, bindingContext);
2943
3720
  }
2944
3721
  });
2945
3722
  }
@@ -2999,11 +3776,12 @@
2999
3776
  }
3000
3777
  };
3001
3778
 
3779
+ var dataDomDataPrefix = ko.utils.domData.nextKey() + "_";
3002
3780
  ko.templateSources.domElement.prototype['data'] = function(key /*, valueToWrite */) {
3003
3781
  if (arguments.length === 1) {
3004
- return ko.utils.domData.get(this.domElement, "templateSourceData_" + key);
3782
+ return ko.utils.domData.get(this.domElement, dataDomDataPrefix + key);
3005
3783
  } else {
3006
- ko.utils.domData.set(this.domElement, "templateSourceData_" + key, arguments[1]);
3784
+ ko.utils.domData.set(this.domElement, dataDomDataPrefix + key, arguments[1]);
3007
3785
  }
3008
3786
  };
3009
3787
 
@@ -3012,7 +3790,7 @@
3012
3790
  // For compatibility, you can also read "text"; it will be serialized from the nodes on demand.
3013
3791
  // Writing to "text" is still supported, but then the template data will not be available as DOM nodes.
3014
3792
 
3015
- var anonymousTemplatesDomDataKey = "__ko_anon_template__";
3793
+ var anonymousTemplatesDomDataKey = ko.utils.domData.nextKey();
3016
3794
  ko.templateSources.anonymousTemplate = function(element) {
3017
3795
  this.domElement = element;
3018
3796
  }
@@ -3051,12 +3829,11 @@
3051
3829
  _templateEngine = templateEngine;
3052
3830
  }
3053
3831
 
3054
- function invokeForEachNodeOrCommentInContinuousRange(firstNode, lastNode, action) {
3832
+ function invokeForEachNodeInContinuousRange(firstNode, lastNode, action) {
3055
3833
  var node, nextInQueue = firstNode, firstOutOfRangeNode = ko.virtualElements.nextSibling(lastNode);
3056
3834
  while (nextInQueue && ((node = nextInQueue) !== firstOutOfRangeNode)) {
3057
3835
  nextInQueue = ko.virtualElements.nextSibling(node);
3058
- if (node.nodeType === 1 || node.nodeType === 8)
3059
- action(node);
3836
+ action(node, nextInQueue);
3060
3837
  }
3061
3838
  }
3062
3839
 
@@ -3068,16 +3845,52 @@
3068
3845
  // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
3069
3846
 
3070
3847
  if (continuousNodeArray.length) {
3071
- var firstNode = continuousNodeArray[0], lastNode = continuousNodeArray[continuousNodeArray.length - 1];
3848
+ var firstNode = continuousNodeArray[0],
3849
+ lastNode = continuousNodeArray[continuousNodeArray.length - 1],
3850
+ parentNode = firstNode.parentNode,
3851
+ provider = ko.bindingProvider['instance'],
3852
+ preprocessNode = provider['preprocessNode'];
3853
+
3854
+ if (preprocessNode) {
3855
+ invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node, nextNodeInRange) {
3856
+ var nodePreviousSibling = node.previousSibling;
3857
+ var newNodes = preprocessNode.call(provider, node);
3858
+ if (newNodes) {
3859
+ if (node === firstNode)
3860
+ firstNode = newNodes[0] || nextNodeInRange;
3861
+ if (node === lastNode)
3862
+ lastNode = newNodes[newNodes.length - 1] || nodePreviousSibling;
3863
+ }
3864
+ });
3865
+
3866
+ // Because preprocessNode can change the nodes, including the first and last nodes, update continuousNodeArray to match.
3867
+ // We need the full set, including inner nodes, because the unmemoize step might remove the first node (and so the real
3868
+ // first node needs to be in the array).
3869
+ continuousNodeArray.length = 0;
3870
+ if (!firstNode) { // preprocessNode might have removed all the nodes, in which case there's nothing left to do
3871
+ return;
3872
+ }
3873
+ if (firstNode === lastNode) {
3874
+ continuousNodeArray.push(firstNode);
3875
+ } else {
3876
+ continuousNodeArray.push(firstNode, lastNode);
3877
+ ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
3878
+ }
3879
+ }
3072
3880
 
3073
3881
  // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
3074
3882
  // whereas a regular applyBindings won't introduce new memoized nodes
3075
- invokeForEachNodeOrCommentInContinuousRange(firstNode, lastNode, function(node) {
3076
- ko.applyBindings(bindingContext, node);
3883
+ invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
3884
+ if (node.nodeType === 1 || node.nodeType === 8)
3885
+ ko.applyBindings(bindingContext, node);
3077
3886
  });
3078
- invokeForEachNodeOrCommentInContinuousRange(firstNode, lastNode, function(node) {
3079
- ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
3887
+ invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
3888
+ if (node.nodeType === 1 || node.nodeType === 8)
3889
+ ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
3080
3890
  });
3891
+
3892
+ // Make sure any changes done by applyBindings or unmemoize are reflected in the array
3893
+ ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
3081
3894
  }
3082
3895
  }
3083
3896
 
@@ -3143,7 +3956,8 @@
3143
3956
  : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
3144
3957
 
3145
3958
  // Support selecting template as a function of the data being rendered
3146
- var templateName = typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
3959
+ var templateName = ko.isObservable(template) ? template()
3960
+ : typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
3147
3961
 
3148
3962
  var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
3149
3963
  if (renderMode == "replaceNode") {
@@ -3170,8 +3984,9 @@
3170
3984
  // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
3171
3985
  var executeTemplateForArrayItem = function (arrayValue, index) {
3172
3986
  // Support selecting template as a function of the data being rendered
3173
- arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as']);
3174
- arrayItemContext['$index'] = index;
3987
+ arrayItemContext = parentBindingContext['createChildContext'](arrayValue, options['as'], function(context) {
3988
+ context['$index'] = index;
3989
+ });
3175
3990
  var templateName = typeof(template) == 'function' ? template(arrayValue, arrayItemContext) : template;
3176
3991
  return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
3177
3992
  }
@@ -3200,7 +4015,7 @@
3200
4015
  }, null, { disposeWhenNodeIsRemoved: targetNode });
3201
4016
  };
3202
4017
 
3203
- var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
4018
+ var templateComputedDomDataKey = ko.utils.domData.nextKey();
3204
4019
  function disposeOldComputedAndStoreNewOne(element, newComputed) {
3205
4020
  var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
3206
4021
  if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
@@ -3212,24 +4027,30 @@
3212
4027
  'init': function(element, valueAccessor) {
3213
4028
  // Support anonymous templates
3214
4029
  var bindingValue = ko.utils.unwrapObservable(valueAccessor());
3215
- if ((typeof bindingValue != "string") && (!bindingValue['name']) && (element.nodeType == 1 || element.nodeType == 8)) {
4030
+ if (typeof bindingValue == "string" || bindingValue['name']) {
4031
+ // It's a named template - clear the element
4032
+ ko.virtualElements.emptyNode(element);
4033
+ } else {
3216
4034
  // It's an anonymous template - store the element contents, then clear the element
3217
- var templateNodes = element.nodeType == 1 ? element.childNodes : ko.virtualElements.childNodes(element),
4035
+ var templateNodes = ko.virtualElements.childNodes(element),
3218
4036
  container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent
3219
4037
  new ko.templateSources.anonymousTemplate(element)['nodes'](container);
3220
4038
  }
3221
4039
  return { 'controlsDescendantBindings': true };
3222
4040
  },
3223
- 'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
3224
- var templateName = ko.utils.unwrapObservable(valueAccessor()),
3225
- options = {},
3226
- shouldDisplay = true,
4041
+ 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
4042
+ var value = valueAccessor(),
3227
4043
  dataValue,
3228
- templateComputed = null;
4044
+ options = ko.utils.unwrapObservable(value),
4045
+ shouldDisplay = true,
4046
+ templateComputed = null,
4047
+ templateName;
3229
4048
 
3230
- if (typeof templateName != "string") {
3231
- options = templateName;
3232
- templateName = ko.utils.unwrapObservable(options['name']);
4049
+ if (typeof options == "string") {
4050
+ templateName = value;
4051
+ options = {};
4052
+ } else {
4053
+ templateName = options['name'];
3233
4054
 
3234
4055
  // Support "if"/"ifnot" conditions
3235
4056
  if ('if' in options)
@@ -3276,22 +4097,43 @@
3276
4097
 
3277
4098
  ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
3278
4099
  ko.exportSymbol('renderTemplate', ko.renderTemplate);
4100
+ // Go through the items that have been added and deleted and try to find matches between them.
4101
+ ko.utils.findMovesInArrayComparison = function (left, right, limitFailedCompares) {
4102
+ if (left.length && right.length) {
4103
+ var failedCompares, l, r, leftItem, rightItem;
4104
+ for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) {
4105
+ for (r = 0; rightItem = right[r]; ++r) {
4106
+ if (leftItem['value'] === rightItem['value']) {
4107
+ leftItem['moved'] = rightItem['index'];
4108
+ rightItem['moved'] = leftItem['index'];
4109
+ right.splice(r, 1); // This item is marked as moved; so remove it from right list
4110
+ failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures
4111
+ break;
4112
+ }
4113
+ }
4114
+ failedCompares += r;
4115
+ }
4116
+ }
4117
+ };
3279
4118
 
3280
4119
  ko.utils.compareArrays = (function () {
3281
4120
  var statusNotInOld = 'added', statusNotInNew = 'deleted';
3282
4121
 
3283
4122
  // Simple calculation based on Levenshtein distance.
3284
- function compareArrays(oldArray, newArray, dontLimitMoves) {
4123
+ function compareArrays(oldArray, newArray, options) {
4124
+ // For backward compatibility, if the third arg is actually a bool, interpret
4125
+ // it as the old parameter 'dontLimitMoves'. Newer code should use { dontLimitMoves: true }.
4126
+ options = (typeof options === 'boolean') ? { 'dontLimitMoves': options } : (options || {});
3285
4127
  oldArray = oldArray || [];
3286
4128
  newArray = newArray || [];
3287
4129
 
3288
4130
  if (oldArray.length <= newArray.length)
3289
- return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, dontLimitMoves);
4131
+ return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options);
3290
4132
  else
3291
- return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, dontLimitMoves);
4133
+ return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options);
3292
4134
  }
3293
4135
 
3294
- function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, dontLimitMoves) {
4136
+ function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, options) {
3295
4137
  var myMin = Math.min,
3296
4138
  myMax = Math.max,
3297
4139
  editDistanceMatrix = [],
@@ -3336,32 +4178,20 @@
3336
4178
  'value': smlArray[--smlIndex],
3337
4179
  'index': smlIndex });
3338
4180
  } else {
3339
- editScript.push({
3340
- 'status': "retained",
3341
- 'value': bigArray[--bigIndex] });
4181
+ --bigIndex;
3342
4182
  --smlIndex;
3343
- }
3344
- }
3345
-
3346
- if (notInSml.length && notInBig.length) {
3347
- // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
3348
- // smlIndexMax keeps the time complexity of this algorithm linear.
3349
- var limitFailedCompares = smlIndexMax * 10, failedCompares,
3350
- a, d, notInSmlItem, notInBigItem;
3351
- // Go through the items that have been added and deleted and try to find matches between them.
3352
- for (failedCompares = a = 0; (dontLimitMoves || failedCompares < limitFailedCompares) && (notInSmlItem = notInSml[a]); a++) {
3353
- for (d = 0; notInBigItem = notInBig[d]; d++) {
3354
- if (notInSmlItem['value'] === notInBigItem['value']) {
3355
- notInSmlItem['moved'] = notInBigItem['index'];
3356
- notInBigItem['moved'] = notInSmlItem['index'];
3357
- notInBig.splice(d,1); // This item is marked as moved; so remove it from notInBig list
3358
- failedCompares = d = 0; // Reset failed compares count because we're checking for consecutive failures
3359
- break;
3360
- }
4183
+ if (!options['sparse']) {
4184
+ editScript.push({
4185
+ 'status': "retained",
4186
+ 'value': bigArray[bigIndex] });
3361
4187
  }
3362
- failedCompares += d;
3363
4188
  }
3364
4189
  }
4190
+
4191
+ // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
4192
+ // smlIndexMax keeps the time complexity of this algorithm linear.
4193
+ ko.utils.findMovesInArrayComparison(notInSml, notInBig, smlIndexMax * 10);
4194
+
3365
4195
  return editScript.reverse();
3366
4196
  }
3367
4197
 
@@ -3369,7 +4199,6 @@
3369
4199
  })();
3370
4200
 
3371
4201
  ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3372
-
3373
4202
  (function () {
3374
4203
  // Objective:
3375
4204
  // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
@@ -3381,47 +4210,11 @@
3381
4210
  // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
3382
4211
  // You can use this, for example, to activate bindings on those nodes.
3383
4212
 
3384
- function fixUpNodesToBeMovedOrRemoved(contiguousNodeArray) {
3385
- // Before moving, deleting, or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
3386
- // them against what is in the DOM right now. It may be that some of the nodes have already been removed from the document,
3387
- // or that new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
3388
- // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
3389
- // So, this function translates the old "map" output array into its best guess of what set of current DOM nodes should be removed.
3390
- //
3391
- // Rules:
3392
- // [A] Any leading nodes that aren't in the document any more should be ignored
3393
- // These most likely correspond to memoization nodes that were already removed during binding
3394
- // See https://github.com/SteveSanderson/knockout/pull/440
3395
- // [B] We want to output a contiguous series of nodes that are still in the document. So, ignore any nodes that
3396
- // have already been removed, and include any nodes that have been inserted among the previous collection
3397
-
3398
- // Rule [A]
3399
- while (contiguousNodeArray.length && !ko.utils.domNodeIsAttachedToDocument(contiguousNodeArray[0]))
3400
- contiguousNodeArray.splice(0, 1);
3401
-
3402
- // Rule [B]
3403
- if (contiguousNodeArray.length > 1) {
3404
- // Build up the actual new contiguous node set
3405
- var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
3406
- while (current !== last) {
3407
- current = current.nextSibling;
3408
- if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
3409
- return;
3410
- newContiguousSet.push(current);
3411
- }
3412
-
3413
- // ... then mutate the input array to match this.
3414
- // (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
3415
- Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
3416
- }
3417
- return contiguousNodeArray;
3418
- }
3419
-
3420
4213
  function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
3421
4214
  // Map this array value inside a dependentObservable so we re-map when any dependency changes
3422
4215
  var mappedNodes = [];
3423
4216
  var dependentObservable = ko.dependentObservable(function() {
3424
- var newMappedNodes = mapping(valueToMap, index, fixUpNodesToBeMovedOrRemoved(mappedNodes)) || [];
4217
+ var newMappedNodes = mapping(valueToMap, index, ko.utils.fixUpContinuousNodeArray(mappedNodes, containerNode)) || [];
3425
4218
 
3426
4219
  // On subsequent evaluations, just replace the previously-inserted DOM nodes
3427
4220
  if (mappedNodes.length > 0) {
@@ -3432,13 +4225,13 @@
3432
4225
 
3433
4226
  // Replace the contents of the mappedNodes array, thereby updating the record
3434
4227
  // of which nodes would be deleted if valueToMap was itself later removed
3435
- mappedNodes.splice(0, mappedNodes.length);
4228
+ mappedNodes.length = 0;
3436
4229
  ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
3437
4230
  }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
3438
4231
  return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
3439
4232
  }
3440
4233
 
3441
- var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
4234
+ var lastMappingResultDomDataKey = ko.utils.domData.nextKey();
3442
4235
 
3443
4236
  ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
3444
4237
  // Compare the provided array against the previous one
@@ -3465,9 +4258,9 @@
3465
4258
  mapData = lastMappingResult[oldPosition];
3466
4259
  if (newMappingResultIndex !== oldPosition)
3467
4260
  itemsForMoveCallbacks[editScriptIndex] = mapData;
3468
- // Since updating the index might change the nodes, do so before calling fixUpNodesToBeMovedOrRemoved
4261
+ // Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray
3469
4262
  mapData.indexObservable(newMappingResultIndex++);
3470
- fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes);
4263
+ ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
3471
4264
  newMappingResult.push(mapData);
3472
4265
  itemsToProcess.push(mapData);
3473
4266
  }
@@ -3496,7 +4289,7 @@
3496
4289
  mapData.dependentObservable.dispose();
3497
4290
 
3498
4291
  // Queue these nodes for later removal
3499
- nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
4292
+ nodesToDelete.push.apply(nodesToDelete, ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode));
3500
4293
  if (options['beforeRemove']) {
3501
4294
  itemsForBeforeRemoveCallbacks[i] = mapData;
3502
4295
  itemsToProcess.push(mapData);
@@ -3595,7 +4388,7 @@
3595
4388
  // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later,
3596
4389
  // which KO internally refers to as version "2", so older versions are no longer detected.
3597
4390
  var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
3598
- if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl']))
4391
+ if (!jQuery || !(jQuery['tmpl']))
3599
4392
  return 0;
3600
4393
  // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
3601
4394
  try {