├── .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 | 48 | 49 |