moel@348: // Knockout Mapping plugin v2.1.2
moel@348: // (c) 2012 Steven Sanderson, Roy Jacobs - http://knockoutjs.com/
moel@348: // License: MIT (http://www.opensource.org/licenses/mit-license.php)
moel@348: 
moel@348: (function (factory) {
moel@348: 	// Module systems magic dance.
moel@348: 
moel@348: 	if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
moel@348: 		// CommonJS or Node: hard-coded dependency on "knockout"
moel@348: 		factory(require("knockout"), exports);
moel@348: 	} else if (typeof define === "function" && define["amd"]) {
moel@348: 		// AMD anonymous module with hard-coded dependency on "knockout"
moel@348: 		define(["knockout", "exports"], factory);
moel@348: 	} else {
moel@348: 		// <script> tag: use the global `ko` object, attaching a `mapping` property
moel@348: 		factory(ko, ko.mapping = {});
moel@348: 	}
moel@348: }(function (ko, exports) {
moel@348: 	var DEBUG=true;
moel@348: 	var mappingProperty = "__ko_mapping__";
moel@348: 	var realKoDependentObservable = ko.dependentObservable;
moel@348: 	var mappingNesting = 0;
moel@348: 	var dependentObservables;
moel@348: 	var visitedObjects;
moel@348: 
moel@348: 	var _defaultOptions = {
moel@348: 		include: ["_destroy"],
moel@348: 		ignore: [],
moel@348: 		copy: []
moel@348: 	};
moel@348: 	var defaultOptions = _defaultOptions;
moel@348: 
moel@348: 	exports.isMapped = function (viewModel) {
moel@348: 		var unwrapped = ko.utils.unwrapObservable(viewModel);
moel@348: 		return unwrapped && unwrapped[mappingProperty];
moel@348: 	}
moel@348: 
moel@348: 	exports.fromJS = function (jsObject /*, inputOptions, target*/ ) {
moel@348: 		if (arguments.length == 0) throw new Error("When calling ko.fromJS, pass the object you want to convert.");
moel@348: 
moel@348: 		// When mapping is completed, even with an exception, reset the nesting level
moel@348: 		window.setTimeout(function () {
moel@348: 			mappingNesting = 0;
moel@348: 		}, 0);
moel@348: 
moel@348: 		if (!mappingNesting++) {
moel@348: 			dependentObservables = [];
moel@348: 			visitedObjects = new objectLookup();
moel@348: 		}
moel@348: 
moel@348: 		var options;
moel@348: 		var target;
moel@348: 
moel@348: 		if (arguments.length == 2) {
moel@348: 			if (arguments[1][mappingProperty]) {
moel@348: 				target = arguments[1];
moel@348: 			} else {
moel@348: 				options = arguments[1];
moel@348: 			}
moel@348: 		}
moel@348: 		if (arguments.length == 3) {
moel@348: 			options = arguments[1];
moel@348: 			target = arguments[2];
moel@348: 		}
moel@348: 
moel@348: 		if (target) {
moel@348: 			options = mergeOptions(target[mappingProperty], options);
moel@348: 		} else {
moel@348: 			options = mergeOptions(options);
moel@348: 		}
moel@348: 		options.mappedProperties = options.mappedProperties || {};
moel@348: 
moel@348: 		var result = updateViewModel(target, jsObject, options);
moel@348: 		if (target) {
moel@348: 			result = target;
moel@348: 		}
moel@348: 
moel@348: 		// Evaluate any dependent observables that were proxied.
moel@348: 		// Do this in a timeout to defer execution. Basically, any user code that explicitly looks up the DO will perform the first evaluation. Otherwise,
moel@348: 		// it will be done by this code.
moel@348: 		if (!--mappingNesting) {
moel@348: 			window.setTimeout(function () {
moel@348: 				while (dependentObservables.length) {
moel@348: 					var DO = dependentObservables.pop();
moel@348: 					if (DO) DO();
moel@348: 				}
moel@348: 			}, 0);
moel@348: 		}
moel@348: 
moel@348: 		// Save any new mapping options in the view model, so that updateFromJS can use them later.
moel@348: 		result[mappingProperty] = mergeOptions(result[mappingProperty], options);
moel@348: 
moel@348: 		return result;
moel@348: 	};
moel@348: 
moel@348: 	exports.fromJSON = function (jsonString /*, options, target*/ ) {
moel@348: 		var parsed = ko.utils.parseJson(jsonString);
moel@348: 		arguments[0] = parsed;
moel@348: 		return exports.fromJS.apply(this, arguments);
moel@348: 	};
moel@348: 
moel@348: 	exports.updateFromJS = function (viewModel) {
moel@348: 		throw new Error("ko.mapping.updateFromJS, use ko.mapping.fromJS instead. Please note that the order of parameters is different!");
moel@348: 	};
moel@348: 
moel@348: 	exports.updateFromJSON = function (viewModel) {
moel@348: 		throw new Error("ko.mapping.updateFromJSON, use ko.mapping.fromJSON instead. Please note that the order of parameters is different!");
moel@348: 	};
moel@348: 
moel@348: 	exports.toJS = function (rootObject, options) {
moel@348: 		if (arguments.length == 0) throw new Error("When calling ko.mapping.toJS, pass the object you want to convert.");
moel@348: 		// Merge in the options used in fromJS
moel@348: 		options = mergeOptions(rootObject[mappingProperty], options);
moel@348: 
moel@348: 		// We just unwrap everything at every level in the object graph
moel@348: 		return visitModel(rootObject, function (x) {
moel@348: 			return ko.utils.unwrapObservable(x)
moel@348: 		}, options);
moel@348: 	};
moel@348: 
moel@348: 	exports.toJSON = function (rootObject, options) {
moel@348: 		var plainJavaScriptObject = exports.toJS(rootObject, options);
moel@348: 		return ko.utils.stringifyJson(plainJavaScriptObject);
moel@348: 	};
moel@348: 
moel@348: 	exports.visitModel = function (rootObject, callback, options) {
moel@348: 		if (arguments.length == 0) throw new Error("When calling ko.mapping.visitModel, pass the object you want to visit.");
moel@348: 		// Merge in the options used in fromJS
moel@348: 		options = mergeOptions(rootObject[mappingProperty], options);
moel@348: 
moel@348: 		return visitModel(rootObject, callback, options);
moel@348: 	};
moel@348: 
moel@348: 	exports.defaultOptions = function () {
moel@348: 		if (arguments.length > 0) {
moel@348: 			defaultOptions = arguments[0];
moel@348: 		} else {
moel@348: 			return defaultOptions;
moel@348: 		}
moel@348: 	};
moel@348: 
moel@348: 	exports.resetDefaultOptions = function () {
moel@348: 		defaultOptions = {
moel@348: 			include: _defaultOptions.include.slice(0),
moel@348: 			ignore: _defaultOptions.ignore.slice(0),
moel@348: 			copy: _defaultOptions.copy.slice(0)
moel@348: 		};
moel@348: 	};
moel@348: 
moel@348: 	exports.getType = function(x) {
moel@348: 		if ((x) && (typeof (x) === "object")) {
moel@348: 			if (x.constructor == (new Date).constructor) return "date";
moel@348: 			if (x.constructor == (new Array).constructor) return "array";
moel@348: 		}
moel@348: 		return typeof x;
moel@348: 	}
moel@348: 
moel@348: 	function extendOptionsArray(distArray, sourceArray) {
moel@348: 		return ko.utils.arrayGetDistinctValues(
moel@348: 			ko.utils.arrayPushAll(distArray, sourceArray)
moel@348: 		);
moel@348: 	}
moel@348: 
moel@348: 	function extendOptionsObject(target, options) {
moel@348: 		var type = exports.getType,
moel@348: 			name, special = { "include": true, "ignore": true, "copy": true },
moel@348: 			t, o, i = 1, l = arguments.length;
moel@348: 		if (type(target) !== "object") {
moel@348: 			target = {};
moel@348: 		}
moel@348: 		for (; i < l; i++) {
moel@348: 			options = arguments[i];
moel@348: 			if (type(options) !== "object") {
moel@348: 				options = {};
moel@348: 			}
moel@348: 			for (name in options) {
moel@348: 				t = target[name]; o = options[name];
moel@348: 				if (name !== "constructor" && special[name] && type(o) !== "array") {
moel@348: 					if (type(o) !== "string") {
moel@348: 						throw new Error("ko.mapping.defaultOptions()." + name + " should be an array or string.");
moel@348: 					}
moel@348: 					o = [o];
moel@348: 				}
moel@348: 				switch (type(o)) {
moel@348: 				case "object": // Recurse
moel@348: 					t = type(t) === "object" ? t : {};
moel@348: 					target[name] = extendOptionsObject(t, o);
moel@348: 					break;
moel@348: 				case "array":
moel@348: 					t = type(t) === "array" ? t : [];
moel@348: 					target[name] = extendOptionsArray(t, o);
moel@348: 					break;
moel@348: 				default:
moel@348: 					target[name] = o;
moel@348: 				}
moel@348: 			}
moel@348: 		}
moel@348: 		return target;
moel@348: 	}
moel@348: 
moel@348: 	function mergeOptions() {
moel@348: 		var options = ko.utils.arrayPushAll([{}, defaultOptions], arguments); // Always use empty object as target to avoid changing default options
moel@348: 		options = extendOptionsObject.apply(this, options);
moel@348: 		return options;
moel@348: 	}
moel@348: 
moel@348: 	// When using a 'create' callback, we proxy the dependent observable so that it doesn't immediately evaluate on creation.
moel@348: 	// The reason is that the dependent observables in the user-specified callback may contain references to properties that have not been mapped yet.
moel@348: 	function withProxyDependentObservable(dependentObservables, callback) {
moel@348: 		var localDO = ko.dependentObservable;
moel@348: 		ko.dependentObservable = function (read, owner, options) {
moel@348: 			options = options || {};
moel@348: 
moel@348: 			if (read && typeof read == "object") { // mirrors condition in knockout implementation of DO's
moel@348: 				options = read;
moel@348: 			}
moel@348: 
moel@348: 			var realDeferEvaluation = options.deferEvaluation;
moel@348: 
moel@348: 			var isRemoved = false;
moel@348: 
moel@348: 			// We wrap the original dependent observable so that we can remove it from the 'dependentObservables' list we need to evaluate after mapping has
moel@348: 			// completed if the user already evaluated the DO themselves in the meantime.
moel@348: 			var wrap = function (DO) {
moel@348: 				var wrapped = realKoDependentObservable({
moel@348: 					read: function () {
moel@348: 						if (!isRemoved) {
moel@348: 							ko.utils.arrayRemoveItem(dependentObservables, DO);
moel@348: 							isRemoved = true;
moel@348: 						}
moel@348: 						return DO.apply(DO, arguments);
moel@348: 					},
moel@348: 					write: function (val) {
moel@348: 						return DO(val);
moel@348: 					},
moel@348: 					deferEvaluation: true
moel@348: 				});
moel@348: 				if(DEBUG) wrapped._wrapper = true;
moel@348: 				return wrapped;
moel@348: 			};
moel@348: 			
moel@348: 			options.deferEvaluation = true; // will either set for just options, or both read/options.
moel@348: 			var realDependentObservable = new realKoDependentObservable(read, owner, options);
moel@348: 
moel@348: 			if (!realDeferEvaluation) {
moel@348: 				realDependentObservable = wrap(realDependentObservable);
moel@348: 				dependentObservables.push(realDependentObservable);
moel@348: 			}
moel@348: 
moel@348: 			return realDependentObservable;
moel@348: 		}
moel@348: 		ko.dependentObservable.fn = realKoDependentObservable.fn;
moel@348: 		ko.computed = ko.dependentObservable;
moel@348: 		var result = callback();
moel@348: 		ko.dependentObservable = localDO;
moel@348: 		ko.computed = ko.dependentObservable;
moel@348: 		return result;
moel@348: 	}
moel@348: 
moel@348: 	function updateViewModel(mappedRootObject, rootObject, options, parentName, parent, parentPropertyName) {
moel@348: 		var isArray = ko.utils.unwrapObservable(rootObject) instanceof Array;
moel@348: 
moel@348: 		// If nested object was already mapped previously, take the options from it
moel@348: 		if (parentName !== undefined && exports.isMapped(mappedRootObject)) {
moel@348: 			options = ko.utils.unwrapObservable(mappedRootObject)[mappingProperty];
moel@348: 			parentName = "";
moel@348: 			parentPropertyName = "";
moel@348: 		}
moel@348: 
moel@348: 		parentName = parentName || "";
moel@348: 		parentPropertyName = parentPropertyName || "";
moel@348: 
moel@348: 		var callbackParams = {
moel@348: 			data: rootObject,
moel@348: 			parent: parent
moel@348: 		};
moel@348: 
moel@348: 		var getCallback = function (name) {
moel@348: 			var callback;
moel@348: 			if (parentName === "") {
moel@348: 				callback = options[name];
moel@348: 			} else if (callback = options[parentName]) {
moel@348: 				callback = callback[name]
moel@348: 			}
moel@348: 			return callback;
moel@348: 		};
moel@348: 
moel@348: 		var hasCreateCallback = function () {
moel@348: 			return getCallback("create") instanceof Function;
moel@348: 		};
moel@348: 
moel@348: 		var createCallback = function (data) {
moel@348: 			return withProxyDependentObservable(dependentObservables, function () {
moel@348: 				return getCallback("create")({
moel@348: 					data: data || callbackParams.data,
moel@348: 					parent: callbackParams.parent
moel@348: 				});
moel@348: 			});
moel@348: 		};
moel@348: 
moel@348: 		var hasUpdateCallback = function () {
moel@348: 			return getCallback("update") instanceof Function;
moel@348: 		};
moel@348: 
moel@348: 		var updateCallback = function (obj, data) {
moel@348: 			var params = {
moel@348: 				data: data || callbackParams.data,
moel@348: 				parent: callbackParams.parent,
moel@348: 				target: ko.utils.unwrapObservable(obj)
moel@348: 			};
moel@348: 
moel@348: 			if (ko.isWriteableObservable(obj)) {
moel@348: 				params.observable = obj;
moel@348: 			}
moel@348: 
moel@348: 			return getCallback("update")(params);
moel@348: 		}
moel@348: 
moel@348: 		var alreadyMapped = visitedObjects.get(rootObject);
moel@348: 		if (alreadyMapped) {
moel@348: 			return alreadyMapped;
moel@348: 		}
moel@348: 
moel@348: 		if (!isArray) {
moel@348: 			// For atomic types, do a direct update on the observable
moel@348: 			if (!canHaveProperties(rootObject)) {
moel@348: 				switch (exports.getType(rootObject)) {
moel@348: 				case "function":
moel@348: 					if (hasUpdateCallback()) {
moel@348: 						if (ko.isWriteableObservable(rootObject)) {
moel@348: 							rootObject(updateCallback(rootObject));
moel@348: 							mappedRootObject = rootObject;
moel@348: 						} else {
moel@348: 							mappedRootObject = updateCallback(rootObject);
moel@348: 						}
moel@348: 					} else {
moel@348: 						mappedRootObject = rootObject;
moel@348: 					}
moel@348: 					break;
moel@348: 				default:
moel@348: 					if (ko.isWriteableObservable(mappedRootObject)) {
moel@348: 						if (hasUpdateCallback()) {
moel@348: 							mappedRootObject(updateCallback(mappedRootObject));
moel@348: 						} else {
moel@348: 							mappedRootObject(ko.utils.unwrapObservable(rootObject));
moel@348: 						}
moel@348: 					} else {
moel@348: 						if (hasCreateCallback()) {
moel@348: 							mappedRootObject = createCallback();
moel@348: 						} else {
moel@348: 							mappedRootObject = ko.observable(ko.utils.unwrapObservable(rootObject));
moel@348: 						}
moel@348: 
moel@348: 						if (hasUpdateCallback()) {
moel@348: 							mappedRootObject(updateCallback(mappedRootObject));
moel@348: 						}
moel@348: 					}
moel@348: 					break;
moel@348: 				}
moel@348: 
moel@348: 			} else {
moel@348: 				mappedRootObject = ko.utils.unwrapObservable(mappedRootObject);
moel@348: 				if (!mappedRootObject) {
moel@348: 					if (hasCreateCallback()) {
moel@348: 						var result = createCallback();
moel@348: 
moel@348: 						if (hasUpdateCallback()) {
moel@348: 							result = updateCallback(result);
moel@348: 						}
moel@348: 
moel@348: 						return result;
moel@348: 					} else {
moel@348: 						if (hasUpdateCallback()) {
moel@348: 							return updateCallback(result);
moel@348: 						}
moel@348: 
moel@348: 						mappedRootObject = {};
moel@348: 					}
moel@348: 				}
moel@348: 
moel@348: 				if (hasUpdateCallback()) {
moel@348: 					mappedRootObject = updateCallback(mappedRootObject);
moel@348: 				}
moel@348: 
moel@348: 				visitedObjects.save(rootObject, mappedRootObject);
moel@348: 
moel@348: 				// For non-atomic types, visit all properties and update recursively
moel@348: 				visitPropertiesOrArrayEntries(rootObject, function (indexer) {
moel@348: 					var fullPropertyName = getPropertyName(parentPropertyName, rootObject, indexer);
moel@348: 
moel@348: 					if (ko.utils.arrayIndexOf(options.ignore, fullPropertyName) != -1) {
moel@348: 						return;
moel@348: 					}
moel@348: 
moel@348: 					if (ko.utils.arrayIndexOf(options.copy, fullPropertyName) != -1) {
moel@348: 						mappedRootObject[indexer] = rootObject[indexer];
moel@348: 						return;
moel@348: 					}
moel@348: 
moel@348: 					// In case we are adding an already mapped property, fill it with the previously mapped property value to prevent recursion.
moel@348: 					// If this is a property that was generated by fromJS, we should use the options specified there
moel@348: 					var prevMappedProperty = visitedObjects.get(rootObject[indexer]);
moel@348: 					var value = prevMappedProperty || updateViewModel(mappedRootObject[indexer], rootObject[indexer], options, indexer, mappedRootObject, fullPropertyName);
moel@348: 
moel@348: 					if (ko.isWriteableObservable(mappedRootObject[indexer])) {
moel@348: 						mappedRootObject[indexer](ko.utils.unwrapObservable(value));
moel@348: 					} else {
moel@348: 						mappedRootObject[indexer] = value;
moel@348: 					}
moel@348: 
moel@348: 					options.mappedProperties[fullPropertyName] = true;
moel@348: 				});
moel@348: 			}
moel@348: 		} else {
moel@348: 			var changes = [];
moel@348: 
moel@348: 			var hasKeyCallback = getCallback("key") instanceof Function;
moel@348: 			var keyCallback = hasKeyCallback ? getCallback("key") : function (x) {
moel@348: 				return x;
moel@348: 			};
moel@348: 			if (!ko.isObservable(mappedRootObject)) {
moel@348: 				// When creating the new observable array, also add a bunch of utility functions that take the 'key' of the array items into account.
moel@348: 				mappedRootObject = ko.observableArray([]);
moel@348: 
moel@348: 				mappedRootObject.mappedRemove = function (valueOrPredicate) {
moel@348: 					var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
moel@348: 							return value === keyCallback(valueOrPredicate);
moel@348: 						};
moel@348: 					return mappedRootObject.remove(function (item) {
moel@348: 						return predicate(keyCallback(item));
moel@348: 					});
moel@348: 				}
moel@348: 
moel@348: 				mappedRootObject.mappedRemoveAll = function (arrayOfValues) {
moel@348: 					var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
moel@348: 					return mappedRootObject.remove(function (item) {
moel@348: 						return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
moel@348: 					});
moel@348: 				}
moel@348: 
moel@348: 				mappedRootObject.mappedDestroy = function (valueOrPredicate) {
moel@348: 					var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
moel@348: 							return value === keyCallback(valueOrPredicate);
moel@348: 						};
moel@348: 					return mappedRootObject.destroy(function (item) {
moel@348: 						return predicate(keyCallback(item));
moel@348: 					});
moel@348: 				}
moel@348: 
moel@348: 				mappedRootObject.mappedDestroyAll = function (arrayOfValues) {
moel@348: 					var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
moel@348: 					return mappedRootObject.destroy(function (item) {
moel@348: 						return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
moel@348: 					});
moel@348: 				}
moel@348: 
moel@348: 				mappedRootObject.mappedIndexOf = function (item) {
moel@348: 					var keys = filterArrayByKey(mappedRootObject(), keyCallback);
moel@348: 					var key = keyCallback(item);
moel@348: 					return ko.utils.arrayIndexOf(keys, key);
moel@348: 				}
moel@348: 
moel@348: 				mappedRootObject.mappedCreate = function (value) {
moel@348: 					if (mappedRootObject.mappedIndexOf(value) !== -1) {
moel@348: 						throw new Error("There already is an object with the key that you specified.");
moel@348: 					}
moel@348: 
moel@348: 					var item = hasCreateCallback() ? createCallback(value) : value;
moel@348: 					if (hasUpdateCallback()) {
moel@348: 						var newValue = updateCallback(item, value);
moel@348: 						if (ko.isWriteableObservable(item)) {
moel@348: 							item(newValue);
moel@348: 						} else {
moel@348: 							item = newValue;
moel@348: 						}
moel@348: 					}
moel@348: 					mappedRootObject.push(item);
moel@348: 					return item;
moel@348: 				}
moel@348: 			}
moel@348: 
moel@348: 			var currentArrayKeys = filterArrayByKey(ko.utils.unwrapObservable(mappedRootObject), keyCallback).sort();
moel@348: 			var newArrayKeys = filterArrayByKey(rootObject, keyCallback);
moel@348: 			if (hasKeyCallback) newArrayKeys.sort();
moel@348: 			var editScript = ko.utils.compareArrays(currentArrayKeys, newArrayKeys);
moel@348: 
moel@348: 			var ignoreIndexOf = {};
moel@348: 
moel@348: 			var newContents = [];
moel@348: 			for (var i = 0, j = editScript.length; i < j; i++) {
moel@348: 				var key = editScript[i];
moel@348: 				var mappedItem;
moel@348: 				var fullPropertyName = getPropertyName(parentPropertyName, rootObject, i);
moel@348: 				switch (key.status) {
moel@348: 				case "added":
moel@348: 					var item = getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
moel@348: 					mappedItem = updateViewModel(undefined, item, options, parentName, mappedRootObject, fullPropertyName);
moel@348: 					if(!hasCreateCallback()) {
moel@348: 						mappedItem = ko.utils.unwrapObservable(mappedItem);
moel@348: 					}
moel@348: 
moel@348: 					var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
moel@348: 					newContents[index] = mappedItem;
moel@348: 					ignoreIndexOf[index] = true;
moel@348: 					break;
moel@348: 				case "retained":
moel@348: 					var item = getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
moel@348: 					mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
moel@348: 					updateViewModel(mappedItem, item, options, parentName, mappedRootObject, fullPropertyName);
moel@348: 
moel@348: 					var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
moel@348: 					newContents[index] = mappedItem;
moel@348: 					ignoreIndexOf[index] = true;
moel@348: 					break;
moel@348: 				case "deleted":
moel@348: 					mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
moel@348: 					break;
moel@348: 				}
moel@348: 
moel@348: 				changes.push({
moel@348: 					event: key.status,
moel@348: 					item: mappedItem
moel@348: 				});
moel@348: 			}
moel@348: 
moel@348: 			mappedRootObject(newContents);
moel@348: 
moel@348: 			var arrayChangedCallback = getCallback("arrayChanged");
moel@348: 			if (arrayChangedCallback instanceof Function) {
moel@348: 				ko.utils.arrayForEach(changes, function (change) {
moel@348: 					arrayChangedCallback(change.event, change.item);
moel@348: 				});
moel@348: 			}
moel@348: 		}
moel@348: 
moel@348: 		return mappedRootObject;
moel@348: 	}
moel@348: 
moel@348: 	function ignorableIndexOf(array, item, ignoreIndices) {
moel@348: 		for (var i = 0, j = array.length; i < j; i++) {
moel@348: 			if (ignoreIndices[i] === true) continue;
moel@348: 			if (array[i] === item) return i;
moel@348: 		}
moel@348: 		return null;
moel@348: 	}
moel@348: 
moel@348: 	function mapKey(item, callback) {
moel@348: 		var mappedItem;
moel@348: 		if (callback) mappedItem = callback(item);
moel@348: 		if (exports.getType(mappedItem) === "undefined") mappedItem = item;
moel@348: 
moel@348: 		return ko.utils.unwrapObservable(mappedItem);
moel@348: 	}
moel@348: 
moel@348: 	function getItemByKey(array, key, callback) {
moel@348: 		var filtered = ko.utils.arrayFilter(ko.utils.unwrapObservable(array), function (item) {
moel@348: 			return mapKey(item, callback) === key;
moel@348: 		});
moel@348: 
moel@348: 		if (filtered.length == 0) throw new Error("When calling ko.update*, the key '" + key + "' was not found!");
moel@348: 		if ((filtered.length > 1) && (canHaveProperties(filtered[0]))) throw new Error("When calling ko.update*, the key '" + key + "' was not unique!");
moel@348: 
moel@348: 		return filtered[0];
moel@348: 	}
moel@348: 
moel@348: 	function filterArrayByKey(array, callback) {
moel@348: 		return ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (item) {
moel@348: 			if (callback) {
moel@348: 				return mapKey(item, callback);
moel@348: 			} else {
moel@348: 				return item;
moel@348: 			}
moel@348: 		});
moel@348: 	}
moel@348: 
moel@348: 	function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
moel@348: 		if (rootObject instanceof Array) {
moel@348: 			for (var i = 0; i < rootObject.length; i++)
moel@348: 			visitorCallback(i);
moel@348: 		} else {
moel@348: 			for (var propertyName in rootObject)
moel@348: 			visitorCallback(propertyName);
moel@348: 		}
moel@348: 	};
moel@348: 
moel@348: 	function canHaveProperties(object) {
moel@348: 		var type = exports.getType(object);
moel@348: 		return (type === "object" || type === "array") && (object !== null) && (type !== "undefined");
moel@348: 	}
moel@348: 
moel@348: 	// Based on the parentName, this creates a fully classified name of a property
moel@348: 
moel@348: 	function getPropertyName(parentName, parent, indexer) {
moel@348: 		var propertyName = parentName || "";
moel@348: 		if (parent instanceof Array) {
moel@348: 			if (parentName) {
moel@348: 				propertyName += "[" + indexer + "]";
moel@348: 			}
moel@348: 		} else {
moel@348: 			if (parentName) {
moel@348: 				propertyName += ".";
moel@348: 			}
moel@348: 			propertyName += indexer;
moel@348: 		}
moel@348: 		return propertyName;
moel@348: 	}
moel@348: 
moel@348: 	function visitModel(rootObject, callback, options, parentName, fullParentName) {
moel@348: 		// If nested object was already mapped previously, take the options from it
moel@348: 		if (parentName !== undefined && exports.isMapped(rootObject)) {
moel@348: 			//options = ko.utils.unwrapObservable(rootObject)[mappingProperty];
moel@348: 			options = mergeOptions(ko.utils.unwrapObservable(rootObject)[mappingProperty], options);
moel@348: 			parentName = "";
moel@348: 		}
moel@348: 
moel@348: 		if (parentName === undefined) { // the first call
moel@348: 			visitedObjects = new objectLookup();
moel@348: 		}
moel@348: 
moel@348: 		parentName = parentName || "";
moel@348: 
moel@348: 		var mappedRootObject;
moel@348: 		var unwrappedRootObject = ko.utils.unwrapObservable(rootObject);
moel@348: 		if (!canHaveProperties(unwrappedRootObject)) {
moel@348: 			return callback(rootObject, fullParentName);
moel@348: 		} else {
moel@348: 			// Only do a callback, but ignore the results
moel@348: 			callback(rootObject, fullParentName);
moel@348: 			mappedRootObject = unwrappedRootObject instanceof Array ? [] : {};
moel@348: 		}
moel@348: 
moel@348: 		visitedObjects.save(rootObject, mappedRootObject);
moel@348: 
moel@348: 		var origFullParentName = fullParentName;
moel@348: 		visitPropertiesOrArrayEntries(unwrappedRootObject, function (indexer) {
moel@348: 			if (options.ignore && ko.utils.arrayIndexOf(options.ignore, indexer) != -1) return;
moel@348: 
moel@348: 			var propertyValue = unwrappedRootObject[indexer];
moel@348: 			var fullPropertyName = getPropertyName(parentName, unwrappedRootObject, indexer);
moel@348: 			
moel@348: 			// If we don't want to explicitly copy the unmapped property...
moel@348: 			if (ko.utils.arrayIndexOf(options.copy, indexer) === -1) {
moel@348: 				// ...find out if it's a property we want to explicitly include
moel@348: 				if (ko.utils.arrayIndexOf(options.include, indexer) === -1) {
moel@348: 					// Options contains all the properties that were part of the original object.
moel@348: 					// 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.
moel@348: 					if (options.mappedProperties && !options.mappedProperties[fullPropertyName] && !(unwrappedRootObject instanceof Array)) {
moel@348: 						return;
moel@348: 					}
moel@348: 				}
moel@348: 			}
moel@348: 
moel@348: 			fullParentName = getPropertyName(origFullParentName, unwrappedRootObject, indexer);
moel@348: 			
moel@348: 			var propertyType = exports.getType(ko.utils.unwrapObservable(propertyValue));
moel@348: 			switch (propertyType) {
moel@348: 			case "object":
moel@348: 			case "array":
moel@348: 			case "undefined":
moel@348: 				var previouslyMappedValue = visitedObjects.get(propertyValue);
moel@348: 				mappedRootObject[indexer] = (exports.getType(previouslyMappedValue) !== "undefined") ? previouslyMappedValue : visitModel(propertyValue, callback, options, fullPropertyName, fullParentName);
moel@348: 				break;
moel@348: 			default:
moel@348: 				mappedRootObject[indexer] = callback(propertyValue, fullParentName);
moel@348: 			}
moel@348: 		});
moel@348: 
moel@348: 		return mappedRootObject;
moel@348: 	}
moel@348: 
moel@348: 	function objectLookup() {
moel@348: 		var keys = [];
moel@348: 		var values = [];
moel@348: 		this.save = function (key, value) {
moel@348: 			var existingIndex = ko.utils.arrayIndexOf(keys, key);
moel@348: 			if (existingIndex >= 0) values[existingIndex] = value;
moel@348: 			else {
moel@348: 				keys.push(key);
moel@348: 				values.push(value);
moel@348: 			}
moel@348: 		};
moel@348: 		this.get = function (key) {
moel@348: 			var existingIndex = ko.utils.arrayIndexOf(keys, key);
moel@348: 			return (existingIndex >= 0) ? values[existingIndex] : undefined;
moel@348: 		};
moel@348: 	};
moel@348: }));