PageRenderTime 26ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/extensions/filter/backgrid-filter.js

https://github.com/antonyraj/backgrid
JavaScript | 365 lines | 169 code | 48 blank | 148 comment | 25 complexity | 6941c08fade08ccd5214857c09235221 MD5 | raw file
Possible License(s): MIT
  1. /*
  2. backgrid-filter
  3. http://github.com/wyuenho/backgrid
  4. Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  5. Licensed under the MIT @license.
  6. */
  7. (function ($, _, Backbone, Backgrid, lunr) {
  8. "use strict";
  9. /**
  10. ServerSideFilter is a search form widget that submits a query to the server
  11. for filtering the current collection.
  12. @class Backgrid.Extension.ServerSideFilter
  13. */
  14. var ServerSideFilter = Backgrid.Extension.ServerSideFilter = Backbone.View.extend({
  15. /** @property */
  16. tagName: "form",
  17. /** @property */
  18. className: "backgrid-filter form-search",
  19. /** @property {function(Object, ?Object=): string} template */
  20. template: _.template('<div class="input-prepend input-append"><span class="add-on"><i class="icon-search"></i></span><input type="text" <% if (placeholder) { %> placeholder="<%- placeholder %>" <% } %> name="<%- name %>" /><span class="add-on"><a class="close" href="#">&times;</a></span></div>'),
  21. /** @property */
  22. events: {
  23. "click .close": "clear",
  24. "submit": "search"
  25. },
  26. /** @property {string} [name='q'] Query key */
  27. name: "q",
  28. /** @property The HTML5 placeholder to appear beneath the search box. */
  29. placeholder: null,
  30. /**
  31. @param {Object} options
  32. @param {Backbone.Collection} options.collection
  33. @param {String} [options.name]
  34. @param {String} [options.placeholder]
  35. */
  36. initialize: function (options) {
  37. Backgrid.requireOptions(options, ["collection"]);
  38. Backbone.View.prototype.initialize.apply(this, arguments);
  39. this.name = options.name || this.name;
  40. this.placeholder = options.placeholder || this.placeholder;
  41. var collection = this.collection, self = this;
  42. if (Backbone.PageableCollection &&
  43. collection instanceof Backbone.PageableCollection &&
  44. collection.mode == "server") {
  45. collection.queryParams[this.name] = function () {
  46. return self.$el.find("input[type=text]").val();
  47. };
  48. }
  49. },
  50. /**
  51. Upon search form submission, this event handler constructs a query
  52. parameter object and pass it to Collection#fetch for server-side
  53. filtering.
  54. */
  55. search: function (e) {
  56. if (e) e.preventDefault();
  57. var data = {};
  58. data[this.name] = this.$el.find("input[type=text]").val();
  59. this.collection.fetch({data: data});
  60. },
  61. /**
  62. Event handler for the close button. Clears the search box and refetch the
  63. collection.
  64. */
  65. clear: function (e) {
  66. if (e) e.preventDefault();
  67. this.$("input[type=text]").val(null);
  68. this.collection.fetch();
  69. },
  70. /**
  71. Renders a search form with a text box, optionally with a placeholder and
  72. a preset value if supplied during initialization.
  73. */
  74. render: function () {
  75. this.$el.empty().append(this.template({
  76. name: this.name,
  77. placeholder: this.placeholder,
  78. value: this.value
  79. }));
  80. this.delegateEvents();
  81. return this;
  82. }
  83. });
  84. /**
  85. ClientSideFilter is a search form widget that searches a collection for
  86. model matches against a query on the client side. The exact matching
  87. algorithm can be overriden by subclasses.
  88. @class Backgrid.Extension.ClientSideFilter
  89. @extends Backgrid.Extension.ServerSideFilter
  90. */
  91. var ClientSideFilter = Backgrid.Extension.ClientSideFilter = ServerSideFilter.extend({
  92. /** @property */
  93. events: {
  94. "click .close": function (e) {
  95. e.preventDefault();
  96. this.clear();
  97. },
  98. "change input[type=text]": "search",
  99. "keyup input[type=text]": "search",
  100. "submit": function (e) {
  101. e.preventDefault();
  102. this.search();
  103. }
  104. },
  105. /**
  106. @property {?Array.<string>} A list of model field names to search
  107. for matches. If null, all of the fields will be searched.
  108. */
  109. fields: null,
  110. /**
  111. @property wait The time in milliseconds to wait since for since the last
  112. change to the search box's value before searching. This value can be
  113. adjusted depending on how often the search box is used and how large the
  114. search index is.
  115. */
  116. wait: 149,
  117. /**
  118. Debounces the #search and #clear methods and makes a copy of the given
  119. collection for searching.
  120. @param {Object} options
  121. @param {Backbone.Collection} options.collection
  122. @param {String} [options.placeholder]
  123. @param {String} [options.fields]
  124. @param {String} [options.wait=149]
  125. */
  126. initialize: function (options) {
  127. ServerSideFilter.prototype.initialize.apply(this, arguments);
  128. this.fields = options.fields || this.fields;
  129. this.wait = options.wait || this.wait;
  130. this._debounceMethods(["search", "clear"]);
  131. var collection = this.collection;
  132. var shadowCollection = this.shadowCollection = collection.clone();
  133. shadowCollection.url = collection.url;
  134. shadowCollection.sync = collection.sync;
  135. shadowCollection.parse = collection.parse;
  136. this.listenTo(collection, "add", function (model, collection, options) {
  137. shadowCollection.add(model, options);
  138. });
  139. this.listenTo(collection, "remove", function (model, collection, options) {
  140. shadowCollection.remove(model, options);
  141. });
  142. this.listenTo(collection, "sort reset", function (collection, options) {
  143. options = _.extend({reindex: true}, options || {});
  144. if (options.reindex) shadowCollection.reset(collection.models);
  145. });
  146. },
  147. _debounceMethods: function (methodNames) {
  148. if (_.isString(methodNames)) methodNames = [methodNames];
  149. this.undelegateEvents();
  150. for (var i = 0, l = methodNames.length; i < l; i++) {
  151. var methodName = methodNames[i];
  152. var method = this[methodName];
  153. this[methodName] = _.debounce(method, this.wait);
  154. }
  155. this.delegateEvents();
  156. },
  157. /**
  158. This default implementation takes a query string and returns a matcher
  159. function that looks for matches in the model's #fields or all of its
  160. fields if #fields is null, for any of the words in the query
  161. case-insensitively.
  162. Subclasses overriding this method must take care to conform to the
  163. signature of the matcher function. In addition, when the matcher function
  164. is called, its context will be bound to this ClientSideFilter object so
  165. it has access to the filter's attributes and methods.
  166. @param {string} query The search query in the search box.
  167. @return {function(Backbone.Model):boolean} A matching function.
  168. */
  169. makeMatcher: function (query) {
  170. var regexp = new RegExp(query.trim().split(/\W/).join("|"), "i");
  171. return function (model) {
  172. var keys = this.fields || model.keys();
  173. for (var i = 0, l = keys.length; i < l; i++) {
  174. if (regexp.test(model.get(keys[i]) + "")) return true;
  175. }
  176. return false;
  177. };
  178. },
  179. /**
  180. Takes the query from the search box, constructs a matcher with it and
  181. loops through collection looking for matches. Reset the given collection
  182. when all the matches have been found.
  183. */
  184. search: function () {
  185. var matcher = _.bind(this.makeMatcher(this.$("input[type=text]").val()), this);
  186. this.collection.reset(this.shadowCollection.filter(matcher), {reindex: false});
  187. },
  188. /**
  189. Clears the search box and reset the collection to its original.
  190. */
  191. clear: function () {
  192. this.$("input[type=text]").val(null);
  193. this.collection.reset(this.shadowCollection.models, {reindex: false});
  194. }
  195. });
  196. /**
  197. LunrFilter is a ClientSideFilter that uses [lunrjs](http://lunrjs.com/) to
  198. index the text fields of each model for a collection, and performs
  199. full-text searching.
  200. @class Backgrid.Extension.LunrFilter
  201. @extends Backgrid.Extension.ClientSideFilter
  202. */
  203. Backgrid.Extension.LunrFilter = ClientSideFilter.extend({
  204. /**
  205. @property {string} [ref="id"]`lunrjs` document reference attribute name.
  206. */
  207. ref: "id",
  208. /**
  209. @property {Object} fields A hash of `lunrjs` index field names and boost
  210. value. Unlike ClientSideFilter#fields, LunrFilter#fields is _required_ to
  211. initialize the index.
  212. */
  213. fields: null,
  214. /**
  215. Indexes the underlying collection on construction. The index will refresh
  216. when the underlying collection is reset. If any model is added, removed
  217. or if any indexed fields of any models has changed, the index will be
  218. updated.
  219. @param {Object} options
  220. @param {Backbone.Collection} options.collection
  221. @param {String} [options.placeholder]
  222. @param {string} [options.ref] `lunrjs` document reference attribute name.
  223. @param {Object} [options.fields] A hash of `lunrjs` index field names and
  224. boost value.
  225. @param {number} [options.wait]
  226. */
  227. initialize: function (options) {
  228. ClientSideFilter.prototype.initialize.apply(this, arguments);
  229. this.ref = options.ref || this.ref;
  230. var collection = this.collection;
  231. this.listenTo(collection, "add", this.addToIndex);
  232. this.listenTo(collection, "remove", this.removeFromIndex);
  233. this.listenTo(collection, "reset", this.resetIndex);
  234. this.listenTo(collection, "change", this.updateIndex);
  235. this.resetIndex(collection);
  236. },
  237. /**
  238. Reindex the collection. If `options.reindex` is `false`, this method is a
  239. no-op.
  240. @param {Backbone.Collection} collection
  241. @param {Object} [options]
  242. @param {boolean} [options.reindex=true]
  243. */
  244. resetIndex: function (collection, options) {
  245. options = _.extend({reindex: true}, options || {});
  246. if (options.reindex) {
  247. var self = this;
  248. this.index = lunr(function () {
  249. _.each(self.fields, function (boost, fieldName) {
  250. this.field(fieldName, boost);
  251. this.ref(self.ref);
  252. }, this);
  253. });
  254. collection.each(function (model) {
  255. this.addToIndex(model);
  256. }, this);
  257. }
  258. },
  259. /**
  260. Adds the given model to the index.
  261. @param {Backbone.Model} model
  262. */
  263. addToIndex: function (model) {
  264. var index = this.index;
  265. var doc = model.toJSON();
  266. if (index.documentStore.has(doc[this.ref])) index.update(doc);
  267. else index.add(doc);
  268. },
  269. /**
  270. Removes the given model from the index.
  271. @param {Backbone.Model} model
  272. */
  273. removeFromIndex: function (model) {
  274. var index = this.index;
  275. var doc = model.toJSON();
  276. if (index.documentStore.has(doc[this.ref])) index.remove(doc);
  277. },
  278. /**
  279. Updates the index for the given model.
  280. @param {Backbone.Model} model
  281. */
  282. updateIndex: function (model) {
  283. var changed = model.changedAttributes();
  284. if (changed && !_.isEmpty(_.intersection(_.keys(this.fields),
  285. _.keys(changed)))) {
  286. this.index.update(model.toJSON());
  287. }
  288. },
  289. /**
  290. Takes the query from the search box and performs a full-text search on
  291. the client-side. The search result is returned by resetting the
  292. underlying collection to the models after interrogating the index for the
  293. query answer.
  294. */
  295. search: function () {
  296. var searchResults = this.index.search(this.$("input[type=text]").val());
  297. var models = [];
  298. for (var i = 0; i < searchResults.length; i++) {
  299. var result = searchResults[i];
  300. models.push(this.shadowCollection.get(result.ref));
  301. }
  302. this.collection.reset(models, {reindex: false});
  303. }
  304. });
  305. }(jQuery, _, Backbone, Backgrid, lunr));