UNPKG

forerunnerdb

Version:

A NoSQL document store database for browsers and Node.js.

691 lines (584 loc) 18.3 kB
"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;