├── .travis.yml ├── src ├── version.js ├── binding │ ├── defaultBindings │ │ ├── click.js │ │ ├── uniqueName.js │ │ ├── visible.js │ │ ├── html.js │ │ ├── style.js │ │ ├── text.js │ │ ├── enableDisable.js │ │ ├── css.js │ │ ├── submit.js │ │ ├── selectedOptions.js │ │ ├── attr.js │ │ ├── ifIfnotWith.js │ │ ├── foreach.js │ │ ├── event.js │ │ ├── hasfocus.js │ │ ├── value.js │ │ └── checked.js │ ├── bindingProvider.js │ ├── selectExtensions.js │ └── editDetection │ │ └── compareArrays.js ├── namespace.js ├── google-closure-compiler-utils.js ├── templating │ ├── native │ │ └── nativeTemplateEngine.js │ ├── templateEngine.js │ ├── templateRewriting.js │ ├── jquery.tmpl │ │ └── jqueryTmplTemplateEngine.js │ └── templateSources.js ├── subscribables │ ├── dependencyDetection.js │ ├── observable.js │ ├── subscribable.js │ ├── extenders.js │ ├── mappingHelpers.js │ ├── observableArray.changeTracking.js │ └── observableArray.js ├── utils.domData.js ├── memoization.js ├── utils.domNodeDisposal.js └── utils.domManipulation.js ├── spec ├── lib │ ├── jasmine.extensions.css │ ├── innershiv.js │ ├── jasmine-1.2.0 │ │ ├── MIT.LICENSE │ │ ├── jasmine-tap.js │ │ └── jasmine.css │ └── jasmine.extensions.js ├── defaultBindings │ ├── uniqueNameBehaviors.js │ ├── submitBehaviors.js │ ├── styleBehaviors.js │ ├── clickBehaviors.js │ ├── visibleBehaviors.js │ ├── enableDisableBehaviors.js │ ├── htmlBehaviors.js │ ├── attrBehaviors.js │ ├── cssBehaviors.js │ ├── ifnotBehaviors.js │ ├── hasfocusBehaviors.js │ ├── textBehaviors.js │ ├── eventBehaviors.js │ ├── ifBehaviors.js │ └── selectedOptionsBehaviors.js ├── extenderBehaviors.js ├── dependentObservableDomBehaviors.js ├── utilsBehaviors.js ├── memoizationBehaviors.js ├── jsonPostingBehaviors.js ├── runner.node.js ├── bindingPreprocessingBehaviors.js ├── domNodeDisposalBehaviors.js ├── arrayEditDetectionBehaviors.js ├── subscribableBehaviors.js ├── nodePreprocessingBehaviors.js ├── runner.phantom.js └── runner.html ├── .npmignore ├── .gitignore ├── package.json ├── README.md └── Gruntfile.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | ko.version = "##VERSION##"; 2 | 3 | ko.exportSymbol('version', ko.version); 4 | -------------------------------------------------------------------------------- /spec/lib/jasmine.extensions.css: -------------------------------------------------------------------------------- 1 | #HTMLReporter { 2 | position: absolute; 3 | background: #EEE; 4 | } 5 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/click.js: -------------------------------------------------------------------------------- 1 | // 'click' is just a shorthand for the usual full-length event:{click:handler} 2 | makeEventHandlerShortcut('click'); 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Only package the build output for npm. Developers who want sources/tests can get them from the KO repo. 2 | * 3 | !build/output/knockout-latest.js 4 | !build/output/knockout-latest.debug.js 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.swp 3 | *.csproj.user 4 | bin 5 | obj 6 | *.pdb 7 | _ReSharper* 8 | *.ReSharper.user 9 | *.ReSharper 10 | desktop.ini 11 | .eprj 12 | perf/* 13 | *.orig 14 | 15 | .DS_Store 16 | npm-debug.log 17 | node_modules -------------------------------------------------------------------------------- /src/namespace.js: -------------------------------------------------------------------------------- 1 | // Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler). 2 | // In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable. 3 | var ko = typeof koExports !== 'undefined' ? koExports : {}; 4 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/uniqueName.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['uniqueName'] = { 2 | 'init': function (element, valueAccessor) { 3 | if (valueAccessor()) { 4 | var name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex); 5 | ko.utils.setElementName(element, name); 6 | } 7 | } 8 | }; 9 | ko.bindingHandlers['uniqueName'].currentIndex = 0; 10 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/visible.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['visible'] = { 2 | 'update': function (element, valueAccessor) { 3 | var value = ko.utils.unwrapObservable(valueAccessor()); 4 | var isCurrentlyVisible = !(element.style.display == "none"); 5 | if (value && !isCurrentlyVisible) 6 | element.style.display = ""; 7 | else if ((!value) && isCurrentlyVisible) 8 | element.style.display = "none"; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/html.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['html'] = { 2 | 'init': function() { 3 | // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications) 4 | return { 'controlsDescendantBindings': true }; 5 | }, 6 | 'update': function (element, valueAccessor) { 7 | // setHtml will unwrap the value if needed 8 | ko.utils.setHtml(element, valueAccessor()); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/style.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['style'] = { 2 | 'update': function (element, valueAccessor) { 3 | var value = ko.utils.unwrapObservable(valueAccessor() || {}); 4 | ko.utils.objectForEach(value, function(styleName, styleValue) { 5 | styleValue = ko.utils.unwrapObservable(styleValue); 6 | element.style[styleName] = styleValue || ""; // Empty string removes the value, whereas null/undefined have no effect 7 | }); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/text.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['text'] = { 2 | 'init': function() { 3 | // Prevent binding on the dynamically-injected text node (as developers are unlikely to expect that, and it has security implications). 4 | // It should also make things faster, as we no longer have to consider whether the text node might be bindable. 5 | return { 'controlsDescendantBindings': true }; 6 | }, 7 | 'update': function (element, valueAccessor) { 8 | ko.utils.setTextContent(element, valueAccessor()); 9 | } 10 | }; 11 | ko.virtualElements.allowedBindings['text'] = true; 12 | -------------------------------------------------------------------------------- /spec/defaultBindings/uniqueNameBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Unique Name', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should apply a different name to each element', function () { 5 | testNode.innerHTML = "
"; 6 | ko.applyBindings({}, testNode); 7 | 8 | expect(testNode.childNodes[0].name.length > 0).toEqual(true); 9 | expect(testNode.childNodes[1].name.length > 0).toEqual(true); 10 | expect(testNode.childNodes[0].name == testNode.childNodes[1].name).toEqual(false); 11 | }); 12 | }); -------------------------------------------------------------------------------- /src/binding/defaultBindings/enableDisable.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['enable'] = { 2 | 'update': function (element, valueAccessor) { 3 | var value = ko.utils.unwrapObservable(valueAccessor()); 4 | if (value && element.disabled) 5 | element.removeAttribute("disabled"); 6 | else if ((!value) && (!element.disabled)) 7 | element.disabled = true; 8 | } 9 | }; 10 | 11 | ko.bindingHandlers['disable'] = { 12 | 'update': function (element, valueAccessor) { 13 | ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/google-closure-compiler-utils.js: -------------------------------------------------------------------------------- 1 | // Google Closure Compiler helpers (used only to make the minified file smaller) 2 | ko.exportSymbol = function(koPath, object) { 3 | var tokens = koPath.split("."); 4 | 5 | // In the future, "ko" may become distinct from "koExports" (so that non-exported objects are not reachable) 6 | // At that point, "target" would be set to: (typeof koExports !== "undefined" ? koExports : ko) 7 | var target = ko; 8 | 9 | for (var i = 0; i < tokens.length - 1; i++) 10 | target = target[tokens[i]]; 11 | target[tokens[tokens.length - 1]] = object; 12 | }; 13 | ko.exportProperty = function(owner, publicName, object) { 14 | owner[publicName] = object; 15 | }; 16 | -------------------------------------------------------------------------------- /spec/defaultBindings/submitBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Submit', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should invoke the supplied function on submit and prevent default action, using model as \'this\' param and the form node as a param to the handler', function () { 5 | var firstParamStored; 6 | var model = { wasCalled: false, doCall: function (firstParam) { this.wasCalled = true; firstParamStored = firstParam; } }; 7 | testNode.innerHTML = "
"; 8 | var formNode = testNode.childNodes[0]; 9 | ko.applyBindings(model, testNode); 10 | ko.utils.triggerEvent(testNode.childNodes[0], "submit"); 11 | expect(model.wasCalled).toEqual(true); 12 | expect(firstParamStored).toEqual(formNode); 13 | }); 14 | }); -------------------------------------------------------------------------------- /spec/defaultBindings/styleBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: CSS style', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should give the element the specified CSS style value', function () { 5 | var myObservable = new ko.observable("red"); 6 | testNode.innerHTML = "
Hallo
"; 7 | ko.applyBindings({ colorValue: myObservable }, testNode); 8 | 9 | expect(testNode.childNodes[0].style.backgroundColor).toEqualOneOf(["red", "#ff0000"]); // Opera returns style color values in #rrggbb notation, unlike other browsers 10 | myObservable("green"); 11 | expect(testNode.childNodes[0].style.backgroundColor).toEqualOneOf(["green", "#008000"]); 12 | myObservable(undefined); 13 | expect(testNode.childNodes[0].style.backgroundColor).toEqual(""); 14 | }); 15 | }); -------------------------------------------------------------------------------- /spec/defaultBindings/clickBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Click', function() { 2 | // This is just a special case of the "event" binding, so not necessary to respecify all its behaviours 3 | beforeEach(jasmine.prepareTestNode); 4 | 5 | it('Should invoke the supplied function on click, using model as \'this\' param and first arg, and event as second arg', function () { 6 | var model = { 7 | wasCalled: false, 8 | doCall: function (arg1, arg2) { 9 | this.wasCalled = true; 10 | expect(arg1).toEqual(model); 11 | expect(arg2.type).toEqual("click"); 12 | } 13 | }; 14 | testNode.innerHTML = ""; 15 | ko.applyBindings(model, testNode); 16 | ko.utils.triggerEvent(testNode.childNodes[0], "click"); 17 | expect(model.wasCalled).toEqual(true); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/css.js: -------------------------------------------------------------------------------- 1 | var classesWrittenByBindingKey = '__ko__cssValue'; 2 | ko.bindingHandlers['css'] = { 3 | 'update': function (element, valueAccessor) { 4 | var value = ko.utils.unwrapObservable(valueAccessor()); 5 | if (typeof value == "object") { 6 | ko.utils.objectForEach(value, function(className, shouldHaveClass) { 7 | shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass); 8 | ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass); 9 | }); 10 | } else { 11 | value = String(value || ''); // Make sure we don't try to store or set a non-string value 12 | ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false); 13 | element[classesWrittenByBindingKey] = value; 14 | ko.utils.toggleDomNodeCssClass(element, value, true); 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /spec/defaultBindings/visibleBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Visible', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should display the node only when the value is true', function () { 5 | var observable = new ko.observable(false); 6 | testNode.innerHTML = ""; 7 | ko.applyBindings({ myModelProperty: observable }, testNode); 8 | 9 | expect(testNode.childNodes[0].style.display).toEqual("none"); 10 | observable(true); 11 | expect(testNode.childNodes[0].style.display).toEqual(""); 12 | }); 13 | 14 | it('Should unwrap observables implicitly', function () { 15 | var observable = new ko.observable(false); 16 | testNode.innerHTML = ""; 17 | ko.applyBindings({ myModelProperty: observable }, testNode); 18 | expect(testNode.childNodes[0].style.display).toEqual("none"); 19 | }); 20 | }); -------------------------------------------------------------------------------- /spec/lib/innershiv.js: -------------------------------------------------------------------------------- 1 | // http://bit.ly/ishiv | WTFPL License 2 | window.innerShiv=function(){function h(c,e,b){return/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i.test(b)?c:e+">"}var c,e=document,j,g="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" ");return function(d,i){if(!c&&(c=e.createElement("div"),c.innerHTML="",j=c.childNodes.length!==1)){for(var b=e.createDocumentFragment(),f=g.length;f--;)b.createElement(g[f]);b.appendChild(c)}d=d.replace(/^\s\s*/,"").replace(/\s\s*$/,"").replace(/)<[^<]*)*<\/script>/gi,"").replace(/(<([\w:]+)[^>]*?)\/>/g,h);c.innerHTML=(b=d.match(/^<(tbody|tr|td|col|colgroup|thead|tfoot)/i))?""+d+"
":d;b=b?c.getElementsByTagName(b[1])[0].parentNode:c;if(i===!1)return b.childNodes;for(var f=e.createDocumentFragment(),k=b.childNodes.length;k--;)f.appendChild(b.firstChild);return f}}(); 3 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/submit.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['submit'] = { 2 | 'init': function (element, valueAccessor, allBindings, viewModel, bindingContext) { 3 | if (typeof valueAccessor() != "function") 4 | throw new Error("The value for a submit binding must be a function"); 5 | ko.utils.registerEventHandler(element, "submit", function (event) { 6 | var handlerReturnValue; 7 | var value = valueAccessor(); 8 | try { handlerReturnValue = value.call(bindingContext['$data'], element); } 9 | finally { 10 | if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true. 11 | if (event.preventDefault) 12 | event.preventDefault(); 13 | else 14 | event.returnValue = false; 15 | } 16 | } 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /spec/extenderBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Extenders', function() { 3 | it('Should be able to extend any subscribable', function () { 4 | ko.extenders.setDummyProperty = function(target, value) { 5 | target.dummyProperty = value; 6 | }; 7 | 8 | var subscribable = new ko.subscribable(); 9 | expect(subscribable.dummyProperty).toEqual(undefined); 10 | 11 | subscribable.extend({ setDummyProperty : 123 }); 12 | expect(subscribable.dummyProperty).toEqual(123); 13 | }); 14 | 15 | it('Should be able to chain extenders', function() { 16 | ko.extenders.wrapInParentObject = function(target, value) { 17 | return { inner : target, extend : target.extend } 18 | }; 19 | var underlyingSubscribable = new ko.subscribable(); 20 | var result = underlyingSubscribable.extend({ wrapInParentObject:true }).extend({ wrapInParentObject:true }); 21 | expect(result.inner.inner).toEqual(underlyingSubscribable); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/templating/native/nativeTemplateEngine.js: -------------------------------------------------------------------------------- 1 | ko.nativeTemplateEngine = function () { 2 | this['allowTemplateRewriting'] = false; 3 | } 4 | 5 | ko.nativeTemplateEngine.prototype = new ko.templateEngine(); 6 | ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine; 7 | ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) { 8 | var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly 9 | templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null, 10 | templateNodes = templateNodesFunc ? templateSource['nodes']() : null; 11 | 12 | if (templateNodes) { 13 | return ko.utils.makeArray(templateNodes.cloneNode(true).childNodes); 14 | } else { 15 | var templateText = templateSource['text'](); 16 | return ko.utils.parseHtmlFragment(templateText); 17 | } 18 | }; 19 | 20 | ko.nativeTemplateEngine.instance = new ko.nativeTemplateEngine(); 21 | ko.setTemplateEngine(ko.nativeTemplateEngine.instance); 22 | 23 | ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine); 24 | -------------------------------------------------------------------------------- /spec/lib/jasmine-1.2.0/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockout", 3 | "description": "Knockout makes it easier to create rich, responsive UIs with JavaScript", 4 | "homepage": "http://knockoutjs.com/", 5 | "version": "3.0.0beta", 6 | "license": "MIT", 7 | "author": "The Knockout.js team", 8 | "main": "build/output/knockout-latest.debug.js", 9 | "scripts": { 10 | "prepublish": "grunt", 11 | "test": "node spec/runner.node.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/SteveSanderson/knockout.git" 16 | }, 17 | "bugs": "https://github.com/SteveSanderson/knockout/issues", 18 | "testling": { 19 | "preprocess": "build/build.sh", 20 | "html": "spec/runner.html?src=build/output/knockout-latest.js&testling=true", 21 | "browsers": [ 22 | "ie/6..latest", 23 | "chrome/20..latest", 24 | "firefox/3..latest", 25 | "safari/5.0.5..latest", 26 | "opera/11.0..latest", 27 | "iphone/6..latest", 28 | "ipad/6..latest" 29 | ] 30 | }, 31 | "licenses": [ 32 | { "type": "MIT", "url": "http://www.opensource.org/licenses/mit-license.php" } 33 | ], 34 | "devDependencies": { 35 | "grunt": "~0.4.1", 36 | "grunt-cli": "~0.1.0", 37 | "closure-compiler": "~0.2.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/subscribables/dependencyDetection.js: -------------------------------------------------------------------------------- 1 | 2 | ko.dependencyDetection = (function () { 3 | var _frames = []; 4 | 5 | return { 6 | begin: function (callback) { 7 | _frames.push(callback && { callback: callback, distinctDependencies:[] }); 8 | }, 9 | 10 | end: function () { 11 | _frames.pop(); 12 | }, 13 | 14 | registerDependency: function (subscribable) { 15 | if (!ko.isSubscribable(subscribable)) 16 | throw new Error("Only subscribable things can act as dependencies"); 17 | if (_frames.length > 0) { 18 | var topFrame = _frames[_frames.length - 1]; 19 | if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0) 20 | return; 21 | topFrame.distinctDependencies.push(subscribable); 22 | topFrame.callback(subscribable); 23 | } 24 | }, 25 | 26 | ignore: function(callback, callbackTarget, callbackArgs) { 27 | try { 28 | _frames.push(null); 29 | return callback.apply(callbackTarget, callbackArgs || []); 30 | } finally { 31 | _frames.pop(); 32 | } 33 | } 34 | }; 35 | })(); 36 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/selectedOptions.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['selectedOptions'] = { 2 | 'after': ['options', 'foreach'], 3 | 'init': function (element, valueAccessor, allBindings) { 4 | ko.utils.registerEventHandler(element, "change", function () { 5 | var value = valueAccessor(), valueToWrite = []; 6 | ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) { 7 | if (node.selected) 8 | valueToWrite.push(ko.selectExtensions.readValue(node)); 9 | }); 10 | ko.expressionRewriting.writeValueToProperty(value, allBindings, 'selectedOptions', valueToWrite); 11 | }); 12 | }, 13 | 'update': function (element, valueAccessor) { 14 | if (ko.utils.tagNameLower(element) != "select") 15 | throw new Error("values binding applies only to SELECT elements"); 16 | 17 | var newValue = ko.utils.unwrapObservable(valueAccessor()); 18 | if (newValue && typeof newValue.length == "number") { 19 | ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) { 20 | var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0; 21 | ko.utils.setOptionNodeSelectionState(node, isSelected); 22 | }); 23 | } 24 | } 25 | }; 26 | ko.expressionRewriting.twoWayBindings['selectedOptions'] = true; 27 | -------------------------------------------------------------------------------- /spec/dependentObservableDomBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Dependent Observable DOM', function() { 3 | it('Should register DOM node disposal callback only if active after the initial evaluation', function() { 4 | // Set up an active one 5 | var nodeForActive = document.createElement('DIV'), 6 | observable = ko.observable('initial'), 7 | activeDependentObservable = ko.dependentObservable({ read: function() { return observable(); }, disposeWhenNodeIsRemoved: nodeForActive }); 8 | var nodeForInactive = document.createElement('DIV') 9 | inactiveDependentObservable = ko.dependentObservable({ read: function() { return 123; }, disposeWhenNodeIsRemoved: nodeForInactive }); 10 | 11 | expect(activeDependentObservable.isActive()).toEqual(true); 12 | expect(inactiveDependentObservable.isActive()).toEqual(false); 13 | 14 | // Infer existence of disposal callbacks from presence/absence of DOM data. This is really just an implementation detail, 15 | // and so it's unusual to rely on it in a spec. However, the presence/absence of the callback isn't exposed in any other way, 16 | // and if the implementation ever changes, this spec should automatically fail because we're checking for both the positive 17 | // and negative cases. 18 | expect(ko.utils.domData.clear(nodeForActive)).toEqual(true); // There was a callback 19 | expect(ko.utils.domData.clear(nodeForInactive)).toEqual(false); // There was no callback 20 | }); 21 | }) 22 | -------------------------------------------------------------------------------- /spec/utilsBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('unwrapObservable', function() { 2 | it('Should return the underlying value of observables', function() { 3 | var someObject = { abc: 123 }, 4 | observablePrimitiveValue = ko.observable(123), 5 | observableObjectValue = ko.observable(someObject), 6 | observableNullValue = ko.observable(null), 7 | observableUndefinedValue = ko.observable(undefined), 8 | computedValue = ko.computed(function() { return observablePrimitiveValue() + 1; }); 9 | 10 | expect(ko.utils.unwrapObservable(observablePrimitiveValue)).toBe(123); 11 | expect(ko.utils.unwrapObservable(observableObjectValue)).toBe(someObject); 12 | expect(ko.utils.unwrapObservable(observableNullValue)).toBe(null); 13 | expect(ko.utils.unwrapObservable(observableUndefinedValue)).toBe(undefined); 14 | expect(ko.utils.unwrapObservable(computedValue)).toBe(124); 15 | }); 16 | 17 | it('Should return the supplied value for non-observables', function() { 18 | var someObject = { abc: 123 }; 19 | 20 | expect(ko.utils.unwrapObservable(123)).toBe(123); 21 | expect(ko.utils.unwrapObservable(someObject)).toBe(someObject); 22 | expect(ko.utils.unwrapObservable(null)).toBe(null); 23 | expect(ko.utils.unwrapObservable(undefined)).toBe(undefined); 24 | }); 25 | 26 | it('Should be aliased as ko.unwrap', function() { 27 | expect(ko.unwrap).toBe(ko.utils.unwrapObservable); 28 | expect(ko.unwrap(ko.observable('some value'))).toBe('some value'); 29 | }); 30 | }); -------------------------------------------------------------------------------- /spec/defaultBindings/enableDisableBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Enable/Disable', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Enable means the node is enabled only when the value is true', function () { 5 | var observable = new ko.observable(); 6 | testNode.innerHTML = ""; 7 | ko.applyBindings({ myModelProperty: observable }, testNode); 8 | 9 | expect(testNode.childNodes[0].disabled).toEqual(true); 10 | observable(1); 11 | expect(testNode.childNodes[0].disabled).toEqual(false); 12 | }); 13 | 14 | it('Disable means the node is enabled only when the value is false', function () { 15 | var observable = new ko.observable(); 16 | testNode.innerHTML = ""; 17 | ko.applyBindings({ myModelProperty: observable }, testNode); 18 | 19 | expect(testNode.childNodes[0].disabled).toEqual(false); 20 | observable(1); 21 | expect(testNode.childNodes[0].disabled).toEqual(true); 22 | }); 23 | 24 | it('Enable should unwrap observables implicitly', function () { 25 | var observable = new ko.observable(false); 26 | testNode.innerHTML = ""; 27 | ko.applyBindings({ myModelProperty: observable }, testNode); 28 | expect(testNode.childNodes[0].disabled).toEqual(true); 29 | }); 30 | 31 | it('Disable should unwrap observables implicitly', function () { 32 | var observable = new ko.observable(false); 33 | testNode.innerHTML = ""; 34 | ko.applyBindings({ myModelProperty: observable }, testNode); 35 | expect(testNode.childNodes[0].disabled).toEqual(false); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/utils.domData.js: -------------------------------------------------------------------------------- 1 | 2 | ko.utils.domData = new (function () { 3 | var uniqueId = 0; 4 | var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime(); 5 | var dataStore = {}; 6 | 7 | function getAll(node, createIfNotFound) { 8 | var dataStoreKey = node[dataStoreKeyExpandoPropertyName]; 9 | var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey]; 10 | if (!hasExistingDataStore) { 11 | if (!createIfNotFound) 12 | return undefined; 13 | dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++; 14 | dataStore[dataStoreKey] = {}; 15 | } 16 | return dataStore[dataStoreKey]; 17 | } 18 | 19 | return { 20 | get: function (node, key) { 21 | var allDataForNode = getAll(node, false); 22 | return allDataForNode === undefined ? undefined : allDataForNode[key]; 23 | }, 24 | set: function (node, key, value) { 25 | if (value === undefined) { 26 | // Make sure we don't actually create a new domData key if we are actually deleting a value 27 | if (getAll(node, false) === undefined) 28 | return; 29 | } 30 | var allDataForNode = getAll(node, true); 31 | allDataForNode[key] = value; 32 | }, 33 | clear: function (node) { 34 | var dataStoreKey = node[dataStoreKeyExpandoPropertyName]; 35 | if (dataStoreKey) { 36 | delete dataStore[dataStoreKey]; 37 | node[dataStoreKeyExpandoPropertyName] = null; 38 | return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended 39 | } 40 | return false; 41 | }, 42 | 43 | nextKey: function () { 44 | return (uniqueId++) + dataStoreKeyExpandoPropertyName; 45 | } 46 | }; 47 | })(); 48 | 49 | ko.exportSymbol('utils.domData', ko.utils.domData); 50 | ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully 51 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/attr.js: -------------------------------------------------------------------------------- 1 | var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' }; 2 | ko.bindingHandlers['attr'] = { 3 | 'update': function(element, valueAccessor, allBindings) { 4 | var value = ko.utils.unwrapObservable(valueAccessor()) || {}; 5 | ko.utils.objectForEach(value, function(attrName, attrValue) { 6 | attrValue = ko.utils.unwrapObservable(attrValue); 7 | 8 | // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely 9 | // when someProp is a "no value"-like value (strictly null, false, or undefined) 10 | // (because the absence of the "checked" attr is how to mark an element as not checked, etc.) 11 | var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined); 12 | if (toRemove) 13 | element.removeAttribute(attrName); 14 | 15 | // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the 16 | // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior, 17 | // but instead of figuring out the mode, we'll just set the attribute through the Javascript 18 | // property for IE <= 8. 19 | if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) { 20 | attrName = attrHtmlToJavascriptMap[attrName]; 21 | if (toRemove) 22 | element.removeAttribute(attrName); 23 | else 24 | element[attrName] = attrValue; 25 | } else if (!toRemove) { 26 | element.setAttribute(attrName, attrValue.toString()); 27 | } 28 | 29 | // Treat "name" specially - although you can think of it as an attribute, it also needs 30 | // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333) 31 | // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing 32 | // entirely, and there's no strong reason to allow for such casing in HTML. 33 | if (attrName === "name") { 34 | ko.utils.setElementName(element, toRemove ? "" : attrValue.toString()); 35 | } 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/ifIfnotWith.js: -------------------------------------------------------------------------------- 1 | var withIfDomDataKey = ko.utils.domData.nextKey(); 2 | // Makes a binding like with or if 3 | function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) { 4 | ko.bindingHandlers[bindingKey] = { 5 | 'init': function(element) { 6 | ko.utils.domData.set(element, withIfDomDataKey, {}); 7 | return { 'controlsDescendantBindings': true }; 8 | }, 9 | 'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) { 10 | var withIfData = ko.utils.domData.get(element, withIfDomDataKey), 11 | dataValue = ko.utils.unwrapObservable(valueAccessor()), 12 | shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue 13 | isFirstRender = !withIfData.savedNodes, 14 | needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate); 15 | 16 | if (needsRefresh) { 17 | if (isFirstRender) { 18 | withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */); 19 | } 20 | 21 | if (shouldDisplay) { 22 | if (!isFirstRender) { 23 | ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes)); 24 | } 25 | ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element); 26 | } else { 27 | ko.virtualElements.emptyNode(element); 28 | } 29 | 30 | withIfData.didDisplayOnLastUpdate = shouldDisplay; 31 | } 32 | } 33 | }; 34 | ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings 35 | ko.virtualElements.allowedBindings[bindingKey] = true; 36 | } 37 | 38 | // Construct the actual binding handlers 39 | makeWithIfBinding('if'); 40 | makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */); 41 | makeWithIfBinding('with', true /* isWith */, false /* isNot */, 42 | function(bindingContext, dataValue) { 43 | return bindingContext['createChildContext'](dataValue); 44 | } 45 | ); 46 | -------------------------------------------------------------------------------- /spec/memoizationBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | function parseMemoCommentHtml(commentHtml) { 3 | commentHtml = commentHtml.replace("", ""); 4 | return ko.memoization.parseMemoText(commentHtml); 5 | } 6 | 7 | describe('Memoization', function() { 8 | it("Should only accept a function", function () { 9 | var threw = false; 10 | try { ko.memoization.memoize({}) } 11 | catch (ex) { threw = true; } 12 | expect(threw).toEqual(true); 13 | }); 14 | 15 | it("Should return an HTML comment", function () { 16 | var result = ko.memoization.memoize(function () { }); 17 | expect(typeof result).toEqual("string"); 18 | expect(result.substring(0, 4)).toEqual(""; 31 | }, 32 | 33 | unmemoize: function (memoId, callbackParams) { 34 | var callback = memos[memoId]; 35 | if (callback === undefined) 36 | throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized."); 37 | try { 38 | callback.apply(null, callbackParams || []); 39 | return true; 40 | } 41 | finally { delete memos[memoId]; } 42 | }, 43 | 44 | unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) { 45 | var memos = []; 46 | findMemoNodes(domNode, memos); 47 | for (var i = 0, j = memos.length; i < j; i++) { 48 | var node = memos[i].domNode; 49 | var combinedParams = [node]; 50 | if (extraCallbackParamsArray) 51 | ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray); 52 | ko.memoization.unmemoize(memos[i].memoId, combinedParams); 53 | node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again 54 | if (node.parentNode) 55 | node.parentNode.removeChild(node); // If possible, erase it totally (not always possible - someone else might just hold a reference to it then call unmemoizeDomNodeAndDescendants again) 56 | } 57 | }, 58 | 59 | parseMemoText: function (memoText) { 60 | var match = memoText.match(/^\[ko_memo\:(.*?)\]$/); 61 | return match ? match[1] : null; 62 | } 63 | }; 64 | })(); 65 | 66 | ko.exportSymbol('memoization', ko.memoization); 67 | ko.exportSymbol('memoization.memoize', ko.memoization.memoize); 68 | ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize); 69 | ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText); 70 | ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants); 71 | -------------------------------------------------------------------------------- /src/subscribables/extenders.js: -------------------------------------------------------------------------------- 1 | ko.extenders = { 2 | 'throttle': function(target, options) { 3 | // Throttling means two things: 4 | 5 | // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies 6 | // notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate. 7 | // By default, use "debounce" algorithm, which will limit evaluations until changes have stopped 8 | // for a certain period of time. Optionally, "throttle" will re-evaluate at a specified rate. 9 | var throttleType = 'debounce', 10 | throttleRate = 0, 11 | throttleNoTrailingEval = false, 12 | writeTimeoutInstance = null, 13 | optionsValid; 14 | if (typeof options === 'number') { 15 | // For backwards compatibility with v2.3, allow numeric parameter and default to "debounce". 16 | throttleRate = options; 17 | } 18 | else { 19 | throttleType = options['type']; 20 | throttleRate = options['delay']; 21 | throttleNoTrailingEval = options['noTrailing'] === true; 22 | } 23 | 24 | // Validate parameters 25 | optionsValid = ( 26 | throttleRate >= 0 27 | && (throttleType === 'debounce' 28 | || throttleType === 'throttle') 29 | ); 30 | 31 | if (!optionsValid) { 32 | return target; // do not modify 33 | } 34 | else { 35 | // Add properties to be picked up by dependentObservable.js internals 36 | target['throttleType'] = throttleType; 37 | target['throttleEvaluation'] = throttleRate; 38 | target['throttleNoTrailing'] = throttleNoTrailingEval; 39 | } 40 | 41 | // (2) For writable targets (observables, or writable dependent observables), we throttle *writes* 42 | // so the target cannot change value synchronously or faster than a certain rate 43 | if (ko.isWriteableObservable(target)) { 44 | return ko.dependentObservable({ 45 | 'read': target, 46 | 'write': function(value) { 47 | // TODO: Apply "throttle" algorithm to writes, if desired. Currently uses "debounce" 48 | clearTimeout(writeTimeoutInstance); 49 | writeTimeoutInstance = setTimeout(function() { 50 | target(value); 51 | }, throttleRate); 52 | } 53 | }); 54 | } 55 | else { 56 | // (3) For read-only targets, just return target to avoid overhead of wrapping in a new dependent observable. 57 | return target; 58 | } 59 | }, 60 | 61 | 'notify': function(target, notifyWhen) { 62 | target["equalityComparer"] = notifyWhen == "always" ? 63 | null : // null equalityComparer means to always notify 64 | valuesArePrimitiveAndEqual; 65 | } 66 | }; 67 | 68 | var primitiveTypes = { 'undefined':1, 'boolean':1, 'number':1, 'string':1 }; 69 | function valuesArePrimitiveAndEqual(a, b) { 70 | var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes); 71 | return oldValueIsPrimitive ? (a === b) : false; 72 | } 73 | 74 | function applyExtenders(requestedExtenders) { 75 | var target = this; 76 | if (requestedExtenders) { 77 | ko.utils.objectForEach(requestedExtenders, function(key, value) { 78 | var extenderHandler = ko.extenders[key]; 79 | if (typeof extenderHandler == 'function') { 80 | target = extenderHandler(target, value) || target; 81 | } 82 | }); 83 | } 84 | return target; 85 | } 86 | 87 | ko.exportSymbol('extenders', ko.extenders); 88 | -------------------------------------------------------------------------------- /spec/domNodeDisposalBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('DOM node disposal', function() { 3 | beforeEach(jasmine.prepareTestNode); 4 | 5 | it('Should run registered disposal callbacks when a node is cleaned', function () { 6 | var didRun = false; 7 | ko.utils.domNodeDisposal.addDisposeCallback(testNode, function() { didRun = true }); 8 | 9 | expect(didRun).toEqual(false); 10 | ko.cleanNode(testNode); 11 | expect(didRun).toEqual(true); 12 | }); 13 | 14 | it('Should run registered disposal callbacks on descendants when a node is cleaned', function () { 15 | var didRun = false; 16 | var childNode = document.createElement("DIV"); 17 | var grandChildNode = document.createElement("DIV"); 18 | testNode.appendChild(childNode); 19 | childNode.appendChild(grandChildNode); 20 | ko.utils.domNodeDisposal.addDisposeCallback(grandChildNode, function() { didRun = true }); 21 | 22 | expect(didRun).toEqual(false); 23 | ko.cleanNode(testNode); 24 | expect(didRun).toEqual(true); 25 | }); 26 | 27 | it('Should run registered disposal callbacks and detach from DOM when a node is removed', function () { 28 | var didRun = false; 29 | var childNode = document.createElement("DIV"); 30 | testNode.appendChild(childNode); 31 | ko.utils.domNodeDisposal.addDisposeCallback(childNode, function() { didRun = true }); 32 | 33 | expect(didRun).toEqual(false); 34 | expect(testNode.childNodes.length).toEqual(1); 35 | ko.removeNode(childNode); 36 | expect(didRun).toEqual(true); 37 | expect(testNode.childNodes.length).toEqual(0); 38 | }); 39 | 40 | it('Should be able to remove previously-registered disposal callbacks', function() { 41 | var didRun = false; 42 | var callback = function() { didRun = true }; 43 | ko.utils.domNodeDisposal.addDisposeCallback(testNode, callback); 44 | 45 | expect(didRun).toEqual(false); 46 | ko.utils.domNodeDisposal.removeDisposeCallback(testNode, callback); 47 | ko.cleanNode(testNode); 48 | expect(didRun).toEqual(false); // Didn't run only because we removed it 49 | }); 50 | 51 | it('Should be able to attach disposal callback to a node that has been cloned', function() { 52 | // This represents bug https://github.com/SteveSanderson/knockout/issues/324 53 | // IE < 9 copies expando properties when cloning nodes, so if the node already has some DOM data associated with it, 54 | // the DOM data key will be copied too. This causes a problem for disposal, because if the original node gets disposed, 55 | // the shared DOM data is disposed, and then it becomes an error to try to set new DOM data on the clone. 56 | // The solution is to make the DOM-data-setting logic able to recover from the scenario by detecting that the original 57 | // DOM data is gone, and therefore recreating a new DOM data store for the clone. 58 | 59 | // Create an element with DOM data 60 | var originalNode = document.createElement("DIV"); 61 | ko.utils.domNodeDisposal.addDisposeCallback(originalNode, function() { }); 62 | 63 | // Clone it, then dispose it. Then check it's still safe to associate DOM data with the clone. 64 | var cloneNode = originalNode.cloneNode(true); 65 | ko.cleanNode(originalNode); 66 | ko.utils.domNodeDisposal.addDisposeCallback(cloneNode, function() { }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/binding/bindingProvider.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var defaultBindingAttributeName = "data-bind"; 3 | 4 | ko.bindingProvider = function() { 5 | this.bindingCache = {}; 6 | }; 7 | 8 | ko.utils.extend(ko.bindingProvider.prototype, { 9 | 'nodeHasBindings': function(node) { 10 | switch (node.nodeType) { 11 | case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element 12 | case 8: return ko.virtualElements.hasBindingValue(node); // Comment node 13 | default: return false; 14 | } 15 | }, 16 | 17 | 'getBindings': function(node, bindingContext) { 18 | var bindingsString = this['getBindingsString'](node, bindingContext); 19 | return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null; 20 | }, 21 | 22 | 'getBindingAccessors': function(node, bindingContext) { 23 | var bindingsString = this['getBindingsString'](node, bindingContext); 24 | return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, {'valueAccessors':true}) : null; 25 | }, 26 | 27 | // The following function is only used internally by this default provider. 28 | // It's not part of the interface definition for a general binding provider. 29 | 'getBindingsString': function(node, bindingContext) { 30 | switch (node.nodeType) { 31 | case 1: return node.getAttribute(defaultBindingAttributeName); // Element 32 | case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node 33 | default: return null; 34 | } 35 | }, 36 | 37 | // The following function is only used internally by this default provider. 38 | // It's not part of the interface definition for a general binding provider. 39 | 'parseBindingsString': function(bindingsString, bindingContext, node, options) { 40 | try { 41 | var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options); 42 | return bindingFunction(bindingContext, node); 43 | } catch (ex) { 44 | ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message; 45 | throw ex; 46 | } 47 | } 48 | }); 49 | 50 | ko.bindingProvider['instance'] = new ko.bindingProvider(); 51 | 52 | function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) { 53 | var cacheKey = bindingsString + (options && options['valueAccessors'] || ''); 54 | return cache[cacheKey] 55 | || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); 56 | } 57 | 58 | function createBindingsStringEvaluator(bindingsString, options) { 59 | // Build the source for a function that evaluates "expression" 60 | // For each scope variable, add an extra level of "with" nesting 61 | // Example result: with(sc1) { with(sc0) { return (expression) } } 62 | var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options), 63 | functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}"; 64 | return new Function("$context", "$element", functionBody); 65 | } 66 | })(); 67 | 68 | ko.exportSymbol('bindingProvider', ko.bindingProvider); 69 | -------------------------------------------------------------------------------- /spec/defaultBindings/attrBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Attr', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should be able to set arbitrary attribute values', function() { 5 | var model = { myValue: "first value" }; 6 | testNode.innerHTML = "
"; 7 | ko.applyBindings(model, testNode); 8 | expect(testNode.childNodes[0].getAttribute("firstAttribute")).toEqual("first value"); 9 | expect(testNode.childNodes[0].getAttribute("second-attribute")).toEqual("true"); 10 | }); 11 | 12 | it('Should be able to set \"name\" attribute, even on IE6-7', function() { 13 | var myValue = ko.observable("myName"); 14 | testNode.innerHTML = ""; 15 | ko.applyBindings({ myValue: myValue }, testNode); 16 | expect(testNode.childNodes[0].name).toEqual("myName"); 17 | if (testNode.childNodes[0].outerHTML) { // Old Firefox doesn't support outerHTML 18 | expect(testNode.childNodes[0].outerHTML).toMatch('name="?myName"?'); 19 | } 20 | expect(testNode.childNodes[0].getAttribute("name")).toEqual("myName"); 21 | 22 | // Also check we can remove it (which, for a name attribute, means setting it to an empty string) 23 | myValue(false); 24 | expect(testNode.childNodes[0].name).toEqual(""); 25 | if (testNode.childNodes[0].outerHTML) { // Old Firefox doesn't support outerHTML 26 | expect(testNode.childNodes[0].outerHTML).toNotMatch('name="?([^">]+)'); 27 | } 28 | expect(testNode.childNodes[0].getAttribute("name")).toEqual(""); 29 | }); 30 | 31 | it('Should respond to changes in an observable value', function() { 32 | var model = { myprop : ko.observable("initial value") }; 33 | testNode.innerHTML = "
"; 34 | ko.applyBindings(model, testNode); 35 | expect(testNode.childNodes[0].getAttribute("someAttrib")).toEqual("initial value"); 36 | 37 | // Change the observable; observe it reflected in the DOM 38 | model.myprop("new value"); 39 | expect(testNode.childNodes[0].getAttribute("someAttrib")).toEqual("new value"); 40 | }); 41 | 42 | it('Should remove the attribute if the value is strictly false, null, or undefined', function() { 43 | var model = { myprop : ko.observable() }; 44 | testNode.innerHTML = "
"; 45 | ko.applyBindings(model, testNode); 46 | ko.utils.arrayForEach([false, null, undefined], function(testValue) { 47 | model.myprop("nonempty value"); 48 | expect(testNode.childNodes[0].getAttribute("someAttrib")).toEqual("nonempty value"); 49 | model.myprop(testValue); 50 | expect(testNode.childNodes[0].getAttribute("someAttrib")).toEqual(null); 51 | }); 52 | }); 53 | 54 | it('Should be able to set class attribute and access it using className property', function() { 55 | var model = { myprop : ko.observable("newClass") }; 56 | testNode.innerHTML = "
"; 57 | expect(testNode.childNodes[0].className).toEqual("oldClass"); 58 | ko.applyBindings(model, testNode); 59 | expect(testNode.childNodes[0].className).toEqual("newClass"); 60 | // Should be able to clear class also 61 | model.myprop(undefined); 62 | expect(testNode.childNodes[0].className).toEqual(""); 63 | expect(testNode.childNodes[0].getAttribute("class")).toEqual(null); 64 | }); 65 | }); -------------------------------------------------------------------------------- /spec/defaultBindings/cssBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: CSS class name', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should give the element the specific CSS class only when the specified value is true', function () { 5 | var observable1 = new ko.observable(); 6 | var observable2 = new ko.observable(true); 7 | testNode.innerHTML = "
Hallo
"; 8 | ko.applyBindings({ someModelProperty: observable1, anotherModelProperty: observable2 }, testNode); 9 | 10 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 unrelatedClass2 anotherRule"); 11 | observable1(true); 12 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 unrelatedClass2 anotherRule myRule"); 13 | observable2(false); 14 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 unrelatedClass2 myRule"); 15 | }); 16 | 17 | it('Should give the element a single CSS class without a leading space when the specified value is true', function() { 18 | var observable1 = new ko.observable(); 19 | testNode.innerHTML = "
Hallo
"; 20 | ko.applyBindings({ someModelProperty: observable1 }, testNode); 21 | 22 | expect(testNode.childNodes[0].className).toEqual(""); 23 | observable1(true); 24 | expect(testNode.childNodes[0].className).toEqual("myRule"); 25 | }); 26 | 27 | it('Should toggle multiple CSS classes if specified as a single string separated by spaces', function() { 28 | var observable1 = new ko.observable(); 29 | testNode.innerHTML = "
Hallo
"; 30 | ko.applyBindings({ someModelProperty: observable1 }, testNode); 31 | 32 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1"); 33 | observable1(true); 34 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 myRule _another-Rule123"); 35 | observable1(false); 36 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1"); 37 | }); 38 | 39 | it('Should set/change dynamic CSS class(es) if string is specified', function() { 40 | var observable1 = new ko.observable(""); 41 | testNode.innerHTML = "
Hallo
"; 42 | ko.applyBindings({ someModelProperty: observable1 }, testNode); 43 | 44 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1"); 45 | observable1("my-Rule"); 46 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 my-Rule"); 47 | observable1("another_Rule my-Rule"); 48 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1 another_Rule my-Rule"); 49 | observable1(undefined); 50 | expect(testNode.childNodes[0].className).toEqual("unrelatedClass1"); 51 | }); 52 | 53 | it('Should work with any arbitrary class names', function() { 54 | // See https://github.com/SteveSanderson/knockout/issues/704 55 | var observable1 = new ko.observable(); 56 | testNode.innerHTML = "
Something
"; 57 | ko.applyBindings({ someModelProperty: observable1 }, testNode); 58 | 59 | expect(testNode.childNodes[0].className).toEqual(""); 60 | observable1(true); 61 | expect(testNode.childNodes[0].className).toEqual("complex/className complex.className"); 62 | }); 63 | }); -------------------------------------------------------------------------------- /spec/defaultBindings/ifnotBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Ifnot', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should remove descendant nodes from the document (and not bind them) if the value is truey', function() { 5 | testNode.innerHTML = "
"; 6 | expect(testNode.childNodes[0].childNodes.length).toEqual(1); 7 | ko.applyBindings({ someItem: null, condition: true }, testNode); 8 | expect(testNode.childNodes[0].childNodes.length).toEqual(0); 9 | }); 10 | 11 | it('Should leave descendant nodes in the document (and bind them) if the value is falsey, independently of the active template engine', function() { 12 | ko.setTemplateEngine(new ko.templateEngine()); // This template engine will just throw errors if you try to use it 13 | testNode.innerHTML = "
"; 14 | expect(testNode.childNodes.length).toEqual(1); 15 | ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' }, condition: false }, testNode); 16 | expect(testNode.childNodes[0].childNodes.length).toEqual(1); 17 | expect(testNode.childNodes[0].childNodes[0]).toContainText("Child prop value"); 18 | }); 19 | 20 | it('Should leave descendant nodes unchanged if the value is falsey and remains falsey when changed', function() { 21 | var someItem = ko.observable(false); 22 | testNode.innerHTML = "
"; 23 | var originalNode = testNode.childNodes[0].childNodes[0]; 24 | 25 | // Value is initially true, so nodes are retained 26 | ko.applyBindings({ someItem: someItem }, testNode); 27 | expect(testNode.childNodes[0].childNodes[0]).toContainText("false"); 28 | expect(testNode.childNodes[0].childNodes[0]).toEqual(originalNode); 29 | 30 | // Change the value to a different falsey value 31 | someItem(0); 32 | expect(testNode.childNodes[0].childNodes[0]).toContainText("0"); 33 | expect(testNode.childNodes[0].childNodes[0]).toEqual(originalNode); 34 | }); 35 | 36 | it('Should toggle the presence and bindedness of descendant nodes according to the falsiness of the value', function() { 37 | var someItem = ko.observable(undefined); 38 | var condition = ko.observable(true); 39 | testNode.innerHTML = "
"; 40 | ko.applyBindings({ someItem: someItem, condition: condition }, testNode); 41 | 42 | // First it's not there 43 | expect(testNode.childNodes[0].childNodes.length).toEqual(0); 44 | 45 | // Then it's there 46 | someItem({ occasionallyExistentChildProp: 'Child prop value' }); 47 | condition(false); 48 | expect(testNode.childNodes[0].childNodes.length).toEqual(1); 49 | expect(testNode.childNodes[0].childNodes[0]).toContainText("Child prop value"); 50 | 51 | // Then it's gone again 52 | condition(true); 53 | someItem(null); 54 | expect(testNode.childNodes[0].childNodes.length).toEqual(0); 55 | }); 56 | 57 | it('Should not interfere with binding context', function() { 58 | testNode.innerHTML = "
Parents:
"; 59 | ko.applyBindings({ }, testNode); 60 | expect(testNode.childNodes[0]).toContainText("Parents: 0"); 61 | expect(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).toEqual(0); 62 | }); 63 | }); -------------------------------------------------------------------------------- /src/templating/templateEngine.js: -------------------------------------------------------------------------------- 1 | // If you want to make a custom template engine, 2 | // 3 | // [1] Inherit from this class (like ko.nativeTemplateEngine does) 4 | // [2] Override 'renderTemplateSource', supplying a function with this signature: 5 | // 6 | // function (templateSource, bindingContext, options) { 7 | // // - templateSource.text() is the text of the template you should render 8 | // // - bindingContext.$data is the data you should pass into the template 9 | // // - you might also want to make bindingContext.$parent, bindingContext.$parents, 10 | // // and bindingContext.$root available in the template too 11 | // // - options gives you access to any other properties set on "data-bind: { template: options }" 12 | // // 13 | // // Return value: an array of DOM nodes 14 | // } 15 | // 16 | // [3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature: 17 | // 18 | // function (script) { 19 | // // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result" 20 | // // For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }' 21 | // } 22 | // 23 | // This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables. 24 | // If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does) 25 | // and then you don't need to override 'createJavaScriptEvaluatorBlock'. 26 | 27 | ko.templateEngine = function () { }; 28 | 29 | ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) { 30 | throw new Error("Override renderTemplateSource"); 31 | }; 32 | 33 | ko.templateEngine.prototype['createJavaScriptEvaluatorBlock'] = function (script) { 34 | throw new Error("Override createJavaScriptEvaluatorBlock"); 35 | }; 36 | 37 | ko.templateEngine.prototype['makeTemplateSource'] = function(template, templateDocument) { 38 | // Named template 39 | if (typeof template == "string") { 40 | templateDocument = templateDocument || document; 41 | var elem = templateDocument.getElementById(template); 42 | if (!elem) 43 | throw new Error("Cannot find template with ID " + template); 44 | return new ko.templateSources.domElement(elem); 45 | } else if ((template.nodeType == 1) || (template.nodeType == 8)) { 46 | // Anonymous template 47 | return new ko.templateSources.anonymousTemplate(template); 48 | } else 49 | throw new Error("Unknown template type: " + template); 50 | }; 51 | 52 | ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options, templateDocument) { 53 | var templateSource = this['makeTemplateSource'](template, templateDocument); 54 | return this['renderTemplateSource'](templateSource, bindingContext, options); 55 | }; 56 | 57 | ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templateDocument) { 58 | // Skip rewriting if requested 59 | if (this['allowTemplateRewriting'] === false) 60 | return true; 61 | return this['makeTemplateSource'](template, templateDocument)['data']("isRewritten"); 62 | }; 63 | 64 | ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCallback, templateDocument) { 65 | var templateSource = this['makeTemplateSource'](template, templateDocument); 66 | var rewritten = rewriterCallback(templateSource['text']()); 67 | templateSource['text'](rewritten); 68 | templateSource['data']("isRewritten", true); 69 | }; 70 | 71 | ko.exportSymbol('templateEngine', ko.templateEngine); 72 | -------------------------------------------------------------------------------- /spec/lib/jasmine-1.2.0/jasmine-tap.js: -------------------------------------------------------------------------------- 1 | // This code from https://github.com/larrymyers/jasmine-reporters 2 | // License: MIT (https://github.com/larrymyers/jasmine-reporters/blob/master/LICENSE) 3 | 4 | (function() { 5 | if (! jasmine) { 6 | throw new Exception("jasmine library does not exist in global namespace!"); 7 | } 8 | 9 | /** 10 | * TAP (http://en.wikipedia.org/wiki/Test_Anything_Protocol) reporter. 11 | * outputs spec results to the console. 12 | * 13 | * Heavily inspired by ConsoleReporter found at: 14 | * https://github.com/larrymyers/jasmine-reporters/ 15 | * 16 | * Usage: 17 | * 18 | * jasmine.getEnv().addReporter(new jasmine.TapReporter()); 19 | * jasmine.getEnv().execute(); 20 | */ 21 | var TapReporter = function() { 22 | this.started = false; 23 | this.finished = false; 24 | }; 25 | 26 | TapReporter.prototype = { 27 | 28 | reportRunnerStarting: function(runner) { 29 | this.started = true; 30 | this.start_time = (new Date()).getTime(); 31 | this.executed_specs = 0; 32 | this.passed_specs = 0; 33 | this.executed_asserts = 0; 34 | this.passed_asserts = 0; 35 | // should have at least 1 spec, otherwise it's considered a failure 36 | this.log('1..'+ Math.max(runner.specs().length, 1)); 37 | }, 38 | 39 | reportSpecStarting: function(spec) { 40 | this.executed_specs++; 41 | }, 42 | 43 | reportSpecResults: function(spec) { 44 | var resultText = "not ok"; 45 | var errorMessage = ''; 46 | 47 | var results = spec.results(); 48 | if (!results.skipped) { 49 | var passed = results.passed(); 50 | 51 | this.passed_asserts += results.passedCount; 52 | this.executed_asserts += results.totalCount; 53 | 54 | if (passed) { 55 | this.passed_specs++; 56 | resultText = "ok"; 57 | } else { 58 | var items = results.getItems(); 59 | var i = 0; 60 | var expectationResult, stackMessage; 61 | while (expectationResult = items[i++]) { 62 | if (expectationResult.trace) { 63 | stackMessage = expectationResult.trace.stack? expectationResult.trace.stack : expectationResult.message; 64 | errorMessage += '\n '+ stackMessage; 65 | } 66 | } 67 | } 68 | 69 | this.log(resultText +" "+ (spec.id + 1) +" - "+ spec.suite.description +" : "+ spec.description + errorMessage); 70 | } 71 | }, 72 | 73 | reportRunnerResults: function(runner) { 74 | var dur = (new Date()).getTime() - this.start_time; 75 | var failed = this.executed_specs - this.passed_specs; 76 | var spec_str = this.executed_specs + (this.executed_specs === 1 ? " spec, " : " specs, "); 77 | var fail_str = failed + (failed === 1 ? " failure in " : " failures in "); 78 | var assert_str = this.executed_asserts + (this.executed_asserts === 1 ? " assertion, " : " assertions, "); 79 | 80 | if (this.executed_asserts) { 81 | this.log("# "+ spec_str + assert_str + fail_str + (dur/1000) + "s."); 82 | } else { 83 | this.log('not ok 1 - no asserts run.'); 84 | } 85 | this.finished = true; 86 | }, 87 | 88 | log: function(str) { 89 | var console = jasmine.getGlobal().console; 90 | if (console && console.log) { 91 | console.log(str); 92 | } 93 | } 94 | }; 95 | 96 | // export public 97 | jasmine.TapReporter = TapReporter; 98 | })(); 99 | -------------------------------------------------------------------------------- /src/binding/selectExtensions.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__'; 3 | 4 | // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values 5 | // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values 6 | // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns. 7 | ko.selectExtensions = { 8 | readValue : function(element) { 9 | switch (ko.utils.tagNameLower(element)) { 10 | case 'option': 11 | if (element[hasDomDataExpandoProperty] === true) 12 | return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey); 13 | return ko.utils.ieVersion <= 7 14 | ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text) 15 | : element.value; 16 | case 'select': 17 | return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined; 18 | default: 19 | return element.value; 20 | } 21 | }, 22 | 23 | writeValue: function(element, value) { 24 | switch (ko.utils.tagNameLower(element)) { 25 | case 'option': 26 | switch(typeof value) { 27 | case "string": 28 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined); 29 | if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node 30 | delete element[hasDomDataExpandoProperty]; 31 | } 32 | element.value = value; 33 | break; 34 | default: 35 | // Store arbitrary object using DomData 36 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value); 37 | element[hasDomDataExpandoProperty] = true; 38 | 39 | // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value. 40 | element.value = typeof value === "number" ? value : ""; 41 | break; 42 | } 43 | break; 44 | case 'select': 45 | if (value === "") 46 | value = undefined; 47 | if (value === null || value === undefined) 48 | element.selectedIndex = -1; 49 | for (var i = element.options.length - 1; i >= 0; i--) { 50 | if (ko.selectExtensions.readValue(element.options[i]) == value) { 51 | element.selectedIndex = i; 52 | break; 53 | } 54 | } 55 | // for drop-down select, ensure first is selected 56 | if (!(element.size > 1) && element.selectedIndex === -1) { 57 | element.selectedIndex = 0; 58 | } 59 | break; 60 | default: 61 | if ((value === null) || (value === undefined)) 62 | value = ""; 63 | element.value = value; 64 | break; 65 | } 66 | } 67 | }; 68 | })(); 69 | 70 | ko.exportSymbol('selectExtensions', ko.selectExtensions); 71 | ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue); 72 | ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue); 73 | -------------------------------------------------------------------------------- /src/templating/templateRewriting.js: -------------------------------------------------------------------------------- 1 | 2 | ko.templateRewriting = (function () { 3 | var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi; 4 | var memoizeVirtualContainerBindingSyntaxRegex = //g; 5 | 6 | function validateDataBindValuesForRewriting(keyValueArray) { 7 | var allValidators = ko.expressionRewriting.bindingRewriteValidators; 8 | for (var i = 0; i < keyValueArray.length; i++) { 9 | var key = keyValueArray[i]['key']; 10 | if (allValidators.hasOwnProperty(key)) { 11 | var validator = allValidators[key]; 12 | 13 | if (typeof validator === "function") { 14 | var possibleErrorMessage = validator(keyValueArray[i]['value']); 15 | if (possibleErrorMessage) 16 | throw new Error(possibleErrorMessage); 17 | } else if (!validator) { 18 | throw new Error("This template engine does not support the '" + key + "' binding within its templates"); 19 | } 20 | } 21 | } 22 | } 23 | 24 | function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, nodeName, templateEngine) { 25 | var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue); 26 | validateDataBindValuesForRewriting(dataBindKeyValueArray); 27 | var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray, {'valueAccessors':true}); 28 | 29 | // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional 30 | // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this 31 | // extra indirection. 32 | var applyBindingsToNextSiblingScript = 33 | "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()},'" + nodeName.toLowerCase() + "')"; 34 | return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain; 35 | } 36 | 37 | return { 38 | ensureTemplateIsRewritten: function (template, templateEngine, templateDocument) { 39 | if (!templateEngine['isTemplateRewritten'](template, templateDocument)) 40 | templateEngine['rewriteTemplate'](template, function (htmlString) { 41 | return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine); 42 | }, templateDocument); 43 | }, 44 | 45 | memoizeBindingAttributeSyntax: function (htmlString, templateEngine) { 46 | return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () { 47 | return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[4], /* tagToRetain: */ arguments[1], /* nodeName: */ arguments[2], templateEngine); 48 | }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() { 49 | return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "", /* nodeName: */ "#comment", templateEngine); 50 | }); 51 | }, 52 | 53 | applyMemoizedBindingsToNextSibling: function (bindings, nodeName) { 54 | return ko.memoization.memoize(function (domNode, bindingContext) { 55 | var nodeToBind = domNode.nextSibling; 56 | if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) { 57 | ko.applyBindingAccessorsToNode(nodeToBind, bindings, bindingContext); 58 | } 59 | }); 60 | } 61 | } 62 | })(); 63 | 64 | 65 | // Exported only because it has to be referenced by string lookup from within rewritten template 66 | ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextSibling); 67 | -------------------------------------------------------------------------------- /src/subscribables/mappingHelpers.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | var maxNestedObservableDepth = 10; // Escape the (unlikely) pathalogical case where an observable's current value is itself (or similar reference cycle) 4 | 5 | ko.toJS = function(rootObject) { 6 | if (arguments.length == 0) 7 | throw new Error("When calling ko.toJS, pass the object you want to convert."); 8 | 9 | // We just unwrap everything at every level in the object graph 10 | return mapJsObjectGraph(rootObject, function(valueToMap) { 11 | // Loop because an observable's value might in turn be another observable wrapper 12 | for (var i = 0; ko.isObservable(valueToMap) && (i < maxNestedObservableDepth); i++) 13 | valueToMap = valueToMap(); 14 | return valueToMap; 15 | }); 16 | }; 17 | 18 | ko.toJSON = function(rootObject, replacer, space) { // replacer and space are optional 19 | var plainJavaScriptObject = ko.toJS(rootObject); 20 | return ko.utils.stringifyJson(plainJavaScriptObject, replacer, space); 21 | }; 22 | 23 | function mapJsObjectGraph(rootObject, mapInputCallback, visitedObjects) { 24 | visitedObjects = visitedObjects || new objectLookup(); 25 | 26 | rootObject = mapInputCallback(rootObject); 27 | var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean)); 28 | if (!canHaveProperties) 29 | return rootObject; 30 | 31 | var outputProperties = rootObject instanceof Array ? [] : {}; 32 | visitedObjects.save(rootObject, outputProperties); 33 | 34 | visitPropertiesOrArrayEntries(rootObject, function(indexer) { 35 | var propertyValue = mapInputCallback(rootObject[indexer]); 36 | 37 | switch (typeof propertyValue) { 38 | case "boolean": 39 | case "number": 40 | case "string": 41 | case "function": 42 | outputProperties[indexer] = propertyValue; 43 | break; 44 | case "object": 45 | case "undefined": 46 | var previouslyMappedValue = visitedObjects.get(propertyValue); 47 | outputProperties[indexer] = (previouslyMappedValue !== undefined) 48 | ? previouslyMappedValue 49 | : mapJsObjectGraph(propertyValue, mapInputCallback, visitedObjects); 50 | break; 51 | } 52 | }); 53 | 54 | return outputProperties; 55 | } 56 | 57 | function visitPropertiesOrArrayEntries(rootObject, visitorCallback) { 58 | if (rootObject instanceof Array) { 59 | for (var i = 0; i < rootObject.length; i++) 60 | visitorCallback(i); 61 | 62 | // For arrays, also respect toJSON property for custom mappings (fixes #278) 63 | if (typeof rootObject['toJSON'] == 'function') 64 | visitorCallback('toJSON'); 65 | } else { 66 | for (var propertyName in rootObject) { 67 | if (propertyName === 'toJSON' || typeof rootObject[propertyName] !== 'function' || ko.isObservable(rootObject[propertyName])) 68 | visitorCallback(propertyName); 69 | } 70 | } 71 | }; 72 | 73 | function objectLookup() { 74 | this.keys = []; 75 | this.values = []; 76 | }; 77 | 78 | objectLookup.prototype = { 79 | constructor: objectLookup, 80 | save: function(key, value) { 81 | var existingIndex = ko.utils.arrayIndexOf(this.keys, key); 82 | if (existingIndex >= 0) 83 | this.values[existingIndex] = value; 84 | else { 85 | this.keys.push(key); 86 | this.values.push(value); 87 | } 88 | }, 89 | get: function(key) { 90 | var existingIndex = ko.utils.arrayIndexOf(this.keys, key); 91 | return (existingIndex >= 0) ? this.values[existingIndex] : undefined; 92 | } 93 | }; 94 | })(); 95 | 96 | ko.exportSymbol('toJS', ko.toJS); 97 | ko.exportSymbol('toJSON', ko.toJSON); 98 | -------------------------------------------------------------------------------- /src/binding/defaultBindings/value.js: -------------------------------------------------------------------------------- 1 | ko.bindingHandlers['value'] = { 2 | 'after': ['options', 'foreach'], 3 | 'init': function (element, valueAccessor, allBindings) { 4 | // Always catch "change" event; possibly other events too if asked 5 | var eventsToCatch = ["change"]; 6 | var requestedEventsToCatch = allBindings.get("valueUpdate"); 7 | var propertyChangedFired = false; 8 | if (requestedEventsToCatch) { 9 | if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names 10 | requestedEventsToCatch = [requestedEventsToCatch]; 11 | ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch); 12 | eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch); 13 | } 14 | 15 | var valueUpdateHandler = function() { 16 | propertyChangedFired = false; 17 | var modelValue = valueAccessor(); 18 | var elementValue = ko.selectExtensions.readValue(element); 19 | ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'value', elementValue); 20 | } 21 | 22 | // Workaround for https://github.com/SteveSanderson/knockout/issues/122 23 | // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list 24 | var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text" 25 | && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off"); 26 | if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) { 27 | ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true }); 28 | ko.utils.registerEventHandler(element, "blur", function() { 29 | if (propertyChangedFired) { 30 | valueUpdateHandler(); 31 | } 32 | }); 33 | } 34 | 35 | ko.utils.arrayForEach(eventsToCatch, function(eventName) { 36 | // The syntax "after" means "run the handler asynchronously after the event" 37 | // This is useful, for example, to catch "keydown" events after the browser has updated the control 38 | // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event) 39 | var handler = valueUpdateHandler; 40 | if (ko.utils.stringStartsWith(eventName, "after")) { 41 | handler = function() { setTimeout(valueUpdateHandler, 0) }; 42 | eventName = eventName.substring("after".length); 43 | } 44 | ko.utils.registerEventHandler(element, eventName, handler); 45 | }); 46 | }, 47 | 'update': function (element, valueAccessor) { 48 | var valueIsSelectOption = ko.utils.tagNameLower(element) === "select"; 49 | var newValue = ko.utils.unwrapObservable(valueAccessor()); 50 | var elementValue = ko.selectExtensions.readValue(element); 51 | var valueHasChanged = (newValue !== elementValue); 52 | 53 | if (valueHasChanged) { 54 | var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); }; 55 | applyValueAction(); 56 | 57 | if (valueIsSelectOption) { 58 | if (newValue !== ko.selectExtensions.readValue(element)) { 59 | // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change, 60 | // because you're not allowed to have a model value that disagrees with a visible UI selection. 61 | ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]); 62 | } else { 63 | // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread 64 | // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread 65 | // to apply the value as well. 66 | setTimeout(applyValueAction, 0); 67 | } 68 | } 69 | } 70 | } 71 | }; 72 | ko.expressionRewriting.twoWayBindings['value'] = true; 73 | -------------------------------------------------------------------------------- /spec/defaultBindings/hasfocusBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Hasfocus', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | beforeEach(function() { waits(1); }); // Workaround for spurious focus-timing-related failures on IE8 (issue #736) 4 | 5 | it('Should respond to changes on an observable value by blurring or focusing the element', function() { 6 | var currentState; 7 | var model = { myVal: ko.observable() } 8 | testNode.innerHTML = ""; 9 | ko.applyBindings(model, testNode); 10 | ko.utils.registerEventHandler(testNode.childNodes[0], "focusin", function() { currentState = true }); 11 | ko.utils.registerEventHandler(testNode.childNodes[0], "focusout", function() { currentState = false }); 12 | 13 | // When the value becomes true, we focus 14 | model.myVal(true); 15 | expect(currentState).toEqual(true); 16 | 17 | // When the value becomes false, we blur 18 | model.myVal(false); 19 | expect(currentState).toEqual(false); 20 | }); 21 | 22 | it('Should set an observable value to be true on focus and false on blur', function() { 23 | var model = { myVal: ko.observable() } 24 | testNode.innerHTML = ""; 25 | ko.applyBindings(model, testNode); 26 | 27 | // Need to raise "focusin" and "focusout" manually, because simply calling ".focus()" and ".blur()" 28 | // in IE doesn't reliably trigger the "focus" and "blur" events synchronously 29 | 30 | testNode.childNodes[0].focus(); 31 | ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); 32 | expect(model.myVal()).toEqual(true); 33 | 34 | // Move the focus elsewhere 35 | testNode.childNodes[1].focus(); 36 | ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); 37 | expect(model.myVal()).toEqual(false); 38 | 39 | // If the model value becomes true after a blur, we re-focus the element 40 | // (Represents issue #672, where this wasn't working) 41 | var didFocusExpectedElement = false; 42 | ko.utils.registerEventHandler(testNode.childNodes[0], "focusin", function() { didFocusExpectedElement = true }); 43 | model.myVal(true); 44 | expect(didFocusExpectedElement).toEqual(true); 45 | }); 46 | 47 | it('Should set a non-observable value to be true on focus and false on blur', function() { 48 | var model = { myVal: null } 49 | testNode.innerHTML = ""; 50 | ko.applyBindings(model, testNode); 51 | 52 | testNode.childNodes[0].focus(); 53 | ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); 54 | expect(model.myVal).toEqual(true); 55 | 56 | // Move the focus elsewhere 57 | testNode.childNodes[1].focus(); 58 | ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); 59 | expect(model.myVal).toEqual(false); 60 | }); 61 | 62 | it('Should be aliased as hasFocus as well as hasfocus', function() { 63 | expect(ko.bindingHandlers.hasFocus).toEqual(ko.bindingHandlers.hasfocus); 64 | }); 65 | 66 | it('Should not unnecessarily focus or blur an element that is already focused/blurred', function() { 67 | // This is the closest we can get to representing issue #698 as a spec 68 | var model = { isFocused: ko.observable({}) }; 69 | testNode.innerHTML = ""; 70 | ko.applyBindings(model, testNode); 71 | 72 | // The elem is already focused, so changing the model value to a different truthy value 73 | // shouldn't cause any additional focus events 74 | var didFocusAgain = false; 75 | ko.utils.registerEventHandler(testNode.childNodes[0], "focusin", function() { didFocusAgain = true }); 76 | model.isFocused.valueHasMutated(); 77 | expect(didFocusAgain).toEqual(false); 78 | 79 | // Similarly, when the elem is already blurred, changing the model value to a different 80 | // falsey value shouldn't cause any additional blur events 81 | model.isFocused(false); 82 | var didBlurAgain = false; 83 | ko.utils.registerEventHandler(testNode.childNodes[0], "focusout", function() { didBlurAgain = true }); 84 | model.isFocused(null); 85 | expect(didBlurAgain).toEqual(false); 86 | }); 87 | }); -------------------------------------------------------------------------------- /src/templating/jquery.tmpl/jqueryTmplTemplateEngine.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | ko.jqueryTmplTemplateEngine = function () { 3 | // Detect which version of jquery-tmpl you're using. Unfortunately jquery-tmpl 4 | // doesn't expose a version number, so we have to infer it. 5 | // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later, 6 | // which KO internally refers to as version "2", so older versions are no longer detected. 7 | var jQueryTmplVersion = this.jQueryTmplVersion = (function() { 8 | if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl'])) 9 | return 0; 10 | // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves. 11 | try { 12 | if (jQuery['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) { 13 | // Since 1.0.0pre, custom tags should append markup to an array called "__" 14 | return 2; // Final version of jquery.tmpl 15 | } 16 | } catch(ex) { /* Apparently not the version we were looking for */ } 17 | 18 | return 1; // Any older version that we don't support 19 | })(); 20 | 21 | function ensureHasReferencedJQueryTemplates() { 22 | if (jQueryTmplVersion < 2) 23 | throw new Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later."); 24 | } 25 | 26 | function executeTemplate(compiledTemplate, data, jQueryTemplateOptions) { 27 | return jQuery['tmpl'](compiledTemplate, data, jQueryTemplateOptions); 28 | } 29 | 30 | this['renderTemplateSource'] = function(templateSource, bindingContext, options) { 31 | options = options || {}; 32 | ensureHasReferencedJQueryTemplates(); 33 | 34 | // Ensure we have stored a precompiled version of this template (don't want to reparse on every render) 35 | var precompiled = templateSource['data']('precompiled'); 36 | if (!precompiled) { 37 | var templateText = templateSource['text']() || ""; 38 | // Wrap in "with($whatever.koBindingContext) { ... }" 39 | templateText = "{{ko_with $item.koBindingContext}}" + templateText + "{{/ko_with}}"; 40 | 41 | precompiled = jQuery['template'](null, templateText); 42 | templateSource['data']('precompiled', precompiled); 43 | } 44 | 45 | var data = [bindingContext['$data']]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays 46 | var jQueryTemplateOptions = jQuery['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']); 47 | 48 | var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions); 49 | resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work 50 | 51 | jQuery['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders 52 | return resultNodes; 53 | }; 54 | 55 | this['createJavaScriptEvaluatorBlock'] = function(script) { 56 | return "{{ko_code ((function() { return " + script + " })()) }}"; 57 | }; 58 | 59 | this['addTemplate'] = function(templateName, templateMarkup) { 60 | document.write(" 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /spec/lib/jasmine-1.2.0/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 23 | #HTMLReporter .runningAlert { background-color: #666666; } 24 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 25 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 26 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 27 | #HTMLReporter .passingAlert { background-color: #a6b779; } 28 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 29 | #HTMLReporter .failingAlert { background-color: #cf867e; } 30 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 31 | #HTMLReporter .results { margin-top: 14px; } 32 | #HTMLReporter #details { display: none; } 33 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 34 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 35 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 36 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 37 | #HTMLReporter.showDetails .summary { display: none; } 38 | #HTMLReporter.showDetails #details { display: block; } 39 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 40 | #HTMLReporter .summary { margin-top: 14px; } 41 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 42 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 43 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 44 | #HTMLReporter .description + .suite { margin-top: 0; } 45 | #HTMLReporter .suite { margin-top: 14px; } 46 | #HTMLReporter .suite a { color: #333333; } 47 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 48 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 49 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 50 | #HTMLReporter .resultMessage span.result { display: block; } 51 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 52 | 53 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 54 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 55 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 56 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 57 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 58 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 59 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 60 | #TrivialReporter .runner.running { background-color: yellow; } 61 | #TrivialReporter .options { text-align: right; font-size: .8em; } 62 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 63 | #TrivialReporter .suite .suite { margin: 5px; } 64 | #TrivialReporter .suite.passed { background-color: #dfd; } 65 | #TrivialReporter .suite.failed { background-color: #fdd; } 66 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 67 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 68 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 69 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 70 | #TrivialReporter .spec.skipped { background-color: #bbb; } 71 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 72 | #TrivialReporter .passed { background-color: #cfc; display: none; } 73 | #TrivialReporter .failed { background-color: #fbb; } 74 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 75 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 76 | #TrivialReporter .resultMessage .mismatch { color: black; } 77 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 78 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 79 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 80 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 81 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 82 | -------------------------------------------------------------------------------- /spec/defaultBindings/selectedOptionsBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('Binding: Selected Options', function() { 2 | beforeEach(jasmine.prepareTestNode); 3 | 4 | it('Should only be applicable to SELECT nodes', function () { 5 | var threw = false; 6 | testNode.innerHTML = ""; 7 | try { ko.applyBindings({}, testNode); } 8 | catch (ex) { threw = true; } 9 | expect(threw).toEqual(true); 10 | }); 11 | 12 | it('Should set selection in the SELECT node to match the model', function () { 13 | var bObject = {}; 14 | var values = new ko.observableArray(["A", bObject, "C"]); 15 | var selection = new ko.observableArray([bObject]); 16 | testNode.innerHTML = ""; 17 | ko.applyBindings({ myValues: values, mySelection: selection }, testNode); 18 | 19 | expect(testNode.childNodes[0]).toHaveSelectedValues([bObject]); 20 | selection.push("C"); 21 | expect(testNode.childNodes[0]).toHaveSelectedValues([bObject, "C"]); 22 | }); 23 | 24 | it('Should update the model when selection in the SELECT node changes', function () { 25 | function setMultiSelectOptionSelectionState(optionElement, state) { 26 | // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) 27 | if (/MSIE 6/i.test(navigator.userAgent)) 28 | optionElement.setAttribute('selected', state); 29 | else 30 | optionElement.selected = state; 31 | } 32 | 33 | var cObject = {}; 34 | var values = new ko.observableArray(["A", "B", cObject]); 35 | var selection = new ko.observableArray(["B"]); 36 | testNode.innerHTML = ""; 37 | ko.applyBindings({ myValues: values, mySelection: selection }, testNode); 38 | 39 | expect(selection()).toEqual(["B"]); 40 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0], true); 41 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[1], false); 42 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[2], true); 43 | ko.utils.triggerEvent(testNode.childNodes[0], "change"); 44 | 45 | expect(selection()).toEqual(["A", cObject]); 46 | expect(selection()[1] === cObject).toEqual(true); // Also check with strict equality, because we don't want to falsely accept [object Object] == cObject 47 | }); 48 | 49 | it('Should update the model when selection in the SELECT node changes for non-observable property values', function () { 50 | function setMultiSelectOptionSelectionState(optionElement, state) { 51 | // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) 52 | if (/MSIE 6/i.test(navigator.userAgent)) 53 | optionElement.setAttribute('selected', state); 54 | else 55 | optionElement.selected = state; 56 | } 57 | 58 | var cObject = {}; 59 | var values = new ko.observableArray(["A", "B", cObject]); 60 | var selection = ["B"]; 61 | var myModel = { myValues: values, mySelection: selection }; 62 | testNode.innerHTML = ""; 63 | ko.applyBindings(myModel, testNode); 64 | 65 | expect(myModel.mySelection).toEqual(["B"]); 66 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0], true); 67 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[1], false); 68 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[2], true); 69 | ko.utils.triggerEvent(testNode.childNodes[0], "change"); 70 | 71 | expect(myModel.mySelection).toEqual(["A", cObject]); 72 | expect(myModel.mySelection[1] === cObject).toEqual(true); // Also check with strict equality, because we don't want to falsely accept [object Object] == cObject 73 | }); 74 | 75 | it('Should update the model when selection in the SELECT node inside an optgroup changes', function () { 76 | function setMultiSelectOptionSelectionState(optionElement, state) { 77 | // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) 78 | if (/MSIE 6/i.test(navigator.userAgent)) 79 | optionElement.setAttribute('selected', state); 80 | else 81 | optionElement.selected = state; 82 | } 83 | 84 | var selection = new ko.observableArray([]); 85 | testNode.innerHTML = ""; 86 | ko.applyBindings({ mySelection: selection }, testNode); 87 | 88 | expect(selection()).toEqual([]); 89 | 90 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[0], true); 91 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[1], false); 92 | setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[2], true); 93 | ko.utils.triggerEvent(testNode.childNodes[0], "change"); 94 | 95 | expect(selection()).toEqual(['a', 'c']); 96 | }); 97 | 98 | it('Should set selection in the SELECT node inside an optgroup to match the model', function () { 99 | var selection = new ko.observableArray(['a']); 100 | testNode.innerHTML = ""; 101 | ko.applyBindings({ mySelection: selection }, testNode); 102 | 103 | expect(testNode.childNodes[0].childNodes[0]).toHaveSelectedValues(['a']); 104 | expect(testNode.childNodes[0].childNodes[1]).toHaveSelectedValues([]); 105 | selection.push('c'); 106 | expect(testNode.childNodes[0].childNodes[0]).toHaveSelectedValues(['a', 'c']); 107 | expect(testNode.childNodes[0].childNodes[1]).toHaveSelectedValues([]); 108 | selection.push('d'); 109 | expect(testNode.childNodes[0].childNodes[0]).toHaveSelectedValues(['a', 'c']); 110 | expect(testNode.childNodes[0].childNodes[1]).toHaveSelectedValues(['d']); 111 | }); 112 | }); --------------------------------------------------------------------------------