├── .gitignore ├── README.md ├── build ├── build-linux ├── build-windows.bat ├── knockout-raw.js ├── output │ ├── knockout-latest.debug.js │ └── knockout-latest.js ├── source-references.js ├── tools │ └── curl.exe └── version-header.js ├── deps ├── rdf_store.js ├── rdf_store_min.js └── sem_ko.js ├── dist ├── semko.js └── semko.min.js ├── spec ├── bindingAttributeBehaviors.js ├── defaultBindingsBehaviors.js ├── dependentObservableBehaviors.js ├── domNodeDisposalBehaviors.js ├── editDetectionBehaviors.js ├── jsonExpressionRewritingBehaviors.js ├── jsonPostingBehaviors.js ├── lib │ ├── JSSpec.css │ ├── JSSpec.js │ ├── diff_match_patch.js │ └── json2.js ├── mappingHelperBehaviors.js ├── memoizationBehaviors.js ├── observableArrayBehaviors.js ├── observableBehaviors.js ├── runner.html ├── subscribableBehaviors.js └── templatingBehaviors.js ├── src ├── binding │ ├── bindingAttributeSyntax.js │ ├── defaultBindings.js │ ├── editDetection │ │ ├── arrayToDomNodeChildren.js │ │ └── compareArrays.js │ ├── jsonExpressionRewriting.js │ └── selectExtensions.js ├── google-closure-compiler-utils.js ├── memoization.js ├── namespace.js ├── subscribables │ ├── dependencyDetection.js │ ├── dependentObservable.js │ ├── mappingHelpers.js │ ├── observable.js │ ├── observableArray.js │ └── subscribable.js ├── templating │ ├── jquery.tmpl │ │ └── jqueryTmplTemplateEngine.js │ ├── templateEngine.js │ ├── templateRewriting.js │ └── templating.js ├── utils.domData.js ├── utils.domManipulation.js ├── utils.domNodeDisposal.js └── utils.js ├── test.html ├── test2.html ├── test3.html └── tests ├── index.html ├── qunit.css ├── qunit.js └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.csproj.user 3 | bin 4 | obj 5 | *.pdb 6 | _ReSharper* 7 | *.ReSharper.user 8 | *.ReSharper 9 | desktop.ini 10 | .eprj 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SemanticKO is a JS library to build web application using RDF data. 2 | 3 | It is a fork of KnockoutJS (http://knockoutjs.com/) that uses 4 | RDFStore-JS to handle the RDF/SPARQL data layer. 5 | 6 | More documentation can be found here: 7 | 8 | http://antoniogarrote.com/semantic_ko/ 9 | 10 | A collection of small interactive examples is available here: 11 | 12 | http://antoniogarrote.github.com/semantic-ko/index.html -------------------------------------------------------------------------------- /build/build-linux: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OutDebugFile='output/knockout-latest.debug.js' 4 | OutMinFile='output/knockout-latest.js' 5 | 6 | # Combine the source files 7 | SourceFiles=`grep js < source-references.js | # Find JS references 8 | sed "s/[ \',]//g" | # Strip off JSON fluff (whitespace, commas, quotes) 9 | sed -e 's/.*/..\/&/' | # Fix the paths by prefixing with ../ 10 | tr '\n' ' '` # Combine into single line 11 | cat $SourceFiles > $OutDebugFile.temp 12 | 13 | # Now call Google Closure Compiler to produce a minified version 14 | cp version-header.js $OutMinFile 15 | curl -d output_info=compiled_code -d output_format=text -d compilation_level=SIMPLE_OPTIMIZATIONS --data-urlencode js_code@$OutDebugFile.temp "http://closure-compiler.appspot.com/compile" > $OutMinFile.temp 16 | 17 | # Finalise each file by prefixing with version header and surrounding in function closure 18 | cp version-header.js $OutDebugFile 19 | echo "(function(window,undefined){" >> $OutDebugFile 20 | cat $OutDebugFile.temp >> $OutDebugFile 21 | echo "})(window);" >> $OutDebugFile 22 | rm $OutDebugFile.temp 23 | 24 | cp version-header.js $OutMinFile 25 | echo "(function(window,undefined){" >> $OutMinFile 26 | cat $OutMinFile.temp >> $OutMinFile 27 | echo "})(window);" >> $OutMinFile 28 | rm $OutMinFile.temp -------------------------------------------------------------------------------- /build/build-windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set OutDebugFile=output\knockout-latest.debug.js 3 | set OutMinFile=output\knockout-latest.js 4 | set AllFiles= 5 | for /f "eol=] skip=1 delims=' " %%i in (source-references.js) do set Filename=%%i& call :Concatenate 6 | 7 | goto :Combine 8 | :Concatenate 9 | if /i "%AllFiles%"=="" ( 10 | set AllFiles=..\%Filename:/=\% 11 | ) else ( 12 | set AllFiles=%AllFiles% ..\%Filename:/=\% 13 | ) 14 | goto :EOF 15 | 16 | :Combine 17 | type %AllFiles% > %OutDebugFile%.temp 18 | 19 | @rem Now call Google Closure Compiler to produce a minified version 20 | copy /y version-header.js %OutMinFile% 21 | tools\curl -d output_info=compiled_code -d output_format=text -d compilation_level=ADVANCED_OPTIMIZATIONS --data-urlencode js_code@%OutDebugFile%.temp "http://closure-compiler.appspot.com/compile" > %OutMinFile%.temp 22 | 23 | @rem Finalise each file by prefixing with version header and surrounding in function closure 24 | copy /y version-header.js %OutDebugFile% 25 | echo (function(window,undefined){ >> %OutDebugFile% 26 | type %OutDebugFile%.temp >> %OutDebugFile% 27 | echo })(window); >> %OutDebugFile% 28 | del %OutDebugFile%.temp 29 | 30 | copy /y version-header.js %OutMinFile% 31 | echo (function(window,undefined){ >> %OutMinFile% 32 | type %OutMinFile%.temp >> %OutMinFile% 33 | echo })(window); >> %OutMinFile% 34 | del %OutMinFile%.temp -------------------------------------------------------------------------------- /build/knockout-raw.js: -------------------------------------------------------------------------------- 1 | // This script adds "); 21 | }; 22 | 23 | var buildFolderPath = getPathToScriptTagSrc(debugFileName); 24 | window.knockoutDebugCallback = function (scriptUrls) { 25 | for (var i = 0; i < scriptUrls.length; i++) 26 | referenceScript(buildFolderPath + scriptUrls[i]); 27 | }; 28 | referenceScript(buildFolderPath + sourcesReferenceFile); 29 | })(); -------------------------------------------------------------------------------- /build/source-references.js: -------------------------------------------------------------------------------- 1 | knockoutDebugCallback([ 2 | 'src/namespace.js', 3 | 'src/google-closure-compiler-utils.js', 4 | 'src/utils.js', 5 | 'src/utils.domData.js', 6 | 'src/utils.domNodeDisposal.js', 7 | 'src/utils.domManipulation.js', 8 | 'src/memoization.js', 9 | 'src/subscribables/subscribable.js', 10 | 'src/subscribables/dependencyDetection.js', 11 | 'src/subscribables/observable.js', 12 | 'src/subscribables/observableArray.js', 13 | 'src/subscribables/dependentObservable.js', 14 | 'src/subscribables/mappingHelpers.js', 15 | 'src/binding/selectExtensions.js', 16 | 'src/binding/jsonExpressionRewriting.js', 17 | 'src/binding/bindingAttributeSyntax.js', 18 | 'src/binding/defaultBindings.js', 19 | 'src/templating/templateEngine.js', 20 | 'src/templating/templateRewriting.js', 21 | 'src/templating/templating.js', 22 | 'src/binding/editDetection/compareArrays.js', 23 | 'src/binding/editDetection/arrayToDomNodeChildren.js', 24 | 'src/templating/jquery.tmpl/jqueryTmplTemplateEngine.js', 25 | 'deps/sem_ko.js' 26 | ]); 27 | -------------------------------------------------------------------------------- /build/tools/curl.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoniogarrote/semantic-ko/5c15756176af29343d552723a20907ecd196d64e/build/tools/curl.exe -------------------------------------------------------------------------------- /build/version-header.js: -------------------------------------------------------------------------------- 1 | // Knockout JavaScript library v1.2.1 2 | // (c) Steven Sanderson - http://knockoutjs.com/ 3 | // License: MIT (http://www.opensource.org/licenses/mit-license.php) 4 | 5 | -------------------------------------------------------------------------------- /spec/bindingAttributeBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Binding attribute syntax', { 3 | before_each: function () { 4 | var existingNode = document.getElementById("testNode"); 5 | if (existingNode != null) 6 | existingNode.parentNode.removeChild(existingNode); 7 | testNode = document.createElement("div"); 8 | testNode.id = "testNode"; 9 | document.body.appendChild(testNode); 10 | }, 11 | 12 | 'applyBindings should accept no parameters and then act on document.body with undefined model': function() { 13 | var didInit = false; 14 | ko.bindingHandlers.test = { 15 | init: function (element, valueAccessor, allBindingsAccessor, viewModel) { 16 | value_of(element.id).should_be("testElement"); 17 | value_of(viewModel).should_be(undefined); 18 | didInit = true; 19 | } 20 | }; 21 | testNode.innerHTML = "
"; 22 | ko.applyBindings(); 23 | value_of(didInit).should_be(true); 24 | }, 25 | 26 | 'applyBindings should accept one parameter and then act on document.body with parameter as model': function() { 27 | var didInit = false; 28 | var suppliedViewModel = {}; 29 | ko.bindingHandlers.test = { 30 | init: function (element, valueAccessor, allBindingsAccessor, viewModel) { 31 | value_of(element.id).should_be("testElement"); 32 | value_of(viewModel).should_be(suppliedViewModel); 33 | didInit = true; 34 | } 35 | }; 36 | testNode.innerHTML = "
"; 37 | ko.applyBindings(suppliedViewModel); 38 | value_of(didInit).should_be(true); 39 | }, 40 | 41 | 'applyBindings should accept two parameters and then act on second param as DOM node with first param as model': function() { 42 | var didInit = false; 43 | var suppliedViewModel = {}; 44 | ko.bindingHandlers.test = { 45 | init: function (element, valueAccessor, allBindingsAccessor, viewModel) { 46 | value_of(element.id).should_be("testElement"); 47 | value_of(viewModel).should_be(suppliedViewModel); 48 | didInit = true; 49 | } 50 | }; 51 | testNode.innerHTML = "
"; 52 | var shouldNotMatchNode = document.createElement("DIV"); 53 | shouldNotMatchNode.innerHTML = "
"; 54 | document.body.appendChild(shouldNotMatchNode); 55 | try { 56 | ko.applyBindings(suppliedViewModel, testNode); 57 | value_of(didInit).should_be(true); 58 | } finally { 59 | shouldNotMatchNode.parentNode.removeChild(shouldNotMatchNode); 60 | } 61 | }, 62 | 63 | 'Should tolerate whitespace and nonexistent handlers': function () { 64 | testNode.innerHTML = "
"; 65 | ko.applyBindings(null, testNode); // No exception means success 66 | }, 67 | 68 | 'Should tolerate arbitrary literals as the values for a handler': function () { 69 | testNode.innerHTML = "
"; 70 | ko.applyBindings(null, testNode); // No exception means success 71 | }, 72 | 73 | 'Should invoke registered handlers\' init() then update() methods passing binding data': function () { 74 | var methodsInvoked = []; 75 | ko.bindingHandlers.test = { 76 | init: function (element, valueAccessor, allBindingsAccessor) { 77 | methodsInvoked.push("init"); 78 | value_of(element.id).should_be("testElement"); 79 | value_of(valueAccessor()).should_be("Hello"); 80 | value_of(allBindingsAccessor().another).should_be(123); 81 | }, 82 | update: function (element, valueAccessor, allBindingsAccessor) { 83 | methodsInvoked.push("update"); 84 | value_of(element.id).should_be("testElement"); 85 | value_of(valueAccessor()).should_be("Hello"); 86 | value_of(allBindingsAccessor().another).should_be(123); 87 | } 88 | } 89 | testNode.innerHTML = "
"; 90 | ko.applyBindings(null, testNode); 91 | value_of(methodsInvoked.length).should_be(2); 92 | value_of(methodsInvoked[0]).should_be("init"); 93 | value_of(methodsInvoked[1]).should_be("update"); 94 | }, 95 | 96 | 'If the binding handler depends on an observable, invokes the init handler once and the update handler whenever a new value is available': function () { 97 | var observable = new ko.observable(); 98 | var initPassedValues = [], updatePassedValues = []; 99 | ko.bindingHandlers.test = { 100 | init: function (element, valueAccessor) { initPassedValues.push(valueAccessor()()); }, 101 | update: function (element, valueAccessor) { updatePassedValues.push(valueAccessor()()); } 102 | }; 103 | testNode.innerHTML = "
"; 104 | 105 | ko.applyBindings({ myObservable: observable }, testNode); 106 | value_of(initPassedValues.length).should_be(1); 107 | value_of(updatePassedValues.length).should_be(1); 108 | value_of(initPassedValues[0]).should_be(undefined); 109 | value_of(updatePassedValues[0]).should_be(undefined); 110 | 111 | observable("A"); 112 | value_of(initPassedValues.length).should_be(1); 113 | value_of(updatePassedValues.length).should_be(2); 114 | value_of(updatePassedValues[1]).should_be("A"); 115 | }, 116 | 117 | 'If the associated DOM element was removed by KO, handler subscriptions are disposed immediately': function () { 118 | var observable = new ko.observable("A"); 119 | testNode.innerHTML = "
"; 120 | ko.applyBindings({ myObservable: observable }, testNode); 121 | 122 | value_of(observable.getSubscriptionsCount()).should_be(1); 123 | 124 | ko.removeNode(testNode); 125 | 126 | value_of(observable.getSubscriptionsCount()).should_be(0); 127 | }, 128 | 129 | 'If the associated DOM element was removed independently of KO, handler subscriptions are disposed on the next evaluation': function () { 130 | var observable = new ko.observable("A"); 131 | testNode.innerHTML = "
"; 132 | ko.applyBindings({ myObservable: observable }, testNode); 133 | 134 | value_of(observable.getSubscriptionsCount()).should_be(1); 135 | 136 | testNode.parentNode.removeChild(testNode); 137 | observable("B"); // Force re-evaluation 138 | 139 | value_of(observable.getSubscriptionsCount()).should_be(0); 140 | }, 141 | 142 | 'If the binding attribute involves an observable, re-invokes the bindings if the observable notifies a change': function () { 143 | var observable = new ko.observable({ message: "hello" }); 144 | var passedValues = []; 145 | ko.bindingHandlers.test = { update: function (element, valueAccessor) { passedValues.push(valueAccessor()); } }; 146 | testNode.innerHTML = "
"; 147 | 148 | ko.applyBindings({ myObservable: observable }, testNode); 149 | value_of(passedValues.length).should_be(1); 150 | value_of(passedValues[0]).should_be("hello"); 151 | 152 | observable({ message: "goodbye" }); 153 | value_of(passedValues.length).should_be(2); 154 | value_of(passedValues[1]).should_be("goodbye"); 155 | } 156 | }) -------------------------------------------------------------------------------- /spec/dependentObservableBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Dependent Observable', { 3 | 'Should be subscribable': function () { 4 | var instance = new ko.dependentObservable(function () { }); 5 | value_of(ko.isSubscribable(instance)).should_be(true); 6 | }, 7 | 8 | 'Should advertise that instances are observable': function () { 9 | var instance = new ko.dependentObservable(function () { }); 10 | value_of(ko.isObservable(instance)).should_be(true); 11 | }, 12 | 13 | 'Should advertise that instances cannot have values written to them': function () { 14 | var instance = new ko.dependentObservable(function () { }); 15 | value_of(ko.isWriteableObservable(instance)).should_be(false); 16 | }, 17 | 18 | 'Should require an evaluator function as constructor param': function () { 19 | var threw = false; 20 | try { var instance = new ko.dependentObservable(); } 21 | catch (ex) { threw = true; } 22 | value_of(threw).should_be(true); 23 | }, 24 | 25 | 'Should be able to read the current value of the evaluator function': function () { 26 | var instance = new ko.dependentObservable(function () { return 123; }); 27 | value_of(instance()).should_be(123); 28 | }, 29 | 30 | 'Should not be able to write a value to it if there is no "write" callback': function () { 31 | var instance = new ko.dependentObservable(function () { return 123; }); 32 | 33 | var threw = false; 34 | try { instance(456); } 35 | catch (ex) { threw = true; } 36 | 37 | value_of(instance()).should_be(123); 38 | value_of(threw).should_be(true); 39 | }, 40 | 41 | 'Should invoke the "write" callback, where present, if you attempt to write a value to it': function() { 42 | var invokedWriteWithValue; 43 | var instance = new ko.dependentObservable({ 44 | read: function() {}, 45 | write: function(value) { invokedWriteWithValue = value } 46 | }); 47 | 48 | instance("some value"); 49 | value_of(invokedWriteWithValue).should_be("some value"); 50 | }, 51 | 52 | 'Should be able to pass evaluator function using "options" parameter called "read"': function() { 53 | var instance = new ko.dependentObservable({ 54 | read: function () { return 123; } 55 | }); 56 | value_of(instance()).should_be(123); 57 | }, 58 | 59 | 'Should cache result of evaluator function and not call it again until dependencies change': function () { 60 | var timesEvaluated = 0; 61 | var instance = new ko.dependentObservable(function () { timesEvaluated++; return 123; }); 62 | value_of(instance()).should_be(123); 63 | value_of(instance()).should_be(123); 64 | value_of(timesEvaluated).should_be(1); 65 | }, 66 | 67 | 'Should automatically update value when a dependency changes': function () { 68 | var observable = new ko.observable(1); 69 | var depedentObservable = new ko.dependentObservable(function () { return observable() + 1; }); 70 | value_of(depedentObservable()).should_be(2); 71 | 72 | observable(50); 73 | value_of(depedentObservable()).should_be(51); 74 | }, 75 | 76 | 'Should unsubscribe from previous dependencies each time a dependency changes': function () { 77 | var observableA = new ko.observable("A"); 78 | var observableB = new ko.observable("B"); 79 | var observableToUse = "A"; 80 | var timesEvaluated = 0; 81 | var depedentObservable = new ko.dependentObservable(function () { 82 | timesEvaluated++; 83 | return observableToUse == "A" ? observableA() : observableB(); 84 | }); 85 | 86 | value_of(depedentObservable()).should_be("A"); 87 | value_of(timesEvaluated).should_be(1); 88 | 89 | // Changing an unrelated observable doesn't trigger evaluation 90 | observableB("B2"); 91 | value_of(timesEvaluated).should_be(1); 92 | 93 | // Switch to other observable 94 | observableToUse = "B"; 95 | observableA("A2"); 96 | value_of(depedentObservable()).should_be("B2"); 97 | value_of(timesEvaluated).should_be(2); 98 | 99 | // Now changing the first observable doesn't trigger evaluation 100 | observableA("A3"); 101 | value_of(timesEvaluated).should_be(2); 102 | }, 103 | 104 | 'Should notify subscribers of changes': function () { 105 | var notifiedValue; 106 | var observable = new ko.observable(1); 107 | var depedentObservable = new ko.dependentObservable(function () { return observable() + 1; }); 108 | depedentObservable.subscribe(function (value) { notifiedValue = value; }); 109 | 110 | value_of(notifiedValue).should_be(undefined); 111 | observable(2); 112 | value_of(notifiedValue).should_be(3); 113 | }, 114 | 115 | 'Should only update once when each dependency changes, even if evaluation calls the dependency multiple times': function () { 116 | var notifiedValues = []; 117 | var observable = new ko.observable(); 118 | var depedentObservable = new ko.dependentObservable(function () { return observable() * observable(); }); 119 | depedentObservable.subscribe(function (value) { notifiedValues.push(value); }); 120 | observable(2); 121 | value_of(notifiedValues.length).should_be(1); 122 | value_of(notifiedValues[0]).should_be(4); 123 | }, 124 | 125 | 'Should be able to chain dependentObservables': function () { 126 | var underlyingObservable = new ko.observable(1); 127 | var dependent1 = new ko.dependentObservable(function () { return underlyingObservable() + 1; }); 128 | var dependent2 = new ko.dependentObservable(function () { return dependent1() + 1; }); 129 | value_of(dependent2()).should_be(3); 130 | 131 | underlyingObservable(11); 132 | value_of(dependent2()).should_be(13); 133 | }, 134 | 135 | 'Should accept "owner" parameter to define the object on which the evaluator function should be called': function () { 136 | var model = new (function () { 137 | this.greeting = "hello"; 138 | this.fullMessageWithoutOwner = new ko.dependentObservable(function () { return this.greeting + " world" }); 139 | this.fullMessageWithOwner = new ko.dependentObservable(function () { return this.greeting + " world" }, this); 140 | })(); 141 | value_of(model.fullMessageWithoutOwner()).should_be("undefined world"); 142 | value_of(model.fullMessageWithOwner()).should_be("hello world"); 143 | }, 144 | 145 | 'Should dispose and not call its evaluator function when the disposeWhen function returns true': function () { 146 | var underlyingObservable = new ko.observable(100); 147 | var timeToDispose = false; 148 | var timesEvaluated = 0; 149 | var dependent = new ko.dependentObservable( 150 | function () { timesEvaluated++; return underlyingObservable() + 1; }, 151 | null, 152 | { disposeWhen: function () { return timeToDispose; } } 153 | ); 154 | value_of(timesEvaluated).should_be(1); 155 | value_of(dependent.getDependenciesCount()).should_be(1); 156 | 157 | timeToDispose = true; 158 | underlyingObservable(101); 159 | value_of(timesEvaluated).should_be(1); 160 | value_of(dependent.getDependenciesCount()).should_be(0); 161 | }, 162 | 163 | 'Should advertise that instances *can* have values written to them if you supply a "write" callback': function() { 164 | var instance = new ko.dependentObservable({ 165 | read: function() {}, 166 | write: function() {} 167 | }); 168 | value_of(ko.isWriteableObservable(instance)).should_be(true); 169 | }, 170 | 171 | 'Should allow deferring of evaluation (and hence dependency detection)': function () { 172 | var timesEvaluated = 0; 173 | var instance = new ko.dependentObservable({ 174 | read: function () { timesEvaluated++; return 123 }, 175 | deferEvaluation: true 176 | }); 177 | value_of(timesEvaluated).should_be(0); 178 | value_of(instance()).should_be(123); 179 | value_of(timesEvaluated).should_be(1); 180 | } 181 | }) -------------------------------------------------------------------------------- /spec/domNodeDisposalBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('DOM node disposal', { 3 | before_each: function () { 4 | testNode = document.createElement("div"); 5 | }, 6 | 7 | 'Should run registered disposal callbacks when a node is cleaned': function () { 8 | var didRun = false; 9 | ko.utils.domNodeDisposal.addDisposeCallback(testNode, function() { didRun = true }); 10 | 11 | value_of(didRun).should_be(false); 12 | ko.cleanNode(testNode); 13 | value_of(didRun).should_be(true); 14 | }, 15 | 16 | 'Should run registered disposal callbacks on descendants when a node is cleaned': function () { 17 | var didRun = false; 18 | var childNode = document.createElement("DIV"); 19 | var grandChildNode = document.createElement("DIV"); 20 | testNode.appendChild(childNode); 21 | childNode.appendChild(grandChildNode); 22 | ko.utils.domNodeDisposal.addDisposeCallback(grandChildNode, function() { didRun = true }); 23 | 24 | value_of(didRun).should_be(false); 25 | ko.cleanNode(testNode); 26 | value_of(didRun).should_be(true); 27 | }, 28 | 29 | 'Should run registered disposal callbacks and detach from DOM when a node is removed': function () { 30 | var didRun = false; 31 | var childNode = document.createElement("DIV"); 32 | testNode.appendChild(childNode); 33 | ko.utils.domNodeDisposal.addDisposeCallback(childNode, function() { didRun = true }); 34 | 35 | value_of(didRun).should_be(false); 36 | value_of(testNode.childNodes.length).should_be(1); 37 | ko.removeNode(childNode); 38 | value_of(didRun).should_be(true); 39 | value_of(testNode.childNodes.length).should_be(0); 40 | }, 41 | 42 | 'Should be able to remove previously-registered disposal callbacks': function() { 43 | var didRun = false; 44 | var callback = function() { didRun = true }; 45 | ko.utils.domNodeDisposal.addDisposeCallback(testNode, callback); 46 | 47 | value_of(didRun).should_be(false); 48 | ko.utils.domNodeDisposal.removeDisposeCallback(testNode, callback); 49 | ko.cleanNode(testNode); 50 | value_of(didRun).should_be(false); // Didn't run only because we removed it 51 | } 52 | }); -------------------------------------------------------------------------------- /spec/editDetectionBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | function copyDomNodeChildren(domNode) { 3 | var copy = []; 4 | for (var i = 0; i < domNode.childNodes.length; i++) 5 | copy.push(domNode.childNodes[i]); 6 | return copy; 7 | } 8 | 9 | describe('Compare Arrays', { 10 | 'Should recognize when two arrays have the same contents': function () { 11 | var subject = ["A", {}, function () { } ]; 12 | var compareResult = ko.utils.compareArrays(subject, subject.slice(0)); 13 | 14 | value_of(compareResult.length).should_be(subject.length); 15 | for (var i = 0; i < subject.length; i++) { 16 | value_of(compareResult[i].status).should_be("retained"); 17 | value_of(compareResult[i].value).should_be(subject[i]); 18 | } 19 | }, 20 | 21 | 'Should recognize added items': function () { 22 | var oldArray = ["A", "B"]; 23 | var newArray = ["A", "A2", "A3", "B", "B2"]; 24 | var compareResult = ko.utils.compareArrays(oldArray, newArray); 25 | value_of(compareResult).should_be([ 26 | { status: "retained", value: "A" }, 27 | { status: "added", value: "A2" }, 28 | { status: "added", value: "A3" }, 29 | { status: "retained", value: "B" }, 30 | { status: "added", value: "B2" } 31 | ]); 32 | }, 33 | 34 | 'Should recognize deleted items': function () { 35 | var oldArray = ["A", "B", "C", "D", "E"]; 36 | var newArray = ["B", "C", "E"]; 37 | var compareResult = ko.utils.compareArrays(oldArray, newArray); 38 | value_of(compareResult).should_be([ 39 | { status: "deleted", value: "A" }, 40 | { status: "retained", value: "B" }, 41 | { status: "retained", value: "C" }, 42 | { status: "deleted", value: "D" }, 43 | { status: "retained", value: "E" } 44 | ]); 45 | }, 46 | 47 | 'Should recognize mixed edits': function () { 48 | var oldArray = ["A", "B", "C", "D", "E"]; 49 | var newArray = [123, "A", "E", "C", "D"]; 50 | var compareResult = ko.utils.compareArrays(oldArray, newArray); 51 | value_of(compareResult).should_be([ 52 | { status: "added", value: 123 }, 53 | { status: "retained", value: "A" }, 54 | { status: "deleted", value: "B" }, 55 | { status: "added", value: "E" }, 56 | { status: "retained", value: "C" }, 57 | { status: "retained", value: "D" }, 58 | { status: "deleted", value: "E" } 59 | ]); 60 | } 61 | }); 62 | 63 | describe('Array to DOM node children mapping', { 64 | before_each: function () { 65 | testNode = document.createElement("div"); 66 | }, 67 | 68 | 'Should populate the DOM node by mapping array elements': function () { 69 | var array = ["A", "B"]; 70 | var mapping = function (arrayItem) { 71 | var output1 = document.createElement("DIV"); 72 | var output2 = document.createElement("DIV"); 73 | output1.innerHTML = arrayItem + "1"; 74 | output2.innerHTML = arrayItem + "2"; 75 | return [output1, output2]; 76 | }; 77 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, array, mapping); 78 | value_of(testNode.childNodes.length).should_be(4); 79 | value_of(testNode.childNodes[0].innerHTML).should_be("A1"); 80 | value_of(testNode.childNodes[1].innerHTML).should_be("A2"); 81 | value_of(testNode.childNodes[2].innerHTML).should_be("B1"); 82 | value_of(testNode.childNodes[3].innerHTML).should_be("B2"); 83 | }, 84 | 85 | 'Should only call the mapping function for new array elements': function () { 86 | var mappingInvocations = []; 87 | var mapping = function (arrayItem) { 88 | mappingInvocations.push(arrayItem); 89 | return null; 90 | }; 91 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B"], mapping); 92 | value_of(mappingInvocations).should_be(["A", "B"]); 93 | 94 | mappingInvocations = []; 95 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "A2", "B"], mapping); 96 | value_of(mappingInvocations).should_be(["A2"]); 97 | }, 98 | 99 | 'Should retain existing node instances if the array is unchanged': function () { 100 | var array = ["A", "B"]; 101 | var mapping = function (arrayItem) { 102 | var output1 = document.createElement("DIV"); 103 | var output2 = document.createElement("DIV"); 104 | output1.innerHTML = arrayItem + "1"; 105 | output2.innerHTML = arrayItem + "2"; 106 | return [output1, output2]; 107 | }; 108 | 109 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, array, mapping); 110 | var existingInstances = copyDomNodeChildren(testNode); 111 | 112 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, array, mapping); 113 | var newInstances = copyDomNodeChildren(testNode); 114 | 115 | value_of(newInstances).should_be(existingInstances); 116 | }, 117 | 118 | 'Should insert added nodes at the corresponding place in the DOM': function () { 119 | var mappingInvocations = []; 120 | var mapping = function (arrayItem) { 121 | mappingInvocations.push(arrayItem); 122 | var output = document.createElement("DIV"); 123 | output.innerHTML = arrayItem; 124 | return [output]; 125 | }; 126 | 127 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B"], mapping); 128 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A", "B"]); 129 | value_of(mappingInvocations).should_be(["A", "B"]); 130 | 131 | mappingInvocations = []; 132 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["first", "A", "middle1", "middle2", "B", "last"], mapping); 133 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["first", "A", "middle1", "middle2", "B", "last"]); 134 | value_of(mappingInvocations).should_be(["first", "middle1", "middle2", "last"]); 135 | }, 136 | 137 | 'Should remove deleted nodes from the DOM': function () { 138 | var mappingInvocations = []; 139 | var mapping = function (arrayItem) { 140 | mappingInvocations.push(arrayItem); 141 | var output = document.createElement("DIV"); 142 | output.innerHTML = arrayItem; 143 | return [output]; 144 | }; 145 | 146 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["first", "A", "middle1", "middle2", "B", "last"], mapping); 147 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["first", "A", "middle1", "middle2", "B", "last"]); 148 | value_of(mappingInvocations).should_be(["first", "A", "middle1", "middle2", "B", "last"]); 149 | 150 | mappingInvocations = []; 151 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B"], mapping); 152 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A", "B"]); 153 | value_of(mappingInvocations).should_be([]); 154 | }, 155 | 156 | 'Should handle sequences of mixed insertions and deletions': function () { 157 | var mappingInvocations = []; 158 | var mapping = function (arrayItem) { 159 | mappingInvocations.push(arrayItem); 160 | var output = document.createElement("DIV"); 161 | output.innerHTML = arrayItem || "null"; 162 | return [output]; 163 | }; 164 | 165 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A"], mapping); 166 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A"]); 167 | value_of(mappingInvocations).should_be(["A"]); 168 | 169 | mappingInvocations = []; 170 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["B"], mapping); // Delete and replace single item 171 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["B"]); 172 | value_of(mappingInvocations).should_be(["B"]); 173 | 174 | mappingInvocations = []; 175 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B", "C"], mapping); // Add at beginning and end 176 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A", "B", "C"]); 177 | value_of(mappingInvocations).should_be(["A", "C"]); 178 | 179 | mappingInvocations = []; 180 | ko.utils.setDomNodeChildrenFromArrayMapping(testNode, [1, null, "B"], mapping); // Add to beginning; delete from end 181 | value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["1", "null", "B"]); 182 | value_of(mappingInvocations).should_be([1, null]); 183 | } 184 | }); -------------------------------------------------------------------------------- /spec/jsonExpressionRewritingBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('JSON Expression Rewriting', { 3 | 'Should be able to get the source code corresponding to a top-level key': function () { 4 | var parsed = ko.jsonExpressionRewriting.parseJson('{ a : { a : 123, b : 2 }, b : 1 + 1, c : "yeah" }'); 5 | value_of(parsed.b).should_be("1 + 1"); 6 | }, 7 | 8 | 'Should convert JSON values to property accessors': function () { 9 | var rewritten = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson('a : 1, b : firstName, c : function() { return "returnValue"; }'); 10 | 11 | var model = { firstName: "bob", lastName: "smith" }; 12 | with (model) { 13 | var parsedRewritten = eval("({" + rewritten + "})"); 14 | value_of(parsedRewritten.a).should_be(1); 15 | value_of(parsedRewritten.b).should_be("bob"); 16 | value_of(parsedRewritten.c()).should_be("returnValue"); 17 | 18 | parsedRewritten._ko_property_writers.b("bob2"); 19 | value_of(model.firstName).should_be("bob2"); 20 | } 21 | 22 | } 23 | }); -------------------------------------------------------------------------------- /spec/jsonPostingBehaviors.js: -------------------------------------------------------------------------------- 1 | describe('JSON posting', { 2 | 'Should stringify and post the supplied data to a supplied URL': function () { 3 | var submittedForm; 4 | ko.utils.postJson('http://example.com/some/url', {myModel : {a : 1}}, { submitter : function(x) { submittedForm = x } }); 5 | 6 | value_of(submittedForm.action).should_be('http://example.com/some/url'); 7 | var input = submittedForm.childNodes[0]; 8 | value_of(input.tagName).should_be('INPUT'); 9 | value_of(input.name).should_be('myModel'); 10 | value_of(input.value).should_be('{"a":1}'); 11 | }, 12 | 13 | 'Given an existing form, should take the URL from the form\'s \'action\' attribute': function() { 14 | var existingForm = document.createElement("FORM"); 15 | existingForm.action = 'http://example.com/blah'; 16 | 17 | var submittedForm; 18 | ko.utils.postJson(existingForm, {myModel : {a : 1}}, { submitter : function(x) { submittedForm = x } }); 19 | 20 | value_of(submittedForm.action).should_be('http://example.com/blah'); 21 | }, 22 | 23 | 'Given an existing form, should include any requested field values from that form': function() { 24 | var existingForm = document.createElement("FORM"); 25 | existingForm.innerHTML = ''; 26 | 27 | var submittedForm; 28 | ko.utils.postJson(existingForm, {myModel : {a : 1}}, { includeFields : ['someField'], submitter : function(x) { submittedForm = x } }); 29 | 30 | value_of(ko.utils.getFormFields(submittedForm, 'someField')[0].value).should_be('myValue'); 31 | value_of(ko.utils.getFormFields(submittedForm, 'anotherField').length).should_be(0); 32 | }, 33 | 34 | 'Given an existing form, should include Rails and ASP.NET MVC auth tokens by default' : function() { 35 | var existingForm = document.createElement("FORM"); 36 | existingForm.innerHTML = '' 37 | + '' 38 | + ''; 39 | 40 | var submittedForm; 41 | ko.utils.postJson(existingForm, {myModel : {a : 1}}, { submitter : function(x) { submittedForm = x } }); 42 | 43 | value_of(ko.utils.getFormFields(submittedForm, '__RequestVerificationToken_Lr4e')[0].value).should_be('wantedval1'); 44 | value_of(ko.utils.getFormFields(submittedForm, '__RequestVe').length).should_be(0); 45 | value_of(ko.utils.getFormFields(submittedForm, 'authenticity_token')[0].value).should_be('wantedval2'); 46 | } 47 | }); -------------------------------------------------------------------------------- /spec/lib/JSSpec.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | 3 | /* -------------------- 4 | * @Layout 5 | */ 6 | 7 | html { 8 | overflow: hidden; 9 | } 10 | 11 | body, #jsspec_container { 12 | overflow: hidden; 13 | padding: 0; 14 | margin: 0; 15 | width: 100%; 16 | height: 100%; 17 | background-color: white; 18 | } 19 | 20 | #title { 21 | padding: 0; 22 | margin: 0; 23 | position: absolute; 24 | top: 0px; 25 | left: 0px; 26 | width: 100%; 27 | height: 40px; 28 | overflow: hidden; 29 | } 30 | 31 | #list { 32 | padding: 0; 33 | margin: 0; 34 | position: absolute; 35 | top: 40px; 36 | left: 0px; 37 | bottom: 0px; 38 | overflow: auto; 39 | width: 250px; 40 | _height:expression(document.body.clientHeight-40); 41 | } 42 | 43 | #log { 44 | padding: 0; 45 | margin: 0; 46 | position: absolute; 47 | top: 40px; 48 | left: 250px; 49 | right: 0px; 50 | bottom: 0px; 51 | overflow: auto; 52 | _height:expression(document.body.clientHeight-40); 53 | _width:expression(document.body.clientWidth-250); 54 | } 55 | 56 | 57 | 58 | /* -------------------- 59 | * @Decorations and colors 60 | */ 61 | * { 62 | padding: 0; 63 | margin: 0; 64 | font-family: "Lucida Grande", Helvetica, sans-serif; 65 | } 66 | 67 | li { 68 | list-style: none; 69 | } 70 | 71 | /* hiding subtitles */ 72 | h2 { 73 | display: none; 74 | } 75 | 76 | /* title section */ 77 | div#title { 78 | padding: 0em 0.5em; 79 | } 80 | 81 | div#title h1 { 82 | font-size: 1.5em; 83 | float: left; 84 | } 85 | 86 | div#title ul li { 87 | float: left; 88 | padding: 0.5em 0em 0.5em 0.75em; 89 | } 90 | 91 | div#title p { 92 | float:right; 93 | margin-right:1em; 94 | font-size: 0.75em; 95 | } 96 | 97 | /* spec container */ 98 | ul.specs { 99 | margin: 0.5em; 100 | } 101 | ul.specs li { 102 | margin-bottom: 0.1em; 103 | } 104 | 105 | /* spec title */ 106 | ul.specs li h3 { 107 | font-weight: bold; 108 | font-size: 0.75em; 109 | padding: 0.2em 1em; 110 | cursor: pointer; 111 | _cursor: hand; 112 | } 113 | 114 | /* example container */ 115 | ul.examples li { 116 | border-style: solid; 117 | border-width: 0px 0px 1px 5px; 118 | margin: 0.2em 0em 0.2em 1em; 119 | } 120 | 121 | /* example title */ 122 | ul.examples li h4 { 123 | font-weight: normal; 124 | font-size: 0.75em; 125 | margin-left: 1em; 126 | } 127 | 128 | pre.examples-code { 129 | margin: 0.5em 2em; 130 | padding: 0.5em; 131 | background: white; 132 | border: solid 1px #CCC; 133 | } 134 | 135 | /* example explaination */ 136 | ul.examples li div { 137 | padding: 1em 2em; 138 | font-size: 0.75em; 139 | } 140 | 141 | /* styles for ongoing, success, failure, error */ 142 | div.success, div.success a { 143 | color: #FFFFFF; 144 | background-color: #65C400; 145 | } 146 | 147 | ul.specs li.success h3, ul.specs li.success h3 a { 148 | color: #FFFFFF; 149 | background-color: #65C400; 150 | } 151 | 152 | ul.examples li.success, ul.examples li.success a { 153 | color: #3D7700; 154 | background-color: #DBFFB4; 155 | border-color: #65C400; 156 | } 157 | 158 | div.exception, div.exception a { 159 | color: #FFFFFF; 160 | background-color: #C20000; 161 | } 162 | 163 | ul.specs li.exception h3, ul.specs li.exception h3 a { 164 | color: #FFFFFF; 165 | background-color: #C20000; 166 | } 167 | 168 | ul.examples li.exception, ul.examples li.exception a { 169 | color: #C20000; 170 | background-color: #FFFBD3; 171 | border-color: #C20000; 172 | } 173 | 174 | div.ongoing, div.ongoing a { 175 | color: #000000; 176 | background-color: #FFFF80; 177 | } 178 | 179 | ul.specs li.ongoing h3, ul.specs li.ongoing h3 a { 180 | color: #000000; 181 | background-color: #FFFF80; 182 | } 183 | 184 | ul.examples li.ongoing, ul.examples li.ongoing a { 185 | color: #000000; 186 | background-color: #FFFF80; 187 | border-color: #DDDD00; 188 | } 189 | 190 | 191 | 192 | /* -------------------- 193 | * values 194 | */ 195 | .number_value, .string_value, .regexp_value, .boolean_value, .dom_value { 196 | font-family: monospace; 197 | color: blue; 198 | } 199 | .object_value, .array_value { 200 | line-height: 2em; 201 | padding: 0.1em 0.2em; 202 | margin: 0.1em 0; 203 | } 204 | .date_value { 205 | font-family: monospace; 206 | color: olive; 207 | } 208 | .undefined_value, .null_value { 209 | font-style: italic; 210 | color: blue; 211 | } 212 | .dom_attr_name { 213 | } 214 | .dom_attr_value { 215 | color: red; 216 | } 217 | .dom_path { 218 | font-size: 0.75em; 219 | color: gray; 220 | } 221 | strong { 222 | font-weight: normal; 223 | background-color: #FFC6C6; 224 | } -------------------------------------------------------------------------------- /spec/lib/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.JSON.org/json2.js 3 | 2010-03-20 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, strict: false */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (!this.JSON) { 163 | this.JSON = {}; 164 | } 165 | 166 | (function () { 167 | 168 | function f(n) { 169 | // Format integers to have at least two digits. 170 | return n < 10 ? '0' + n : n; 171 | } 172 | 173 | if (typeof Date.prototype.toJSON !== 'function') { 174 | 175 | Date.prototype.toJSON = function (key) { 176 | 177 | return isFinite(this.valueOf()) ? 178 | this.getUTCFullYear() + '-' + 179 | f(this.getUTCMonth() + 1) + '-' + 180 | f(this.getUTCDate()) + 'T' + 181 | f(this.getUTCHours()) + ':' + 182 | f(this.getUTCMinutes()) + ':' + 183 | f(this.getUTCSeconds()) + 'Z' : null; 184 | }; 185 | 186 | String.prototype.toJSON = 187 | Number.prototype.toJSON = 188 | Boolean.prototype.toJSON = function (key) { 189 | return this.valueOf(); 190 | }; 191 | } 192 | 193 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 194 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 195 | gap, 196 | indent, 197 | meta = { // table of character substitutions 198 | '\b': '\\b', 199 | '\t': '\\t', 200 | '\n': '\\n', 201 | '\f': '\\f', 202 | '\r': '\\r', 203 | '"' : '\\"', 204 | '\\': '\\\\' 205 | }, 206 | rep; 207 | 208 | 209 | function quote(string) { 210 | 211 | // If the string contains no control characters, no quote characters, and no 212 | // backslash characters, then we can safely slap some quotes around it. 213 | // Otherwise we must also replace the offending characters with safe escape 214 | // sequences. 215 | 216 | escapable.lastIndex = 0; 217 | return escapable.test(string) ? 218 | '"' + string.replace(escapable, function (a) { 219 | var c = meta[a]; 220 | return typeof c === 'string' ? c : 221 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 222 | }) + '"' : 223 | '"' + string + '"'; 224 | } 225 | 226 | 227 | function str(key, holder) { 228 | 229 | // Produce a string from holder[key]. 230 | 231 | var i, // The loop counter. 232 | k, // The member key. 233 | v, // The member value. 234 | length, 235 | mind = gap, 236 | partial, 237 | value = holder[key]; 238 | 239 | // If the value has a toJSON method, call it to obtain a replacement value. 240 | 241 | if (value && typeof value === 'object' && 242 | typeof value.toJSON === 'function') { 243 | value = value.toJSON(key); 244 | } 245 | 246 | // If we were called with a replacer function, then call the replacer to 247 | // obtain a replacement value. 248 | 249 | if (typeof rep === 'function') { 250 | value = rep.call(holder, key, value); 251 | } 252 | 253 | // What happens next depends on the value's type. 254 | 255 | switch (typeof value) { 256 | case 'string': 257 | return quote(value); 258 | 259 | case 'number': 260 | 261 | // JSON numbers must be finite. Encode non-finite numbers as null. 262 | 263 | return isFinite(value) ? String(value) : 'null'; 264 | 265 | case 'boolean': 266 | case 'null': 267 | 268 | // If the value is a boolean or null, convert it to a string. Note: 269 | // typeof null does not produce 'null'. The case is included here in 270 | // the remote chance that this gets fixed someday. 271 | 272 | return String(value); 273 | 274 | // If the type is 'object', we might be dealing with an object or an array or 275 | // null. 276 | 277 | case 'object': 278 | 279 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 280 | // so watch out for that case. 281 | 282 | if (!value) { 283 | return 'null'; 284 | } 285 | 286 | // Make an array to hold the partial results of stringifying this object value. 287 | 288 | gap += indent; 289 | partial = []; 290 | 291 | // Is the value an array? 292 | 293 | if (Object.prototype.toString.apply(value) === '[object Array]') { 294 | 295 | // The value is an array. Stringify every element. Use null as a placeholder 296 | // for non-JSON values. 297 | 298 | length = value.length; 299 | for (i = 0; i < length; i += 1) { 300 | partial[i] = str(i, value) || 'null'; 301 | } 302 | 303 | // Join all of the elements together, separated with commas, and wrap them in 304 | // brackets. 305 | 306 | v = partial.length === 0 ? '[]' : 307 | gap ? '[\n' + gap + 308 | partial.join(',\n' + gap) + '\n' + 309 | mind + ']' : 310 | '[' + partial.join(',') + ']'; 311 | gap = mind; 312 | return v; 313 | } 314 | 315 | // If the replacer is an array, use it to select the members to be stringified. 316 | 317 | if (rep && typeof rep === 'object') { 318 | length = rep.length; 319 | for (i = 0; i < length; i += 1) { 320 | k = rep[i]; 321 | if (typeof k === 'string') { 322 | v = str(k, value); 323 | if (v) { 324 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 325 | } 326 | } 327 | } 328 | } else { 329 | 330 | // Otherwise, iterate through all of the keys in the object. 331 | 332 | for (k in value) { 333 | if (Object.hasOwnProperty.call(value, k)) { 334 | v = str(k, value); 335 | if (v) { 336 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 337 | } 338 | } 339 | } 340 | } 341 | 342 | // Join all of the member texts together, separated with commas, 343 | // and wrap them in braces. 344 | 345 | v = partial.length === 0 ? '{}' : 346 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + 347 | mind + '}' : '{' + partial.join(',') + '}'; 348 | gap = mind; 349 | return v; 350 | } 351 | } 352 | 353 | // If the JSON object does not yet have a stringify method, give it one. 354 | 355 | if (typeof JSON.stringify !== 'function') { 356 | JSON.stringify = function (value, replacer, space) { 357 | 358 | // The stringify method takes a value and an optional replacer, and an optional 359 | // space parameter, and returns a JSON text. The replacer can be a function 360 | // that can replace values, or an array of strings that will select the keys. 361 | // A default replacer method can be provided. Use of the space parameter can 362 | // produce text that is more easily readable. 363 | 364 | var i; 365 | gap = ''; 366 | indent = ''; 367 | 368 | // If the space parameter is a number, make an indent string containing that 369 | // many spaces. 370 | 371 | if (typeof space === 'number') { 372 | for (i = 0; i < space; i += 1) { 373 | indent += ' '; 374 | } 375 | 376 | // If the space parameter is a string, it will be used as the indent string. 377 | 378 | } else if (typeof space === 'string') { 379 | indent = space; 380 | } 381 | 382 | // If there is a replacer, it must be a function or an array. 383 | // Otherwise, throw an error. 384 | 385 | rep = replacer; 386 | if (replacer && typeof replacer !== 'function' && 387 | (typeof replacer !== 'object' || 388 | typeof replacer.length !== 'number')) { 389 | throw new Error('JSON.stringify'); 390 | } 391 | 392 | // Make a fake root object containing our value under the key of ''. 393 | // Return the result of stringifying the value. 394 | 395 | return str('', {'': value}); 396 | }; 397 | } 398 | 399 | 400 | // If the JSON object does not yet have a parse method, give it one. 401 | 402 | if (typeof JSON.parse !== 'function') { 403 | JSON.parse = function (text, reviver) { 404 | 405 | // The parse method takes a text and an optional reviver function, and returns 406 | // a JavaScript value if the text is a valid JSON text. 407 | 408 | var j; 409 | 410 | function walk(holder, key) { 411 | 412 | // The walk method is used to recursively walk the resulting structure so 413 | // that modifications can be made. 414 | 415 | var k, v, value = holder[key]; 416 | if (value && typeof value === 'object') { 417 | for (k in value) { 418 | if (Object.hasOwnProperty.call(value, k)) { 419 | v = walk(value, k); 420 | if (v !== undefined) { 421 | value[k] = v; 422 | } else { 423 | delete value[k]; 424 | } 425 | } 426 | } 427 | } 428 | return reviver.call(holder, key, value); 429 | } 430 | 431 | 432 | // Parsing happens in four stages. In the first stage, we replace certain 433 | // Unicode characters with escape sequences. JavaScript handles many characters 434 | // incorrectly, either silently deleting them, or treating them as line endings. 435 | 436 | text = String(text); 437 | cx.lastIndex = 0; 438 | if (cx.test(text)) { 439 | text = text.replace(cx, function (a) { 440 | return '\\u' + 441 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 442 | }); 443 | } 444 | 445 | // In the second stage, we run the text against regular expressions that look 446 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 447 | // because they can cause invocation, and '=' because it can cause mutation. 448 | // But just to be safe, we want to reject all unexpected forms. 449 | 450 | // We split the second stage into 4 regexp operations in order to work around 451 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 452 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 453 | // replace all simple value tokens with ']' characters. Third, we delete all 454 | // open brackets that follow a colon or comma or that begin the text. Finally, 455 | // we look to see that the remaining characters are only whitespace or ']' or 456 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 457 | 458 | if (/^[\],:{}\s]*$/. 459 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). 460 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). 461 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 462 | 463 | // In the third stage we use the eval function to compile the text into a 464 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 465 | // in JavaScript: it can begin a block or an object literal. We wrap the text 466 | // in parens to eliminate the ambiguity. 467 | 468 | j = eval('(' + text + ')'); 469 | 470 | // In the optional fourth stage, we recursively walk the new structure, passing 471 | // each name/value pair to a reviver function for possible transformation. 472 | 473 | return typeof reviver === 'function' ? 474 | walk({'': j}, '') : j; 475 | } 476 | 477 | // If the text is not JSON parseable, then a SyntaxError is thrown. 478 | 479 | throw new SyntaxError('JSON.parse'); 480 | }; 481 | } 482 | }()); 483 | -------------------------------------------------------------------------------- /spec/mappingHelperBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Mapping helpers', { 3 | 'ko.toJS should require a parameter': function() { 4 | var didThrow = false; 5 | try { ko.toJS() } 6 | catch(ex) { didThow = true } 7 | value_of(didThow).should_be(true); 8 | }, 9 | 10 | 'ko.toJS should unwrap observable values': function() { 11 | var atomicValues = ["hello", 123, true, null, undefined, { a : 1 }]; 12 | for (var i = 0; i < atomicValues.length; i++) { 13 | var data = ko.observable(atomicValues[i]); 14 | var result = ko.toJS(data); 15 | value_of(ko.isObservable(result)).should_be(false); 16 | value_of(result).should_be(atomicValues[i]); 17 | } 18 | }, 19 | 20 | 'ko.toJS should recursively unwrap observables whose values are themselves observable': function() { 21 | var weirdlyNestedObservable = ko.observable( 22 | ko.observable( 23 | ko.observable( 24 | ko.observable('Hello') 25 | ) 26 | ) 27 | ); 28 | var result = ko.toJS(weirdlyNestedObservable); 29 | value_of(result).should_be('Hello'); 30 | }, 31 | 32 | 'ko.toJS should unwrap observable properties, including nested ones': function() { 33 | var data = { 34 | a : ko.observable(123), 35 | b : { 36 | b1 : ko.observable(456), 37 | b2 : [789, ko.observable('X')] 38 | } 39 | }; 40 | var result = ko.toJS(data); 41 | value_of(result.a).should_be(123); 42 | value_of(result.b.b1).should_be(456); 43 | value_of(result.b.b2[0]).should_be(789); 44 | value_of(result.b.b2[1]).should_be('X'); 45 | }, 46 | 47 | 'ko.toJS should unwrap observable arrays and things inside them': function() { 48 | var data = ko.observableArray(['a', 1, { someProp : ko.observable('Hey') }]); 49 | var result = ko.toJS(data); 50 | value_of(result.length).should_be(3); 51 | value_of(result[0]).should_be('a'); 52 | value_of(result[1]).should_be(1); 53 | value_of(result[2].someProp).should_be('Hey'); 54 | }, 55 | 56 | 'ko.toJS should resolve reference cycles': function() { 57 | var obj = {}; 58 | obj.someProp = { owner : ko.observable(obj) }; 59 | var result = ko.toJS(obj); 60 | value_of(result.someProp.owner).should_be(result); 61 | }, 62 | 63 | 'ko.toJSON should unwrap everything and then stringify': function() { 64 | var data = ko.observableArray(['a', 1, { someProp : ko.observable('Hey') }]); 65 | var result = ko.toJSON(data); 66 | 67 | // Check via parsing so the specs are independent of browser-specific JSON string formatting 68 | value_of(typeof result).should_be('string'); 69 | var parsedResult = ko.utils.parseJson(result); 70 | value_of(parsedResult.length).should_be(3); 71 | value_of(parsedResult[0]).should_be('a'); 72 | value_of(parsedResult[1]).should_be(1); 73 | value_of(parsedResult[2].someProp).should_be('Hey'); 74 | } 75 | }) -------------------------------------------------------------------------------- /spec/memoizationBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | function parseMemoCommentHtml(commentHtml) { 3 | commentHtml = commentHtml.replace("", ""); 4 | return ko.memoization.parseMemoText(commentHtml); 5 | } 6 | 7 | describe('Memoization', { 8 | "Should only accept a function": function () { 9 | var threw = false; 10 | try { ko.memoization.memoize({}) } 11 | catch (ex) { threw = true; } 12 | value_of(threw).should_be(true); 13 | }, 14 | 15 | "Should return an HTML comment": function () { 16 | var result = ko.memoization.memoize(function () { }); 17 | value_of(typeof result).should_be("string"); 18 | value_of(result.substring(0, 4)).should_be(" 8 | 9 | 10 | 11 | 12 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |

A

B

48 | 49 | -------------------------------------------------------------------------------- /spec/subscribableBehaviors.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Subscribable', { 3 | 'Should declare that it is subscribable': function () { 4 | var instance = new ko.subscribable(); 5 | value_of(ko.isSubscribable(instance)).should_be(true); 6 | }, 7 | 8 | 'Should be able to notify subscribers': function () { 9 | var instance = new ko.subscribable(); 10 | var notifiedValue; 11 | instance.subscribe(function (value) { notifiedValue = value; }); 12 | instance.notifySubscribers(123); 13 | value_of(notifiedValue).should_be(123); 14 | }, 15 | 16 | 'Should be able to unsubscribe': function () { 17 | var instance = new ko.subscribable(); 18 | var notifiedValue; 19 | var subscription = instance.subscribe(function (value) { notifiedValue = value; }); 20 | subscription.dispose(); 21 | instance.notifySubscribers(123); 22 | value_of(notifiedValue).should_be(undefined); 23 | }, 24 | 25 | 'Should be able to specify a \'this\' pointer for the callback': function () { 26 | var model = { 27 | someProperty: 123, 28 | myCallback: function (arg) { value_of(arg).should_be('notifiedValue'); value_of(this.someProperty).should_be(123); } 29 | }; 30 | var instance = new ko.subscribable(); 31 | instance.subscribe(model.myCallback, model); 32 | instance.notifySubscribers('notifiedValue'); 33 | }, 34 | 35 | 'Should not notify subscribers after unsubscription, even if the unsubscription occurs midway through a notification cycle': function() { 36 | // This spec represents the unusual case where during notification, subscription1's callback causes subscription2 to be disposed. 37 | // Since subscription2 was still active at the start of the cycle, it is scheduled to be notified. This spec verifies that 38 | // even though it is scheduled to be notified, it does not get notified, because the unsubscription just happened. 39 | var instance = new ko.subscribable(); 40 | var subscription1 = instance.subscribe(function() { 41 | subscription2.dispose(); 42 | }); 43 | var subscription2wasNotified = false; 44 | var subscription2 = instance.subscribe(function() { 45 | subscription2wasNotified = true; 46 | }); 47 | 48 | instance.notifySubscribers('ignored'); 49 | value_of(subscription2wasNotified).should_be(false); 50 | } 51 | }); -------------------------------------------------------------------------------- /src/binding/bindingAttributeSyntax.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var defaultBindingAttributeName = "data-bind"; 4 | ko.bindingHandlers = {}; 5 | 6 | function parseBindingAttribute(attributeText, viewModel, node) { 7 | try { 8 | var json = " { " + ko.jsonExpressionRewriting.insertPropertyReaderWritersIntoJson(attributeText) + " } "; 9 | return ko.utils.evalWithinScope(json, viewModel === null ? window : viewModel, node); 10 | } catch (ex) { 11 | if(typeof(console) !== 'undefined') { 12 | console.log("!!! ERROR"); 13 | console.log(attributeText); 14 | console.log(ex); 15 | } 16 | //@modified 17 | //throw new Error("Unable to parse binding attribute.\nMessage: " + ex + ";\nAttribute value: " + attributeText); 18 | } 19 | }; 20 | 21 | function invokeBindingHandler(handler, element, dataValue, allBindings, viewModel) { 22 | handler(element, dataValue, allBindings, viewModel); 23 | } 24 | 25 | ko.applyBindingsToNode = function (node, bindings, viewModel, bindingAttributeName) { 26 | var isFirstEvaluation = true; 27 | bindingAttributeName = bindingAttributeName || defaultBindingAttributeName; 28 | 29 | // Each time the dependentObservable is evaluated (after data changes), 30 | // the binding attribute is reparsed so that it can pick out the correct 31 | // model properties in the context of the changed data. 32 | // DOM event callbacks need to be able to access this changed data, 33 | // so we need a single parsedBindings variable (shared by all callbacks 34 | // associated with this node's bindings) that all the closures can access. 35 | var parsedBindings; 36 | function makeValueAccessor(bindingKey) { 37 | return function () { return parsedBindings[bindingKey] } 38 | } 39 | function parsedBindingsAccessor() { 40 | return parsedBindings; 41 | } 42 | 43 | new ko.dependentObservable( 44 | function () { 45 | 46 | 47 | var evaluatedBindings, bindingsToBeEvaluated; 48 | if(typeof(bindings) == 'function') { 49 | viewModel['skonode'] = node; 50 | bindingsToBeEvaluated = bindings; 51 | with(viewModel){ evaluatedBindings = bindingsToBeEvaluated() }; 52 | } else { 53 | evaluatedBindings = bindings; 54 | } 55 | 56 | parsedBindings = evaluatedBindings || parseBindingAttribute(node.getAttribute(bindingAttributeName), viewModel, node); 57 | 58 | 59 | // First run all the inits, so bindings can register for notification on changes 60 | if (isFirstEvaluation) { 61 | for (var bindingKey in parsedBindings) { 62 | if (ko.bindingHandlers[bindingKey] && typeof ko.bindingHandlers[bindingKey]["init"] == "function") 63 | invokeBindingHandler(ko.bindingHandlers[bindingKey]["init"], node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel); 64 | } 65 | } 66 | 67 | // ... then run all the updates, which might trigger changes even on the first evaluation 68 | for (var bindingKey in parsedBindings) { 69 | if (ko.bindingHandlers[bindingKey] && typeof ko.bindingHandlers[bindingKey]["update"] == "function") 70 | invokeBindingHandler(ko.bindingHandlers[bindingKey]["update"], node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel); 71 | } 72 | }, 73 | null, 74 | { 'disposeWhenNodeIsRemoved' : node } 75 | ); 76 | isFirstEvaluation = false; 77 | }; 78 | 79 | ko.applyBindings = function (viewModel, rootNode) { 80 | if (rootNode && (rootNode.nodeType == undefined)) 81 | throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node (note: this is a breaking change since KO version 1.05)"); 82 | rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional 83 | 84 | var elemsWithBindingAttribute = ko.utils.getElementsHavingAttribute(rootNode, defaultBindingAttributeName); 85 | ko.utils.arrayForEach(elemsWithBindingAttribute, function (element) { 86 | ko.applyBindingsToNode(element, null, viewModel); 87 | }); 88 | }; 89 | 90 | ko.exportSymbol('ko.bindingHandlers', ko.bindingHandlers); 91 | ko.exportSymbol('ko.applyBindings', ko.applyBindings); 92 | ko.exportSymbol('ko.applyBindingsToNode', ko.applyBindingsToNode); 93 | })(); 94 | -------------------------------------------------------------------------------- /src/binding/editDetection/arrayToDomNodeChildren.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | // Objective: 4 | // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes, 5 | // map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node 6 | // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node 7 | // so that its children is again the concatenation of the mappings of the array elements, but don't re-map any array elements that we 8 | // previously mapped - retain those nodes, and just insert/delete other ones 9 | 10 | function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap) { 11 | // Map this array value inside a dependentObservable so we re-map when any dependency changes 12 | var mappedNodes = []; 13 | var dependentObservable = ko.dependentObservable(function() { 14 | var newMappedNodes = mapping(valueToMap) || []; 15 | 16 | // On subsequent evaluations, just replace the previously-inserted DOM nodes 17 | if (mappedNodes.length > 0) 18 | ko.utils.replaceDomNodes(mappedNodes, newMappedNodes); 19 | 20 | // Replace the contents of the mappedNodes array, thereby updating the record 21 | // of which nodes would be deleted if valueToMap was itself later removed 22 | mappedNodes.splice(0, mappedNodes.length); 23 | ko.utils.arrayPushAll(mappedNodes, newMappedNodes); 24 | }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } }); 25 | return { mappedNodes : mappedNodes, dependentObservable : dependentObservable }; 26 | } 27 | 28 | ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options) { 29 | // Compare the provided array against the previous one 30 | array = array || []; 31 | options = options || {}; 32 | var isFirstExecution = ko.utils.domData.get(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult") === undefined; 33 | var lastMappingResult = ko.utils.domData.get(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult") || []; 34 | var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; }); 35 | var editScript = ko.utils.compareArrays(lastArray, array); 36 | 37 | // Build the new mapping result 38 | var newMappingResult = []; 39 | var lastMappingResultIndex = 0; 40 | var nodesToDelete = []; 41 | var nodesAdded = []; 42 | var insertAfterNode = null; 43 | for (var i = 0, j = editScript.length; i < j; i++) { 44 | switch (editScript[i].status) { 45 | case "retained": 46 | // Just keep the information - don't touch the nodes 47 | var dataToRetain = lastMappingResult[lastMappingResultIndex]; 48 | newMappingResult.push(dataToRetain); 49 | if (dataToRetain.domNodes.length > 0) 50 | insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1]; 51 | lastMappingResultIndex++; 52 | break; 53 | 54 | case "deleted": 55 | // Stop tracking changes to the mapping for these nodes 56 | lastMappingResult[lastMappingResultIndex].dependentObservable.dispose(); 57 | 58 | // Queue these nodes for later removal 59 | ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) { 60 | nodesToDelete.push({ 61 | element: node, 62 | index: i, 63 | value: editScript[i].value 64 | }); 65 | insertAfterNode = node; 66 | }); 67 | lastMappingResultIndex++; 68 | break; 69 | 70 | case "added": 71 | var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, editScript[i].value); 72 | var mappedNodes = mapData.mappedNodes; 73 | 74 | // On the first evaluation, insert the nodes at the current insertion point 75 | newMappingResult.push({ arrayEntry: editScript[i].value, domNodes: mappedNodes, dependentObservable: mapData.dependentObservable }); 76 | for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) { 77 | var node = mappedNodes[nodeIndex]; 78 | nodesAdded.push({ 79 | element: node, 80 | index: i, 81 | value: editScript[i].value 82 | }); 83 | if (insertAfterNode == null) { 84 | // Insert at beginning 85 | if (domNode.firstChild) 86 | domNode.insertBefore(node, domNode.firstChild); 87 | else 88 | domNode.appendChild(node); 89 | } else { 90 | // Insert after insertion point 91 | if (insertAfterNode.nextSibling) 92 | domNode.insertBefore(node, insertAfterNode.nextSibling); 93 | else 94 | domNode.appendChild(node); 95 | } 96 | insertAfterNode = node; 97 | } 98 | break; 99 | } 100 | } 101 | 102 | ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) }); 103 | 104 | var invokedBeforeRemoveCallback = false; 105 | if (!isFirstExecution) { 106 | if (options['afterAdd']) { 107 | for (var i = 0; i < nodesAdded.length; i++) 108 | options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value); 109 | } 110 | if (options['beforeRemove']) { 111 | for (var i = 0; i < nodesToDelete.length; i++) 112 | options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value); 113 | invokedBeforeRemoveCallback = true; 114 | } 115 | } 116 | if (!invokedBeforeRemoveCallback) 117 | ko.utils.arrayForEach(nodesToDelete, function (node) { 118 | if (node.element.parentNode) 119 | node.element.parentNode.removeChild(node.element); 120 | }); 121 | 122 | // Store a copy of the array items we just considered so we can difference it next time 123 | ko.utils.domData.set(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult", newMappingResult); 124 | } 125 | })(); 126 | 127 | ko.exportSymbol('ko.utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping); 128 | -------------------------------------------------------------------------------- /src/binding/editDetection/compareArrays.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | // Simple calculation based on Levenshtein distance. 4 | function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) { 5 | var distances = []; 6 | for (var i = 0; i <= newArray.length; i++) 7 | distances[i] = []; 8 | 9 | // Top row - transform old array into empty array via deletions 10 | for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++) 11 | distances[0][i] = i; 12 | 13 | // Left row - transform empty array into new array via additions 14 | for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) { 15 | distances[i][0] = i; 16 | } 17 | 18 | // Fill out the body of the array 19 | var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length; 20 | var distanceViaAddition, distanceViaDeletion; 21 | for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) { 22 | var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance); 23 | var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance); 24 | for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) { 25 | if (oldArray[oldIndex - 1] === newArray[newIndex - 1]) 26 | distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1]; 27 | else { 28 | var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1; 29 | var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1; 30 | distances[newIndex][oldIndex] = Math.min(northDistance, westDistance); 31 | } 32 | } 33 | } 34 | 35 | return distances; 36 | } 37 | 38 | function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) { 39 | var oldIndex = oldArray.length; 40 | var newIndex = newArray.length; 41 | var editScript = []; 42 | var maxDistance = editDistanceMatrix[newIndex][oldIndex]; 43 | if (maxDistance === undefined) 44 | return null; // maxAllowedDistance must be too small 45 | while ((oldIndex > 0) || (newIndex > 0)) { 46 | var me = editDistanceMatrix[newIndex][oldIndex]; 47 | var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1; 48 | var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1; 49 | var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1; 50 | if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1; 51 | if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1; 52 | if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1; 53 | 54 | if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) { 55 | editScript.push({ status: "added", value: newArray[newIndex - 1] }); 56 | newIndex--; 57 | } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) { 58 | editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] }); 59 | oldIndex--; 60 | } else { 61 | editScript.push({ status: "retained", value: oldArray[oldIndex - 1] }); 62 | newIndex--; 63 | oldIndex--; 64 | } 65 | } 66 | return editScript.reverse(); 67 | } 68 | 69 | ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) { 70 | if (maxEditsToConsider === undefined) { 71 | return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast) 72 | || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast 73 | || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time 74 | } else { 75 | oldArray = oldArray || []; 76 | newArray = newArray || []; 77 | var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider); 78 | return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray); 79 | } 80 | }; 81 | })(); 82 | 83 | ko.exportSymbol('ko.utils.compareArrays', ko.utils.compareArrays); 84 | -------------------------------------------------------------------------------- /src/binding/jsonExpressionRewriting.js: -------------------------------------------------------------------------------- 1 | 2 | ko.jsonExpressionRewriting = (function () { 3 | var restoreCapturedTokensRegex = /\[ko_token_(\d+)\]/g; 4 | var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i; 5 | var javaScriptReservedWords = ["true", "false"]; 6 | 7 | function restoreTokens(string, tokens) { 8 | return string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) { 9 | return tokens[tokenIndex]; 10 | }); 11 | } 12 | 13 | function isWriteableValue(expression) { 14 | if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0) 15 | return false; 16 | if(expression[0]=="<" && expression[expression.length-1]==">") 17 | return true; 18 | return expression.match(javaScriptAssignmentTarget) !== null; 19 | } 20 | 21 | return { 22 | parseJson: function (jsonString) { 23 | jsonString = ko.utils.stringTrim(jsonString); 24 | if (jsonString.length < 3) 25 | return {}; 26 | 27 | //@modified 28 | // added counter of nested curly braces 29 | 30 | // We're going to split on commas, so first extract any blocks that may contain commas other than those at the top level 31 | var tokens = []; 32 | var tokenStart = null, tokenEndChar, tokenCounter; 33 | for (var position = jsonString.charAt(0) == "{" ? 1 : 0; position < jsonString.length; position++) { 34 | var c = jsonString.charAt(position); 35 | if (tokenStart === null) { 36 | switch (c) { 37 | case '"': 38 | case "'": 39 | case "/": 40 | tokenStart = position; 41 | tokenEndChar = c; 42 | break; 43 | case "<": 44 | tokenStart = position; 45 | tokenEndChar = ">"; 46 | break; 47 | case "{": 48 | tokenStart = position; 49 | tokenCounter = 1; 50 | tokenEndChar = "}"; 51 | break; 52 | case "[": 53 | tokenStart = position; 54 | tokenEndChar = "]"; 55 | break; 56 | } 57 | } else if(tokenEndChar == "}" && c == "{") { 58 | tokenCounter++; 59 | } else if(tokenEndChar == "}" && c == "}" && tokenCounter>1) { 60 | tokenCounter--; 61 | } else if (c == tokenEndChar) { 62 | var token = jsonString.substring(tokenStart, position + 1); 63 | tokens.push(token); 64 | var replacement = "[ko_token_" + (tokens.length - 1) + "]"; 65 | jsonString = jsonString.substring(0, tokenStart) + replacement + jsonString.substring(position + 1); 66 | position -= (token.length - replacement.length); 67 | tokenStart = null; 68 | } 69 | } 70 | 71 | // Now we can safely split on commas to get the key/value pairs 72 | var result = {}; 73 | var keyValuePairs = jsonString.split(","); 74 | for (var i = 0, j = keyValuePairs.length; i < j; i++) { 75 | var pair = keyValuePairs[i]; 76 | var colonPos = pair.indexOf(":"); 77 | if ((colonPos > 0) && (colonPos < pair.length - 1)) { 78 | var key = ko.utils.stringTrim(pair.substring(0, colonPos)); 79 | var value = ko.utils.stringTrim(pair.substring(colonPos + 1)); 80 | if (key.charAt(0) == "{") 81 | key = key.substring(1); 82 | if (value.charAt(value.length - 1) == "}") 83 | value = value.substring(0, value.length - 1); 84 | key = ko.utils.stringTrim(restoreTokens(key, tokens)); 85 | value = ko.utils.stringTrim(restoreTokens(value, tokens)); 86 | result[key] = value; 87 | } 88 | } 89 | return result; 90 | }, 91 | 92 | insertPropertyAccessorsIntoJson: function (jsonString) { 93 | var parsed = ko.jsonExpressionRewriting.parseJson(jsonString); 94 | var propertyAccessorTokens = []; 95 | for (var key in parsed) { 96 | var value = parsed[key]; 97 | if (isWriteableValue(value)) { 98 | if (propertyAccessorTokens.length > 0) 99 | propertyAccessorTokens.push(", "); 100 | propertyAccessorTokens.push(key + " : function(__ko_value) { " + value + " = __ko_value; }"); 101 | } 102 | } 103 | 104 | if (propertyAccessorTokens.length > 0) { 105 | var allPropertyAccessors = propertyAccessorTokens.join(""); 106 | jsonString = jsonString + ", '_ko_property_writers' : { " + allPropertyAccessors + " } "; 107 | } 108 | 109 | return jsonString; 110 | }, 111 | 112 | //@modified 113 | parseURIsInJSONString: function(jsonString) { 114 | var re = /["']?(<|\[)[a-z:\/.#?&%]+(\]|>)['"]?/g; 115 | var acum = ""; 116 | var found = re.exec(jsonString); 117 | while(found != null) { 118 | if((found[0][0] === "'" || found[0][0] === '"') && 119 | (found[0][found[0].length-1] === "'" || found[0][found[0].length-1] === '"')) { 120 | var parts = jsonString.split(found[0]); 121 | acum = acum + parts[0] + found[0]; 122 | jsonString = parts[1]; 123 | } else { 124 | var w = found[0]; 125 | var index = found.index; 126 | var pref = jsonString.substring(0,index); 127 | acum = pref+"sko.current().tryProperty('"+w+"')"; 128 | jsonString= jsonString.substring(index+w.length); 129 | } 130 | found = re.exec(jsonString); 131 | } 132 | 133 | return acum+jsonString; 134 | }, 135 | 136 | insertPropertyReaderWritersIntoJson: function (jsonString) { 137 | var parsed = ko.jsonExpressionRewriting.parseJson(jsonString); 138 | var propertyAccessorTokens = []; 139 | var readers = ""; 140 | var isFirst = true; 141 | for (var key in parsed) { 142 | var value = parsed[key]; 143 | 144 | value = this.parseURIsInJSONString(value); 145 | if (isWriteableValue(value)) { 146 | if (propertyAccessorTokens.length > 0) 147 | propertyAccessorTokens.push(", "); 148 | if(value[0]==="<" && value[value.length-1]===">" && key !== 'about' && key !== 'rel') { 149 | propertyAccessorTokens.push(key + " : function(__ko_value) { sko.current = function() { return sko.currentResource(innerNode); }; sko.current().tryProperty('" + value + "') = __ko_value; }"); 150 | } else if(value.match(/^\[[^,;"\]\}\{\[\.:]+:[^,;"\}\]\{\[\.:]+\]$/) != null && key !== 'about' && key !== 'rel') { 151 | propertyAccessorTokens.push(key + " : function(__ko_value) { sko.current = function() { return sko.currentResource(innerNode); }; sko.current().tryProperty('" + value + "') = __ko_value; }"); 152 | } else if(value[0]==="<" && value[value.length-1]===">" && (key === 'about' || key === 'rel')) { 153 | // nothing here 154 | } else if(value[0]==="[" && value[value.length-1]==="]" && (key === 'about' || key === 'rel')) { 155 | // nothing here 156 | } else { 157 | if(/tryProperty\([^)]+\)$/.test(value) || /prop\([^)]+\)$/.test(value)) { 158 | propertyAccessorTokens.push(key + " : function(__ko_value) { sko.current = function() { return sko.currentResource(innerNode); }; " + value + "(__ko_value); }"); 159 | } else { 160 | propertyAccessorTokens.push(key + " : function(__ko_value) { sko.current = function() { return sko.currentResource(innerNode); }; " + value + " = __ko_value; }"); 161 | } 162 | } 163 | } 164 | if(!isFirst) { 165 | readers = readers+", "; 166 | } else { 167 | isFirst = false; 168 | } 169 | if(value[0]==='<' && value[value.length-1]==='>' && key !== 'about' && key !== 'rel') { 170 | readers = readers+key+": (function(){ sko.current = function() { return sko.currentResource(innerNode); }; return sko.current().tryProperty('"+value+"') })()"; 171 | } else if(value.match(/^\[[^,;"\]\}\{\[\.:]+:[^,;"\}\]\{\[\.:]+\]$/) != null && key !== 'about' && key !== 'rel') { 172 | readers = readers+key+": (function(){ sko.current = function() { return sko.currentResource(innerNode); }; return sko.current().tryProperty('"+value+"') })()"; 173 | } else if(value[0]==="<" && value[value.length-1]===">" && (key === 'about' || key === 'rel')) { 174 | readers = readers+key+": '"+value.slice(1,value.length-1)+"'"; 175 | } else if(value.match(/^\[[^,;"\]\}\{\[\.:]+:[^,;"\}\]\{\[\.:]+\]$/) != null && (key === 'about' || key === 'rel')) { 176 | readers = readers+key+": sko.rdf.prefixes.resolve('"+value.slice(1,value.length-1)+"')"; 177 | } else { 178 | readers = readers+key+": (function(){ sko.current = function() { return sko.currentResource(innerNode); }; return "+value+" })()"; 179 | } 180 | } 181 | 182 | jsonString = readers; 183 | 184 | if (propertyAccessorTokens.length > 0) { 185 | var allPropertyAccessors = propertyAccessorTokens.join(""); 186 | jsonString = jsonString + ", '_ko_property_writers' : { " + allPropertyAccessors + " } "; 187 | } 188 | 189 | return jsonString; 190 | } 191 | 192 | }; 193 | })(); 194 | 195 | ko.exportSymbol('ko.jsonExpressionRewriting', ko.jsonExpressionRewriting); 196 | ko.exportSymbol('ko.jsonExpressionRewriting.parseJson', ko.jsonExpressionRewriting.parseJson); 197 | ko.exportSymbol('ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson); 198 | ko.exportSymbol('ko.jsonExpressionRewriting.insertPropertyReaderWritersIntoJson', ko.jsonExpressionRewriting.insertPropertyReaderWritersIntoJson); 199 | -------------------------------------------------------------------------------- /src/binding/selectExtensions.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values 3 | // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values 4 | // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns. 5 | ko.selectExtensions = { 6 | readValue : function(element) { 7 | if (element.tagName == 'OPTION') { 8 | if (element['__ko__hasDomDataOptionValue__'] === true) 9 | return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey); 10 | return element.getAttribute("value"); 11 | } else if (element.tagName == 'SELECT') 12 | return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined; 13 | else 14 | return element.value; 15 | }, 16 | 17 | writeValue: function(element, value) { 18 | if (element.tagName == 'OPTION') { 19 | switch(typeof value) { 20 | case "string": 21 | case "number": 22 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined); 23 | if ('__ko__hasDomDataOptionValue__' in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node 24 | delete element['__ko__hasDomDataOptionValue__']; 25 | } 26 | element.value = value; 27 | break; 28 | default: 29 | // Store arbitrary object using DomData 30 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value); 31 | element['__ko__hasDomDataOptionValue__'] = true; 32 | element.value = ""; 33 | break; 34 | } 35 | } else if (element.tagName == 'SELECT') { 36 | for (var i = element.options.length - 1; i >= 0; i--) { 37 | if (ko.selectExtensions.readValue(element.options[i]) == value) { 38 | element.selectedIndex = i; 39 | break; 40 | } 41 | } 42 | } else { 43 | if ((value === null) || (value === undefined)) 44 | value = ""; 45 | element.value = value; 46 | } 47 | } 48 | }; 49 | })(); 50 | 51 | ko.exportSymbol('ko.selectExtensions', ko.selectExtensions); 52 | ko.exportSymbol('ko.selectExtensions.readValue', ko.selectExtensions.readValue); 53 | ko.exportSymbol('ko.selectExtensions.writeValue', ko.selectExtensions.writeValue); 54 | -------------------------------------------------------------------------------- /src/google-closure-compiler-utils.js: -------------------------------------------------------------------------------- 1 | // Google Closure Compiler helpers (used only to make the minified file smaller) 2 | ko.exportSymbol = function(publicPath, object) { 3 | var tokens = publicPath.split("."); 4 | var target = window; 5 | for (var i = 0; i < tokens.length - 1; i++) 6 | target = target[tokens[i]]; 7 | target[tokens[tokens.length - 1]] = object; 8 | }; 9 | ko.exportProperty = function(owner, publicName, object) { 10 | owner[publicName] = object; 11 | }; -------------------------------------------------------------------------------- /src/memoization.js: -------------------------------------------------------------------------------- 1 | 2 | ko.memoization = (function () { 3 | var memos = {}; 4 | 5 | function randomMax8HexChars() { 6 | return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); 7 | } 8 | function generateRandomId() { 9 | return randomMax8HexChars() + randomMax8HexChars(); 10 | } 11 | function findMemoNodes(rootNode, appendToArray) { 12 | if (!rootNode) 13 | return; 14 | if (rootNode.nodeType == 8) { 15 | var memoId = ko.memoization.parseMemoText(rootNode.nodeValue); 16 | if (memoId != null) 17 | appendToArray.push({ domNode: rootNode, memoId: memoId }); 18 | } else if (rootNode.nodeType == 1) { 19 | for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++) 20 | findMemoNodes(childNodes[i], appendToArray); 21 | } 22 | } 23 | 24 | return { 25 | memoize: function (callback) { 26 | if (typeof callback != "function") 27 | throw new Error("You can only pass a function to ko.memoization.memoize()"); 28 | var memoId = generateRandomId(); 29 | memos[memoId] = callback; 30 | return ""; 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 | 51 | if (extraCallbackParamsArray) 52 | ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray); 53 | 54 | var viewModel = extraCallbackParamsArray[0]; 55 | sko.traceResources(domNode, viewModel, function(){ 56 | sko.traceRelations(domNode, viewModel, function(){ 57 | try{ 58 | if(memos[i]) { 59 | ko.memoization.unmemoize(memos[i].memoId, combinedParams); 60 | node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again 61 | } 62 | 63 | if (node.parentNode) 64 | 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) 65 | }catch(e) {} 66 | }); 67 | }); 68 | } 69 | }, 70 | 71 | parseMemoText: function (memoText) { 72 | var match = memoText.match(/^\[ko_memo\:(.*?)\]$/); 73 | return match ? match[1] : null; 74 | } 75 | }; 76 | })(); 77 | 78 | ko.exportSymbol('ko.memoization', ko.memoization); 79 | ko.exportSymbol('ko.memoization.memoize', ko.memoization.memoize); 80 | ko.exportSymbol('ko.memoization.unmemoize', ko.memoization.unmemoize); 81 | ko.exportSymbol('ko.memoization.parseMemoText', ko.memoization.parseMemoText); 82 | ko.exportSymbol('ko.memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants); 83 | -------------------------------------------------------------------------------- /src/namespace.js: -------------------------------------------------------------------------------- 1 | var ko = window["ko"] = {}; 2 | -------------------------------------------------------------------------------- /src/subscribables/dependencyDetection.js: -------------------------------------------------------------------------------- 1 | 2 | ko.dependencyDetection = (function () { 3 | var _detectedDependencies = []; 4 | 5 | return { 6 | begin: function () { 7 | _detectedDependencies.push([]); 8 | }, 9 | 10 | end: function () { 11 | return _detectedDependencies.pop(); 12 | }, 13 | 14 | registerDependency: function (subscribable) { 15 | if (!ko.isSubscribable(subscribable)) 16 | throw "Only subscribable things can act as dependencies"; 17 | if (_detectedDependencies.length > 0) { 18 | _detectedDependencies[_detectedDependencies.length - 1].push(subscribable); 19 | } 20 | } 21 | }; 22 | })(); -------------------------------------------------------------------------------- /src/subscribables/dependentObservable.js: -------------------------------------------------------------------------------- 1 | ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) { 2 | var _latestValue, _hasBeenEvaluated = false; 3 | 4 | if (evaluatorFunctionOrOptions && typeof evaluatorFunctionOrOptions == "object") { 5 | // Single-parameter syntax - everything is on this "options" param 6 | options = evaluatorFunctionOrOptions; 7 | } else { 8 | // Multi-parameter syntax - construct the options according to the params passed 9 | options = options || {}; 10 | options["read"] = evaluatorFunctionOrOptions || options["read"]; 11 | options["owner"] = evaluatorFunctionTarget || options["owner"]; 12 | } 13 | // By here, "options" is always non-null 14 | 15 | if (typeof options["read"] != "function") 16 | throw "Pass a function that returns the value of the dependentObservable"; 17 | 18 | // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values 19 | // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(), 20 | // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.) 21 | var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null; 22 | var disposeWhenNodeIsRemovedCallback = null; 23 | if (disposeWhenNodeIsRemoved) { 24 | disposeWhenNodeIsRemovedCallback = function() { dependentObservable.dispose() }; 25 | ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback); 26 | var existingDisposeWhenFunction = options["disposeWhen"]; 27 | options["disposeWhen"] = function () { 28 | return (!ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved)) 29 | || ((typeof existingDisposeWhenFunction == "function") && existingDisposeWhenFunction()); 30 | } 31 | } 32 | 33 | var _subscriptionsToDependencies = []; 34 | function disposeAllSubscriptionsToDependencies() { 35 | ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) { 36 | subscription.dispose(); 37 | }); 38 | _subscriptionsToDependencies = []; 39 | } 40 | 41 | function replaceSubscriptionsToDependencies(newDependencies) { 42 | disposeAllSubscriptionsToDependencies(); 43 | ko.utils.arrayForEach(newDependencies, function (dependency) { 44 | _subscriptionsToDependencies.push(dependency.subscribe(evaluate)); 45 | }); 46 | }; 47 | 48 | function evaluate() { 49 | // Don't dispose on first evaluation, because the "disposeWhen" callback might 50 | // e.g., dispose when the associated DOM element isn't in the doc, and it's not 51 | // going to be in the doc until *after* the first evaluation 52 | if ((_hasBeenEvaluated) && typeof options["disposeWhen"] == "function") { 53 | if (options["disposeWhen"]()) { 54 | dependentObservable.dispose(); 55 | return; 56 | } 57 | } 58 | 59 | try { 60 | ko.dependencyDetection.begin(); 61 | _latestValue = options["owner"] ? options["read"].call(options["owner"]) : options["read"](); 62 | } finally { 63 | var distinctDependencies = ko.utils.arrayGetDistinctValues(ko.dependencyDetection.end()); 64 | replaceSubscriptionsToDependencies(distinctDependencies); 65 | } 66 | 67 | dependentObservable.notifySubscribers(_latestValue); 68 | _hasBeenEvaluated = true; 69 | } 70 | 71 | function dependentObservable() { 72 | if (arguments.length > 0) { 73 | if (typeof options["write"] === "function") { 74 | // Writing a value 75 | var valueToWrite = arguments[0]; 76 | options["owner"] ? options["write"].call(options["owner"], valueToWrite) : options["write"](valueToWrite); 77 | } else { 78 | throw "Cannot write a value to a dependentObservable unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters."; 79 | } 80 | } else { 81 | // Reading the value 82 | if (!_hasBeenEvaluated) 83 | evaluate(); 84 | ko.dependencyDetection.registerDependency(dependentObservable); 85 | return _latestValue; 86 | } 87 | } 88 | dependentObservable.__ko_proto__ = ko.dependentObservable; 89 | dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; } 90 | dependentObservable.hasWriteFunction = typeof options["write"] === "function"; 91 | dependentObservable.dispose = function () { 92 | if (disposeWhenNodeIsRemoved) 93 | ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback); 94 | disposeAllSubscriptionsToDependencies(); 95 | }; 96 | 97 | ko.subscribable.call(dependentObservable); 98 | if (options['deferEvaluation'] !== true) 99 | evaluate(); 100 | 101 | ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose); 102 | ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount); 103 | 104 | return dependentObservable; 105 | }; 106 | ko.dependentObservable.__ko_proto__ = ko.observable; 107 | 108 | ko.exportSymbol('ko.dependentObservable', ko.dependentObservable); 109 | -------------------------------------------------------------------------------- /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) { 19 | var plainJavaScriptObject = ko.toJS(rootObject); 20 | return ko.utils.stringifyJson(plainJavaScriptObject); 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); 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 | } else { 62 | for (var propertyName in rootObject) 63 | visitorCallback(propertyName); 64 | } 65 | }; 66 | 67 | function objectLookup() { 68 | var keys = []; 69 | var values = []; 70 | this.save = function(key, value) { 71 | var existingIndex = ko.utils.arrayIndexOf(keys, key); 72 | if (existingIndex >= 0) 73 | values[existingIndex] = value; 74 | else { 75 | keys.push(key); 76 | values.push(value); 77 | } 78 | }; 79 | this.get = function(key) { 80 | var existingIndex = ko.utils.arrayIndexOf(keys, key); 81 | return (existingIndex >= 0) ? values[existingIndex] : undefined; 82 | }; 83 | }; 84 | })(); 85 | 86 | ko.exportSymbol('ko.toJS', ko.toJS); 87 | ko.exportSymbol('ko.toJSON', ko.toJSON); -------------------------------------------------------------------------------- /src/subscribables/observable.js: -------------------------------------------------------------------------------- 1 | var primitiveTypes = { 'undefined':true, 'boolean':true, 'number':true, 'string':true }; 2 | 3 | function valuesArePrimitiveAndEqual(a, b) { 4 | var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes); 5 | return oldValueIsPrimitive ? (a === b) : false; 6 | } 7 | 8 | ko.observable = function (initialValue) { 9 | var _latestValue = initialValue; 10 | 11 | function observable() { 12 | if (arguments.length > 0) { 13 | // Write 14 | 15 | // Ignore writes if the value hasn't changed 16 | if ((!observable['equalityComparer']) || !observable['equalityComparer'](_latestValue, arguments[0])) { 17 | _latestValue = arguments[0]; 18 | observable.notifySubscribers(_latestValue); 19 | } 20 | return this; // Permits chained assignments 21 | } 22 | else { 23 | // Read 24 | ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation 25 | return _latestValue; 26 | } 27 | } 28 | observable.__ko_proto__ = ko.observable; 29 | observable.valueHasMutated = function () { observable.notifySubscribers(_latestValue); } 30 | observable['equalityComparer'] = valuesArePrimitiveAndEqual; 31 | 32 | ko.subscribable.call(observable); 33 | 34 | ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated); 35 | 36 | return observable; 37 | } 38 | ko.isObservable = function (instance) { 39 | if ((instance === null) || (instance === undefined) || (instance.__ko_proto__ === undefined)) return false; 40 | if (instance.__ko_proto__ === ko.observable) return true; 41 | return ko.isObservable(instance.__ko_proto__); // Walk the prototype chain 42 | } 43 | ko.isWriteableObservable = function (instance) { 44 | // Observable 45 | if ((typeof instance == "function") && instance.__ko_proto__ === ko.observable) 46 | return true; 47 | // Writeable dependent observable 48 | if ((typeof instance == "function") && (instance.__ko_proto__ === ko.dependentObservable) && (instance.hasWriteFunction)) 49 | return true; 50 | // Anything else 51 | return false; 52 | } 53 | 54 | 55 | ko.exportSymbol('ko.observable', ko.observable); 56 | ko.exportSymbol('ko.isObservable', ko.isObservable); 57 | ko.exportSymbol('ko.isWriteableObservable', ko.isWriteableObservable); 58 | -------------------------------------------------------------------------------- /src/subscribables/observableArray.js: -------------------------------------------------------------------------------- 1 | ko.observableArray = function (initialValues) { 2 | if (arguments.length == 0) { 3 | // Zero-parameter constructor initializes to empty array 4 | initialValues = []; 5 | } 6 | if ((initialValues !== null) && (initialValues !== undefined) && !('length' in initialValues)) 7 | throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined."); 8 | var result = new ko.observable(initialValues); 9 | 10 | ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) { 11 | result[methodName] = function () { 12 | var underlyingArray = result(); 13 | var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments); 14 | result.valueHasMutated(); 15 | return methodCallResult; 16 | }; 17 | }); 18 | 19 | ko.utils.arrayForEach(["slice"], function (methodName) { 20 | result[methodName] = function () { 21 | var underlyingArray = result(); 22 | return underlyingArray[methodName].apply(underlyingArray, arguments); 23 | }; 24 | }); 25 | 26 | result.remove = function (valueOrPredicate) { 27 | var underlyingArray = result(); 28 | var remainingValues = []; 29 | var removedValues = []; 30 | var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; }; 31 | for (var i = 0, j = underlyingArray.length; i < j; i++) { 32 | var value = underlyingArray[i]; 33 | if (!predicate(value)) 34 | remainingValues.push(value); 35 | else 36 | removedValues.push(value); 37 | } 38 | result(remainingValues); 39 | return removedValues; 40 | }; 41 | 42 | result.removeAll = function (arrayOfValues) { 43 | // If you passed zero args, we remove everything 44 | if (arrayOfValues === undefined) { 45 | var allValues = result(); 46 | result([]); 47 | return allValues; 48 | } 49 | 50 | // If you passed an arg, we interpret it as an array of entries to remove 51 | if (!arrayOfValues) 52 | return []; 53 | return result.remove(function (value) { 54 | return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; 55 | }); 56 | }; 57 | 58 | result.destroy = function (valueOrPredicate) { 59 | var underlyingArray = result(); 60 | var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; }; 61 | for (var i = underlyingArray.length - 1; i >= 0; i--) { 62 | var value = underlyingArray[i]; 63 | if (predicate(value)) 64 | underlyingArray[i]["_destroy"] = true; 65 | } 66 | result.valueHasMutated(); 67 | }; 68 | 69 | result.destroyAll = function (arrayOfValues) { 70 | // If you passed zero args, we destroy everything 71 | if (arrayOfValues === undefined) 72 | return result.destroy(function() { return true }); 73 | 74 | // If you passed an arg, we interpret it as an array of entries to destroy 75 | if (!arrayOfValues) 76 | return []; 77 | return result.destroy(function (value) { 78 | return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; 79 | }); 80 | }; 81 | 82 | result.indexOf = function (item) { 83 | var underlyingArray = result(); 84 | return ko.utils.arrayIndexOf(underlyingArray, item); 85 | }; 86 | 87 | result.replace = function(oldItem, newItem) { 88 | var index = result.indexOf(oldItem); 89 | if (index >= 0) { 90 | result()[index] = newItem; 91 | result.valueHasMutated(); 92 | } 93 | }; 94 | 95 | ko.exportProperty(result, "remove", result.remove); 96 | ko.exportProperty(result, "removeAll", result.removeAll); 97 | ko.exportProperty(result, "destroy", result.destroy); 98 | ko.exportProperty(result, "destroyAll", result.destroyAll); 99 | ko.exportProperty(result, "indexOf", result.indexOf); 100 | 101 | return result; 102 | } 103 | 104 | ko.exportSymbol('ko.observableArray', ko.observableArray); 105 | -------------------------------------------------------------------------------- /src/subscribables/subscribable.js: -------------------------------------------------------------------------------- 1 | 2 | ko.subscription = function (callback, disposeCallback) { 3 | this.callback = callback; 4 | this.dispose = function () { 5 | this.isDisposed = true; 6 | disposeCallback(); 7 | }['bind'](this); 8 | 9 | ko.exportProperty(this, 'dispose', this.dispose); 10 | }; 11 | 12 | ko.subscribable = function () { 13 | var _subscriptions = []; 14 | 15 | this.subscribe = function (callback, callbackTarget) { 16 | var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback; 17 | 18 | var subscription = new ko.subscription(boundCallback, function () { 19 | ko.utils.arrayRemoveItem(_subscriptions, subscription); 20 | }); 21 | _subscriptions.push(subscription); 22 | return subscription; 23 | }; 24 | 25 | this.notifySubscribers = function (valueToNotify) { 26 | ko.utils.arrayForEach(_subscriptions.slice(0), function (subscription) { 27 | // In case a subscription was disposed during the arrayForEach cycle, check 28 | // for isDisposed on each subscription before invoking its callback 29 | if (subscription && (subscription.isDisposed !== true)) 30 | subscription.callback(valueToNotify); 31 | }); 32 | }; 33 | 34 | this.getSubscriptionsCount = function () { 35 | return _subscriptions.length; 36 | }; 37 | 38 | ko.exportProperty(this, 'subscribe', this.subscribe); 39 | ko.exportProperty(this, 'notifySubscribers', this.notifySubscribers); 40 | ko.exportProperty(this, 'getSubscriptionsCount', this.getSubscriptionsCount); 41 | } 42 | 43 | ko.isSubscribable = function (instance) { 44 | return typeof instance.subscribe == "function" && typeof instance.notifySubscribers == "function"; 45 | }; 46 | 47 | ko.exportSymbol('ko.subscribable', ko.subscribable); 48 | ko.exportSymbol('ko.isSubscribable', ko.isSubscribable); 49 | -------------------------------------------------------------------------------- /src/templating/jquery.tmpl/jqueryTmplTemplateEngine.js: -------------------------------------------------------------------------------- 1 | 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 | this.jQueryTmplVersion = (function() { 6 | if ((typeof(jQuery) == "undefined") || !jQuery['tmpl']) 7 | return 0; 8 | // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves. 9 | if (jQuery['tmpl']['tag']) { 10 | if (jQuery['tmpl']['tag']['tmpl'] && jQuery['tmpl']['tag']['tmpl']['open']) { 11 | if (jQuery['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) { 12 | return 3; // Since 1.0.0pre, custom tags should append markup to an array called "__" 13 | } 14 | } 15 | return 2; // Prior to 1.0.0pre, custom tags should append markup to an array called "_" 16 | } 17 | return 1; // Very old version doesn't have an extensible tag system 18 | })(); 19 | 20 | this['getTemplateNode'] = function (template) { 21 | var templateNode = document.getElementById(template); 22 | if (templateNode == null) 23 | throw new Error("Cannot find template with ID=" + template); 24 | return templateNode; 25 | } 26 | 27 | // These two only needed for jquery-tmpl v1 28 | var aposMarker = "__ko_apos__"; 29 | var aposRegex = new RegExp(aposMarker, "g"); 30 | 31 | this['renderTemplate'] = function (templateId, data, options) { 32 | options = options || {}; 33 | if (this.jQueryTmplVersion == 0) 34 | throw new Error("jquery.tmpl not detected.\nTo use KO's default template engine, reference jQuery and jquery.tmpl. See Knockout installation documentation for more details."); 35 | 36 | if (this.jQueryTmplVersion == 1) { 37 | // jquery.tmpl v1 doesn't like it if the template returns just text content or nothing - it only likes you to return DOM nodes. 38 | // To make things more flexible, we can wrap the whole template in a "; 42 | var renderedMarkupInWrapper = jQuery['tmpl'](templateTextInWrapper, data); 43 | var renderedMarkup = renderedMarkupInWrapper[0].text.replace(aposRegex, "'");; 44 | return jQuery['clean']([renderedMarkup], document); 45 | } 46 | 47 | // It's easier with jquery.tmpl v2 and later - it handles any DOM structure 48 | if (!(templateId in jQuery['template'])) { 49 | // Precache a precompiled version of this template (don't want to reparse on every render) 50 | var templateText = this['getTemplateNode'](templateId).text; 51 | jQuery['template'](templateId, templateText); 52 | } 53 | data = [data]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays 54 | 55 | var resultNodes = jQuery['tmpl'](templateId, data, options['templateOptions']); 56 | resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work 57 | jQuery['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders 58 | return resultNodes; 59 | }, 60 | 61 | this['isTemplateRewritten'] = function (templateId) { 62 | // It must already be rewritten if we've already got a cached version of it 63 | // (this optimisation helps on IE < 9, because it greatly reduces the number of getElementById calls) 64 | if (templateId in jQuery['template']) 65 | return true; 66 | 67 | return this['getTemplateNode'](templateId).isRewritten === true; 68 | }, 69 | 70 | this['rewriteTemplate'] = function (template, rewriterCallback) { 71 | var templateNode = this['getTemplateNode'](template); 72 | var rewritten = rewriterCallback(templateNode.text); 73 | 74 | if (this.jQueryTmplVersion == 1) { 75 | // jquery.tmpl v1 falls over if you use single-quotes, so replace these with a temporary marker for template rendering, 76 | // and then replace back after the template was rendered. This is slightly complicated by the fact that we must not interfere 77 | // with any code blocks - only replace apos characters outside code blocks. 78 | rewritten = ko.utils.stringTrim(rewritten); 79 | rewritten = rewritten.replace(/([\s\S]*?)(\${[\s\S]*?}|{{[\=a-z][\s\S]*?}}|$)/g, function(match) { 80 | // Called for each non-code-block followed by a code block (or end of template) 81 | var nonCodeSnippet = arguments[1]; 82 | var codeSnippet = arguments[2]; 83 | return nonCodeSnippet.replace(/\'/g, aposMarker) + codeSnippet; 84 | }); 85 | } 86 | 87 | templateNode.text = rewritten; 88 | templateNode.isRewritten = true; 89 | }, 90 | 91 | this['createJavaScriptEvaluatorBlock'] = function (script) { 92 | var splitTemplate = function(dataBindCode) { 93 | var regexp1 = /<\$[^>]*>/g; 94 | 95 | if(dataBindCode.split(regexp1).length > 1) { 96 | var acum = "" 97 | var rem = null; 98 | 99 | dataBindCode.replace(regexp1,function( all, slash, type, fnargs, target, parens, args ){ 100 | if(rem === null) { 101 | rem = type; 102 | } 103 | 104 | var parts = rem.split(all); 105 | 106 | 107 | acum = acum + parts[0] + all.replace(//,"+'>"); 108 | parts.shift(); 109 | if(parts.length === 1) { 110 | acum = acum + parts[0]; 111 | } else { 112 | rem = parts.join(all); 113 | } 114 | }); 115 | 116 | return acum; 117 | } else { 118 | return dataBindCode; 119 | } 120 | }; 121 | 122 | var transformedTemplate = splitTemplate(script); 123 | 124 | // nothing to escape -> regular execution 125 | if (this.jQueryTmplVersion == 1) 126 | return "{{= " + transformedTemplate + "}}" 127 | 128 | // From v2, jquery-tmpl does some parameter parsing that fails on nontrivial expressions. 129 | // Prevent it from messing with the code by wrapping it in a further function. 130 | return "{{ko_code ((function() { return " + transformedTemplate + " })()) }}" 131 | }, 132 | 133 | this.addTemplate = function (templateName, templateMarkup) { 134 | document.write(""); 135 | } 136 | ko.exportProperty(this, 'addTemplate', this.addTemplate); 137 | 138 | if (this.jQueryTmplVersion > 1) { 139 | jQuery['tmpl']['tag']['ko_code'] = { 140 | open: (this.jQueryTmplVersion < 3 ? "_" : "__") + ".push($1 || '');" 141 | }; 142 | } 143 | }; 144 | 145 | ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine(); 146 | 147 | // Use this one by default 148 | ko.setTemplateEngine(new ko.jqueryTmplTemplateEngine()); 149 | 150 | ko.exportSymbol('ko.jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine); 151 | -------------------------------------------------------------------------------- /src/templating/templateEngine.js: -------------------------------------------------------------------------------- 1 | 2 | ko.templateEngine = function () { 3 | this['renderTemplate'] = function (templateName, data, options) { 4 | throw "Override renderTemplate in your ko.templateEngine subclass"; 5 | }, 6 | this['isTemplateRewritten'] = function (templateName) { 7 | throw "Override isTemplateRewritten in your ko.templateEngine subclass"; 8 | }, 9 | this['rewriteTemplate'] = function (templateName, rewriterCallback) { 10 | throw "Override rewriteTemplate in your ko.templateEngine subclass"; 11 | }, 12 | this['createJavaScriptEvaluatorBlock'] = function (script) { 13 | throw "Override createJavaScriptEvaluatorBlock in your ko.templateEngine subclass"; 14 | } 15 | }; 16 | 17 | ko.exportSymbol('ko.templateEngine', ko.templateEngine); 18 | -------------------------------------------------------------------------------- /src/templating/templateRewriting.js: -------------------------------------------------------------------------------- 1 | 2 | ko.templateRewriting = (function () { 3 | var memoizeBindingAttributeSyntaxRegex = /(<[a-z]+\d*(\s+(?!data-bind=)[a-z0-9\-]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])([\s\S]*?)\5/gi; 4 | 5 | return { 6 | ensureTemplateIsRewritten: function (template, templateEngine) { 7 | if (!templateEngine['isTemplateRewritten'](template)) 8 | templateEngine['rewriteTemplate'](template, function (htmlString) { 9 | return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine); 10 | }); 11 | }, 12 | 13 | memoizeBindingAttributeSyntax: function (htmlString, templateEngine) { 14 | return htmlString.replace(memoizeBindingAttributeSyntaxRegex, function () { 15 | var tagToRetain = arguments[1]; 16 | var dataBindAttributeValue = arguments[6]; 17 | 18 | // @modified 19 | // modified the rewritting function used 20 | //dataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindAttributeValue); 21 | dataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyReaderWritersIntoJson(dataBindAttributeValue); 22 | 23 | // For no obvious reason, Opera fails to evaluate dataBindAttributeValue unless it's wrapped in an additional anonymous function, 24 | // even though Opera's built-in debugger can evaluate it anyway. No other browser requires this extra indirection. 25 | var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \ 26 | return (function() { var innerNode=skonode; return { " + dataBindAttributeValue + " } })() \ 27 | })"; 28 | return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain; 29 | }); 30 | }, 31 | 32 | applyMemoizedBindingsToNextSibling: function (bindings) { 33 | return ko.memoization.memoize(function (domNode, viewModel) { 34 | if (domNode.nextSibling) { 35 | // @modified 36 | sko.traceResources(domNode.nextSibling, viewModel, function(){ 37 | sko.traceRelations(domNode.nextSibling, viewModel, function(){ 38 | ko.applyBindingsToNode(domNode.nextSibling, bindings, viewModel); 39 | }); 40 | }); 41 | } 42 | }); 43 | } 44 | } 45 | })(); 46 | 47 | ko.exportSymbol('ko.templateRewriting', ko.templateRewriting); 48 | ko.exportSymbol('ko.templateRewriting.applyMemoizedBindingsToNextSibling', ko.templateRewriting.applyMemoizedBindingsToNextSibling); // Exported only because it has to be referenced by string lookup from within rewritten template 49 | -------------------------------------------------------------------------------- /src/templating/templating.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var _templateEngine; 4 | ko.setTemplateEngine = function (templateEngine) { 5 | if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine)) 6 | throw "templateEngine must inherit from ko.templateEngine"; 7 | _templateEngine = templateEngine; 8 | } 9 | 10 | function getFirstNodeFromPossibleArray(nodeOrNodeArray) { 11 | return nodeOrNodeArray.nodeType ? nodeOrNodeArray 12 | : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0] 13 | : null; 14 | } 15 | 16 | function executeTemplate(targetNodeOrNodeArray, renderMode, template, data, options) { 17 | var dataForTemplate = ko.utils.unwrapObservable(data); 18 | 19 | options = options || {}; 20 | var templateEngineToUse = (options['templateEngine'] || _templateEngine); 21 | ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse); 22 | var renderedNodesArray = templateEngineToUse['renderTemplate'](template, dataForTemplate, options); 23 | 24 | // Loosely check result is an array of DOM nodes 25 | if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number")) 26 | throw "Template engine must return an array of DOM nodes"; 27 | 28 | // @modified 29 | // Change the positoin of switch and if(render 30 | // so the rendered node is added to the DOM before being unmemoized 31 | switch (renderMode) { 32 | case "replaceChildren": ko.utils.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray); break; 33 | case "replaceNode": ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray); break; 34 | case "ignoreTargetNode": break; 35 | default: throw new Error("Unknown renderMode: " + renderMode); 36 | } 37 | 38 | if (renderedNodesArray) 39 | ko.utils.arrayForEach(renderedNodesArray, function (renderedNode) { 40 | ko.memoization.unmemoizeDomNodeAndDescendants(renderedNode, [data]); 41 | }); 42 | 43 | if (options['afterRender']) 44 | options['afterRender'](renderedNodesArray, data); 45 | 46 | return renderedNodesArray; 47 | } 48 | 49 | ko.renderTemplate = function (template, data, options, targetNodeOrNodeArray, renderMode) { 50 | options = options || {}; 51 | if ((options['templateEngine'] || _templateEngine) == undefined) 52 | throw "Set a template engine before calling renderTemplate"; 53 | renderMode = renderMode || "replaceChildren"; 54 | 55 | if (targetNodeOrNodeArray) { 56 | var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray); 57 | 58 | var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation) 59 | var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode; 60 | 61 | return new ko.dependentObservable( // So the DOM is automatically updated when any dependency changes 62 | function () { 63 | // Support selecting template as a function of the data being rendered 64 | var templateName = typeof(template) == 'function' ? template(data) : template; 65 | 66 | var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, data, options); 67 | if (renderMode == "replaceNode") { 68 | targetNodeOrNodeArray = renderedNodesArray; 69 | firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray); 70 | } 71 | }, 72 | null, 73 | { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved } 74 | ); 75 | } else { 76 | // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node 77 | return ko.memoization.memoize(function (domNode) { 78 | ko.renderTemplate(template, data, options, domNode, "replaceNode"); 79 | }); 80 | } 81 | }; 82 | 83 | ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode) { 84 | return new ko.dependentObservable(function () { 85 | var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || []; 86 | 87 | // @modified 88 | if (unwrappedArray.constructor != Array) // Coerce single value into array 89 | unwrappedArray = [unwrappedArray]; 90 | 91 | // @modified 92 | // wrapping automatically non objects 93 | for(var i=0; i", ""] || 11 | !tags.indexOf("", ""] || 12 | (!tags.indexOf("", ""] || 13 | /* anything else */ [0, "", ""]; 14 | 15 | // Go to html and back, then peel off extra wrappers 16 | div.innerHTML = wrap[1] + html + wrap[2]; 17 | 18 | // Move to the right depth 19 | while (wrap[0]--) 20 | div = div.lastChild; 21 | 22 | return ko.utils.makeArray(div.childNodes); 23 | } 24 | 25 | ko.utils.parseHtmlFragment = function(html) { 26 | return typeof jQuery != 'undefined' ? jQuery['clean']([html]) // As below, benefit from jQuery's optimisations where possible 27 | : simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases. 28 | }; 29 | 30 | ko.utils.setHtml = function(node, html) { 31 | ko.utils.emptyDomNode(node); 32 | 33 | if ((html !== null) && (html !== undefined)) { 34 | if (typeof html != 'string') 35 | html = html.toString(); 36 | 37 | // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments, 38 | // for example elements which are not normally allowed to exist on their own. 39 | // If you've referenced jQuery we'll use that rather than duplicating its code. 40 | if (typeof jQuery != 'undefined') { 41 | try { 42 | jQuery(node)['html'](html); 43 | } catch (x) { 44 | jQuery(node)['html'](html.replace("<","<").replace(">",">")); 45 | } 46 | } else { 47 | // ... otherwise, use KO's own parsing logic. 48 | var parsedNodes = ko.utils.parseHtmlFragment(html); 49 | for (var i = 0; i < parsedNodes.length; i++) 50 | node.appendChild(parsedNodes[i]); 51 | } 52 | } 53 | }; 54 | })(); -------------------------------------------------------------------------------- /src/utils.domNodeDisposal.js: -------------------------------------------------------------------------------- 1 | 2 | ko.utils.domNodeDisposal = new (function () { 3 | var domDataKey = "__ko_domNodeDisposal__" + (new Date).getTime(); 4 | 5 | function getDisposeCallbacksCollection(node, createIfNotFound) { 6 | var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey); 7 | if ((allDisposeCallbacks === undefined) && createIfNotFound) { 8 | allDisposeCallbacks = []; 9 | ko.utils.domData.set(node, domDataKey, allDisposeCallbacks); 10 | } 11 | return allDisposeCallbacks; 12 | } 13 | function destroyCallbacksCollection(node) { 14 | ko.utils.domData.set(node, domDataKey, undefined); 15 | } 16 | 17 | function cleanSingleNode(node) { 18 | // @modified 19 | // clean RDF observers 20 | sko.cleanNode(node) 21 | 22 | // Run all the dispose callbacks 23 | var callbacks = getDisposeCallbacksCollection(node, false); 24 | if (callbacks) { 25 | callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves) 26 | for (var i = 0; i < callbacks.length; i++) 27 | callbacks[i](node); 28 | } 29 | 30 | // Also erase the DOM data 31 | ko.utils.domData.clear(node); 32 | 33 | // Special support for jQuery here because it's so commonly used. 34 | // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData 35 | // so notify it to tear down any resources associated with the node & descendants here. 36 | if ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function")) 37 | jQuery['cleanData']([node]); 38 | } 39 | 40 | return { 41 | addDisposeCallback : function(node, callback) { 42 | if (typeof callback != "function") 43 | throw new Error("Callback must be a function"); 44 | getDisposeCallbacksCollection(node, true).push(callback); 45 | }, 46 | 47 | removeDisposeCallback : function(node, callback) { 48 | var callbacksCollection = getDisposeCallbacksCollection(node, false); 49 | if (callbacksCollection) { 50 | ko.utils.arrayRemoveItem(callbacksCollection, callback); 51 | if (callbacksCollection.length == 0) 52 | destroyCallbacksCollection(node); 53 | } 54 | }, 55 | 56 | cleanNode : function(node) { 57 | if ((node.nodeType != 1) && (node.nodeType != 9)) 58 | return; 59 | cleanSingleNode(node); 60 | 61 | // Clone the descendants list in case it changes during iteration 62 | var descendants = []; 63 | ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*")); 64 | for (var i = 0, j = descendants.length; i < j; i++) 65 | cleanSingleNode(descendants[i]); 66 | }, 67 | 68 | removeNode : function(node) { 69 | ko.cleanNode(node); 70 | if (node.parentNode) 71 | node.parentNode.removeChild(node); 72 | } 73 | } 74 | })(); 75 | ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience 76 | ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience 77 | ko.exportSymbol('ko.cleanNode', ko.cleanNode); 78 | ko.exportSymbol('ko.removeNode', ko.removeNode); 79 | ko.exportSymbol('ko.utils.domNodeDisposal', ko.utils.domNodeDisposal); 80 | ko.exportSymbol('ko.utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback); 81 | ko.exportSymbol('ko.utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback); 82 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | ko.utils = new (function () { 3 | var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 4 | var isIe6 = /MSIE 6/i.test(navigator.userAgent); 5 | var isIe7 = /MSIE 7/i.test(navigator.userAgent); 6 | 7 | // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup) 8 | var knownEvents = {}, knownEventTypesByEventName = {}; 9 | var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents'; 10 | knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress']; 11 | knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave']; 12 | for (var eventType in knownEvents) { 13 | var knownEventsForType = knownEvents[eventType]; 14 | if (knownEventsForType.length) { 15 | for (var i = 0, j = knownEventsForType.length; i < j; i++) 16 | knownEventTypesByEventName[knownEventsForType[i]] = eventType; 17 | } 18 | } 19 | 20 | function isClickOnCheckableElement(element, eventType) { 21 | if ((element.tagName != "INPUT") || !element.type) return false; 22 | if (eventType.toLowerCase() != "click") return false; 23 | var inputType = element.type.toLowerCase(); 24 | return (inputType == "checkbox") || (inputType == "radio"); 25 | } 26 | 27 | return { 28 | fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/], 29 | 30 | arrayForEach: function (array, action) { 31 | for (var i = 0, j = array.length; i < j; i++) 32 | action(array[i]); 33 | }, 34 | 35 | arrayIndexOf: function (array, item) { 36 | if (typeof array.indexOf == "function") 37 | return array.indexOf(item); 38 | for (var i = 0, j = array.length; i < j; i++) 39 | if (array[i] === item) 40 | return i; 41 | return -1; 42 | }, 43 | 44 | arrayFirst: function (array, predicate, predicateOwner) { 45 | for (var i = 0, j = array.length; i < j; i++) 46 | if (predicate.call(predicateOwner, array[i])) 47 | return array[i]; 48 | return null; 49 | }, 50 | 51 | arrayRemoveItem: function (array, itemToRemove) { 52 | var index = ko.utils.arrayIndexOf(array, itemToRemove); 53 | if (index >= 0) 54 | array.splice(index, 1); 55 | }, 56 | 57 | arrayGetDistinctValues: function (array) { 58 | array = array || []; 59 | var result = []; 60 | for (var i = 0, j = array.length; i < j; i++) { 61 | if (ko.utils.arrayIndexOf(result, array[i]) < 0) 62 | result.push(array[i]); 63 | } 64 | return result; 65 | }, 66 | 67 | arrayMap: function (array, mapping) { 68 | array = array || []; 69 | var result = []; 70 | for (var i = 0, j = array.length; i < j; i++) 71 | result.push(mapping(array[i])); 72 | return result; 73 | }, 74 | 75 | arrayFilter: function (array, predicate) { 76 | array = array || []; 77 | var result = []; 78 | for (var i = 0, j = array.length; i < j; i++) 79 | if (predicate(array[i])) 80 | result.push(array[i]); 81 | return result; 82 | }, 83 | 84 | arrayPushAll: function (array, valuesToPush) { 85 | for (var i = 0, j = valuesToPush.length; i < j; i++) 86 | array.push(valuesToPush[i]); 87 | }, 88 | 89 | emptyDomNode: function (domNode) { 90 | while (domNode.firstChild) { 91 | ko.removeNode(domNode.firstChild); 92 | } 93 | }, 94 | 95 | setDomNodeChildren: function (domNode, childNodes) { 96 | ko.utils.emptyDomNode(domNode); 97 | if (childNodes) { 98 | ko.utils.arrayForEach(childNodes, function (childNode) { 99 | domNode.appendChild(childNode); 100 | }); 101 | } 102 | }, 103 | 104 | replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) { 105 | var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray; 106 | if (nodesToReplaceArray.length > 0) { 107 | var insertionPoint = nodesToReplaceArray[0]; 108 | var parent = insertionPoint.parentNode; 109 | for (var i = 0, j = newNodesArray.length; i < j; i++) 110 | parent.insertBefore(newNodesArray[i], insertionPoint); 111 | for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) { 112 | ko.removeNode(nodesToReplaceArray[i]); 113 | } 114 | } 115 | }, 116 | 117 | setOptionNodeSelectionState: function (optionNode, isSelected) { 118 | // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser. 119 | if (navigator.userAgent.indexOf("MSIE 6") >= 0) 120 | optionNode.setAttribute("selected", isSelected); 121 | else 122 | optionNode.selected = isSelected; 123 | }, 124 | 125 | getElementsHavingAttribute: function (rootNode, attributeName) { 126 | if ((!rootNode) || (rootNode.nodeType != 1)) return []; 127 | var results = []; 128 | if (rootNode.getAttribute(attributeName) !== null) 129 | results.push(rootNode); 130 | var descendants = rootNode.getElementsByTagName("*"); 131 | for (var i = 0, j = descendants.length; i < j; i++) 132 | if (descendants[i].getAttribute(attributeName) !== null) 133 | results.push(descendants[i]); 134 | return results; 135 | }, 136 | 137 | stringTrim: function (string) { 138 | return (string || "").replace(stringTrimRegex, ""); 139 | }, 140 | 141 | stringTokenize: function (string, delimiter) { 142 | var result = []; 143 | var tokens = (string || "").split(delimiter); 144 | for (var i = 0, j = tokens.length; i < j; i++) { 145 | var trimmed = ko.utils.stringTrim(tokens[i]); 146 | if (trimmed !== "") 147 | result.push(trimmed); 148 | } 149 | return result; 150 | }, 151 | 152 | stringStartsWith: function (string, startsWith) { 153 | string = string || ""; 154 | if (startsWith.length > string.length) 155 | return false; 156 | return string.substring(0, startsWith.length) === startsWith; 157 | }, 158 | 159 | evalWithinScope: function (expression, scope, node) { 160 | // Always do the evaling within a "new Function" to block access to parent scope 161 | if (scope === undefined) 162 | return (new Function("return " + expression))(); 163 | 164 | scope['skonode'] = node; 165 | 166 | // Ensure "expression" is flattened into a source code string *before* it runs, otherwise 167 | // the variable name "expression" itself will clash with a subproperty called "expression" 168 | // The model must available in the chain scope for arbritrary JS code to execute, but it 169 | // also must be reference by <> and [] URIs anc CURIES 170 | return (new Function("__SKO__sc", "with(__SKO__sc){ var innerNode=skonode; return (" + expression + ") }"))(scope); 171 | }, 172 | 173 | domNodeIsContainedBy: function (node, containedByNode) { 174 | if (containedByNode.compareDocumentPosition) 175 | return (containedByNode.compareDocumentPosition(node) & 16) == 16; 176 | while (node != null) { 177 | if (node == containedByNode) 178 | return true; 179 | node = node.parentNode; 180 | } 181 | return false; 182 | }, 183 | 184 | domNodeIsAttachedToDocument: function (node) { 185 | return ko.utils.domNodeIsContainedBy(node, document); 186 | }, 187 | 188 | registerEventHandler: function (element, eventType, handler) { 189 | if (typeof jQuery != "undefined") { 190 | if (isClickOnCheckableElement(element, eventType)) { 191 | // For click events on checkboxes, jQuery interferes with the event handling in an awkward way: 192 | // it toggles the element checked state *after* the click event handlers run, whereas native 193 | // click events toggle the checked state *before* the event handler. 194 | // Fix this by intecepting the handler and applying the correct checkedness before it runs. 195 | var originalHandler = handler; 196 | handler = function(event, eventData) { 197 | var jQuerySuppliedCheckedState = this.checked; 198 | if (eventData) 199 | this.checked = eventData.checkedStateBeforeEvent !== true; 200 | originalHandler.call(this, event); 201 | this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied 202 | }; 203 | } 204 | jQuery(element)['bind'](eventType, handler); 205 | } else if (typeof element.addEventListener == "function") 206 | element.addEventListener(eventType, handler, false); 207 | else if (typeof element.attachEvent != "undefined") 208 | element.attachEvent("on" + eventType, function (event) { 209 | handler.call(element, event); 210 | }); 211 | else 212 | throw new Error("Browser doesn't support addEventListener or attachEvent"); 213 | }, 214 | 215 | triggerEvent: function (element, eventType) { 216 | if (!(element && element.nodeType)) 217 | throw new Error("element must be a DOM node when calling triggerEvent"); 218 | 219 | if (typeof jQuery != "undefined") { 220 | var eventData = []; 221 | if (isClickOnCheckableElement(element, eventType)) { 222 | // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler 223 | eventData.push({ checkedStateBeforeEvent: element.checked }); 224 | } 225 | jQuery(element)['trigger'](eventType, eventData); 226 | } else if (typeof document.createEvent == "function") { 227 | if (typeof element.dispatchEvent == "function") { 228 | var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents"; 229 | var event = document.createEvent(eventCategory); 230 | event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element); 231 | element.dispatchEvent(event); 232 | } 233 | else 234 | throw new Error("The supplied element doesn't support dispatchEvent"); 235 | } else if (typeof element.fireEvent != "undefined") { 236 | // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event 237 | // so to make it consistent, we'll do it manually here 238 | if (eventType == "click") { 239 | if ((element.tagName == "INPUT") && ((element.type.toLowerCase() == "checkbox") || (element.type.toLowerCase() == "radio"))) 240 | element.checked = element.checked !== true; 241 | } 242 | element.fireEvent("on" + eventType); 243 | } 244 | else 245 | throw new Error("Browser doesn't support triggering events"); 246 | }, 247 | 248 | unwrapObservable: function (value) { 249 | return ko.isObservable(value) ? value() : value; 250 | }, 251 | 252 | domNodeHasCssClass: function (node, className) { 253 | var currentClassNames = (node.className || "").split(/\s+/); 254 | return ko.utils.arrayIndexOf(currentClassNames, className) >= 0; 255 | }, 256 | 257 | toggleDomNodeCssClass: function (node, className, shouldHaveClass) { 258 | var hasClass = ko.utils.domNodeHasCssClass(node, className); 259 | if (shouldHaveClass && !hasClass) { 260 | node.className = (node.className || "") + " " + className; 261 | } else if (hasClass && !shouldHaveClass) { 262 | var currentClassNames = (node.className || "").split(/\s+/); 263 | var newClassName = ""; 264 | for (var i = 0; i < currentClassNames.length; i++) 265 | if (currentClassNames[i] != className) 266 | newClassName += currentClassNames[i] + " "; 267 | node.className = ko.utils.stringTrim(newClassName); 268 | } 269 | }, 270 | 271 | range: function (min, max) { 272 | min = ko.utils.unwrapObservable(min); 273 | max = ko.utils.unwrapObservable(max); 274 | var result = []; 275 | for (var i = min; i <= max; i++) 276 | result.push(i); 277 | return result; 278 | }, 279 | 280 | makeArray: function(arrayLikeObject) { 281 | var result = []; 282 | for (var i = 0, j = arrayLikeObject.length; i < j; i++) { 283 | result.push(arrayLikeObject[i]); 284 | }; 285 | return result; 286 | }, 287 | 288 | isIe6 : isIe6, 289 | isIe7 : isIe7, 290 | 291 | getFormFields: function(form, fieldName) { 292 | var fields = ko.utils.makeArray(form.getElementsByTagName("INPUT")).concat(ko.utils.makeArray(form.getElementsByTagName("TEXTAREA"))); 293 | var isMatchingField = (typeof fieldName == 'string') 294 | ? function(field) { return field.name === fieldName } 295 | : function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate 296 | var matches = []; 297 | for (var i = fields.length - 1; i >= 0; i--) { 298 | if (isMatchingField(fields[i])) 299 | matches.push(fields[i]); 300 | }; 301 | return matches; 302 | }, 303 | 304 | parseJson: function (jsonString) { 305 | if (typeof jsonString == "string") { 306 | jsonString = ko.utils.stringTrim(jsonString); 307 | if (jsonString) { 308 | if (window.JSON && window.JSON.parse) // Use native parsing where available 309 | return window.JSON.parse(jsonString); 310 | return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers 311 | } 312 | } 313 | return null; 314 | }, 315 | 316 | stringifyJson: function (data) { 317 | if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined")) 318 | throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js"); 319 | return JSON.stringify(ko.utils.unwrapObservable(data)); 320 | }, 321 | 322 | postJson: function (urlOrForm, data, options) { 323 | options = options || {}; 324 | var params = options['params'] || {}; 325 | var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost; 326 | var url = urlOrForm; 327 | 328 | // If we were given a form, use its 'action' URL and pick out any requested field values 329 | if((typeof urlOrForm == 'object') && (urlOrForm.tagName == "FORM")) { 330 | var originalForm = urlOrForm; 331 | url = originalForm.action; 332 | for (var i = includeFields.length - 1; i >= 0; i--) { 333 | var fields = ko.utils.getFormFields(originalForm, includeFields[i]); 334 | for (var j = fields.length - 1; j >= 0; j--) 335 | params[fields[j].name] = fields[j].value; 336 | } 337 | } 338 | 339 | data = ko.utils.unwrapObservable(data); 340 | var form = document.createElement("FORM"); 341 | form.style.display = "none"; 342 | form.action = url; 343 | form.method = "post"; 344 | for (var key in data) { 345 | var input = document.createElement("INPUT"); 346 | input.name = key; 347 | input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key])); 348 | form.appendChild(input); 349 | } 350 | for (var key in params) { 351 | var input = document.createElement("INPUT"); 352 | input.name = key; 353 | input.value = params[key]; 354 | form.appendChild(input); 355 | } 356 | document.body.appendChild(form); 357 | options['submitter'] ? options['submitter'](form) : form.submit(); 358 | setTimeout(function () { form.parentNode.removeChild(form); }, 0); 359 | } 360 | } 361 | })(); 362 | 363 | ko.exportSymbol('ko.utils', ko.utils); 364 | ko.exportSymbol('ko.utils.arrayForEach', ko.utils.arrayForEach); 365 | ko.exportSymbol('ko.utils.arrayFirst', ko.utils.arrayFirst); 366 | ko.exportSymbol('ko.utils.arrayFilter', ko.utils.arrayFilter); 367 | ko.exportSymbol('ko.utils.arrayGetDistinctValues', ko.utils.arrayGetDistinctValues); 368 | ko.exportSymbol('ko.utils.arrayIndexOf', ko.utils.arrayIndexOf); 369 | ko.exportSymbol('ko.utils.arrayMap', ko.utils.arrayMap); 370 | ko.exportSymbol('ko.utils.arrayPushAll', ko.utils.arrayPushAll); 371 | ko.exportSymbol('ko.utils.arrayRemoveItem', ko.utils.arrayRemoveItem); 372 | ko.exportSymbol('ko.utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost); 373 | ko.exportSymbol('ko.utils.getElementsHavingAttribute', ko.utils.getElementsHavingAttribute); 374 | ko.exportSymbol('ko.utils.getFormFields', ko.utils.getFormFields); 375 | ko.exportSymbol('ko.utils.postJson', ko.utils.postJson); 376 | ko.exportSymbol('ko.utils.parseJson', ko.utils.parseJson); 377 | ko.exportSymbol('ko.utils.registerEventHandler', ko.utils.registerEventHandler); 378 | ko.exportSymbol('ko.utils.stringifyJson', ko.utils.stringifyJson); 379 | ko.exportSymbol('ko.utils.range', ko.utils.range); 380 | ko.exportSymbol('ko.utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass); 381 | ko.exportSymbol('ko.utils.triggerEvent', ko.utils.triggerEvent); 382 | ko.exportSymbol('ko.utils.unwrapObservable', ko.utils.unwrapObservable); 383 | 384 | if (!Function.prototype['bind']) { 385 | // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf) 386 | // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js 387 | Function.prototype['bind'] = function (object) { 388 | var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift(); 389 | return function () { 390 | return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments))); 391 | }; 392 | }; 393 | } 394 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test semantic knockout.js 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 59 | 60 |

61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test semantic knockout.js 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 57 | 58 |
59 |

60 | 61 |

62 |
63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test semantic knockout.js 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 64 | 65 | 76 | 77 |
78 |

79 | 80 |
81 | 82 | 87 | 88 |
    89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Semantic KO Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

    Semantic KO

    16 |

    17 |
    18 |

    19 |
      20 | 21 |
      22 | 23 |

      24 |
      25 |
      26 | 27 |
      28 | 29 |

      30 |
      31 |
      32 | 33 |
      34 | 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 | 84 |
      85 | 86 |

      87 | 88 | 89 | 90 |
      91 |
      92 | 93 |
      94 | 95 |

      96 | 97 | 98 | 99 |
      100 |
      101 | 102 |
      103 | 104 | 105 | 106 |
      107 | 108 |
      109 | 110 |

      111 |
      112 |
      113 | 114 |
      115 | 116 |

      117 | 118 | 119 | 120 |
      121 |
      122 | 123 | 134 | 135 |
      136 |

      137 | 138 |
      139 | 140 | 141 | 152 | 153 |
      154 |

      155 | 156 |
      157 | 158 | 163 | 164 |
      165 |
        166 |
        167 | 168 |
        169 | 170 |

        171 |
        172 |
        173 | 174 |
        175 | 176 |

        177 |
        178 |
        179 | 180 |
        181 | 182 |

        183 |
        184 |
        185 | 186 |
        187 | 188 |

        189 |
        190 |
        191 | 192 |
        193 | 194 |

        195 |
        196 |
        197 | 198 |
        199 | 200 |

        201 | click me 202 |
        203 |
        204 | 205 |
        206 | 207 |

        208 | 209 |
        210 |
        211 | 212 |
        213 | 214 |
        215 |
        216 | 217 | 222 | 223 |
        224 | 225 |

        226 |

        227 |
        228 |
        229 | 230 |
        231 | 232 | 233 | 234 |
        235 | 236 | 237 | -------------------------------------------------------------------------------- /tests/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } 226 | --------------------------------------------------------------------------------