Source: AutoBind.js

"use strict";

/**
 * Provides data-binding functionality to ForerunnerDB. Allows collections
 * and views to link to selectors and automatically generate DOM elements
 * from jsViews (jsRender) templates.
 * @class AutoBind
 */

var Shared = window.ForerunnerDB.shared,
	AutoBind = {},
	jsviews;

Shared.addModule('AutoBind', AutoBind);

/**
 * Extends the Collection class with new binding capabilities.
 * @extends Collection
 * @param {Collection} Module The Collection class module.
 * @private
 */
AutoBind.extendCollection = function (Module) {
	var superInit = Module.prototype.init,
		superDataReplace = Module.prototype._dataReplace,
		superDataInsertIndex = Module.prototype._dataInsertAtIndex,
		superDataRemoveIndex = Module.prototype._dataRemoveAtIndex,
		superUpdateProperty = Module.prototype._updateProperty,
		superUpdateIncrement = Module.prototype._updateIncrement,
		superUpdateSpliceMove = Module.prototype._updateSpliceMove,
		superUpdateSplicePush = Module.prototype._updateSplicePush,
		superUpdatePush = Module.prototype._updatePush,
		superUpdatePull = Module.prototype._updatePull,
		superUpdateMultiply = Module.prototype._updateMultiply,
		superUpdateRename = Module.prototype._updateRename,
		superUpdateOverwrite = Module.prototype._updateOverwrite,
		superUpdateUnset = Module.prototype._updateUnset,
		superUpdatePop = Module.prototype._updatePop,
		superDrop = Module.prototype.drop;

	Module.prototype.init = function () {
		this._linked = 0;
		superInit.apply(this, arguments);
	};

	/**
	 * Checks if the instance is data-bound to any DOM elements.
	 * @func isLinked
	 * @memberof Collection
	 * @returns {Boolean} True if linked, false if not.
	 */
	Module.prototype.isLinked = function () {
		return Boolean(this._linked);
	};

	/**
	 * Creates a link to the DOM between the collection data and the elements
	 * in the passed output selector. When new elements are needed or changes
	 * occur the passed templateSelector is used to get the template that is
	 * output to the DOM.
	 * @func link
	 * @memberof Collection
	 * @param outputTargetSelector
	 * @param templateSelector
	 * @param {Object=} options Optional extra options.
	 * @see unlink
	 */
	Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
		if (window.jQuery) {
			// Make sure we have a data-binding store object to use
			this._links = this._links || new ForerunnerDB.shared.modules.Collection(this.instanceIdentifier() + '_links'); //jshint ignore:line
			if (!this._linked) { this._linked = 0; }

			var templateId,
				templateHtml,
				targetSelector;

			if (outputTargetSelector && templateSelector) {
				targetSelector = typeof outputTargetSelector === 'string' ? outputTargetSelector : outputTargetSelector.selector;

				if (templateSelector && typeof templateSelector === 'object') {
					// Our second argument is an object, let's inspect
					if (templateSelector.template && typeof templateSelector.template === 'string') {
						// The template has been given to us as a string
						templateId = this.objectId(templateSelector.template);
						templateHtml = templateSelector.template;
					}
				} else {
					templateId = templateSelector;
				}

				if (!this._links.count({
						target: targetSelector,
						template: templateSelector
					})) {
					if (window.jQuery(outputTargetSelector).length) {
						// Ensure the template is in memory and if not, try to get it
						if (!window.jQuery.templates[templateId]) {
							if (!templateHtml) {
								// Grab the template
								var template = window.jQuery(templateSelector);
								if (template.length) {
									templateHtml = window.jQuery(template[0]).html();
								} else {
									throw('ForerunnerDB.AutoBind : ' + this.instanceIdentifier() + ' Unable to bind to target because template "' + templateSelector + '" does not exist');
								}
							}

							window.jQuery.views.templates(templateId, templateHtml);
						}

						if (options && options.$wrap) {
							var wrapper,
								tmpObj,
								doc;

							if (!options.$wrapIn) {
								// Create the data binding wrapped in an object
								wrapper = {};
								wrapper[options.$wrap] = this._data;
							} else if (options.$wrapIn instanceof window.ForerunnerDB.shared.modules.Document) {
								// Document-based wrapper
								// Grab the document instance
								doc = options.$wrapIn;

								// Get the current data by reference
								tmpObj = doc._data;

								// Set the wrapper property to the referenced data
								// of this collection / view
								tmpObj[options.$wrap] = this._data;

								// Set the data back into the document by reference
								doc.setData(tmpObj, {$decouple: false});

								// Set it to data-bound mode
								doc._linked = 1;

								// Provide the document data as wrapper data
								wrapper = doc._data;
							} else if (typeof options.$wrapIn === 'object') {
								wrapper = options.$wrapIn;
								wrapper[options.$wrap] = this._data;
							} else {
								throw('ForerunnerDB.AutoBind: ' + this.instanceIdentifier() + ' Unable to use passed $wrapIn option, should be either a ForerunnerDB Document instance or a JavaScript object!');
							}

							if (this.debug()) {
								console.log('ForerunnerDB.AutoBind: ' + this.instanceIdentifier() + ' Binding data wrapped in an object with field called "' + options.$wrap + '" to output target: ' + outputTargetSelector);
							}

							window.jQuery.templates[templateId].link(outputTargetSelector, wrapper);
						} else {
							// Create the data binding
							window.jQuery.templates[templateId].link(outputTargetSelector, this._data);
						}

						this._links.insert({
							target: targetSelector,
							template: templateSelector,
							templateId: templateId
						});

						// Set the linked flag
						this._linked++;

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Binding to output target: ' + outputTargetSelector);
						}

						return this;
					} else {
						throw(this.logIdentifier() + ' Cannot bind collection to target selector "' + outputTargetSelector + '" because it does not exist in the DOM!');
					}
				}

				throw(this.logIdentifier() + ' Attempt to bind a duplicate link from collection to the target: ' + outputTargetSelector + ' with the template: ' + templateId);
			} else {
				throw(this.logIdentifier() + ' Cannot bind without a target / template!');
			}
		} else {
			throw(this.logIdentifier() + ' Cannot data-bind without jQuery. Please add jQuery to your page!');
		}
	};

	/**
	 * Removes a link to the DOM between the collection data and the elements
	 * in the passed output selector that was created using the link() method.
	 * @func unlink
	 * @memberof Collection
	 * @param outputTargetSelector
	 * @param templateSelector
	 */
	Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
		if (this._links) {
			if (window.jQuery) {
				var templateId,
					targetSelector,
					link,
					linkArr,
					i;

				if (outputTargetSelector && templateSelector) {
					targetSelector = typeof outputTargetSelector === 'string' ? outputTargetSelector : outputTargetSelector.selector;

					if (templateSelector && typeof templateSelector === 'object') {
						// Our second argument is an object, let's inspect
						if (templateSelector.template && typeof templateSelector.template === 'string') {
							// The template has been given to us as a string
							templateId = this.objectId(templateSelector.template);
						}
					} else {
						templateId = templateSelector;
					}


					link = this._links.findOne({
						target: targetSelector,
						template: templateSelector
					});

					if (link) {
						// Remove the data binding
						window.jQuery.templates[templateId].unlink(outputTargetSelector);

						// Remove link from store
						this._links.remove(link);

						// Set the linked count
						this._linked--;

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Removed binding document to target: ' + outputTargetSelector);
						}

						return this;
					}

					if (this.debug()) {
						console.log(this.logIdentifier() + ' Cannot remove binding as it does not exist to the target: ' + outputTargetSelector + ' with the template: ' + templateSelector);
					}
				} else {
					// No parameters passed, unlink all from this module
					linkArr = this._links.find();
					for (i = 0; i < linkArr.length; i++) {
						link = linkArr[i];
						window.jQuery.templates[link.templateId].unlink(jQuery(link.target));

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Removed binding to output target: ' + link.target);
						}
					}

					this._links.remove();
					this._linked = 0;
				}
			} else {
				throw(this.logIdentifier() + ' Cannot data-bind without jQuery. Please add jQuery to your page!');
			}
		}

		return this;
	};

	/**
	 * Get the data-bound links this module is using.
	 * @returns {*}
	 */
	Module.prototype.links = function () {
		return this._links;
	};

	Module.prototype._dataReplace = function (data) {
		if (this._linked) {
			// Remove all items
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Replacing some data in document');
			}
			window.jQuery.observable(this._data).refresh(data);
		} else {
			superDataReplace.apply(this, arguments);
		}
	};

	Module.prototype._dataInsertAtIndex = function (index, doc) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Inserting some data');
			}
			window.jQuery.observable(this._data).insert(index, doc);
		} else {
			superDataInsertIndex.apply(this, arguments);
		}
	};

	Module.prototype._dataRemoveAtIndex = function (index) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Removing some data');
			}
			window.jQuery.observable(this._data).remove(index);
		} else {
			superDataRemoveIndex.apply(this, arguments);
		}
	};

	/**
	 * Updates a property on an object depending on if the collection is
	 * currently running data-binding or not.
	 * @param {Object} doc The object whose property is to be updated.
	 * @param {String} prop The property to update.
	 * @param {*} val The new value of the property.
	 * @private
	 */
	Module.prototype._updateProperty = function (doc, prop, val) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Setting document property "' + prop + '"');
			}
			window.jQuery.observable(doc).setProperty(prop, val);
		} else {
			superUpdateProperty.apply(this, arguments);
		}
	};

	/**
	 * Increments a value for a property on a document by the passed number.
	 * @param {Object} doc The document to modify.
	 * @param {String} prop The property to modify.
	 * @param {Number} val The amount to increment by.
	 * @private
	 */
	Module.prototype._updateIncrement = function (doc, prop, val) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Incrementing document property "' + prop + '"');
			}
			window.jQuery.observable(doc).setProperty(prop, doc[prop] + val);
		} else {
			superUpdateIncrement.apply(this, arguments);
		}
	};

	/**
	 * Changes the index of an item in the passed array.
	 * @param {Array} arr The array to modify.
	 * @param {Number} indexFrom The index to move the item from.
	 * @param {Number} indexTo The index to move the item to.
	 * @private
	 */
	Module.prototype._updateSpliceMove = function (arr, indexFrom, indexTo) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Moving document array index from "' + indexFrom + '" to "' + indexTo + '"');
			}
			window.jQuery.observable(arr).move(indexFrom, indexTo);
		} else {
			superUpdateSpliceMove.apply(this, arguments);
		}
	};

	/**
	 * Inserts an item into the passed array at the specified index.
	 * @param {Array} arr The array to insert into.
	 * @param {Number} index The index to insert at.
	 * @param {Object} doc The document to insert.
	 * @private
	 */
	Module.prototype._updateSplicePush = function (arr, index, doc) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Pushing item into document sub-array');
			}
			if (arr.length > index) {
				window.jQuery.observable(arr).insert(index, doc);
			} else {
				window.jQuery.observable(arr).insert(doc);
			}
		} else {
			superUpdateSplicePush.apply(this, arguments);
		}
	};

	/**
	 * Inserts an item at the end of an array.
	 * @param {Array} arr The array to insert the item into.
	 * @param {Object} doc The document to insert.
	 * @private
	 */
	Module.prototype._updatePush = function (arr, doc) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Pushing item into document sub-array');
			}
			window.jQuery.observable(arr).insert(doc);
		} else {
			superUpdatePush.apply(this, arguments);
		}
	};

	/**
	 * Removes an item from the passed array.
	 * @param {Array} arr The array to modify.
	 * @param {Number} index The index of the item in the array to remove.
	 * @private
	 */
	Module.prototype._updatePull = function (arr, index) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Pulling item from document sub-array');
			}
			window.jQuery.observable(arr).remove(index);
		} else {
			superUpdatePull.apply(this, arguments);
		}
	};

	/**
	 * Multiplies a value for a property on a document by the passed number.
	 * @param {Object} doc The document to modify.
	 * @param {String} prop The property to modify.
	 * @param {Number} val The amount to multiply by.
	 * @private
	 */
	Module.prototype._updateMultiply = function (doc, prop, val) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Multiplying value');
			}
			window.jQuery.observable(doc).setProperty(prop, doc[prop] * val);
		} else {
			superUpdateMultiply.apply(this, arguments);
		}
	};

	/**
	 * Renames a property on a document to the passed property.
	 * @param {Object} doc The document to modify.
	 * @param {String} prop The property to rename.
	 * @param {Number} val The new property name.
	 * @private
	 */
	Module.prototype._updateRename = function (doc, prop, val) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Renaming property "' + prop + '" to "' + val + '" on document');
			}
			window.jQuery.observable(doc).setProperty(val, doc[prop]);
			window.jQuery.observable(doc).removeProperty(prop);
		} else {
			superUpdateRename.apply(this, arguments);
		}
	};

	/**
	 * Overwrites a property on a document to the passed value.
	 * @param {Object} doc The document to modify.
	 * @param {String} prop The property to delete.
	 * @param {*} val The new value to set the property to.
	 * @private
	 */
	Module.prototype._updateOverwrite = function (doc, prop, val) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Setting document property "' + prop + '"');
			}
			window.jQuery.observable(doc).setProperty(prop, val);
		} else {
			superUpdateOverwrite.apply(this, arguments);
		}
	};

	/**
	 * Deletes a property on a document.
	 * @param {Object} doc The document to modify.
	 * @param {String} prop The property to delete.
	 * @private
	 */
	Module.prototype._updateUnset = function (doc, prop) {
		if (this._linked) {
			if (this.debug()) {
				console.log(this.logIdentifier() + ' Removing property "' + prop + '" from document');
			}
			window.jQuery.observable(doc).removeProperty(prop);
		} else {
			superUpdateUnset.apply(this, arguments);
		}
	};

	/**
	 * Pops an item from the array stack.
	 * @param {Object} doc The document to modify.
	 * @param {Number=} val Optional, if set to 1 will pop, if set to -1 will shift.
	 * @return {Boolean}
	 * @private
	 */
	Module.prototype._updatePop = function (doc, val) {
		var index,
			updated = false,
			i;

		if (this._linked) {
			if (doc.length > 0) {
				if (this.debug()) {
					console.log(this.logIdentifier() + ' Popping item from sub-array in document');
				}

				if (val > 0) {
					for (i = 0; i < val; i++) {
						index = doc.length - 1;
						if (index > -1) {
							window.jQuery.observable(doc).remove(index);
						}
					}
					updated = true;
				} else if (val < 0) {
					index = 0;
					for (i = 0; i > val; i--) {
						if (doc.length) {
							window.jQuery.observable(doc).remove(index);
						}
					}
					updated = true;
				}
			}
		} else {
			updated = superUpdatePop.apply(this, arguments);
		}

		return updated;
	};

	Module.prototype.drop = function () {
		// Unlink all linked data
		if (this._linked) {
			var linkArr = this._links.find(),
				i;

			for (i = 0; i < linkArr.length; i++) {
				this.unlink(jQuery(linkArr[i].target), linkArr[i].template);
			}
		}

		if (this._links) {
			this._links.drop();
			delete this._links;
		}

		return superDrop.apply(this, arguments);
	};
};

/**
 * Extends the View class with new binding capabilities.
 * @extends View
 * @param {View} Module The View class module.
 * @private
 */
AutoBind.extendView = function (Module) {
	var superInit = Module.prototype.init;

	Module.prototype.init = function () {
		this._linked = 0;
		superInit.apply(this, arguments);
	};

	/**
	 * Checks if the instance is data-bound to any DOM elements.
	 * @func isLinked
	 * @memberof View
	 * @returns {Boolean} True if linked, false if not.
	 */
	Module.prototype.isLinked = function () {
		return this.publicData().isLinked();
	};

	/**
	 * Data-binds the view data to the elements matched by the passed selector.
	 * @func link
	 * @memberof View
	 * @param {String} outputTargetSelector The jQuery element selector to select the element
	 * into which the data-bound rendered items will be placed. All existing HTML will be
	 * removed from this element.
	 * @param {String|Object} templateSelector This can either be a jQuery selector identifying
	 * which template element to get the template HTML from that each item in the view's data
	 * will use when rendering to the screen, or you can pass an object with a template key
	 * containing a string that represents the HTML template such as:
	 *     { template: '<div>{{:name}}</div>' }
	 * @param {Object=} options An options object.wd
	 * @returns {View}
	 * @see unlink
	 */
	Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
		var publicData = this.publicData();

		publicData.link(outputTargetSelector, templateSelector, options);

		return this;
	};

	/**
	 * Removes a previously set-up data-binding via the link() method.
	 * @func unlink
	 * @memberof View
	 * @param {Selector} outputTargetSelector The jQuery target selector.
	 * @param {Selector} templateSelector The jQuery template selector.
	 * @see link
	 * @returns {View}
	 */
	Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
		var publicData = this.publicData();

		publicData.unlink(outputTargetSelector, templateSelector);

		return this;
	};

	/**
	 * Get the data-bound links this module is using.
	 * @returns {*}
	 */
	Module.prototype.links = function () {
		return this.publicData().links();
	};
};

/**
 * Extends the Overview class with new binding capabilities.
 * @extends Overview
 * @param {Overview} Module The Overview class module.
 * @private
 */
AutoBind.extendOverview = function (Module) {
	/**
	 * Checks if the instance is data-bound to any DOM elements.
	 * @func isLinked
	 * @memberof Overview
	 * @returns {Boolean} True if linked, false if not.
	 */
	Module.prototype.isLinked = function () {
		return this.data().isLinked();
	};

	/**
	 * Creates a link to the DOM between the overview data and the elements
	 * in the passed output selector. When new elements are needed or changes
	 * occur the passed templateSelector is used to get the template that is
	 * output to the DOM.
	 * @func link
	 * @memberof Overview
	 * @param outputTargetSelector
	 * @param templateSelector
	 * @param {Object=} options An options object.
	 * @see unlink
	 */
	Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
		this._data.link.apply(this._data, arguments);
		this._refresh();
	};

	/**
	 * Removes a link to the DOM between the overview data and the elements
	 * in the passed output selector that was created using the link() method.
	 * @func unlink
	 * @memberof Overview
	 * @param outputTargetSelector
	 * @param templateSelector
	 * @see link
	 */
	Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
		this._data.unlink.apply(this._data, arguments);
		this._refresh();
	};

	/**
	 * Get the data-bound links this module is using.
	 * @returns {*}
	 */
	Module.prototype.links = function () {
		return this._data.links();
	};
};

/**
 * Extends the Document class with new binding capabilities.
 * @extends Document
 * @param {Document} Module The Document class module.
 * @private
 */
AutoBind.extendDocument = function (Module) {
	var superUpdatePop = Module.prototype._updatePop;

	/**
	 * Checks if the instance is data-bound to any DOM elements.
	 * @func isLinked
	 * @memberof Document
	 * @returns {Boolean} True if linked, false if not.
	 */
	Module.prototype.isLinked = function () {
		return Boolean(this._linked);
	};

	/**
	 * Creates a link to the DOM between the document data and the elements
	 * in the passed output selector. When new elements are needed or changes
	 * occur the passed templateSelector is used to get the template that is
	 * output to the DOM.
	 * @func link
	 * @memberof Document
	 * @param outputTargetSelector
	 * @param templateSelector
	 * @param {Object=} options An options object.
	 * @see unlink
	 */
	Module.prototype.link = function (outputTargetSelector, templateSelector, options) {
		if (window.jQuery) {
			// Make sure we have a data-binding store object to use
			this._links = this._links || new ForerunnerDB.shared.modules.Collection(this.instanceIdentifier() + '_links'); //jshint ignore:line
			if (!this._linked) { this._linked = 0; }

			if (outputTargetSelector && templateSelector) {
				var templateId,
					templateHtml,
					targetSelector = typeof outputTargetSelector === 'string' ? outputTargetSelector : outputTargetSelector.selector;

				if (templateSelector && typeof templateSelector === 'object') {
					// Our second argument is an object, let's inspect
					if (templateSelector.template && typeof templateSelector.template === 'string') {
						// The template has been given to us as a string
						templateId = this.objectId(templateSelector.template);
						templateHtml = templateSelector.template;
					}
				} else {
					templateId = templateSelector;
				}

				if (!this._links.count({
						target: targetSelector,
						template: templateSelector
					})) {
					if (window.jQuery(outputTargetSelector).length) {
						// Ensure the template is in memory and if not, try to get it
						if (!window.jQuery.templates[templateId]) {
							if (!templateHtml) {
								// Grab the template
								var template = window.jQuery(templateSelector);
								if (template.length) {
									templateHtml = window.jQuery(template[0]).html();
								} else {
									throw(this.logIdentifier() + ' Unable to bind document to target because template does not exist: ' + templateSelector);
								}
							}

							window.jQuery.views.templates(templateId, templateHtml);
						}

						if (options && options.$wrap) {
							// Create the data binding wrapped in an object
							var wrapper,
								tmpObj,
								doc;

							if (!options.$wrapIn) {
								// Create the data binding wrapped in an object
								wrapper = {};
								wrapper[options.$wrap] = this._data;
							} else if (options.$wrapIn instanceof Document) {
								// Document-based wrapper
								// Grab the document instance
								doc = options.$wrapIn;

								// Get the current data by reference
								tmpObj = doc._data;

								// Set the wrapper property to the referenced data
								// of this collection / view
								tmpObj[options.$wrap] = this._data;

								// Set the data back into the document by reference
								doc.setData(tmpObj, {$decouple: false});

								// Set it to data-bound mode
								doc._linked = 1;

								// Provide the document data as wrapper data
								wrapper = options.$wrap._data;
							}

							window.jQuery.templates[templateId].link(outputTargetSelector, wrapper);
						} else {
							// Create the data binding
							window.jQuery.templates[templateId].link(outputTargetSelector, this._data);
						}

						this._links.insert({
							target: targetSelector,
							template: templateSelector,
							templateId: templateId
						});

						// Set the linked flag
						this._linked++;

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Added binding to target: ' + outputTargetSelector);
						}

						return this;
					} else {
						throw(this.logIdentifier() + ' Cannot bind document to target "' + outputTargetSelector + '" because it does not exist in the DOM!');
					}
				}

				throw(this.logIdentifier() + ' Cannot create a duplicate link from document to the target: ' + outputTargetSelector + ' with the template: ' + templateId);
			} else {
				throw(this.logIdentifier() + ' Cannot bind without a target / template!');
			}
		} else {
			throw(this.logIdentifier() + ' Cannot data-bind without jQuery. Please add jQuery to your page!');
		}
	};

	/**
	 * Removes a link to the DOM between the document data and the elements
	 * in the passed output selector that was created using the link() method.
	 * @func unlink
	 * @memberof Document
	 * @param outputTargetSelector
	 * @param templateSelector
	 * @see link
	 */
	Module.prototype.unlink = function (outputTargetSelector, templateSelector) {
		if (this._links) {
			if (window.jQuery) {
				var templateId,
					targetSelector,
					link,
					linkArr,
					i;

				if (outputTargetSelector && templateSelector) {
					targetSelector = typeof outputTargetSelector === 'string' ? outputTargetSelector : outputTargetSelector.selector;

					if (templateSelector && typeof templateSelector === 'object') {
						// Our second argument is an object, let's inspect
						if (templateSelector.template && typeof templateSelector.template === 'string') {
							// The template has been given to us as a string
							templateId = this.objectId(templateSelector.template);
						}
					} else {
						templateId = templateSelector;
					}

					link = this._links.findOne({
						target: targetSelector,
						template: templateSelector
					});

					if (link) {
						// Remove the data binding
						window.jQuery.templates[templateId].unlink(outputTargetSelector);

						// Remove link from store
						this._links.remove(link);

						// Set the linked count
						this._linked--;

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Removed binding document to target: ' + outputTargetSelector);
						}

						return this;
					}

					if (this.debug()) {
						console.log(this.logIdentifier() + ' Cannot remove link from document, one does not exist to the target: ' + outputTargetSelector + ' with the template: ' + templateSelector);
					}
				} else {
					// No parameters passed, unlink all from this module
					linkArr = this._links.find();
					for (i = 0; i < linkArr.length; i++) {
						link = linkArr[i];
						window.jQuery.templates[link.templateId].unlink(jQuery(link.target));

						if (this.debug()) {
							console.log(this.logIdentifier() + ' Removed binding to output target: ' + link.target);
						}
					}

					this._links.remove();
					this._linked = 0;
				}
			} else {
				throw(this.logIdentifier() + ' Cannot data-bind without jQuery. Please add jQuery to your page!');
			}
		}

		return this;
	};

	/**
	 * Get the data-bound links this module is using.
	 * @returns {*}
	 */
	Module.prototype.links = function () {
		return this._links;
	};

	/**
	 * Pops an item from the array stack.
	 * @param {Object} doc The document to modify.
	 * @param {Number=} val Optional, if set to 1 will pop, if set to -1 will shift.
	 * @return {Boolean}
	 * @private
	 */
	Module.prototype._updatePop = function (doc, val) {
		var index,
			updated = false,
			i;

		if (this._linked) {
			if (doc.length > 0) {
				if (this.debug()) {
					console.log(this.logIdentifier() + ' Popping item from sub-array');
				}

				if (val > 0) {
					for (i = 0; i < val; i++) {
						index = doc.length - 1;
						if (index > -1) {
							window.jQuery.observable(doc).remove(index);
						}
					}
					updated = true;
				} else if (val < 0) {
					index = 0;
					for (i = 0; i > val; i--) {
						if (doc.length) {
							window.jQuery.observable(doc).remove(index);
						}
					}
					updated = true;
				}
			}
		} else {
			updated = superUpdatePop.apply(this, arguments);
		}

		return updated;
	};
};

// Check that jQuery exists before doing anything else
if (typeof window.jQuery !== 'undefined') {
	// Load jsViews
	jsviews = require('../lib/vendor/jsviews');

	// Ensure jsviews is registered
	if (typeof window.jQuery.views !== 'undefined') {
		// Define modules that we wish to work on
		var modules = ['Collection', 'View', 'Overview', 'Document'],
			moduleIndex,
			moduleFinished = function (name, module) {
				if (AutoBind['extend' + name]) {
					AutoBind['extend' + name](module);
				}
			};

		// Extend modules that are finished loading
		for (moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) {
			Shared.moduleFinished(modules[moduleIndex], moduleFinished);
		}

		Shared.finishModule('AutoBind');
	} else {
		throw('ForerunnerDB.AutoBind : Plugin cannot continue because jsViews is not loaded. Check your error log for url errors; it should have automatically loaded with this plugin.');
	}
} else {
	throw('ForerunnerDB.AutoBind : Cannot data-bind without jQuery. Please add jQuery to your page!');
}

Shared.finishModule('AutoBind');
module.exports = AutoBind;