Source: OldView.Bind.js

"use strict";

// Grab the view class
var Shared,
	Core,
	OldView,
	OldViewInit;

Shared = require('./Shared');
Core = Shared.modules.Core;
OldView = Shared.modules.OldView;
OldViewInit = OldView.prototype.init;

OldView.prototype.init = function () {
	var self = this;

	this._binds = [];
	this._renderStart = 0;
	this._renderEnd = 0;

	this._deferQueue = {
		insert: [],
		update: [],
		remove: [],
		upsert: [],
		_bindInsert: [],
		_bindUpdate: [],
		_bindRemove: [],
		_bindUpsert: []
	};

	this._deferThreshold = {
		insert: 100,
		update: 100,
		remove: 100,
		upsert: 100,
		_bindInsert: 100,
		_bindUpdate: 100,
		_bindRemove: 100,
		_bindUpsert: 100
	};

	this._deferTime = {
		insert: 100,
		update: 1,
		remove: 1,
		upsert: 1,
		_bindInsert: 100,
		_bindUpdate: 1,
		_bindRemove: 1,
		_bindUpsert: 1
	};

	OldViewInit.apply(this, arguments);

	// Hook view events to update binds
	this.on('insert', function (successArr, failArr) {
		self._bindEvent('insert', successArr, failArr);
	});

	this.on('update', function (successArr, failArr) {
		self._bindEvent('update', successArr, failArr);
	});

	this.on('remove', function (successArr, failArr) {
		self._bindEvent('remove', successArr, failArr);
	});

	this.on('change', self._bindChange);
};

/**
 * Binds a selector to the insert, update and delete events of a particular
 * view and keeps the selector in sync so that updates are reflected on the
 * web page in real-time.
 *
 * @param {String} selector The jQuery selector string to get target elements.
 * @param {Object} options The options object.
 */
OldView.prototype.bind = function (selector, options) {
	if (options && options.template) {
		this._binds[selector] = options;
	} else {
		throw('ForerunnerDB.OldView "' + this.name() + '": Cannot bind data to element, missing options information!');
	}

	return this;
};

/**
 * Un-binds a selector from the view changes.
 * @param {String} selector The jQuery selector string to identify the bind to remove.
 * @returns {Collection}
 */
OldView.prototype.unBind = function (selector) {
	delete this._binds[selector];
	return this;
};

/**
 * Returns true if the selector is bound to the view.
 * @param {String} selector The jQuery selector string to identify the bind to check for.
 * @returns {boolean}
 */
OldView.prototype.isBound = function (selector) {
	return Boolean(this._binds[selector]);
};

/**
 * Sorts items in the DOM based on the bind settings and the passed item array.
 * @param {String} selector The jQuery selector of the bind container.
 * @param {Array} itemArr The array of items used to determine the order the DOM
 * elements should be in based on the order they are in, in the array.
 */
OldView.prototype.bindSortDom = function (selector, itemArr) {
	var container = window.jQuery(selector),
		arrIndex,
		arrItem,
		domItem;

	if (this.debug()) {
		console.log('ForerunnerDB.OldView.Bind: Sorting data in DOM...', itemArr);
	}

	for (arrIndex = 0; arrIndex < itemArr.length; arrIndex++) {
		arrItem = itemArr[arrIndex];

		// Now we've done our inserts into the DOM, let's ensure
		// they are still ordered correctly
		domItem = container.find('#' + arrItem[this._primaryKey]);

		if (domItem.length) {
			if (arrIndex === 0) {
				if (this.debug()) {
					console.log('ForerunnerDB.OldView.Bind: Sort, moving to index 0...', domItem);
				}
				container.prepend(domItem);
			} else {
				if (this.debug()) {
					console.log('ForerunnerDB.OldView.Bind: Sort, moving to index ' + arrIndex + '...', domItem);
				}
				domItem.insertAfter(container.children(':eq(' + (arrIndex - 1) + ')'));
			}
		} else {
			if (this.debug()) {
				console.log('ForerunnerDB.OldView.Bind: Warning, element for array item not found!', arrItem);
			}
		}
	}
};

OldView.prototype.bindRefresh = function (obj) {
	var binds = this._binds,
		bindKey,
		bind;

	if (!obj) {
		// Grab current data
		obj = {
			data: this.find()
		};
	}

	for (bindKey in binds) {
		if (binds.hasOwnProperty(bindKey)) {
			bind = binds[bindKey];

			if (this.debug()) { console.log('ForerunnerDB.OldView.Bind: Sorting DOM...'); }
			this.bindSortDom(bindKey, obj.data);

			if (bind.afterOperation) {
				bind.afterOperation();
			}

			if (bind.refresh) {
				bind.refresh();
			}
		}
	}
};

/**
 * Renders a bind view data to the DOM.
 * @param {String} bindSelector The jQuery selector string to use to identify
 * the bind target. Must match the selector used when defining the original bind.
 * @param {Function=} domHandler If specified, this handler method will be called
 * with the final HTML for the view instead of the DB handling the DOM insertion.
 */
OldView.prototype.bindRender = function (bindSelector, domHandler) {
	// Check the bind exists
	var bind = this._binds[bindSelector],
		domTarget = window.jQuery(bindSelector),
		allData,
		dataItem,
		itemHtml,
		finalHtml = window.jQuery('<ul></ul>'),
		bindCallback,
		i;

	if (bind) {
		allData = this._data.find();

		bindCallback = function (itemHtml) {
			finalHtml.append(itemHtml);
		};

		// Loop all items and add them to the screen
		for (i = 0; i < allData.length; i++) {
			dataItem = allData[i];

			itemHtml = bind.template(dataItem, bindCallback);
		}

		if (!domHandler) {
			domTarget.append(finalHtml.html());
		} else {
			domHandler(bindSelector, finalHtml.html());
		}
	}
};

OldView.prototype.processQueue = function (type, callback) {
	var queue = this._deferQueue[type],
		deferThreshold = this._deferThreshold[type],
		deferTime = this._deferTime[type];

	if (queue.length) {
		var self = this,
			dataArr;

		// Process items up to the threshold
		if (queue.length) {
			if (queue.length > deferThreshold) {
				// Grab items up to the threshold value
				dataArr = queue.splice(0, deferThreshold);
			} else {
				// Grab all the remaining items
				dataArr = queue.splice(0, queue.length);
			}

			this._bindEvent(type, dataArr, []);
		}

		// Queue another process
		setTimeout(function () {
			self.processQueue(type, callback);
		}, deferTime);
	} else {
		if (callback) { callback(); }
		this.emit('bindQueueComplete');
	}
};

OldView.prototype._bindEvent = function (type, successArr, failArr) {
	/*var queue = this._deferQueue[type],
		deferThreshold = this._deferThreshold[type],
		deferTime = this._deferTime[type];*/

	var binds = this._binds,
		unfilteredDataSet = this.find({}),
		filteredDataSet,
		bindKey;

	// Check if the number of inserts is greater than the defer threshold
	/*if (successArr && successArr.length > deferThreshold) {
	 // Break up upsert into blocks
	 this._deferQueue[type] = queue.concat(successArr);

	 // Fire off the insert queue handler
	 this.processQueue(type);

	 return;
	 } else {*/
	for (bindKey in binds) {
		if (binds.hasOwnProperty(bindKey)) {
			if (binds[bindKey].reduce) {
				filteredDataSet = this.find(binds[bindKey].reduce.query, binds[bindKey].reduce.options);
			} else {
				filteredDataSet = unfilteredDataSet;
			}

			switch (type) {
				case 'insert':
					this._bindInsert(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
					break;

				case 'update':
					this._bindUpdate(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
					break;

				case 'remove':
					this._bindRemove(bindKey, binds[bindKey], successArr, failArr, filteredDataSet);
					break;
			}
		}
	}
	//}
};

OldView.prototype._bindChange = function (newDataArr) {
	if (this.debug()) {
		console.log('ForerunnerDB.OldView.Bind: Bind data change, refreshing bind...', newDataArr);
	}

	this.bindRefresh(newDataArr);
};

OldView.prototype._bindInsert = function (selector, options, successArr, failArr, all) {
	var container = window.jQuery(selector),
		itemElem,
		itemHtml,
		makeCallback,
		i;

	makeCallback = function (itemElem, insertedItem, failArr, all) {
		return function (itemHtml) {
			// Check if there is custom DOM insert method
			if (options.insert) {
				options.insert(itemHtml, insertedItem, failArr, all);
			} else {
				// Handle the insert automatically
				// Add the item to the container
				if (options.prependInsert) {
					container.prepend(itemHtml);

				} else {
					container.append(itemHtml);
				}
			}

			if (options.afterInsert) {
				options.afterInsert(itemHtml, insertedItem, failArr, all);
			}
		};
	};

	// Loop the inserted items
	for (i = 0; i < successArr.length; i++) {
		// Check for existing item in the container
		itemElem = container.find('#' + successArr[i][this._primaryKey]);

		if (!itemElem.length) {
			itemHtml = options.template(successArr[i], makeCallback(itemElem, successArr[i], failArr, all));
		}
	}
};

OldView.prototype._bindUpdate = function (selector, options, successArr, failArr, all) {
	var container = window.jQuery(selector),
		itemElem,
		makeCallback,
		i;

	makeCallback = function (itemElem, itemData) {
		return function (itemHtml) {
			// Check if there is custom DOM insert method
			if (options.update) {
				options.update(itemHtml, itemData, all, itemElem.length ? 'update' : 'append');
			} else {
				if (itemElem.length) {
					// An existing item is in the container, replace it with the
					// new rendered item from the updated data
					itemElem.replaceWith(itemHtml);
				} else {
					// The item element does not already exist, append it
					if (options.prependUpdate) {
						container.prepend(itemHtml);
					} else {
						container.append(itemHtml);
					}
				}
			}

			if (options.afterUpdate) {
				options.afterUpdate(itemHtml, itemData, all);
			}
		};
	};

	// Loop the updated items
	for (i = 0; i < successArr.length; i++) {
		// Check for existing item in the container
		itemElem = container.find('#' + successArr[i][this._primaryKey]);

		options.template(successArr[i], makeCallback(itemElem, successArr[i]));
	}
};

OldView.prototype._bindRemove = function (selector, options, successArr, failArr, all) {
	var container = window.jQuery(selector),
		itemElem,
		makeCallback,
		i;

	makeCallback = function (itemElem, data, all) {
		return function () {
			if (options.remove) {
				options.remove(itemElem, data, all);
			} else {
				itemElem.remove();

				if (options.afterRemove) {
					options.afterRemove(itemElem, data, all);
				}
			}
		};
	};

	// Loop the removed items
	for (i = 0; i < successArr.length; i++) {
		// Check for existing item in the container
		itemElem = container.find('#' + successArr[i][this._primaryKey]);

		if (itemElem.length) {
			if (options.beforeRemove) {
				options.beforeRemove(itemElem, successArr[i], all, makeCallback(itemElem, successArr[i], all));
			} else {
				if (options.remove) {
					options.remove(itemElem, successArr[i], all);
				} else {
					itemElem.remove();

					if (options.afterRemove) {
						options.afterRemove(itemElem, successArr[i], all);
					}
				}
			}
		}
	}
};