/*! JsObservable v1.0.16: http://jsviews.com/#jsobservable */
/*
 * Subcomponent of JsViews
 * Data change events for data-linking
 *
 * Copyright 2025, Boris Moore
 * Released under the MIT License.
 */

//jshint -W018, -W041, -W120

(function(factory, global) {
	// global var is the this object, which is window when running in the usual browser environment
	var $ = global.jQuery;

	if (typeof exports === "object") { // CommonJS e.g. Browserify
		module.exports = $
			? factory(global, $)
			: function($) { // If no global jQuery, take jQuery passed as parameter: require("jsobservable")(jQuery)
				return factory(global, $);
			};
	} else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS
		define(["jquery"], function($) {
			return factory(global, $); // Require jQuery
		});
	} else { // Browser using plain <script> tag
		factory(global, false);
	}
} (

// factory (for jsviews.js)
function(global, $) {
"use strict";

//========================== Top-level vars ==========================

// global var is the this object, which is window when running in the usual browser environment
var setGlobals = $ === false; // Only set globals if script block in browser (not AMD and not CommonJS)

$ = $ || global.jQuery;

if (!$ || !$.fn) {
	// jQuery is not loaded.
	throw "jquery.observable.js requires jQuery"; // We require jQuery
}

var versionNumber = "v1.0.16",
	_ocp = "_ocp", // Observable contextual parameter
	$observe, $observable,

	$views = $.views =
		$.views ||
		setGlobals && global.jsrender && jsrender.views || //jsrender was loaded before jquery.observable
		{ // jsrender not loaded so set up $.views and $.views.sub here, and merge back in jsrender if loaded afterwards
			jsviews: versionNumber,
			sub: { // subscription, e.g. JsViews integration
				settings: {}
			},
			settings: {
				advanced: function(value) {
					$subSettingsAdvanced = $subSettings.advanced = $subSettings.advanced || {_jsv: true};
					return value
						? (
							"_jsv" in value && ($subSettingsAdvanced._jsv = value._jsv),
							$sub.advSet(),
							$views.settings
						)
						: $subSettingsAdvanced;
					}
			}
		},
	$sub = $views.sub,
	$subSettings = $sub.settings,
	$subSettingsAdvanced = $subSettings.advanced,
	$isFunction = function(ob) { return typeof ob === "function"; },
	$isArray = Array.isArray,
	$expando = $.expando,
	STRING = "string",
	OBJECT = "object";

if ($views.jsviews !== versionNumber) {
	// Different version of jsRender was loaded
	throw "jquery.observable.js requires jsrender.js " + versionNumber;
}

if (!$.observe) {

	var $eventSpecial = $.event.special,
		slice = [].slice,
		splice = [].splice,
		concat = [].concat,
		PARSEINT = parseInt,
		rNotWhite = /\S+/g,
		rShallowPath = /^[^.[]*$/, // No '.' or '[' in path
		propertyChangeStr = $sub.propChng = $sub.propChng || "propertyChange",// These two settings can be overridden on settings after loading
		arrayChangeStr = $sub.arrChng = $sub.arrChng || "arrayChange",        // jsRender, and prior to loading jquery.observable.js and/or JsViews
		cbBindingsStore = {},
		observeStr = propertyChangeStr + ".observe",
		observeObjKey = 1,
		observeCbKey = 1,
		observeInnerCbKey = 1,
		$data = $.data,
		remove = {}, // flag for removeProperty
		asyncBatch = [],

	//========================== Top-level functions ==========================

	getCbKey = function(cb) {
		return cb
		? (cb._cId = cb._cId || (".obs" + observeCbKey++))
		: "";
	},

	ObjectObservable = function(ns, data) {
		this._data = data;
		this._ns = ns;
		return this;
	},

	ArrayObservable = function(ns, data) {
		this._data = data;
		this._ns = ns;
		return this;
	},

	wrapArray = function(data) {
		return $isArray(data)
			? [data]
			: data;
	},

	dependsPaths = function(paths, root, callback) {
		// Process depends = ... paths to resolve objects, and recursively process functions.
		paths = paths
			? $isArray(paths)
				? paths
				: [paths]
			: [];

		var i, path, object, rt,
			nextObj = object = root,
			l = paths && paths.length,
			out = [];

		for (i = 0; i < l; i++) {
			path = paths[i];
			if ($isFunction(path)) { // path is a depends function, returning [path1, ...]
				rt = root.tagName
						? root.linkCtx.data // root is tag instance. rt is current data context of tag
						: root; // rt = root = current data context of computed prop
				out = out.concat(dependsPaths(path.call(root, rt, callback), rt, callback));
				continue;
			} else if (typeof path !== STRING) {
				root = nextObj = path = (path === undefined ? null : path);
				if (nextObj !== object) {
					out.push(object = nextObj);
				}
				continue;
			}
			if (nextObj !== object) {
				out.push(object = nextObj);
			}
			out.push(path);
		}
		if (out.length) {
			// Switch on allowArray, for depends paths, by passing {_ar: ...} objects to switch on allowArray then return to contextual allowArray value
			out.unshift({_ar: 1});
			out.push({_ar: -1});
		}
		return out;
	},

	onDataChange = function(ev, eventArgs) {
		function isOb(val) {
			return typeof val === OBJECT && (paths[0] || !noArray && $isArray(val));
		}

		if (!(ev.data && ev.data.off)) {
			// Skip if !!ev.data.off: - a handler that has already been removed (maybe was on handler collection at call time - then removed by another handler)
			var allPath, filter, parentObs,
				oldValue = eventArgs.oldValue,
				value = eventArgs.value,
				ctx = ev.data,
				observeAll = ctx.observeAll,
				cb = ctx.cb,
				noArray = ctx._arOk ? 0 : 1,
				paths = ctx.paths,
				ns = ctx.ns;

			if (ev.type === arrayChangeStr) {
				(cb.array || cb).call(ctx, ev, eventArgs); // If there is an arrayHandler expando on the regular handler, use it, otherwise use the regular handler for arrayChange events also - for example: $.observe(array, handler)
				// or observeAll() with an array in the graph. Note that on data-link bindings we ensure always to have an array handler - $.noop if none is specified e.g. on the data-linked tag.
			} else if (ctx.prop === eventArgs.path || ctx.prop === "*") {
				if (observeAll) {
					allPath = observeAll._path + "." + eventArgs.path;
					filter = observeAll.filter;
					parentObs = [ev.target].concat(observeAll.parents());

					if (isOb(oldValue)) {
						observe_apply(undefined, ns, [oldValue], paths, cb, true, filter, [parentObs], allPath); // unobserve
					}
					if (isOb(value)) {
						observe_apply(undefined, ns, [value], paths, cb, undefined, filter, [parentObs], allPath);
					}
				} else {
					if (isOb(oldValue)) { // oldValue is an object, so unobserve
						observe_apply(noArray, ns, [oldValue], paths, cb, true); // unobserve. Observe array change events too if this change is not from an 'observeAndBind' tag binding, or is from a 'depends' path
					}
					if (isOb(value)) { // value is an object, so observe
						observe_apply(noArray, ns, [value], paths, cb); // observe. Observe array change events too if this change is not from an 'observeAndBind' tag binding, or is from a 'depends' path
					}
				}
				ctx.cb(ev, eventArgs);
			}
		}
	},

	observe_apply = function() {
		// $.observe(), but allowing you to include arrays within the arguments - which you want flattened.
		var args = concat.apply([], arguments); // Flatten the arguments
		return $observe.apply(args.shift(), args);
	},

	$observeAll = function(cb, filter, unobserve) {
		observeAll(this._ns, this._data, cb, filter, [], "root", unobserve);
	},

	$unobserveAll = function(cb, filter) {
		$observeAll.call(this, cb, filter, true);
	},

	observeAll = function(namespace, object, cb, filter, parentObs, allPath, unobserve, objMap) {
		function observeArrayItems(arr, unobs) {
			l = arr.length;
			newAllPath = allPath + "[]";
			while (l--) {
				filterAndObserveAll(arr, l, unobs, 1);
			}
		}

		function filterAndObserveAll(obj, prop, unobs, nestedArray) {
			var newObject, newParentObs;
			if (prop !== $expando && (newObject = $observable._fltr(newAllPath, obj[prop], nextParentObs, filter))) {
				newParentObs = nextParentObs.slice();
				if (nestedArray && updatedTgt && newParentObs[0] !== updatedTgt) {
					newParentObs.unshift(updatedTgt); // For array change events when observing an array which is not the root, need to add updated array to parentObs
				}
				observeAll(namespace, newObject, cb, filter || (nestedArray ? undefined : 0), newParentObs, newAllPath, unobs, objMap);
			}
		}

		function wrappedCb(ev, eventArgs) {
			// This object is changing.
			allPath = ev.data.observeAll._path;
			updatedTgt = ev.target;
			switch (eventArgs.change) { // observeAll/unobserveAll on added or removed objects
				case "insert":
					observeArrayItems(eventArgs.items);
					break;
				case "remove":
					observeArrayItems(eventArgs.items, true); // unobserveAll on removed items
					break;
				case "set":
					newAllPath = allPath + "." + eventArgs.path;
					filterAndObserveAll(eventArgs, "oldValue", true); // unobserve old value
					filterAndObserveAll(eventArgs, "value"); // observe new value
			}
			updatedTgt = undefined;
			cb.apply(this, arguments); // Observe this object (invoke the callback)
		}
		wrappedCb._wrp = 1;

		var l, isObject, newAllPath, nextParentObs, updatedTgt, obId,
			notRemoving = !objMap || objMap.un || !unobserve; // true unless it is an observeAll call (not unobserveAll) and we are removing a listener (not adding one)

		if (object && typeof object === OBJECT) {
			nextParentObs = [object].concat(parentObs); // The parentObs chain for the next depth of observeAll
			isObject = $isArray(object) ? "" : "*";
			if (objMap && notRemoving && $.hasData(object) && objMap[obId = $data(object).obId]) {
				objMap[obId]++;
				return; // This object has already being observed/unobserved by this observeAll/unobserveAll call (must be a cyclic object graph) so skip, to avoid
				// stack overflow/multiple instances of listener. See jsviews/pull/305
				// NOTE - WE DO NOT support ObserveAll on data with cyclic graphs which include DUPLICATE REFERENCES TO ARRAY PROPERTIES - such as data.children = data.descendants = []
			}
			if (!objMap) {
				objMap = {un: unobserve}; // Map object to register observed objects for this observeAll
			}

			if (cb) {
				// Observe this object or array - and also listen for changes to object graph, to add or remove observers from the modified object graph
				if (isObject || filter !== 0) {
					// If an object, observe the object. If an array, only add arrayChange binding if has filter or if filter is undefined (!== 0) - which
					// is the case for top-level calls or for nested array (array item of an array - e.g. member of 2-dimensional array).
					// For array properties lower in the tree, with no filter, filter is set to 0 in filterAndObserveAll, so no arrayChange binding here,
					// since they get arrayChange binding added during regular $.observe(array ...) binding.
					wrappedCb._cId = getCbKey(cb); // Identify wrapped callback with unwrapped callback, so unobserveAll will
													// remove previous observeAll wrapped callback, if inner callback was the same;
					if (notRemoving) {
						$observe(namespace, object, isObject, wrappedCb, unobserve, filter, nextParentObs, allPath);
						obId = $data(object).obId;
						objMap[obId] = (objMap[obId] || 0) + 1; // Register on map of objects observed/unobserved by this observeAll/unobserveAll call
							//- or remove from map if we are removing this object from observeAll call. (Avoid dups, for cyclic graphs)
					} else {
						if (--objMap[$data(object).obId]) {
							// Register on map of objects observed/unobserved by this observeAll/unobserveAll call
							//- or remove from map if we are removing this object from observeAll call. (Avoid dups, for cyclic graphs)
							return;
						}
						$observe(namespace, object, isObject, wrappedCb, unobserve, filter, nextParentObs, allPath);
					}
				}
			} else {
				// No callback. Just unobserve if unobserve === true.
				if (objMap) {
					objMap[$data(object).obId] = 1; // Register on map of objects unobserved by this unobserveAll call. (Avoid dups, for cyclic graphs)
				}
				$observe(namespace, object, isObject, undefined, unobserve, filter, nextParentObs, allPath);
			}

			if (isObject) {
				// Continue stepping through object graph, observing object and arrays
				// To override filtering, pass in filter function, or replace $.observable._fltr
				for (l in object) {
					newAllPath = allPath + "." + l;
					filterAndObserveAll(object, l, unobserve);
				}
			} else { // Observe items in Array
				observeArrayItems(object, unobserve);
			}
		}
	},

	shallowFilter = function(path /*, object, parentObs*/) {
		return rShallowPath.test(path); // No '.' and no '[' in path
	},

	$unobserve = function() {
		[].push.call(arguments, true); // Add true as additional final argument
		return $observe.apply(undefined, arguments);
	},

	batchTrigger = function(async) {
		var event,
		batch = this.slice();
		this.length = 0;
		this._go = 0;
		while (event = batch.shift()) {
			if (!event.skip) {
				event[0]._trigger(event[1], event[2], true);
			}
		}
		this.paths = {};
	};

	$observe = function() {
		// $.observe([namespace, ]root, [1 or more objects, path or path Array params...], callback[, contextCallback][, unobserve])

		function innerObserve() {
			var p, parts, unobserve, callback, cbId, inId, data, contextCb, items, cbBindings,
				innerCb, parentObs, allPath, filter, initNsArr, initNsArrLen, view, prop, events, el;

			function unobserveBinding(cb, binding) {
				var object;
				for (data in binding) {
					object = binding[data];
					if ($isArray(object)) {
						bindArray(cb, object, unobserve, unobserve);
					} else {
						observeOnOff(cb, object, undefined, ns, "");
					}
				}
			}

			function observeOnOff(cb, object, fullPath, namespace, pathStr, isArrayBinding, off) {
				var j, evData, dataOb,
					boundObOrArr = wrapArray(object),
					prntObs = parentObs,
					allPth = allPath;

				namespace = initialNs ? namespace + "." + initialNs : namespace;

				if (!unobserve && (off || isArrayBinding)) {
					events = $._data(object).events;
					events = events && events[isArrayBinding ? arrayChangeStr : propertyChangeStr];
					el = events && events.length;
					while (el--) { // Skip duplicates
						data = events[el] && events[el].data;
						if (data && (off && data.ns !== initialNs
							// When observing, don't unbind dups unless they have the same namespace
							|| !off && data.ns === initialNs && data.cb && data.cb._cId === cb._cId && data.cb._inId === cb._inId && (!cb._wrp || data.cb._wrp)))
							// When observing and doing array binding, don't bind dups if they have the same namespace (Dups can happen e.g. with {^{for people ^~foo=people}})
						{
							return;
						}
					}
				}
				if (unobserve || off) {
					$(boundObOrArr).off(namespace, onDataChange);
				} else {
					evData = isArrayBinding ? {}
						: {
							fullPath: fullPath,
							paths: pathStr ? [pathStr] : [],
							prop: prop,
							_arOk: allowArray
						};
					evData.ns = initialNs;
					evData.cb = cb;

					if (allPath) {
						// This is an observeAll call
						evData.observeAll = {
							_path: allPth,
							path: function() { // Step through path and parentObs parent chain, replacing '[]' by '[n]' based on current index of objects in parent arrays.
								j = prntObs.length;
								return allPth.replace(/[[.]/g, function(all) {
									j--;
									return all === "["
										? "[" + $.inArray(prntObs[j - 1], prntObs[j])
										: ".";
								});
							},
							parents: function() {
								return prntObs; // The chain of parents between the modified object and the root object used in the observeAll() call
							},
							filter: filter
						};
					}
					$(boundObOrArr).on(namespace, null, evData, onDataChange);
					if (cbBindings) {
						// Add object to cbBindings
						dataOb = $data(object);
						dataOb = dataOb.obId || (dataOb.obId = observeObjKey++);
						cbBindings[dataOb] = cbBindings[dataOb] || (cbBindings.len++, object);
					}
				}
			}

			function bindArray(cb, arr, unbind, isArray, relPath) {
				if (allowArray) {
					// allowArray is 1 if this is a call to observe that does not come from observeAndBind (tag binding), or is from a 'depends' path,
					// or for a tag with tag.onArrayChange = true - so we allow arrayChange binding. Otherwise allowArray is zero.
					var object,
						prevAllPath = allPath;

					object = arr;
					if (relPath) {
						object = arr[relPath];
						allPath = allPath ? allPath + "." + relPath : allPath;
					}

					if (isArray || $isArray(object)) {
						if (filter && object) {
							object = $observable._fltr(allPath, object, parentObs, filter);
						}
						if (object) {
							observeOnOff(cb, object, undefined, arrayChangeStr + ".observe" + getCbKey(cb), undefined, true, unbind);
						}
					}
					allPath = prevAllPath;
				}
			}

			function observeObjects(paths) {

				function observeObjectPaths(object, pths, callback, contextCb) {

					function getInnerCb(exprOb) {
						exprOb.ob = contextCb(exprOb); // Initialize object
						return exprOb.cb = function(ev, eventArgs) {
							// The innerCb used for updating a computed in a compiled expression (setting the new instance as exprOb.ob, unobserving the previous object,
							// and observing the new one), then calling the outerCB - i.e. the handler for the whole compiled expression.
							// Initialized exprOb.ob to the current object.
							// Uses the contextCb callback to execute the compiled exprOb template in the context of the view/data etc. to get the returned value, typically an object or array.
							// If it is an array, registers array binding
							// Note: For jsviews/issues/292 ctxCb will need var ctxCb = contextCb || function(exprOb, origRt) {return exprOb._cpfn(origRt);};
							var obj = exprOb.ob, // The old object
								sub = exprOb.sb,
								newObj = contextCb(exprOb);

							if (newObj !== obj) {
								if (typeof obj === OBJECT) {
									bindArray(callback, obj, true);
									if (sub || allowArray && $isArray(obj)) {
										innerObserve([obj], sub, callback, contextCb, true); // unobserve on the old object
									}
								}
								exprOb.ob = newObj;
								// Put the updated object instance onto the exprOb in the paths array, so subsequent string paths are relative to this object
								if (typeof newObj === OBJECT) {
									bindArray(callback, newObj);
									if (sub || allowArray && $isArray(newObj)) { // observe on new object
										innerObserve([newObj], sub, callback, contextCb);
									}
								}
							}
							// Call the outerCb - to execute the compiled expression that this computed is part of
							callback(ev, eventArgs);
						};
					}

					function observePath(object, prts) { // Step through the path parts "this.is^some.path" and observe changes (on the leaf, or down to the bound depth)

						function obArrAddRemove(ev, eventArgs) {
							// If a "[].*" or "[].prop" wild card path (for observing properties of array items) we need to observe or unobserve added or removed items
							var l;
							if (eventArgs.change === "insert" || (unobserve = eventArgs.change === "remove")) {
								l = eventArgs.items.length;
								while (l--) {
									observePath(eventArgs.items[l], prts.slice());
								}
								unobserve = false;
							}
						}

						if (callback) {
							obArrAddRemove._cId = getCbKey(callback); // Identify wrapped callback with unwrapped callback, so unobserveAll will
																				// remove previous observeAll wrapped callback, if inner callback was the same;
							obArrAddRemove._inId = ".arIn" + observeInnerCbKey++; // Specific _inId for each distinct obArrAddRemove, so not skipped as dups
						}

						var arrIndex, skip, dep, obArr, prt, fnProp, isGet,
							obj = object;
						if (object && object._cxp) {
							return observeObjectPaths(object[0], [object[1]], callback, contextCb);
						}

						while ((prop = prts.shift()) !== undefined && prop !== "__proto__") { // Prevent prototype pollution attacks
							if (obj && typeof obj === OBJECT && typeof prop === STRING) {
								if (prop === "") {
									continue;
								}
								if (prop.slice(-2) === "()") {
									prop = prop.slice(0, -2);
									isGet = true;
								}
								if ((prts.length < depth + 1) && !obj.nodeType) {
									// Add observer for each token in path starting at depth, and on to the leaf
									if (!unobserve && (events = $._data(obj).events)) {
										events = events && events[propertyChangeStr];
										el = events && events.length;
										skip = 0;
										while (el--) { // Skip duplicates
											data = events[el].data;
											if (data
												&& data.ns === initialNs
												&& data.cb._cId === callback._cId
												&& data.cb._inId === callback._inId
												&& !data._arOk === !allowArray
												&& (data.prop === prop || data.prop === "*" || data.prop === "**")) {
												if (prt = prts.join(".")) {
													data.paths.push(prt); // We will skip this binding, but if it is not a leaf binding,
													// need to keep bindings for rest of path, ready for if the obj gets swapped.
												}
												skip++;
											}
										}
										if (skip) {
											// Duplicate binding(s) found, so move on
											fnProp = obj[prop];
											obj = $isFunction(fnProp) ? fnProp.call(obj) : obj[prop];
											continue;
										}
									}
									if (prop === "*" || prop === "**") { // "*" => all properties. "**" => all properties and sub-properties (i.e. deep observeAll behavior)
										if (!unobserve && events && events.length) {
											// Remove existing bindings, since they will be duplicates with "*" or "**"
											observeOnOff(callback, obj, path, ns, "", false, true);
										}
										if (prop === "*") {
											observeOnOff(callback, obj, path, ns, ""); // observe the object for any property change
											for (prt in obj) {
												// observing "*": So (in addition to listening to prop change, above) listen to arraychange on props of type array
												if (prt !== $expando) {
													bindArray(callback, obj, unobserve, undefined, prt);
												}
											}
										} else {
											$.observable(initialNs, obj)[(unobserve ? "un" : "") + "observeAll"](callback); // observe or unobserve the object for any property change
										}
										break;
									} else if (prop == "[]") { // "[].*" or "[].prop" wild card path, for observing properties of array items
										if ($isArray(obj)) {
											if (unobserve) {
												observeOnOff(callback, obj, path, arrayChangeStr + getCbKey(callback), undefined, unobserve, unobserve);
											} else {
												$observe(initialNs, obj, obArrAddRemove, unobserve); // observe or unobserve added or removed items
											}
										}
									} else if (prop) {
										observeOnOff(callback, obj, path, ns + ".p_" + prop, prts.join("^")); // By using "^" rather than "." we ensure that deep binding will be used on newly inserted object graphs
									}
								}
								if (allPath) {
									allPath += "." + prop;
								}
								if (prop === "[]") {
									if ($isArray(obj)) {
										obArr = obj;
										arrIndex = obj.length;
									}
									while (arrIndex--) {
										obj = obArr[arrIndex];
										observePath(obj, prts.slice());
									}
									return;
								}
								prop = obj[prop];
								if (!prts[0]) {
									bindArray(callback, prop, unobserve); // [un]observe(object, "arrayProperty") observes array changes on property of type array
								}
							}
							if ($isFunction(prop)) {
								fnProp = prop;
								if (dep = fnProp.depends) {
									// This is a computed observable. We will observe any declared dependencies.
									if (obj._vw && obj._ocp) {
										// Observable contextual parameter, so context was ocp object. Now move context to view.data for dependencies
										obj = obj._vw; // storeView or tag (scope of contextual parameter)
										if (obj._tgId) {
											// Is a tag, so get view
											obj = obj.tagCtx.view;
										}
										obj = obj.data; // view.data
									}
									observeObjects(concat.apply([], [[obj], dependsPaths(dep, obj, callback)]));
								}

								if (isGet) {
									if (!prts[0]) {
										bindArray(callback, fnProp.call(obj), unobserve);
										break;
									}
									prop = fnProp.call(obj);
									if (!prop) {
										break;
									}
								}
							}
							obj = prop;
						}
					}

					var i, path,
						depth = 0,
						l = pths.length;
					if (object && !contextCb && ((view = object._is === "view") || object._is === "tag")) {
						contextCb = $sub._gccb(view ? object : object.tagCtx.contentView);
						if (callback && !unobserve) {
							(function() {
								var ob = object,
									cb = callback;
								callback = function(ev, eventArgs) {
									// Wrapped callback so this pointer is tag or view
									cb.call(ob, ev, eventArgs);
								};
								callback._cId = cb._cId;
								callback._inId = cb._inId;
							})();
						}
						object = view ? object.data : object;
					}
					if (!pths[0]) {
						if ($isArray(object)) {
							bindArray(callback, object, unobserve, true); // observe(array, handler)
						} else if (unobserve) {
							observeOnOff(callback, object, undefined, ns, ""); // unobserve(objectOrArray[, handler])
						}
					}
					for (i = 0; i < l; i++) { // Step through objects and paths
						path = pths[i];
						if (path === "") {
							continue;
						}
						if (path && path._ar) {
							allowArray += path._ar; // Switch on allowArray for depends paths, and off, afterwards.
							continue;
						}
						if (typeof path === STRING) {
							parts = path.split("^");
							if (parts[1]) {
								// We bind the leaf, plus additional nodes based on depth.
								// "a.b.c^d.e" is depth 2, so listens to changes of e, plus changes of d and of c
								depth = parts[0].split(".").length;
								path = parts.join(".");
								depth = path.split(".").length - depth;
								// if more than one ^ in the path, the first one determines depth
							}
							if (contextCb && (items = contextCb(path, depth))) {
								//object, paths
								if (items.length) {
									var ob = items[0],
										pth = items[1];
									if (ob && ob._cxp) { // contextual parameter
										pth = ob[1];
										ob = ob[0];
										if (ob._is === "view") {
											observeObjectPaths(ob, [pth], callback); // Setting contextCb to undefined, to use passed in view for new contextCb
											continue;
										}
									}
									if (typeof pth === STRING) {
										observePath(ob, pth.split("."));
									} else {
										observeObjectPaths(items.shift(), items, callback, contextCb);
									}
								}
							} else {
								observePath(object, path.split("."));
							}
						} else if (!$isFunction(path) && path && path._cpfn) {
							// Path is an exprOb returned by a computed property - helper/data function (compiled expr function).
							// Get innerCb for updating the object
							innerCb = unobserve ? path.cb || (path.cb = callback) : getInnerCb(path); // https://github.com/BorisMoore/jsviews/issues/463#issuecomment-2651496030
							// innerCb._ctx = callback._ctx; Could pass context (e.g. linkCtx) for use in a depends = function() {} call, so depends is different for different linkCtx's
							innerCb._cId = callback._cId;
							// Set the same cbBindingsStore key as for callback, so when callback is disposed, disposal of innerCb happens too.
							innerCb._inId = innerCb._inId || ".obIn" + observeInnerCbKey++;
							if (path.bnd || path.prm && path.prm.length || !path.sb) {
								// If the exprOb is bound e.g. foo()^sub.path, or has parameters e.g. foo(bar) or is a leaf object (so no sub path) e.g. foo()
								// then observe changes on the object, or its parameters and sub-path
								innerObserve([object], path.path, (path.prm.length ? [path.root||object] : []), path.prm, innerCb, contextCb, unobserve);
							}
							if (path.sb) { // Has a subPath
								// Observe changes on the sub-path
								if (path.sb.prm) {
									path.sb.root = object;
								}
								// Set current object on exprOb.ob
								observeObjectPaths(path.ob, [path.sb], callback, contextCb);
							}
						}
					}
				}

				var pth,
					pths = [], // Array of paths for current object
					l = paths.length;
				while (l--) { // Step backwards through paths and objects
					pth = paths[l];
					if (typeof pth === STRING || pth && (pth._ar || pth._cpfn)) {
						pths.unshift(pth); // This is a path so add to arr
					} else { // This is an object
						observeObjectPaths(pth, pths, callback, contextCb);
						pths = []; // New array for next object
					}
				}
			}

//END OF FUNCTIONS

			var ns = observeStr,
				paths = this != 1 // Using != for IE<10 bug- see jsviews/issues/237
					? concat.apply([], arguments) // Flatten the arguments - this is a 'recursive call' with params using the 'wrapped array'
													// style - such as innerObserve([object], path.path, [origRoot], path.prm, innerCb, ...);
					: slice.call(arguments), // Don't flatten - this is the first 'top-level call, to innerObserve.apply(1, paths)
				lastArg = paths.pop() || false,
				m = paths.length;

			if (typeof lastArg === STRING) { // If last arg is a string then this observe call is part of an observeAll call,
				allPath = lastArg;           // and the last three args are the filter, the parentObs array, and the allPath string.
				parentObs = paths.pop();
				filter = paths.pop();
				lastArg = !!paths.pop(); // unobserve
				m -= 3;
			}
			if (lastArg === !!lastArg) {
				unobserve = lastArg;
				lastArg = paths[m-1];
				lastArg = m && typeof lastArg !== STRING && (!lastArg || $isFunction(lastArg)) ? (m--, paths.pop()) : undefined;
				if (unobserve && !m && $isFunction(paths[0])) {
					lastArg = paths.shift();
				}
			}
			callback = lastArg;
			if (m && $isFunction(paths[m - 1])) {
				contextCb = callback;
				lastArg = callback = paths.pop();
				m--;
			}

			if (unobserve && callback && !callback._cId) {
				return;
			}

			// Use a unique namespace (e.g. obs7) associated with each observe() callback to allow unobserve to remove handlers
			ns += callback
				? ((inId = callback._inId || ""), unobserve)
					? callback._cId + inId
					: (cbId = getCbKey(callback)) + inId
				: "";

			if (cbId && !unobserve) {
				cbBindings = cbBindingsStore[cbId] = cbBindingsStore[cbId] || {len: 0};
			}

			initNsArr = initialNs && initialNs.match(rNotWhite) || [""];
			initNsArrLen = initNsArr.length;

			while (initNsArrLen--) { // Step through multiple white-space separated namespaces if there are any
				initialNs = initNsArr[initNsArrLen];
				if (unobserve && arguments.length < 3) {
					if (callback) {
						unobserveBinding(callback, cbBindingsStore[callback._cId]); // unobserve(handler) - unobserves this handler, all objects
					} else if (!paths[0]) {
						for (p in cbBindingsStore) {
							unobserveBinding(callback, cbBindingsStore[p]); // unobserve() - unobserves all
						}
					}
				}
				observeObjects(paths);
			}
			if (cbId && !cbBindings.len) {
				// If the cbBindings collection is empty we will remove it from the cbBindingsStore
				delete cbBindingsStore[cbId];
			}

			// Return the cbBindings to the top-level caller, along with the cbId
			return {cbId: cbId, bnd: cbBindings, s: cbBindingsStore};
		}

		var initialNs,
			allowArray = this == 1 ? 0 : 1, // If this == 1, this is a call from observeAndBind (doing binding of datalink expressions),
			// and tag.onArrayChange is not set to true. We don't bind arrayChange events in this scenario. Instead, {^{for}} and similar
			// do specific arrayChange binding to the tagCtx.args[0] value, in onAfterLink.
			// Note deliberately using this == 1, rather than this === 1 because of IE<10 bug - see jsviews/issues/237
			paths = slice.call(arguments),
			pth = paths[0];

		if (typeof pth === STRING) {
			initialNs = pth; // The first arg is a namespace, since it is a string
			paths.shift();
		}
		return innerObserve.apply(1, paths);
	};

	asyncBatch.wait = function() {
		var batch = this;
		batch._go = 1;
		setTimeout(function() {
			batch.trigger(true);
			batch._go = 0;
			batch.paths = {};
		});
	};

	$observable = function(ns, data, delay) {
		if (typeof ns !== STRING) {
			delay = data;
			data = ns;
			ns = "";
		}
		delay = delay === undefined ? $subSettingsAdvanced.asyncObserve : delay;
		var observable = $isArray(data)
			? new ArrayObservable(ns, data)
			: new ObjectObservable(ns, data);
		if (delay) {
			if (delay === true) {
				observable.async = true;
				delay = asyncBatch;
			}
			if (!delay.trigger) {
				if ($isArray(delay)) {
					delay.trigger = batchTrigger;
					delay.paths = {};
				} else {
					delay = undefined;
				}
			}
			observable._batch = delay;
		}
		return observable;
	};

	//========================== Initialize ==========================

	$.observable = $observable;
	$observable._fltr = function(path, object, parentObs, filter) {
		if (filter && $isFunction(filter) && (typeof object === OBJECT || $isFunction(object))
			? filter(path, object, parentObs)
			: true // TODO Consider supporting filter being a string or strings to do RegEx filtering based on key and/or path
		) {
			object = $isFunction(object)
				? object.set && object.call(parentObs[0]) // It is a getter/setter
				: object;
			return typeof object === OBJECT && object;
		}
	};

	$observable.Object = ObjectObservable;
	$observable.Array = ArrayObservable;
	$.observe = $observable.observe = $observe;
	$.unobserve = $observable.unobserve = $unobserve;
	$observable._apply = observe_apply;

	ObjectObservable.prototype = {
		_data: null,

		observeAll: $observeAll,
		unobserveAll: $unobserveAll,

		data: function() {
			return this._data;
		},

		setProperty: function(path, value, nonStrict, isCpfn) {
			path = path || "";
			var key, pair, parts, tempBatch,
				multi = typeof path !== STRING, // Hash of paths
				self = this,
				object = self._data,
				batch = self._batch;

			if (object) {
				if (multi) {
					nonStrict = value;
					if ($isArray(path)) {
						// This is the array format generated by serializeArray. However, this has the problem that it coerces types to string,
						// and does not provide simple support of convertTo and convertFrom functions.
						key = path.length;
						while (key--) {
							pair = path[key];
							self.setProperty(pair.name, pair.value, nonStrict === undefined || nonStrict); //If nonStrict not specified, default to true;
						}
					} else {
						if (!batch) {
							self._batch = tempBatch = [];
							tempBatch.trigger = batchTrigger;
							tempBatch.paths = {};
						}
						for (key in path) { // Object representation where property name is path and property value is value.
							self.setProperty(key, path[key], nonStrict);
						}
						if (tempBatch) {
							self._batch.trigger();
							self._batch = undefined;
						}
					}
				} else if (path !== $expando) {
					// Simple single property case.
					parts = path.split(/[.^]/);
					while (object && parts.length > 1) {
						key = parts.shift();
						object = key !== "__proto__" ? object[key] : undefined; // Avoid prototype pollution
					}
					if (object) {
						self._setProperty(object, parts[0], value, nonStrict, isCpfn);
					}
				}
			}
			return self;
		},

		removeProperty: function(path) {
			this.setProperty(path, remove);
			return this;
		},

		_setProperty: function(leaf, path, value, nonStrict, isCpfn) {
			var setter, getter, removeProp, eventArgs, view,
				property = path ? leaf[path] : leaf;
			if ($isFunction(property) && !$isFunction(value)) {
				if (isCpfn && !property.set) {
					return; // getter function with no setter defined. So will not trigger update
				}	else if (property.set) {
					// Case of property setter/getter - with convention that property is getter and property.set is setter
					view = leaf._vw // Case of JsViews 2-way data-linking to an observable context parameter, with a setter.
						// The view will be the this pointer for getter and setter. Note: this is the one scenario where path is "".
						|| leaf;
					getter = property;
					setter = getter.set === true ? getter : getter.set;
					property = getter.call(view); // get - only treated as getter if also a setter. Otherwise it is simply a property of type function.
					// See unit tests 'Can observe properties of type function'.
				}
			}
			if (property !== value || nonStrict && property != value) {
				// Optional non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc.
				// Date objects don't support != comparison. Treat as special case.
				if (!(property instanceof Date && value instanceof Date) || property > value || property < value) {
					if (setter) {
						setter.call(view, value);   // set
						value = getter.call(view);  // get updated value
					} else if (removeProp = value === remove) {
						if (property !== undefined) {
							delete leaf[path];
							value = undefined;
						} else {
							path = undefined; // If value was already undefined, don't trigger handler for removeProp
						}
					} else if (path) {
						leaf[path] = value;
					}
					if (path) {
						eventArgs = {change: "set", path: path, value: value, oldValue: property, remove: removeProp};
						if (leaf._ocp) {
							eventArgs.ctxPrm = leaf._key;
						}
						this._trigger(leaf, eventArgs);
					}
				}
			}
		},

		_trigger: function(target, eventArgs, force) {
			$subSettings._cchCt++; // Since we are making observable data change, increment cacheCounter to clear cached values and recompute
			var key, batch, previous,
				self = this;
			if ($.hasData(target)) {
				if (!force && (batch = self._batch)) {
					if (self.async && !batch._go) {
						batch.wait();
					}
					batch.push([self, target, eventArgs]);
					key = $data(target).obId + eventArgs.path;
					if (previous = batch.paths[key]) {
						batch[previous-1].skip = 1;
					}
					batch.paths[key] = batch.length;
				} else {
					$(target).triggerHandler(propertyChangeStr + (this._ns ? "." + /^\S+/.exec(this._ns)[0] : ""), eventArgs); // If white-space separated namespaces, use first one only
					eventArgs.oldValue = null; // Avoid holding on to stale objects
				}
			}
		}
	};

	ArrayObservable.prototype = {
		_data: null,

		observeAll: $observeAll,
		unobserveAll: $unobserveAll,

		data: function() {
			return this._data;
		},

		insert: function(index, data) {
			var _data = this._data;
			if (arguments.length === 1) {
				data = index;
				index = _data.length;
			}
			index = PARSEINT(index);
			if (index > -1) {
				data = $isArray(data) ? data : [data];
				// data can be a single item (including a null/undefined value) or an array of items.
				// Note the provided items are inserted without being cloned, as direct references to the provided objects

				if (data.length) {
					this._insert(index, data);
				}
			}
			return this;
		},

		_insert: function(index, data) {
			var _data = this._data,
				oldLength = _data.length;
			if (index > oldLength) {
				index = oldLength;
			}
			splice.apply(_data, [index, 0].concat(data));
			this._trigger({change: "insert", index: index, items: data}, oldLength);
		},

		remove: function(index, numToRemove) {
			var items,
				_data = this._data;

			if (index === undefined) {
				index = _data.length - 1;
			}

			index = PARSEINT(index);
			numToRemove = numToRemove ? PARSEINT(numToRemove) : numToRemove === 0 ? 0 : 1; // if null or undefined: remove 1
			if (numToRemove > 0 && index > -1) {
				items = _data.slice(index, index + numToRemove);
				if (numToRemove = items.length) {
					this._remove(index, numToRemove, items);
				}
			}
			return this;
		},

		_remove: function(index, numToRemove, items) {
			var _data = this._data,
				oldLength = _data.length;

			_data.splice(index, numToRemove);
			this._trigger({change: "remove", index: index, items: items}, oldLength);
		},

		move: function(oldIndex, newIndex, numToMove) {
			numToMove = numToMove ? PARSEINT(numToMove) : numToMove === 0 ? 0 : 1; // if null or undefined: move 1
			oldIndex = PARSEINT(oldIndex);
			newIndex = PARSEINT(newIndex);

			if (numToMove > 0 && oldIndex > -1 && newIndex > -1 && oldIndex !== newIndex) {
				this._move(oldIndex, newIndex, numToMove);
			}
			return this;
		},

		_move: function(oldIndex, newIndex, numToMove) {
			var items,
				_data = this._data,
				oldLength = _data.length,
				excess = oldIndex + numToMove - oldLength;
			if (excess > 0) {
				numToMove -= excess;
			}
			if (numToMove) {
				items = _data.splice(oldIndex, numToMove); // remove
				if (newIndex > _data.length) {
					newIndex = _data.length;
				}
				splice.apply(_data, [newIndex, 0].concat(items)); //re-insert
				if (newIndex !== oldIndex) {
					this._trigger({change: "move", oldIndex: oldIndex, index: newIndex, items: items}, oldLength);
				}
			}
		},

		refresh: function(newItems) {
			function insertAdded() {
				if (k) {
					self.insert(j-k, addedItems); // Not found in original array - so insert
					dataLength += k;
					i += k;
					k = 0;
					addedItems = [];
				}
			}

			// For refresh operation we iteratively step through the target array and sort by move/add/remove operations on the source array until they match
			var i, j, k, newItem, num,
				self = this,
				addedItems = [],
				data = self._data,
				oldItems = data.slice(),
				oldLength = data.length,
				dataLength = oldLength,
				newLength = newItems.length;
			self._srt = true; // Flag for sorting during refresh
			for (j=k=0; j<newLength; j++) {
				if ((newItem = newItems[j]) === data[j-k]) {
					insertAdded();
				} else {
					for (i=j-k; i<dataLength && newItem !== data[i]; i++) {}
					if (i<dataLength) {
						insertAdded();
						num = 0;
						while (num++ < newLength-i && newItems[j+num] === data[i+num]);
						self.move(i, j, num); // Found newItem in original array - so move it to new position
						j += num - 1;
					} else {
						k++;
						addedItems.push(newItem); // Not found in original array - so insert
					}
				}
			}
			insertAdded();
			if (dataLength > j) {
				self.remove(j, dataLength - j);
			}
			self._srt = undefined; // We have finished sort operations during refresh
			if (oldLength || newLength) {
				self._trigger({change: "refresh", oldItems: oldItems}, oldLength);
			}
			return self;
		},

		_trigger: function(eventArgs, oldLength, force) {
			$subSettings._cchCt++; // Since we are making observable data change, increment cacheCounter to clear cached values and recompute
			var length, _data, batch,
				self = this;
			if ($.hasData(_data = self._data)) {
				if (!force && (batch = self._batch)) {
					eventArgs._dly = true; // Delayed event (async or batch change)
					batch.push([self, eventArgs, oldLength]);
					if (self.async && !batch._go) {
						batch.wait();
					}
				} else {
					length = _data.length;
					_data = $([_data]);

					if (self._srt) {
						eventArgs.refresh = true; // We are sorting during refresh
					} else if (length !== oldLength) { // We have finished sort operations during refresh
						_data.triggerHandler(propertyChangeStr, {change: "set", path: "length", value: length, oldValue: oldLength});
					}
					_data.triggerHandler(arrayChangeStr + (self._ns ? "." + /^\S+/.exec(self._ns)[0] : ""), eventArgs); // If white-space separated namespaces, use first one only
				}
			}
		}
	};

	$eventSpecial[propertyChangeStr] = $eventSpecial[arrayChangeStr] = {
		// Register a jQuery special 'remove' event, to access the data associated with handlers being removed by jQuery.off().
		// We get data.cb._cId from the event handleObj and get the corresponding cbBindings hash from the cbBindingsStore,
		// then remove this object from that bindings hash - if the object does not have any other handlers associated with the same callback.
		remove: function (handleObj) {
			var cbBindings, found, events, l, data,
				evData = handleObj.data;
			if ((evData) && (evData.off = true, evData = evData.cb)) { //Set off = true as marker for disposed event
				// Get the cb._cId from handleObj.data.cb._cId
				if (cbBindings = cbBindingsStore[evData._cId]) {
					// There were bindings for this callback. If this was the last one, we'll remove it.
					events = $._data(this).events[handleObj.type];
					l = events.length;
					while (l-- && !found) {
						found = (data = events[l].data) && data.cb && data.cb._cId === evData._cId;
						// Found another one with same callback (though may be a different innerCallback)
					}
					if (!found) {
						if (--cbBindings.len) {
							delete cbBindings[$data(this).obId];
						} else {
							delete cbBindingsStore[evData._cId]; // If the cbBindings collection is empty we will remove it from the cbBindingsStore
						}
					}
				}
			}
		}
	};

//==========================
// dataMap with data-linking
//==========================

	$views.map = function(mapDef) {
		function Map(source, options, oldMapOrTarget, unbound) {
			var changing, updatedMap,
				map = this;
			if (map.src) {
				map.unmap(); // We are re-mapping a new source
			}
			if (options) {
				options.map = map;
			}
			if (typeof source === OBJECT || $isFunction(source)) {
				map.src = source;
				if (unbound) {
					map.tgt = mapDef.getTgt(source, options);
				} else {
					if (oldMapOrTarget) {
						map.tgt = oldMapOrTarget.tgt || $isArray(oldMapOrTarget) && oldMapOrTarget; // Can provide an existing map, or a target array to be used on new map
					}
					map.tgt = map.tgt || [];
					map.options = options || map.options;
					if (updatedMap = map.update()) {
						map = updatedMap; // If updating returns another map, then we can replace this one (so no need to bind it)
					} else {
						if (mapDef.obsSrc) {
							$observable(map.src).observeAll(map.obs = function(ev, eventArgs) {
								if (!changing && !eventArgs.refresh) {
									changing = true;
									mapDef.obsSrc(map, ev, eventArgs);
									changing = undefined;
								}
							}, map.srcFlt);
						}
						if (mapDef.obsTgt) {
							$observable(map.tgt).observeAll(map.obt = function(ev, eventArgs) {
								if (!changing && !map.tgt._updt) {
									changing = true;
									mapDef.obsTgt(map, ev, eventArgs);
									changing = undefined;
								}
							}, map.tgtFlt);
						}
					}
				}
			}
		}

		if ($isFunction(mapDef)) {
			// Simple map declared as function
			mapDef = {
				getTgt: mapDef
			};
		}

		if (mapDef.baseMap) {
			mapDef = $.extend({}, mapDef.baseMap, mapDef);
		}

		mapDef.map = function(source, options, oldMap, unbound) {
			return new Map(source, options, oldMap, unbound);
		};

		(Map.prototype = {
			srcFlt: mapDef.srcFlt || shallowFilter, // default to shallowFilter
			tgtFlt: mapDef.tgtFlt || shallowFilter,
			update: function(options) {
				var oldMap, newMap,
					map = this,
					tgt = map.tgt;
				if (!tgt._updt) {
					tgt._updt = true;
					oldMap = map.options && map.options.map;
					$observable(tgt).refresh(mapDef.getTgt(map.src, map.options = options || map.options));
					tgt._updt = false;
					newMap = map.options && map.options.map;
					if (newMap && oldMap !== newMap) {
						return newMap;
					}
				}
			},
			observe: function(deps, linkCtx) { // Listen to observable changes of mapProps, and call map.update when change happens
				var map = this,
					options = map.options;
				if (map.obmp) {
					// There is a previous handler observing the mapProps
					$unobserve(map.obmp);
				}
				map.obmp = function() {
					// Observe changes in the mapProps ("filter", "sort", "reverse", "start", "end")
					var newTagCtx = linkCtx.fn(linkCtx.data, linkCtx.view, $sub)[options.index]; // Updated tagCtx props and args
					$.extend(options.props, newTagCtx.props); // Update props to new values
					options.args = newTagCtx.args; // Update args to new values
					map.update(); // Update the map target array, based on new mapProp values
				};
				$observable._apply(1, linkCtx.data, dependsPaths(deps, linkCtx.tag, map.obmp), map.obmp, linkCtx._ctxCb);
			},
			unmap: function() {
				var map = this;
				if (map.src && map.obs) {
					$observable(map.src).unobserveAll(map.obs, map.srcFlt);
				}
				if (map.tgt && map.obt) {
					$observable(map.tgt).unobserveAll(map.obt, map.tgtFlt);
				}
				if (map.obmp) {
					$unobserve(map.obmp);
				}
				map.src = undefined;
			},
			map: Map,
			_def: mapDef
		}).constructor = Map;

		return mapDef;
	};

	$sub.advSet = function() { // refresh advanced settings
		$sub = this; // If JsRender is loaded after jquery.observable, then this sets $sub to $.views.sub
		$subSettingsAdvanced = $subSettings.advanced;
		global._jsv = $subSettingsAdvanced._jsv
			? { // create global _jsv, for accessing views, etc
					cbBindings: cbBindingsStore
				}
			: undefined; // In IE8 cannot do delete global._jsv
	};
	$sub._dp = dependsPaths;
	$sub._gck = getCbKey;
	$sub._obs = $observe;
	$subSettings._cchCt = 0; // Counter for clearing cached values for getCache() call
	$subSettingsAdvanced = $subSettings.advanced = $subSettingsAdvanced || {
		useViews: false,
		_jsv: false // For global access to JsViews store
	};
}

return $;
}, window));
