Source: Grid.js

"use strict";

// Import external names locally
var Shared,
	Db,
	Collection,
	CollectionGroup,
	View,
	CollectionInit,
	DbInit,
	ReactorIO;

//Shared = ForerunnerDB.shared;
Shared = require('./Shared');

/**
 * Creates a new grid instance.
 * @name Grid
 * @class Grid
 * @param {String} selector jQuery selector.
 * @param {String} template The template selector.
 * @param {Object=} options The options object to apply to the grid.
 * @constructor
 */
var Grid = function (selector, template, options) {
	this.init.apply(this, arguments);
};

Grid.prototype.init = function (selector, template, options) {
	var self = this;

	this._selector = selector;
	this._template = template;
	this._options = options || {};
	this._debug = {};
	this._id = this.objectId();

	this._collectionDroppedWrap = function () {
		self._collectionDropped.apply(self, arguments);
	};
};

Shared.addModule('Grid', Grid);
Shared.mixin(Grid.prototype, 'Mixin.Common');
Shared.mixin(Grid.prototype, 'Mixin.ChainReactor');
Shared.mixin(Grid.prototype, 'Mixin.Constants');
Shared.mixin(Grid.prototype, 'Mixin.Triggers');
Shared.mixin(Grid.prototype, 'Mixin.Events');

Collection = require('./Collection');
CollectionGroup = require('./CollectionGroup');
View = require('./View');
ReactorIO = require('./ReactorIO');
CollectionInit = Collection.prototype.init;
Db = Shared.modules.Db;
DbInit = Db.prototype.init;

/**
 * Gets / sets the current state.
 * @func state
 * @memberof Grid
 * @param {String=} val The name of the state to set.
 * @returns {Grid}
 */
Shared.synthesize(Grid.prototype, 'state');

/**
 * Gets / sets the current name.
 * @func name
 * @memberof Grid
 * @param {String=} val The name to set.
 * @returns {Grid}
 */
Shared.synthesize(Grid.prototype, 'name');

/**
 * Executes an insert against the grid's underlying data-source.
 * @func insert
 * @memberof Grid
 */
Grid.prototype.insert = function () {
	this._from.insert.apply(this._from, arguments);
};

/**
 * Executes an update against the grid's underlying data-source.
 * @func update
 * @memberof Grid
 */
Grid.prototype.update = function () {
	this._from.update.apply(this._from, arguments);
};

/**
 * Executes an updateById against the grid's underlying data-source.
 * @func updateById
 * @memberof Grid
 */
Grid.prototype.updateById = function () {
	this._from.updateById.apply(this._from, arguments);
};

/**
 * Executes a remove against the grid's underlying data-source.
 * @func remove
 * @memberof Grid
 */
Grid.prototype.remove = function () {
	this._from.remove.apply(this._from, arguments);
};

/**
 * Sets the collection from which the grid will assemble its data.
 * @func from
 * @memberof Grid
 * @param {Collection} collection The collection to use to assemble grid data.
 * @returns {Grid}
 */
Grid.prototype.from = function (collection) {
	//var self = this;

	if (collection !== undefined) {
		// Check if we have an existing from
		if (this._from) {
			// Remove the listener to the drop event
			this._from.off('drop', this._collectionDroppedWrap);
			this._from._removeGrid(this);
		}

		if (typeof(collection) === 'string') {
			collection = this._db.collection(collection);
		}

		this._from = collection;
		this._from.on('drop', this._collectionDroppedWrap);
		this.refresh();
	}

	return this;
};

/**
 * Gets / sets the db instance this class instance belongs to.
 * @func db
 * @memberof Grid
 * @param {Db=} db The db instance.
 * @returns {*}
 */
Shared.synthesize(Grid.prototype, 'db', function (db) {
	if (db) {
		// Apply the same debug settings
		this.debug(db.debug());
	}

	return this.$super.apply(this, arguments);
});

Grid.prototype._collectionDropped = function (collection) {
	if (collection) {
		// Collection was dropped, remove from grid
		delete this._from;
	}
};

/**
 * Drops a grid and all it's stored data from the database.
 * @func drop
 * @memberof Grid
 * @returns {boolean} True on success, false on failure.
 */
Grid.prototype.drop = function () {
	if (!this.isDropped()) {
		if (this._from) {
			// Remove data-binding
			this._from.unlink(this._selector, this.template());

			// Kill listeners and references
			this._from.off('drop', this._collectionDroppedWrap);
			this._from._removeGrid(this);

			if (this.debug() || (this._db && this._db.debug())) {
				console.log(this.logIdentifier() + ' Dropping grid ' + this._selector);
			}

			this._state = 'dropped';

			if (this._db && this._selector) {
				delete this._db._grid[this._selector];
			}

			this.emit('drop', this);

			delete this._selector;
			delete this._template;
			delete this._from;
			delete this._db;

			return true;
		}
	} else {
		return true;
	}

	return false;
};

/**
 * Gets / sets the grid's HTML template to use when rendering.
 * @func template
 * @memberof Grid
 * @param {Selector} template The template's jQuery selector.
 * @returns {*}
 */
Grid.prototype.template = function (template) {
	if (template !== undefined) {
		this._template = template;
		return this;
	}

	return this._template;
};

Grid.prototype._sortGridClick = function (e) {
	var elem = window.jQuery(e.currentTarget),
		sortColText = elem.attr('data-grid-sort') || '',
		sortColDir = parseInt((elem.attr('data-grid-dir') || "-1"), 10) === -1 ? 1 : -1,
		sortCols = sortColText.split(','),
		sortObj = {},
		i;

	// Remove all grid sort tags from the grid
	window.jQuery(this._selector).find('[data-grid-dir]').removeAttr('data-grid-dir');

	// Flip the sort direction
	elem.attr('data-grid-dir', sortColDir);

	for (i = 0; i < sortCols.length; i++) {
		sortObj[sortCols] = sortColDir;
	}

	Shared.mixin(sortObj, this._options.$orderBy);

	this._from.orderBy(sortObj);
	this.emit('sort', sortObj);
};

/**
 * Refreshes the grid data such as ordering etc.
 * @func refresh
 * @memberof Grid
 */
Grid.prototype.refresh = function () {
	if (this._from) {
		if (this._from.link) {
			var self = this,
				elem = window.jQuery(this._selector),
				sortClickListener = function () {
					self._sortGridClick.apply(self, arguments);
				};

			// Clear the container
			elem.html('');

			if (self._from.orderBy) {
				// Remove listeners
				elem.off('click', '[data-grid-sort]', sortClickListener);
			}

			if (self._from.query) {
				// Remove listeners
				elem.off('click', '[data-grid-filter]', sortClickListener );
			}

			// Set wrap name if none is provided
			self._options.$wrap = self._options.$wrap || 'gridRow';

			// Auto-bind the data to the grid template
			self._from.link(self._selector, self.template(), self._options);

			// Check if the data source (collection or view) has an
			// orderBy method (usually only views) and if so activate
			// the sorting system
			if (self._from.orderBy) {
				// Listen for sort requests
				elem.on('click', '[data-grid-sort]', sortClickListener);
			}

			if (self._from.query) {
				// Listen for filter requests
				var queryObj = {};

				elem.find('[data-grid-filter]').each(function (index, filterElem) {
					filterElem = window.jQuery(filterElem);

					var filterField = filterElem.attr('data-grid-filter'),
						filterVarType = filterElem.attr('data-grid-vartype'),
						filterSort = {},
						title = filterElem.html(),
						dropDownButton,
						dropDownMenu,
						template,
						filterQuery,
						filterView = self._db.view('tmpGridFilter_' + self._id + '_' + filterField);

					filterSort[filterField] = 1;

					filterQuery = {
						$distinct: filterSort
					};

					filterView
						.query(filterQuery)
						.orderBy(filterSort)
						.from(self._from._from);

					template = [
						'<div class="dropdown" id="' + self._id + '_' + filterField + '">',
							'<button class="btn btn-default dropdown-toggle" type="button" id="' + self._id + '_' + filterField + '_dropdownButton" data-toggle="dropdown" aria-expanded="true">',
								title + ' <span class="caret"></span>',
							'</button>',
						'</div>'
					];

					dropDownButton = window.jQuery(template.join(''));
					dropDownMenu = window.jQuery('<ul class="dropdown-menu" role="menu" id="' + self._id + '_' + filterField + '_dropdownMenu"></ul>');

					dropDownButton.append(dropDownMenu);

					filterElem.html(dropDownButton);

					// Data-link the underlying data to the grid filter drop-down
					filterView.link(dropDownMenu, {
						template: [
							'<li role="presentation" class="input-group" style="width: 240px; padding-left: 10px; padding-right: 10px; padding-top: 5px;">',
								'<input type="search" class="form-control gridFilterSearch" placeholder="Search...">',
								'<span class="input-group-btn">',
									'<button class="btn btn-default gridFilterClearSearch" type="button"><span class="glyphicon glyphicon-remove-circle glyphicons glyphicons-remove"></span></button>',
								'</span>',
							'</li>',
							'<li role="presentation" class="divider"></li>',
							'<li role="presentation" data-val="$all">',
								'<a role="menuitem" tabindex="-1">',
									'<input type="checkbox" checked>&nbsp;All',
								'</a>',
							'</li>',
							'<li role="presentation" class="divider"></li>',
							'{^{for options}}',
								'<li role="presentation" data-link="data-val{:' + filterField + '}">',
									'<a role="menuitem" tabindex="-1">',
										'<input type="checkbox">&nbsp;{^{:' + filterField + '}}',
									'</a>',
								'</li>',
							'{{/for}}'
						].join('')
					}, {
						$wrap: 'options'
					});

					elem.on('keyup', '#' + self._id + '_' + filterField + '_dropdownMenu .gridFilterSearch', function (e) {
						var elem = window.jQuery(this),
							query = filterView.query(),
							search = elem.val();

						if (search) {
							query[filterField] = new RegExp(search, 'gi');
						} else {
							delete query[filterField];
						}

						filterView.query(query);
					});

					elem.on('click', '#' + self._id + '_' + filterField + '_dropdownMenu .gridFilterClearSearch', function (e) {
						// Clear search text box
						window.jQuery(this).parents('li').find('.gridFilterSearch').val('');

						// Clear view query
						var query = filterView.query();
						delete query[filterField];
						filterView.query(query);
					});

					elem.on('click', '#' + self._id + '_' + filterField + '_dropdownMenu li', function (e) {
						e.stopPropagation();

						var fieldValue,
							elem = $(this),
							checkbox = elem.find('input[type="checkbox"]'),
							checked,
							addMode = true,
							fieldInArr,
							liElem,
							i;

						// If the checkbox is not the one clicked on
						if (!window.jQuery(e.target).is('input')) {
							// Set checkbox to opposite of current value
							checkbox.prop('checked', !checkbox.prop('checked'));
							checked = checkbox.is(':checked');
						} else {
							checkbox.prop('checked', checkbox.prop('checked'));
							checked = checkbox.is(':checked');
						}

						liElem = window.jQuery(this);
						fieldValue = liElem.attr('data-val');

						// Check if the selection is the "all" option
						if (fieldValue === '$all') {
							// Remove the field from the query
							delete queryObj[filterField];

							// Clear all other checkboxes
							liElem.parent().find('li[data-val!="$all"]').find('input[type="checkbox"]').prop('checked', false);
						} else {
							// Clear the "all" checkbox
							liElem.parent().find('[data-val="$all"]').find('input[type="checkbox"]').prop('checked', false);

							// Check if the type needs casting
							switch (filterVarType) {
								case 'integer':
									fieldValue = parseInt(fieldValue, 10);
									break;

								case 'float':
									fieldValue = parseFloat(fieldValue);
									break;

								default:
							}

							// Check if the item exists already
							queryObj[filterField] = queryObj[filterField] || {
								$in: []
							};

							fieldInArr = queryObj[filterField].$in;

							for (i = 0; i < fieldInArr.length; i++) {
								if (fieldInArr[i] === fieldValue) {
									// Item already exists
									if (checked === false) {
										// Remove the item
										fieldInArr.splice(i, 1);
									}
									addMode = false;
									break;
								}
							}

							if (addMode && checked) {
								fieldInArr.push(fieldValue);
							}

							if (!fieldInArr.length) {
								// Remove the field from the query
								delete queryObj[filterField];
							}
						}

						// Set the view query
						self._from.queryData(queryObj);
						if (self._from.pageFirst) {
							self._from.pageFirst();
						}
					});
				});
			}

			self.emit('refresh');
		} else {
			throw('Grid requires the AutoBind module in order to operate!');
		}
	}

	return this;
};

/**
 * Returns the number of documents currently in the grid.
 * @func count
 * @memberof Grid
 * @returns {Number}
 */
Grid.prototype.count = function () {
	return this._from.count();
};

/**
 * Creates a grid and assigns the collection as its data source.
 * @func grid
 * @memberof Collection
 * @param {String} selector jQuery selector of grid output target.
 * @param {String} template The table template to use when rendering the grid.
 * @param {Object=} options The options object to apply to the grid.
 * @returns {*}
 */
Collection.prototype.grid = View.prototype.grid = function (selector, template, options) {
	if (this._db && this._db._grid ) {
		if (selector !== undefined) {
			if (template !== undefined) {
				if (!this._db._grid[selector]) {
					var grid = new Grid(selector, template, options)
						.db(this._db)
						.from(this);

					this._grid = this._grid || [];
					this._grid.push(grid);

					this._db._grid[selector] = grid;

					return grid;
				} else {
					throw(this.logIdentifier() + ' Cannot create a grid because a grid with this name already exists: ' + selector);
				}
			}

			return this._db._grid[selector];
		}

		return this._db._grid;
	}
};

/**
 * Removes a grid safely from the DOM. Must be called when grid is
 * no longer required / is being removed from DOM otherwise references
 * will stick around and cause memory leaks.
 * @func unGrid
 * @memberof Collection
 * @param {String} selector jQuery selector of grid output target.
 * @param {String} template The table template to use when rendering the grid.
 * @param {Object=} options The options object to apply to the grid.
 * @returns {*}
 */
Collection.prototype.unGrid = View.prototype.unGrid = function (selector, template, options) {
	var i,
		grid;

	if (this._db && this._db._grid ) {
		if (selector && template) {
			if (this._db._grid[selector]) {
				grid = this._db._grid[selector];
				delete this._db._grid[selector];

				return grid.drop();
			} else {
				throw(this.logIdentifier() + ' Cannot remove grid because a grid with this name does not exist: ' + name);
			}
		} else {
			// No parameters passed, remove all grids from this module
			for (i in this._db._grid) {
				if (this._db._grid.hasOwnProperty(i)) {
					grid = this._db._grid[i];
					delete this._db._grid[i];

					grid.drop();

					if (this.debug()) {
						console.log(this.logIdentifier() + ' Removed grid binding "' + i + '"');
					}
				}
			}

			this._db._grid = {};
		}
	}
};

/**
 * Adds a grid to the internal grid lookup.
 * @func _addGrid
 * @memberof Collection
 * @param {Grid} grid The grid to add.
 * @returns {Collection}
 * @private
 */
Collection.prototype._addGrid = CollectionGroup.prototype._addGrid = View.prototype._addGrid = function (grid) {
	if (grid !== undefined) {
		this._grid = this._grid || [];
		this._grid.push(grid);
	}

	return this;
};

/**
 * Removes a grid from the internal grid lookup.
 * @func _removeGrid
 * @memberof Collection
 * @param {Grid} grid The grid to remove.
 * @returns {Collection}
 * @private
 */
Collection.prototype._removeGrid = CollectionGroup.prototype._removeGrid = View.prototype._removeGrid = function (grid) {
	if (grid !== undefined && this._grid) {
		var index = this._grid.indexOf(grid);
		if (index > -1) {
			this._grid.splice(index, 1);
		}
	}

	return this;
};

// Extend DB with grids init
Db.prototype.init = function () {
	this._grid = {};
	DbInit.apply(this, arguments);
};

/**
 * Determine if a grid with the passed name already exists.
 * @func gridExists
 * @memberof Db
 * @param {String} selector The jQuery selector to bind the grid to.
 * @returns {boolean}
 */
Db.prototype.gridExists = function (selector) {
	return Boolean(this._grid[selector]);
};

/**
 * Creates a grid based on the passed arguments.
 * @func grid
 * @memberof Db
 * @param {String} selector The jQuery selector of the grid to retrieve.
 * @param {String} template The table template to use when rendering the grid.
 * @param {Object=} options The options object to apply to the grid.
 * @returns {*}
 */
Db.prototype.grid = function (selector, template, options) {
	if (!this._grid[selector]) {
		if (this.debug() || (this._db && this._db.debug())) {
			console.log(this.logIdentifier() + ' Creating grid ' + selector);
		}
	}

	this._grid[selector] = this._grid[selector] || new Grid(selector, template, options).db(this);
	return this._grid[selector];
};

/**
 * Removes a grid based on the passed arguments.
 * @func unGrid
 * @memberof Db
 * @param {String} selector The jQuery selector of the grid to retrieve.
 * @param {String} template The table template to use when rendering the grid.
 * @param {Object=} options The options object to apply to the grid.
 * @returns {*}
 */
Db.prototype.unGrid = function (selector, template, options) {
	if (!this._grid[selector]) {
		if (this.debug() || (this._db && this._db.debug())) {
			console.log(this.logIdentifier() + ' Creating grid ' + selector);
		}
	}

	this._grid[selector] = this._grid[selector] || new Grid(selector, template, options).db(this);
	return this._grid[selector];
};

/**
 * Returns an array of grids the DB currently has.
 * @func grids
 * @memberof Db
 * @returns {Array} An array of objects containing details of each grid
 * the database is currently managing.
 */
Db.prototype.grids = function () {
	var arr = [],
		item,
		i;

	for (i in this._grid) {
		if (this._grid.hasOwnProperty(i)) {
			item = this._grid[i];

			arr.push({
				name: i,
				count: item.count(),
				linked: item.isLinked !== undefined ? item.isLinked() : false
			});
		}
	}

	return arr;
};

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