Added the remote web enhancement developed by Prince Samuel.
1 // Knockout Mapping plugin v2.1.2
2 // (c) 2012 Steven Sanderson, Roy Jacobs - http://knockoutjs.com/
3 // License: MIT (http://www.opensource.org/licenses/mit-license.php)
6 // Module systems magic dance.
8 if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
9 // CommonJS or Node: hard-coded dependency on "knockout"
10 factory(require("knockout"), exports);
11 } else if (typeof define === "function" && define["amd"]) {
12 // AMD anonymous module with hard-coded dependency on "knockout"
13 define(["knockout", "exports"], factory);
15 // <script> tag: use the global `ko` object, attaching a `mapping` property
16 factory(ko, ko.mapping = {});
18 }(function (ko, exports) {
20 var mappingProperty = "__ko_mapping__";
21 var realKoDependentObservable = ko.dependentObservable;
22 var mappingNesting = 0;
23 var dependentObservables;
26 var _defaultOptions = {
27 include: ["_destroy"],
31 var defaultOptions = _defaultOptions;
33 exports.isMapped = function (viewModel) {
34 var unwrapped = ko.utils.unwrapObservable(viewModel);
35 return unwrapped && unwrapped[mappingProperty];
38 exports.fromJS = function (jsObject /*, inputOptions, target*/ ) {
39 if (arguments.length == 0) throw new Error("When calling ko.fromJS, pass the object you want to convert.");
41 // When mapping is completed, even with an exception, reset the nesting level
42 window.setTimeout(function () {
46 if (!mappingNesting++) {
47 dependentObservables = [];
48 visitedObjects = new objectLookup();
54 if (arguments.length == 2) {
55 if (arguments[1][mappingProperty]) {
56 target = arguments[1];
58 options = arguments[1];
61 if (arguments.length == 3) {
62 options = arguments[1];
63 target = arguments[2];
67 options = mergeOptions(target[mappingProperty], options);
69 options = mergeOptions(options);
71 options.mappedProperties = options.mappedProperties || {};
73 var result = updateViewModel(target, jsObject, options);
78 // Evaluate any dependent observables that were proxied.
79 // Do this in a timeout to defer execution. Basically, any user code that explicitly looks up the DO will perform the first evaluation. Otherwise,
80 // it will be done by this code.
81 if (!--mappingNesting) {
82 window.setTimeout(function () {
83 while (dependentObservables.length) {
84 var DO = dependentObservables.pop();
90 // Save any new mapping options in the view model, so that updateFromJS can use them later.
91 result[mappingProperty] = mergeOptions(result[mappingProperty], options);
96 exports.fromJSON = function (jsonString /*, options, target*/ ) {
97 var parsed = ko.utils.parseJson(jsonString);
98 arguments[0] = parsed;
99 return exports.fromJS.apply(this, arguments);
102 exports.updateFromJS = function (viewModel) {
103 throw new Error("ko.mapping.updateFromJS, use ko.mapping.fromJS instead. Please note that the order of parameters is different!");
106 exports.updateFromJSON = function (viewModel) {
107 throw new Error("ko.mapping.updateFromJSON, use ko.mapping.fromJSON instead. Please note that the order of parameters is different!");
110 exports.toJS = function (rootObject, options) {
111 if (arguments.length == 0) throw new Error("When calling ko.mapping.toJS, pass the object you want to convert.");
112 // Merge in the options used in fromJS
113 options = mergeOptions(rootObject[mappingProperty], options);
115 // We just unwrap everything at every level in the object graph
116 return visitModel(rootObject, function (x) {
117 return ko.utils.unwrapObservable(x)
121 exports.toJSON = function (rootObject, options) {
122 var plainJavaScriptObject = exports.toJS(rootObject, options);
123 return ko.utils.stringifyJson(plainJavaScriptObject);
126 exports.visitModel = function (rootObject, callback, options) {
127 if (arguments.length == 0) throw new Error("When calling ko.mapping.visitModel, pass the object you want to visit.");
128 // Merge in the options used in fromJS
129 options = mergeOptions(rootObject[mappingProperty], options);
131 return visitModel(rootObject, callback, options);
134 exports.defaultOptions = function () {
135 if (arguments.length > 0) {
136 defaultOptions = arguments[0];
138 return defaultOptions;
142 exports.resetDefaultOptions = function () {
144 include: _defaultOptions.include.slice(0),
145 ignore: _defaultOptions.ignore.slice(0),
146 copy: _defaultOptions.copy.slice(0)
150 exports.getType = function(x) {
151 if ((x) && (typeof (x) === "object")) {
152 if (x.constructor == (new Date).constructor) return "date";
153 if (x.constructor == (new Array).constructor) return "array";
158 function extendOptionsArray(distArray, sourceArray) {
159 return ko.utils.arrayGetDistinctValues(
160 ko.utils.arrayPushAll(distArray, sourceArray)
164 function extendOptionsObject(target, options) {
165 var type = exports.getType,
166 name, special = { "include": true, "ignore": true, "copy": true },
167 t, o, i = 1, l = arguments.length;
168 if (type(target) !== "object") {
172 options = arguments[i];
173 if (type(options) !== "object") {
176 for (name in options) {
177 t = target[name]; o = options[name];
178 if (name !== "constructor" && special[name] && type(o) !== "array") {
179 if (type(o) !== "string") {
180 throw new Error("ko.mapping.defaultOptions()." + name + " should be an array or string.");
185 case "object": // Recurse
186 t = type(t) === "object" ? t : {};
187 target[name] = extendOptionsObject(t, o);
190 t = type(t) === "array" ? t : [];
191 target[name] = extendOptionsArray(t, o);
201 function mergeOptions() {
202 var options = ko.utils.arrayPushAll([{}, defaultOptions], arguments); // Always use empty object as target to avoid changing default options
203 options = extendOptionsObject.apply(this, options);
207 // When using a 'create' callback, we proxy the dependent observable so that it doesn't immediately evaluate on creation.
208 // The reason is that the dependent observables in the user-specified callback may contain references to properties that have not been mapped yet.
209 function withProxyDependentObservable(dependentObservables, callback) {
210 var localDO = ko.dependentObservable;
211 ko.dependentObservable = function (read, owner, options) {
212 options = options || {};
214 if (read && typeof read == "object") { // mirrors condition in knockout implementation of DO's
218 var realDeferEvaluation = options.deferEvaluation;
220 var isRemoved = false;
222 // We wrap the original dependent observable so that we can remove it from the 'dependentObservables' list we need to evaluate after mapping has
223 // completed if the user already evaluated the DO themselves in the meantime.
224 var wrap = function (DO) {
225 var wrapped = realKoDependentObservable({
228 ko.utils.arrayRemoveItem(dependentObservables, DO);
231 return DO.apply(DO, arguments);
233 write: function (val) {
236 deferEvaluation: true
238 if(DEBUG) wrapped._wrapper = true;
242 options.deferEvaluation = true; // will either set for just options, or both read/options.
243 var realDependentObservable = new realKoDependentObservable(read, owner, options);
245 if (!realDeferEvaluation) {
246 realDependentObservable = wrap(realDependentObservable);
247 dependentObservables.push(realDependentObservable);
250 return realDependentObservable;
252 ko.dependentObservable.fn = realKoDependentObservable.fn;
253 ko.computed = ko.dependentObservable;
254 var result = callback();
255 ko.dependentObservable = localDO;
256 ko.computed = ko.dependentObservable;
260 function updateViewModel(mappedRootObject, rootObject, options, parentName, parent, parentPropertyName) {
261 var isArray = ko.utils.unwrapObservable(rootObject) instanceof Array;
263 // If nested object was already mapped previously, take the options from it
264 if (parentName !== undefined && exports.isMapped(mappedRootObject)) {
265 options = ko.utils.unwrapObservable(mappedRootObject)[mappingProperty];
267 parentPropertyName = "";
270 parentName = parentName || "";
271 parentPropertyName = parentPropertyName || "";
273 var callbackParams = {
278 var getCallback = function (name) {
280 if (parentName === "") {
281 callback = options[name];
282 } else if (callback = options[parentName]) {
283 callback = callback[name]
288 var hasCreateCallback = function () {
289 return getCallback("create") instanceof Function;
292 var createCallback = function (data) {
293 return withProxyDependentObservable(dependentObservables, function () {
294 return getCallback("create")({
295 data: data || callbackParams.data,
296 parent: callbackParams.parent
301 var hasUpdateCallback = function () {
302 return getCallback("update") instanceof Function;
305 var updateCallback = function (obj, data) {
307 data: data || callbackParams.data,
308 parent: callbackParams.parent,
309 target: ko.utils.unwrapObservable(obj)
312 if (ko.isWriteableObservable(obj)) {
313 params.observable = obj;
316 return getCallback("update")(params);
319 var alreadyMapped = visitedObjects.get(rootObject);
321 return alreadyMapped;
325 // For atomic types, do a direct update on the observable
326 if (!canHaveProperties(rootObject)) {
327 switch (exports.getType(rootObject)) {
329 if (hasUpdateCallback()) {
330 if (ko.isWriteableObservable(rootObject)) {
331 rootObject(updateCallback(rootObject));
332 mappedRootObject = rootObject;
334 mappedRootObject = updateCallback(rootObject);
337 mappedRootObject = rootObject;
341 if (ko.isWriteableObservable(mappedRootObject)) {
342 if (hasUpdateCallback()) {
343 mappedRootObject(updateCallback(mappedRootObject));
345 mappedRootObject(ko.utils.unwrapObservable(rootObject));
348 if (hasCreateCallback()) {
349 mappedRootObject = createCallback();
351 mappedRootObject = ko.observable(ko.utils.unwrapObservable(rootObject));
354 if (hasUpdateCallback()) {
355 mappedRootObject(updateCallback(mappedRootObject));
362 mappedRootObject = ko.utils.unwrapObservable(mappedRootObject);
363 if (!mappedRootObject) {
364 if (hasCreateCallback()) {
365 var result = createCallback();
367 if (hasUpdateCallback()) {
368 result = updateCallback(result);
373 if (hasUpdateCallback()) {
374 return updateCallback(result);
377 mappedRootObject = {};
381 if (hasUpdateCallback()) {
382 mappedRootObject = updateCallback(mappedRootObject);
385 visitedObjects.save(rootObject, mappedRootObject);
387 // For non-atomic types, visit all properties and update recursively
388 visitPropertiesOrArrayEntries(rootObject, function (indexer) {
389 var fullPropertyName = getPropertyName(parentPropertyName, rootObject, indexer);
391 if (ko.utils.arrayIndexOf(options.ignore, fullPropertyName) != -1) {
395 if (ko.utils.arrayIndexOf(options.copy, fullPropertyName) != -1) {
396 mappedRootObject[indexer] = rootObject[indexer];
400 // In case we are adding an already mapped property, fill it with the previously mapped property value to prevent recursion.
401 // If this is a property that was generated by fromJS, we should use the options specified there
402 var prevMappedProperty = visitedObjects.get(rootObject[indexer]);
403 var value = prevMappedProperty || updateViewModel(mappedRootObject[indexer], rootObject[indexer], options, indexer, mappedRootObject, fullPropertyName);
405 if (ko.isWriteableObservable(mappedRootObject[indexer])) {
406 mappedRootObject[indexer](ko.utils.unwrapObservable(value));
408 mappedRootObject[indexer] = value;
411 options.mappedProperties[fullPropertyName] = true;
417 var hasKeyCallback = getCallback("key") instanceof Function;
418 var keyCallback = hasKeyCallback ? getCallback("key") : function (x) {
421 if (!ko.isObservable(mappedRootObject)) {
422 // When creating the new observable array, also add a bunch of utility functions that take the 'key' of the array items into account.
423 mappedRootObject = ko.observableArray([]);
425 mappedRootObject.mappedRemove = function (valueOrPredicate) {
426 var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
427 return value === keyCallback(valueOrPredicate);
429 return mappedRootObject.remove(function (item) {
430 return predicate(keyCallback(item));
434 mappedRootObject.mappedRemoveAll = function (arrayOfValues) {
435 var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
436 return mappedRootObject.remove(function (item) {
437 return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
441 mappedRootObject.mappedDestroy = function (valueOrPredicate) {
442 var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
443 return value === keyCallback(valueOrPredicate);
445 return mappedRootObject.destroy(function (item) {
446 return predicate(keyCallback(item));
450 mappedRootObject.mappedDestroyAll = function (arrayOfValues) {
451 var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
452 return mappedRootObject.destroy(function (item) {
453 return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
457 mappedRootObject.mappedIndexOf = function (item) {
458 var keys = filterArrayByKey(mappedRootObject(), keyCallback);
459 var key = keyCallback(item);
460 return ko.utils.arrayIndexOf(keys, key);
463 mappedRootObject.mappedCreate = function (value) {
464 if (mappedRootObject.mappedIndexOf(value) !== -1) {
465 throw new Error("There already is an object with the key that you specified.");
468 var item = hasCreateCallback() ? createCallback(value) : value;
469 if (hasUpdateCallback()) {
470 var newValue = updateCallback(item, value);
471 if (ko.isWriteableObservable(item)) {
477 mappedRootObject.push(item);
482 var currentArrayKeys = filterArrayByKey(ko.utils.unwrapObservable(mappedRootObject), keyCallback).sort();
483 var newArrayKeys = filterArrayByKey(rootObject, keyCallback);
484 if (hasKeyCallback) newArrayKeys.sort();
485 var editScript = ko.utils.compareArrays(currentArrayKeys, newArrayKeys);
487 var ignoreIndexOf = {};
489 var newContents = [];
490 for (var i = 0, j = editScript.length; i < j; i++) {
491 var key = editScript[i];
493 var fullPropertyName = getPropertyName(parentPropertyName, rootObject, i);
494 switch (key.status) {
496 var item = getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
497 mappedItem = updateViewModel(undefined, item, options, parentName, mappedRootObject, fullPropertyName);
498 if(!hasCreateCallback()) {
499 mappedItem = ko.utils.unwrapObservable(mappedItem);
502 var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
503 newContents[index] = mappedItem;
504 ignoreIndexOf[index] = true;
507 var item = getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
508 mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
509 updateViewModel(mappedItem, item, options, parentName, mappedRootObject, fullPropertyName);
511 var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
512 newContents[index] = mappedItem;
513 ignoreIndexOf[index] = true;
516 mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
526 mappedRootObject(newContents);
528 var arrayChangedCallback = getCallback("arrayChanged");
529 if (arrayChangedCallback instanceof Function) {
530 ko.utils.arrayForEach(changes, function (change) {
531 arrayChangedCallback(change.event, change.item);
536 return mappedRootObject;
539 function ignorableIndexOf(array, item, ignoreIndices) {
540 for (var i = 0, j = array.length; i < j; i++) {
541 if (ignoreIndices[i] === true) continue;
542 if (array[i] === item) return i;
547 function mapKey(item, callback) {
549 if (callback) mappedItem = callback(item);
550 if (exports.getType(mappedItem) === "undefined") mappedItem = item;
552 return ko.utils.unwrapObservable(mappedItem);
555 function getItemByKey(array, key, callback) {
556 var filtered = ko.utils.arrayFilter(ko.utils.unwrapObservable(array), function (item) {
557 return mapKey(item, callback) === key;
560 if (filtered.length == 0) throw new Error("When calling ko.update*, the key '" + key + "' was not found!");
561 if ((filtered.length > 1) && (canHaveProperties(filtered[0]))) throw new Error("When calling ko.update*, the key '" + key + "' was not unique!");
566 function filterArrayByKey(array, callback) {
567 return ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (item) {
569 return mapKey(item, callback);
576 function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
577 if (rootObject instanceof Array) {
578 for (var i = 0; i < rootObject.length; i++)
581 for (var propertyName in rootObject)
582 visitorCallback(propertyName);
586 function canHaveProperties(object) {
587 var type = exports.getType(object);
588 return (type === "object" || type === "array") && (object !== null) && (type !== "undefined");
591 // Based on the parentName, this creates a fully classified name of a property
593 function getPropertyName(parentName, parent, indexer) {
594 var propertyName = parentName || "";
595 if (parent instanceof Array) {
597 propertyName += "[" + indexer + "]";
603 propertyName += indexer;
608 function visitModel(rootObject, callback, options, parentName, fullParentName) {
609 // If nested object was already mapped previously, take the options from it
610 if (parentName !== undefined && exports.isMapped(rootObject)) {
611 //options = ko.utils.unwrapObservable(rootObject)[mappingProperty];
612 options = mergeOptions(ko.utils.unwrapObservable(rootObject)[mappingProperty], options);
616 if (parentName === undefined) { // the first call
617 visitedObjects = new objectLookup();
620 parentName = parentName || "";
622 var mappedRootObject;
623 var unwrappedRootObject = ko.utils.unwrapObservable(rootObject);
624 if (!canHaveProperties(unwrappedRootObject)) {
625 return callback(rootObject, fullParentName);
627 // Only do a callback, but ignore the results
628 callback(rootObject, fullParentName);
629 mappedRootObject = unwrappedRootObject instanceof Array ? [] : {};
632 visitedObjects.save(rootObject, mappedRootObject);
634 var origFullParentName = fullParentName;
635 visitPropertiesOrArrayEntries(unwrappedRootObject, function (indexer) {
636 if (options.ignore && ko.utils.arrayIndexOf(options.ignore, indexer) != -1) return;
638 var propertyValue = unwrappedRootObject[indexer];
639 var fullPropertyName = getPropertyName(parentName, unwrappedRootObject, indexer);
641 // If we don't want to explicitly copy the unmapped property...
642 if (ko.utils.arrayIndexOf(options.copy, indexer) === -1) {
643 // ...find out if it's a property we want to explicitly include
644 if (ko.utils.arrayIndexOf(options.include, indexer) === -1) {
645 // Options contains all the properties that were part of the original object.
646 // If a property does not exist, and it is not because it is part of an array (e.g. "myProp[3]"), then it should not be unmapped.
647 if (options.mappedProperties && !options.mappedProperties[fullPropertyName] && !(unwrappedRootObject instanceof Array)) {
653 fullParentName = getPropertyName(origFullParentName, unwrappedRootObject, indexer);
655 var propertyType = exports.getType(ko.utils.unwrapObservable(propertyValue));
656 switch (propertyType) {
660 var previouslyMappedValue = visitedObjects.get(propertyValue);
661 mappedRootObject[indexer] = (exports.getType(previouslyMappedValue) !== "undefined") ? previouslyMappedValue : visitModel(propertyValue, callback, options, fullPropertyName, fullParentName);
664 mappedRootObject[indexer] = callback(propertyValue, fullParentName);
668 return mappedRootObject;
671 function objectLookup() {
674 this.save = function (key, value) {
675 var existingIndex = ko.utils.arrayIndexOf(keys, key);
676 if (existingIndex >= 0) values[existingIndex] = value;
682 this.get = function (key) {
683 var existingIndex = ko.utils.arrayIndexOf(keys, key);
684 return (existingIndex >= 0) ? values[existingIndex] : undefined;