visualsearch.js 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969
  1. // This is the annotated source code for
  2. // [VisualSearch.js](http://documentcloud.github.com/visualsearch/),
  3. // a rich search box for real data.
  4. //
  5. // The annotated source HTML is generated by
  6. // [Docco](http://jashkenas.github.com/docco/).
  7. /** @license VisualSearch.js 0.4.0
  8. * (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc.
  9. * VisualSearch.js may be freely distributed under the MIT license.
  10. * For all details and documentation:
  11. * http://documentcloud.github.com/visualsearch
  12. */
  13. (function() {
  14. var $ = jQuery; // Handle namespaced jQuery
  15. // Setting up VisualSearch globals. These will eventually be made instance-based.
  16. if (!window.VS) window.VS = {};
  17. if (!VS.app) VS.app = {};
  18. if (!VS.ui) VS.ui = {};
  19. if (!VS.model) VS.model = {};
  20. if (!VS.utils) VS.utils = {};
  21. // Sets the version for VisualSearch to be used programatically elsewhere.
  22. VS.VERSION = '0.5.0';
  23. VS.VisualSearch = function(options) {
  24. var defaults = {
  25. container : '',
  26. query : '',
  27. autosearch : true,
  28. unquotable : [],
  29. remainder : 'text',
  30. showFacets : true,
  31. readOnly : false,
  32. callbacks : {
  33. search : $.noop,
  34. focus : $.noop,
  35. blur : $.noop,
  36. facetMatches : $.noop,
  37. valueMatches : $.noop,
  38. clearSearch : $.noop,
  39. removedFacet : $.noop
  40. }
  41. };
  42. this.options = _.extend({}, defaults, options);
  43. this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks);
  44. VS.app.hotkeys.initialize();
  45. this.searchQuery = new VS.model.SearchQuery();
  46. this.searchBox = new VS.ui.SearchBox({
  47. app: this,
  48. showFacets: this.options.showFacets
  49. });
  50. if (options.container) {
  51. var searchBox = this.searchBox.render().el;
  52. $(this.options.container).html(searchBox);
  53. }
  54. this.searchBox.value(this.options.query || '');
  55. // Disable page caching for browsers that incorrectly cache the visual search inputs.
  56. // This forces the browser to re-render the page when it is retrieved in its history.
  57. $(window).bind('unload', function(e) {});
  58. // Gives the user back a reference to the `searchBox` so they
  59. // can use public methods.
  60. return this;
  61. };
  62. // Entry-point used to tie all parts of VisualSearch together. It will either attach
  63. // itself to `options.container`, or pass back the `searchBox` so it can be rendered
  64. // at will.
  65. VS.init = function(options) {
  66. return new VS.VisualSearch(options);
  67. };
  68. })();
  69. (function() {
  70. var $ = jQuery; // Handle namespaced jQuery
  71. // The search box is responsible for managing the many facet views and input views.
  72. VS.ui.SearchBox = Backbone.View.extend({
  73. id : 'search',
  74. events : {
  75. 'click .VS-cancel-search-box' : 'clearSearch',
  76. 'mousedown .VS-search-box' : 'maybeFocusSearch',
  77. 'dblclick .VS-search-box' : 'highlightSearch',
  78. 'click .VS-search-box' : 'maybeTripleClick'
  79. },
  80. // Creating a new SearchBox registers handlers for re-rendering facets when necessary,
  81. // as well as handling typing when a facet is selected.
  82. initialize : function(options) {
  83. this.options = _.extend({}, this.options, options);
  84. this.app = this.options.app;
  85. this.flags = {
  86. allSelected : false
  87. };
  88. this.facetViews = [];
  89. this.inputViews = [];
  90. _.bindAll(this, 'renderFacets', '_maybeDisableFacets', 'disableFacets',
  91. 'deselectAllFacets', 'addedFacet', 'removedFacet', 'changedFacet');
  92. this.app.searchQuery
  93. .bind('reset', this.renderFacets)
  94. .bind('add', this.addedFacet)
  95. .bind('remove', this.removedFacet)
  96. .bind('change', this.changedFacet);
  97. $(document).bind('keydown', this._maybeDisableFacets);
  98. },
  99. // Renders the search box, but requires placement on the page through `this.el`.
  100. render : function() {
  101. $(this.el).append(JST['search_box']({
  102. readOnly: this.app.options.readOnly
  103. }));
  104. $(document.body).setMode('no', 'search');
  105. return this;
  106. },
  107. // # Querying Facets #
  108. // Either gets a serialized query string or sets the faceted query from a query string.
  109. value : function(query) {
  110. if (query == null) return this.serialize();
  111. return this.setQuery(query);
  112. },
  113. // Uses the VS.app.searchQuery collection to serialize the current query from the various
  114. // facets that are in the search box.
  115. serialize : function() {
  116. var query = [];
  117. var inputViewsCount = this.inputViews.length;
  118. this.app.searchQuery.each(_.bind(function(facet, i) {
  119. query.push(this.inputViews[i].value());
  120. query.push(facet.serialize());
  121. }, this));
  122. if (inputViewsCount) {
  123. query.push(this.inputViews[inputViewsCount-1].value());
  124. }
  125. return _.compact(query).join(' ');
  126. },
  127. // Returns any facet views that are currently selected. Useful for changing the value
  128. // callbacks based on what else is in the search box and which facet is being edited.
  129. selected: function() {
  130. return _.select(this.facetViews, function(view) {
  131. return view.modes.editing == 'is' || view.modes.selected == 'is';
  132. });
  133. },
  134. // Similar to `this.selected`, returns any facet models that are currently selected.
  135. selectedModels: function() {
  136. return _.pluck(this.selected(), 'model');
  137. },
  138. // Takes a query string and uses the SearchParser to parse and render it. Note that
  139. // `VS.app.SearchParser` refreshes the `VS.app.searchQuery` collection, which is bound
  140. // here to call `this.renderFacets`.
  141. setQuery : function(query) {
  142. this.currentQuery = query;
  143. VS.app.SearchParser.parse(this.app, query);
  144. },
  145. // Returns the position of a facet/input view. Useful when moving between facets.
  146. viewPosition : function(view) {
  147. var views = view.type == 'facet' ? this.facetViews : this.inputViews;
  148. var position = _.indexOf(views, view);
  149. if (position == -1) position = 0;
  150. return position;
  151. },
  152. // Used to launch a search. Hitting enter or clicking the search button.
  153. searchEvent : function(e) {
  154. var query = this.value();
  155. this.focusSearch(e);
  156. this.value(query);
  157. this.app.options.callbacks.search(query, this.app.searchQuery);
  158. },
  159. // # Rendering Facets #
  160. // Add a new facet. Facet will be focused and ready to accept a value. Can also
  161. // specify position, in the case of adding facets from an inbetween input.
  162. addFacet : function(category, initialQuery, position) {
  163. category = VS.utils.inflector.trim(category);
  164. initialQuery = VS.utils.inflector.trim(initialQuery || '');
  165. if (!category) return;
  166. var model = new VS.model.SearchFacet({
  167. category : category,
  168. value : initialQuery || '',
  169. app : this.app
  170. });
  171. this.app.searchQuery.add(model, {at: position});
  172. },
  173. // Renders a newly added facet, and selects it.
  174. addedFacet : function (model) {
  175. this.renderFacets();
  176. var facetView = _.detect(this.facetViews, function(view) {
  177. if (view.model == model) return true;
  178. });
  179. _.defer(function() {
  180. facetView.enableEdit();
  181. });
  182. },
  183. // Changing a facet programmatically re-renders it.
  184. changedFacet: function () {
  185. this.renderFacets();
  186. },
  187. // When removing a facet, potentially do something. For now, the adjacent
  188. // remaining facet is selected, but this is handled by the facet's view,
  189. // since its position is unknown by the time the collection triggers this
  190. // remove callback.
  191. removedFacet : function (facet, query, options) {
  192. this.app.options.callbacks.removedFacet(facet, query, options);
  193. },
  194. // Renders each facet as a searchFacet view.
  195. renderFacets : function() {
  196. this.facetViews = [];
  197. this.inputViews = [];
  198. this.$('.VS-search-inner').empty();
  199. this.app.searchQuery.each(_.bind(this.renderFacet, this));
  200. // Add on an n+1 empty search input on the very end.
  201. this.renderSearchInput();
  202. this.renderPlaceholder();
  203. },
  204. // Render a single facet, using its category and query value.
  205. renderFacet : function(facet, position) {
  206. var view = new VS.ui.SearchFacet({
  207. app : this.app,
  208. model : facet,
  209. order : position
  210. });
  211. // Input first, facet second.
  212. this.renderSearchInput();
  213. this.facetViews.push(view);
  214. this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
  215. view.calculateSize();
  216. _.defer(_.bind(view.calculateSize, view));
  217. return view;
  218. },
  219. // Render a single input, used to create and autocomplete facets
  220. renderSearchInput : function() {
  221. var input = new VS.ui.SearchInput({
  222. position: this.inputViews.length,
  223. app: this.app,
  224. showFacets: this.options.showFacets
  225. });
  226. this.$('.VS-search-inner').append(input.render().el);
  227. this.inputViews.push(input);
  228. },
  229. // Handles showing/hiding the placeholder text
  230. renderPlaceholder : function() {
  231. var $placeholder = this.$('.VS-placeholder');
  232. if (this.app.searchQuery.length) {
  233. $placeholder.addClass("VS-hidden");
  234. } else {
  235. $placeholder.removeClass("VS-hidden")
  236. .text(this.app.options.placeholder);
  237. }
  238. },
  239. // # Modifying Facets #
  240. // Clears out the search box. Command+A + delete can trigger this, as can a cancel button.
  241. //
  242. // If a `clearSearch` callback was provided, the callback is invoked and
  243. // provided with a function performs the actual removal of the data. This
  244. // allows third-party developers to either clear data asynchronously, or
  245. // prior to performing their custom "clear" logic.
  246. clearSearch : function(e) {
  247. if (this.app.options.readOnly) return;
  248. var actualClearSearch = _.bind(function() {
  249. this.disableFacets();
  250. this.value('');
  251. this.flags.allSelected = false;
  252. this.searchEvent(e);
  253. this.focusSearch(e);
  254. }, this);
  255. if (this.app.options.callbacks.clearSearch != $.noop) {
  256. this.app.options.callbacks.clearSearch(actualClearSearch);
  257. } else {
  258. actualClearSearch();
  259. }
  260. },
  261. // Command+A selects all facets.
  262. selectAllFacets : function() {
  263. this.flags.allSelected = true;
  264. $(document).one('click.selectAllFacets', this.deselectAllFacets);
  265. _.each(this.facetViews, function(facetView, i) {
  266. facetView.selectFacet();
  267. });
  268. _.each(this.inputViews, function(inputView, i) {
  269. inputView.selectText();
  270. });
  271. },
  272. // Used by facets and input to see if all facets are currently selected.
  273. allSelected : function(deselect) {
  274. if (deselect) this.flags.allSelected = false;
  275. return this.flags.allSelected;
  276. },
  277. // After `selectAllFacets` is engaged, this method is bound to the entire document.
  278. // This immediate disables and deselects all facets, but it also checks if the user
  279. // has clicked on either a facet or an input, and properly selects the view.
  280. deselectAllFacets : function(e) {
  281. this.disableFacets();
  282. if (this.$(e.target).is('.category,input')) {
  283. var el = $(e.target).closest('.search_facet,.search_input');
  284. var view = _.detect(this.facetViews.concat(this.inputViews), function(v) {
  285. return v.el == el[0];
  286. });
  287. if (view.type == 'facet') {
  288. view.selectFacet();
  289. } else if (view.type == 'input') {
  290. _.defer(function() {
  291. view.enableEdit(true);
  292. });
  293. }
  294. }
  295. },
  296. // Disables all facets except for the passed in view. Used when switching between
  297. // facets, so as not to have to keep state of active facets.
  298. disableFacets : function(keepView) {
  299. _.each(this.inputViews, function(view) {
  300. if (view && view != keepView &&
  301. (view.modes.editing == 'is' || view.modes.selected == 'is')) {
  302. view.disableEdit();
  303. }
  304. });
  305. _.each(this.facetViews, function(view) {
  306. if (view && view != keepView &&
  307. (view.modes.editing == 'is' || view.modes.selected == 'is')) {
  308. view.disableEdit();
  309. view.deselectFacet();
  310. }
  311. });
  312. this.flags.allSelected = false;
  313. this.removeFocus();
  314. $(document).unbind('click.selectAllFacets');
  315. },
  316. // Resize all inputs to account for extra keystrokes which may be changing the facet
  317. // width incorrectly. This is a safety check to ensure inputs are correctly sized.
  318. resizeFacets : function(view) {
  319. _.each(this.facetViews, function(facetView, i) {
  320. if (!view || facetView == view) {
  321. facetView.resize();
  322. }
  323. });
  324. },
  325. // Handles keydown events on the document. Used to complete the Cmd+A deletion, and
  326. // blurring focus.
  327. _maybeDisableFacets : function(e) {
  328. if (this.flags.allSelected && VS.app.hotkeys.key(e) == 'backspace') {
  329. e.preventDefault();
  330. this.clearSearch(e);
  331. return false;
  332. } else if (this.flags.allSelected && VS.app.hotkeys.printable(e)) {
  333. this.clearSearch(e);
  334. }
  335. },
  336. // # Focusing Facets #
  337. // Move focus between facets and inputs. Takes a direction as well as many options
  338. // for skipping over inputs and only to facets, placement of cursor position in facet
  339. // (i.e. at the end), and selecting the text in the input/facet.
  340. focusNextFacet : function(currentView, direction, options) {
  341. options = options || {};
  342. var viewCount = this.facetViews.length;
  343. var viewPosition = options.viewPosition || this.viewPosition(currentView);
  344. if (!options.skipToFacet) {
  345. // Correct for bouncing between matching text and facet arrays.
  346. if (currentView.type == 'text' && direction > 0) direction -= 1;
  347. if (currentView.type == 'facet' && direction < 0) direction += 1;
  348. } else if (options.skipToFacet && currentView.type == 'text' &&
  349. viewCount == viewPosition && direction >= 0) {
  350. // Special case of looping around to a facet from the last search input box.
  351. return false;
  352. }
  353. var view, next = Math.min(viewCount, viewPosition + direction);
  354. if (currentView.type == 'text') {
  355. if (next >= 0 && next < viewCount) {
  356. view = this.facetViews[next];
  357. } else if (next == viewCount) {
  358. view = this.inputViews[this.inputViews.length-1];
  359. }
  360. if (view && options.selectFacet && view.type == 'facet') {
  361. view.selectFacet();
  362. } else if (view) {
  363. view.enableEdit();
  364. view.setCursorAtEnd(direction || options.startAtEnd);
  365. }
  366. } else if (currentView.type == 'facet') {
  367. if (options.skipToFacet) {
  368. if (next >= viewCount || next < 0) {
  369. view = _.last(this.inputViews);
  370. view.enableEdit();
  371. } else {
  372. view = this.facetViews[next];
  373. view.enableEdit();
  374. view.setCursorAtEnd(direction || options.startAtEnd);
  375. }
  376. } else {
  377. view = this.inputViews[next];
  378. view.enableEdit();
  379. }
  380. }
  381. if (options.selectText) view.selectText();
  382. this.resizeFacets();
  383. return true;
  384. },
  385. maybeFocusSearch : function(e) {
  386. if (this.app.options.readOnly) return;
  387. if ($(e.target).is('.VS-search-box') ||
  388. $(e.target).is('.VS-search-inner') ||
  389. e.type == 'keydown') {
  390. this.focusSearch(e);
  391. }
  392. },
  393. // Bring focus to last input field.
  394. focusSearch : function(e, selectText) {
  395. if (this.app.options.readOnly) return;
  396. var view = this.inputViews[this.inputViews.length-1];
  397. view.enableEdit(selectText);
  398. if (!selectText) view.setCursorAtEnd(-1);
  399. if (e.type == 'keydown') {
  400. view.keydown(e);
  401. view.box.trigger('keydown');
  402. }
  403. _.defer(_.bind(function() {
  404. if (!this.$('input:focus').length) {
  405. view.enableEdit(selectText);
  406. }
  407. }, this));
  408. },
  409. // Double-clicking on the search wrapper should select the existing text in
  410. // the last search input. Also start the triple-click timer.
  411. highlightSearch : function(e) {
  412. if (this.app.options.readOnly) return;
  413. if ($(e.target).is('.VS-search-box') ||
  414. $(e.target).is('.VS-search-inner') ||
  415. e.type == 'keydown') {
  416. var lastinput = this.inputViews[this.inputViews.length-1];
  417. lastinput.startTripleClickTimer();
  418. this.focusSearch(e, true);
  419. }
  420. },
  421. maybeTripleClick : function(e) {
  422. var lastinput = this.inputViews[this.inputViews.length-1];
  423. return lastinput.maybeTripleClick(e);
  424. },
  425. // Used to show the user is focused on some input inside the search box.
  426. addFocus : function() {
  427. if (this.app.options.readOnly) return;
  428. this.app.options.callbacks.focus();
  429. this.$('.VS-search-box').addClass('VS-focus');
  430. },
  431. // User is no longer focused on anything in the search box.
  432. removeFocus : function() {
  433. this.app.options.callbacks.blur();
  434. var focus = _.any(this.facetViews.concat(this.inputViews), function(view) {
  435. return view.isFocused();
  436. });
  437. if (!focus) this.$('.VS-search-box').removeClass('VS-focus');
  438. },
  439. // Show a menu which adds pre-defined facets to the search box. This is unused for now.
  440. showFacetCategoryMenu : function(e) {
  441. e.preventDefault();
  442. e.stopPropagation();
  443. if (this.facetCategoryMenu && this.facetCategoryMenu.modes.open == 'is') {
  444. return this.facetCategoryMenu.close();
  445. }
  446. var items = [
  447. {title: 'Account', onClick: _.bind(this.addFacet, this, 'account', '')},
  448. {title: 'Project', onClick: _.bind(this.addFacet, this, 'project', '')},
  449. {title: 'Filter', onClick: _.bind(this.addFacet, this, 'filter', '')},
  450. {title: 'Access', onClick: _.bind(this.addFacet, this, 'access', '')}
  451. ];
  452. var menu = this.facetCategoryMenu || (this.facetCategoryMenu = new dc.ui.Menu({
  453. items : items,
  454. standalone : true
  455. }));
  456. this.$('.VS-icon-search').after(menu.render().open().content);
  457. return false;
  458. }
  459. });
  460. })();
  461. (function() {
  462. var $ = jQuery; // Handle namespaced jQuery
  463. // This is the visual search facet that holds the category and its autocompleted
  464. // input field.
  465. VS.ui.SearchFacet = Backbone.View.extend({
  466. type : 'facet',
  467. className : 'search_facet',
  468. events : {
  469. 'click .category' : 'selectFacet',
  470. 'keydown input' : 'keydown',
  471. 'mousedown input' : 'enableEdit',
  472. 'mouseover .VS-icon-cancel' : 'showDelete',
  473. 'mouseout .VS-icon-cancel' : 'hideDelete',
  474. 'click .VS-icon-cancel' : 'remove'
  475. },
  476. initialize : function(options) {
  477. this.options = _.extend({}, this.options, options);
  478. this.flags = {
  479. canClose : false
  480. };
  481. _.bindAll(this, 'set', 'keydown', 'deselectFacet', 'deferDisableEdit');
  482. this.app = this.options.app;
  483. },
  484. // Rendering the facet sets up autocompletion, events on blur, and populates
  485. // the facet's input with its starting value.
  486. render : function() {
  487. $(this.el).html(JST['search_facet']({
  488. model : this.model,
  489. readOnly: this.app.options.readOnly
  490. }));
  491. this.setMode('not', 'editing');
  492. this.setMode('not', 'selected');
  493. this.box = this.$('input');
  494. this.box.val(this.model.label());
  495. this.box.bind('blur', this.deferDisableEdit);
  496. // Handle paste events with `propertychange`
  497. this.box.bind('input propertychange', this.keydown);
  498. this.setupAutocomplete();
  499. return this;
  500. },
  501. // This method is used to setup the facet's input to auto-grow.
  502. // This is defered in the searchBox so it can be attached to the
  503. // DOM to get the correct font-size.
  504. calculateSize : function() {
  505. this.box.autoGrowInput();
  506. this.box.unbind('updated.autogrow');
  507. this.box.bind('updated.autogrow', _.bind(this.moveAutocomplete, this));
  508. },
  509. // Forces a recalculation of this facet's input field's value. Called when
  510. // the facet is focused, removed, or otherwise modified.
  511. resize : function(e) {
  512. this.box.trigger('resize.autogrow', e);
  513. },
  514. // Watches the facet's input field to see if it matches the beginnings of
  515. // words in `autocompleteValues`, which is different for every category.
  516. // If the value, when selected from the autocompletion menu, is different
  517. // than what it was, commit the facet and search for it.
  518. setupAutocomplete : function() {
  519. this.box.autocomplete({
  520. source : _.bind(this.autocompleteValues, this),
  521. minLength : 0,
  522. delay : this.app.options.delay,
  523. autoFocus : false,
  524. position : {offset : "0 5"},
  525. create : _.bind(function(e, ui) {
  526. $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
  527. }, this),
  528. select : _.bind(function(e, ui) {
  529. e.preventDefault();
  530. var originalValue = this.model.get('value');
  531. this.set(ui.item.value);
  532. if (originalValue != ui.item.value || this.box.val() != ui.item.value) {
  533. if (this.app.options.autosearch) {
  534. this.search(e);
  535. } else {
  536. this.app.searchBox.renderFacets();
  537. this.app.searchBox.focusNextFacet(this, 1, {viewPosition: this.options.order});
  538. }
  539. }
  540. return false;
  541. }, this),
  542. open : _.bind(function(e, ui) {
  543. var box = this.box;
  544. this.box.autocomplete('widget').find('.ui-menu-item').each(function() {
  545. var $value = $(this),
  546. autoCompleteData = $value.data('item.autocomplete') || $value.data('ui-autocomplete-item');
  547. if (autoCompleteData['value'] == box.val() && box.data('autocomplete').menu.activate) {
  548. box.data('autocomplete').menu.activate(new $.Event("mouseover"), $value);
  549. }
  550. });
  551. }, this)
  552. });
  553. this.box.autocomplete('widget').addClass('VS-interface');
  554. },
  555. // As the facet's input field grows, it may move to the next line in the
  556. // search box. `autoGrowInput` triggers an `updated` event on the input
  557. // field, which is bound to this method to move the autocomplete menu.
  558. moveAutocomplete : function() {
  559. var autocomplete = this.box.data('autocomplete');
  560. if (autocomplete) {
  561. autocomplete.menu.element.position({
  562. my : "left top",
  563. at : "left bottom",
  564. of : this.box.data('autocomplete').element,
  565. collision : "flip",
  566. offset : "0 5"
  567. });
  568. }
  569. },
  570. // When a user enters a facet and it is being edited, immediately show
  571. // the autocomplete menu and size it to match the contents.
  572. searchAutocomplete : function(e) {
  573. var autocomplete = this.box.data('autocomplete');
  574. if (autocomplete) {
  575. var menu = autocomplete.menu.element;
  576. autocomplete.search();
  577. // Resize the menu based on the correctly measured width of what's bigger:
  578. // the menu's original size or the menu items' new size.
  579. menu.outerWidth(Math.max(
  580. menu.width('').outerWidth(),
  581. autocomplete.element.outerWidth()
  582. ));
  583. }
  584. },
  585. // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
  586. // and anything else that takes focus out of the facet's input field.
  587. closeAutocomplete : function() {
  588. var autocomplete = this.box.data('autocomplete');
  589. if (autocomplete) autocomplete.close();
  590. },
  591. // Search terms used in the autocomplete menu. These are specific to the facet,
  592. // and only match for the facet's category. The values are then matched on the
  593. // first letter of any word in matches, and finally sorted according to the
  594. // value's own category. You can pass `preserveOrder` as an option in the
  595. // `facetMatches` callback to skip any further ordering done client-side.
  596. autocompleteValues : function(req, resp) {
  597. var category = this.model.get('category');
  598. var value = this.model.get('value');
  599. var searchTerm = req.term;
  600. this.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) {
  601. options = options || {};
  602. matches = matches || [];
  603. if (searchTerm && value != searchTerm) {
  604. if (options.preserveMatches) {
  605. resp(matches);
  606. } else {
  607. var re = VS.utils.inflector.escapeRegExp(searchTerm || '');
  608. var matcher = new RegExp('\\b' + re, 'i');
  609. matches = $.grep(matches, function(item) {
  610. return matcher.test(item) ||
  611. matcher.test(item.value) ||
  612. matcher.test(item.label);
  613. });
  614. }
  615. }
  616. if (options.preserveOrder) {
  617. resp(matches);
  618. } else {
  619. resp(_.sortBy(matches, function(match) {
  620. if (match == value || match.value == value) return '';
  621. else return match;
  622. }));
  623. }
  624. });
  625. },
  626. // Sets the facet's model's value.
  627. set : function(value) {
  628. if (!value) return;
  629. this.model.set({'value': value});
  630. },
  631. // Before the searchBox performs a search, we need to close the
  632. // autocomplete menu.
  633. search : function(e, direction) {
  634. if (!direction) direction = 1;
  635. this.closeAutocomplete();
  636. this.app.searchBox.searchEvent(e);
  637. _.defer(_.bind(function() {
  638. this.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order});
  639. }, this));
  640. },
  641. // Begin editing the facet's input. This is called when the user enters
  642. // the input either from another facet or directly clicking on it.
  643. //
  644. // This method tells all other facets and inputs to disable so it can have
  645. // the sole focus. It also prepares the autocompletion menu.
  646. enableEdit : function() {
  647. if (this.app.options.readOnly) return;
  648. if (this.modes.editing != 'is') {
  649. this.setMode('is', 'editing');
  650. this.deselectFacet();
  651. if (this.box.val() == '') {
  652. this.box.val(this.model.get('value'));
  653. }
  654. }
  655. this.flags.canClose = false;
  656. this.app.searchBox.disableFacets(this);
  657. this.app.searchBox.addFocus();
  658. _.defer(_.bind(function() {
  659. this.app.searchBox.addFocus();
  660. }, this));
  661. this.resize();
  662. this.searchAutocomplete();
  663. this.box.focus();
  664. },
  665. // When the user blurs the input, they may either be going to another input
  666. // or off the search box entirely. If they go to another input, this facet
  667. // will be instantly disabled, and the canClose flag will be turned back off.
  668. //
  669. // However, if the user clicks elsewhere on the page, this method starts a timer
  670. // that checks if any of the other inputs are selected or are being edited. If
  671. // not, then it can finally close itself and its autocomplete menu.
  672. deferDisableEdit : function() {
  673. this.flags.canClose = true;
  674. _.delay(_.bind(function() {
  675. if (this.flags.canClose && !this.box.is(':focus') &&
  676. this.modes.editing == 'is' && this.modes.selected != 'is') {
  677. this.disableEdit();
  678. }
  679. }, this), 250);
  680. },
  681. // Called either by other facets receiving focus or by the timer in `deferDisableEdit`,
  682. // this method will turn off the facet, remove any text selection, and close
  683. // the autocomplete menu.
  684. disableEdit : function() {
  685. var newFacetQuery = VS.utils.inflector.trim(this.box.val());
  686. if (newFacetQuery != this.model.get('value')) {
  687. this.set(newFacetQuery);
  688. }
  689. this.flags.canClose = false;
  690. this.box.selectRange(0, 0);
  691. this.box.blur();
  692. this.setMode('not', 'editing');
  693. this.closeAutocomplete();
  694. this.app.searchBox.removeFocus();
  695. },
  696. // Selects the facet, which blurs the facet's input and highlights the facet.
  697. // If this is the only facet being selected (and not part of a select all event),
  698. // we attach a mouse/keyboard watcher to check if the next action by the user
  699. // should delete this facet or just deselect it.
  700. selectFacet : function(e) {
  701. if (e) e.preventDefault();
  702. if (this.app.options.readOnly) return;
  703. var allSelected = this.app.searchBox.allSelected();
  704. if (this.modes.selected == 'is') return;
  705. if (this.box.is(':focus')) {
  706. this.box.setCursorPosition(0);
  707. this.box.blur();
  708. }
  709. this.flags.canClose = false;
  710. this.closeAutocomplete();
  711. this.setMode('is', 'selected');
  712. this.setMode('not', 'editing');
  713. if (!allSelected || e) {
  714. $(document).unbind('keydown.facet', this.keydown);
  715. $(document).unbind('click.facet', this.deselectFacet);
  716. _.defer(_.bind(function() {
  717. $(document).unbind('keydown.facet').bind('keydown.facet', this.keydown);
  718. $(document).unbind('click.facet').one('click.facet', this.deselectFacet);
  719. }, this));
  720. this.app.searchBox.disableFacets(this);
  721. this.app.searchBox.addFocus();
  722. }
  723. return false;
  724. },
  725. // Turns off highlighting on the facet. Called in a variety of ways, this
  726. // only deselects the facet if it is selected, and then cleans up the
  727. // keyboard/mouse watchers that were created when the facet was first
  728. // selected.
  729. deselectFacet : function(e) {
  730. if (e) e.preventDefault();
  731. if (this.modes.selected == 'is') {
  732. this.setMode('not', 'selected');
  733. this.closeAutocomplete();
  734. this.app.searchBox.removeFocus();
  735. }
  736. $(document).unbind('keydown.facet', this.keydown);
  737. $(document).unbind('click.facet', this.deselectFacet);
  738. return false;
  739. },
  740. // Is the user currently focused in this facet's input field?
  741. isFocused : function() {
  742. return this.box.is(':focus');
  743. },
  744. // Hovering over the delete button styles the facet so the user knows that
  745. // the delete button will kill the entire facet.
  746. showDelete : function() {
  747. $(this.el).addClass('search_facet_maybe_delete');
  748. },
  749. // On `mouseout`, the user is no longer hovering on the delete button.
  750. hideDelete : function() {
  751. $(this.el).removeClass('search_facet_maybe_delete');
  752. },
  753. // When switching between facets, depending on the direction the cursor is
  754. // coming from, the cursor in this facet's input field should match the original
  755. // direction.
  756. setCursorAtEnd : function(direction) {
  757. if (direction == -1) {
  758. this.box.setCursorPosition(this.box.val().length);
  759. } else {
  760. this.box.setCursorPosition(0);
  761. }
  762. },
  763. // Deletes the facet and sends the cursor over to the nearest input field.
  764. remove : function(e) {
  765. var committed = this.model.get('value');
  766. this.deselectFacet();
  767. this.disableEdit();
  768. this.app.searchQuery.remove(this.model);
  769. if (committed && this.app.options.autosearch) {
  770. this.search(e, -1);
  771. } else {
  772. this.app.searchBox.renderFacets();
  773. this.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order});
  774. }
  775. },
  776. // Selects the text in the facet's input field. When the user tabs between
  777. // facets, convention is to highlight the entire field.
  778. selectText: function() {
  779. this.box.selectRange(0, this.box.val().length);
  780. },
  781. // Handles all keyboard inputs when in the facet's input field. This checks
  782. // for movement between facets and inputs, entering a new value that needs
  783. // to be autocompleted, as well as the removal of this facet.
  784. keydown : function(e) {
  785. var key = VS.app.hotkeys.key(e);
  786. if (key == 'enter' && this.box.val()) {
  787. this.disableEdit();
  788. this.search(e);
  789. } else if (key == 'left') {
  790. if (this.modes.selected == 'is') {
  791. this.deselectFacet();
  792. this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
  793. } else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
  794. this.selectFacet();
  795. }
  796. } else if (key == 'right') {
  797. if (this.modes.selected == 'is') {
  798. e.preventDefault();
  799. this.deselectFacet();
  800. this.setCursorAtEnd(0);
  801. this.enableEdit();
  802. } else if (this.box.getCursorPosition() == this.box.val().length) {
  803. e.preventDefault();
  804. this.disableEdit();
  805. this.app.searchBox.focusNextFacet(this, 1);
  806. }
  807. } else if (VS.app.hotkeys.shift && key == 'tab') {
  808. e.preventDefault();
  809. this.app.searchBox.focusNextFacet(this, -1, {
  810. startAtEnd : -1,
  811. skipToFacet : true,
  812. selectText : true
  813. });
  814. } else if (key == 'tab') {
  815. e.preventDefault();
  816. this.app.searchBox.focusNextFacet(this, 1, {
  817. skipToFacet : true,
  818. selectText : true
  819. });
  820. } else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) {
  821. e.preventDefault();
  822. this.app.searchBox.selectAllFacets();
  823. return false;
  824. } else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') {
  825. this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
  826. this.remove(e);
  827. } else if (key == 'backspace') {
  828. $(document).on('keydown.backspace', function(e) {
  829. if (VS.app.hotkeys.key(e) === 'backspace') {
  830. e.preventDefault();
  831. }
  832. });
  833. $(document).on('keyup.backspace', function(e) {
  834. $(document).off('.backspace');
  835. });
  836. if (this.modes.selected == 'is') {
  837. e.preventDefault();
  838. this.remove(e);
  839. } else if (this.box.getCursorPosition() == 0 &&
  840. !this.box.getSelection().length) {
  841. e.preventDefault();
  842. this.selectFacet();
  843. }
  844. e.stopPropagation();
  845. }
  846. // Handle paste events
  847. if (e.which == null) {
  848. // this.searchAutocomplete(e);
  849. _.defer(_.bind(this.resize, this, e));
  850. } else {
  851. this.resize(e);
  852. }
  853. }
  854. });
  855. })();
  856. (function() {
  857. var $ = jQuery; // Handle namespaced jQuery
  858. // This is the visual search input that is responsible for creating new facets.
  859. // There is one input placed in between all facets.
  860. VS.ui.SearchInput = Backbone.View.extend({
  861. type : 'text',
  862. className : 'search_input ui-menu',
  863. events : {
  864. 'keypress input' : 'keypress',
  865. 'keydown input' : 'keydown',
  866. 'keyup input' : 'keyup',
  867. 'click input' : 'maybeTripleClick',
  868. 'dblclick input' : 'startTripleClickTimer'
  869. },
  870. initialize : function(options) {
  871. this.options = _.extend({}, this.options, options);
  872. this.app = this.options.app;
  873. this.flags = {
  874. canClose : false
  875. };
  876. _.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit');
  877. },
  878. // Rendering the input sets up autocomplete, events on focusing and blurring
  879. // the input, and the auto-grow of the input.
  880. render : function() {
  881. $(this.el).html(JST['search_input']({
  882. readOnly: this.app.options.readOnly
  883. }));
  884. this.setMode('not', 'editing');
  885. this.setMode('not', 'selected');
  886. this.box = this.$('input');
  887. this.box.autoGrowInput();
  888. this.box.bind('updated.autogrow', this.moveAutocomplete);
  889. this.box.bind('blur', this.deferDisableEdit);
  890. this.box.bind('focus', this.addFocus);
  891. this.setupAutocomplete();
  892. return this;
  893. },
  894. // Watches the input and presents an autocompleted menu, taking the
  895. // remainder of the input field and adding a separate facet for it.
  896. //
  897. // See `addTextFacetRemainder` for explanation on how the remainder works.
  898. setupAutocomplete : function() {
  899. this.box.autocomplete({
  900. minLength : this.options.showFacets ? 0 : 1,
  901. delay : 50,
  902. autoFocus : true,
  903. position : {offset : "0 -1"},
  904. source : _.bind(this.autocompleteValues, this),
  905. // Prevent changing the input value on focus of an option
  906. focus : function() { return false; },
  907. create : _.bind(function(e, ui) {
  908. $(this.el).find('.ui-autocomplete-input').css('z-index','auto');
  909. }, this),
  910. select : _.bind(function(e, ui) {
  911. e.preventDefault();
  912. // stopPropogation does weird things in jquery-ui 1.9
  913. // e.stopPropagation();
  914. var remainder = this.addTextFacetRemainder(ui.item.label || ui.item.value);
  915. var position = this.options.position + (remainder ? 1 : 0);
  916. this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position);
  917. return false;
  918. }, this)
  919. });
  920. // Renders the results grouped by the categories they belong to.
  921. this.box.data('autocomplete')._renderMenu = function(ul, items) {
  922. var category = '';
  923. _.each(items, _.bind(function(item, i) {
  924. if (item.category && item.category != category) {
  925. ul.append('<li class="ui-autocomplete-category">'+item.category+'</li>');
  926. category = item.category;
  927. }
  928. if(this._renderItemData) {
  929. this._renderItemData(ul, item);
  930. } else {
  931. this._renderItem(ul, item);
  932. }
  933. }, this));
  934. };
  935. this.box.autocomplete('widget').addClass('VS-interface');
  936. },
  937. // Search terms used in the autocomplete menu. The values are matched on the
  938. // first letter of any word in matches, and finally sorted according to the
  939. // value's own category. You can pass `preserveOrder` as an option in the
  940. // `facetMatches` callback to skip any further ordering done client-side.
  941. autocompleteValues : function(req, resp) {
  942. var searchTerm = req.term;
  943. var lastWord = searchTerm.match(/\w+\*?$/); // Autocomplete only last word.
  944. var re = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || '');
  945. this.app.options.callbacks.facetMatches(function(prefixes, options) {
  946. options = options || {};
  947. prefixes = prefixes || [];
  948. // Only match from the beginning of the word.
  949. var matcher = new RegExp('^' + re, 'i');
  950. var matches = $.grep(prefixes, function(item) {
  951. return item && matcher.test(item.label || item);
  952. });
  953. if (options.preserveOrder) {
  954. resp(matches);
  955. } else {
  956. resp(_.sortBy(matches, function(match) {
  957. if (match.label) return match.category + '-' + match.label;
  958. else return match;
  959. }));
  960. }
  961. });
  962. },
  963. // Closes the autocomplete menu. Called on disabling, selecting, deselecting,
  964. // and anything else that takes focus out of the facet's input field.
  965. closeAutocomplete : function() {
  966. var autocomplete = this.box.data('autocomplete');
  967. if (autocomplete) autocomplete.close();
  968. },
  969. // As the input field grows, it may move to the next line in the
  970. // search box. `autoGrowInput` triggers an `updated` event on the input
  971. // field, which is bound to this method to move the autocomplete menu.
  972. moveAutocomplete : function() {
  973. var autocomplete = this.box.data('autocomplete');
  974. if (autocomplete) {
  975. autocomplete.menu.element.position({
  976. my : "left top",
  977. at : "left bottom",
  978. of : this.box.data('autocomplete').element,
  979. collision : "none",
  980. offset : '0 -1'
  981. });
  982. }
  983. },
  984. // When a user enters a facet and it is being edited, immediately show
  985. // the autocomplete menu and size it to match the contents.
  986. searchAutocomplete : function(e) {
  987. var autocomplete = this.box.data('autocomplete');
  988. if (autocomplete) {
  989. var menu = autocomplete.menu.element;
  990. autocomplete.search();
  991. // Resize the menu based on the correctly measured width of what's bigger:
  992. // the menu's original size or the menu items' new size.
  993. menu.outerWidth(Math.max(
  994. menu.width('').outerWidth(),
  995. autocomplete.element.outerWidth()
  996. ));
  997. }
  998. },
  999. // If a user searches for "word word category", the category would be
  1000. // matched and autocompleted, and when selected, the "word word" would
  1001. // also be caught as the remainder and then added in its own facet.
  1002. addTextFacetRemainder : function(facetValue) {
  1003. var boxValue = this.box.val();
  1004. var lastWord = boxValue.match(/\b(\w+)$/);
  1005. if (!lastWord) {
  1006. return '';
  1007. }
  1008. var matcher = new RegExp(lastWord[0], "i");
  1009. if (facetValue.search(matcher) == 0) {
  1010. boxValue = boxValue.replace(/\b(\w+)$/, '');
  1011. }
  1012. boxValue = boxValue.replace('^\s+|\s+$', '');
  1013. if (boxValue) {
  1014. this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position);
  1015. }
  1016. return boxValue;
  1017. },
  1018. // Directly called to focus the input. This is different from `addFocus`
  1019. // because this is not called by a focus event. This instead calls a
  1020. // focus event causing the input to become focused.
  1021. enableEdit : function(selectText) {
  1022. this.addFocus();
  1023. if (selectText) {
  1024. this.selectText();
  1025. }
  1026. this.box.focus();
  1027. },
  1028. // Event called on user focus on the input. Tells all other input and facets
  1029. // to give up focus, and starts revving the autocomplete.
  1030. addFocus : function() {
  1031. this.flags.canClose = false;
  1032. if (!this.app.searchBox.allSelected()) {
  1033. this.app.searchBox.disableFacets(this);
  1034. }
  1035. this.app.searchBox.addFocus();
  1036. this.setMode('is', 'editing');
  1037. this.setMode('not', 'selected');
  1038. if (!this.app.searchBox.allSelected()) {
  1039. this.searchAutocomplete();
  1040. }
  1041. },
  1042. // Directly called to blur the input. This is different from `removeFocus`
  1043. // because this is not called by a blur event.
  1044. disableEdit : function() {
  1045. this.box.blur();
  1046. this.removeFocus();
  1047. },
  1048. // Event called when user blur's the input, either through the keyboard tabbing
  1049. // away or the mouse clicking off. Cleans up
  1050. removeFocus : function() {
  1051. this.flags.canClose = false;
  1052. this.app.searchBox.removeFocus();
  1053. this.setMode('not', 'editing');
  1054. this.setMode('not', 'selected');
  1055. this.closeAutocomplete();
  1056. },
  1057. // When the user blurs the input, they may either be going to another input
  1058. // or off the search box entirely. If they go to another input, this facet
  1059. // will be instantly disabled, and the canClose flag will be turned back off.
  1060. //
  1061. // However, if the user clicks elsewhere on the page, this method starts a timer
  1062. // that checks if any of the other inputs are selected or are being edited. If
  1063. // not, then it can finally close itself and its autocomplete menu.
  1064. deferDisableEdit : function() {
  1065. this.flags.canClose = true;
  1066. _.delay(_.bind(function() {
  1067. if (this.flags.canClose &&
  1068. !this.box.is(':focus') &&
  1069. this.modes.editing == 'is') {
  1070. this.disableEdit();
  1071. }
  1072. }, this), 250);
  1073. },
  1074. // Starts a timer that will cause a triple-click, which highlights all facets.
  1075. startTripleClickTimer : function() {
  1076. this.tripleClickTimer = setTimeout(_.bind(function() {
  1077. this.tripleClickTimer = null;
  1078. }, this), 500);
  1079. },
  1080. // Event on click that checks if a triple click is in play. The
  1081. // `tripleClickTimer` is counting down, ready to be engaged and intercept
  1082. // the click event to force a select all instead.
  1083. maybeTripleClick : function(e) {
  1084. if (this.app.options.readOnly) return;
  1085. if (!!this.tripleClickTimer) {
  1086. e.preventDefault();
  1087. this.app.searchBox.selectAllFacets();
  1088. return false;
  1089. }
  1090. },
  1091. // Is the user currently focused in the input field?
  1092. isFocused : function() {
  1093. return this.box.is(':focus');
  1094. },
  1095. // When serializing the facets, the inputs need to also have their values represented,
  1096. // in case they contain text that is not yet faceted (but will be once the search is
  1097. // completed).
  1098. value : function() {
  1099. return this.box.val();
  1100. },
  1101. // When switching between facets and inputs, depending on the direction the cursor
  1102. // is coming from, the cursor in this facet's input field should match the original
  1103. // direction.
  1104. setCursorAtEnd : function(direction) {
  1105. if (direction == -1) {
  1106. this.box.setCursorPosition(this.box.val().length);
  1107. } else {
  1108. this.box.setCursorPosition(0);
  1109. }
  1110. },
  1111. // Selects the entire range of text in the input. Useful when tabbing between inputs
  1112. // and facets.
  1113. selectText : function() {
  1114. this.box.selectRange(0, this.box.val().length);
  1115. if (!this.app.searchBox.allSelected()) {
  1116. this.box.focus();
  1117. } else {
  1118. this.setMode('is', 'selected');
  1119. }
  1120. },
  1121. // Before the searchBox performs a search, we need to close the
  1122. // autocomplete menu.
  1123. search : function(e, direction) {
  1124. if (!direction) direction = 0;
  1125. this.closeAutocomplete();
  1126. this.app.searchBox.searchEvent(e);
  1127. _.defer(_.bind(function() {
  1128. this.app.searchBox.focusNextFacet(this, direction);
  1129. }, this));
  1130. },
  1131. // Callback fired on key press in the search box. We search when they hit return.
  1132. keypress : function(e) {
  1133. var key = VS.app.hotkeys.key(e);
  1134. if (key == 'enter') {
  1135. return this.search(e, 100);
  1136. } else if (VS.app.hotkeys.colon(e)) {
  1137. this.box.trigger('resize.autogrow', e);
  1138. var query = this.box.val();
  1139. var prefixes = [];
  1140. this.app.options.callbacks.facetMatches(function(p) {
  1141. prefixes = p;
  1142. });
  1143. var labels = _.map(prefixes, function(prefix) {
  1144. if (prefix.label) return prefix.label;
  1145. else return prefix;
  1146. });
  1147. if (_.contains(labels, query)) {
  1148. e.preventDefault();
  1149. var remainder = this.addTextFacetRemainder(query);
  1150. var position = this.options.position + (remainder?1:0);
  1151. this.app.searchBox.addFacet(query, '', position);
  1152. return false;
  1153. }
  1154. } else if (key == 'backspace') {
  1155. if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
  1156. e.preventDefault();
  1157. e.stopPropagation();
  1158. e.stopImmediatePropagation();
  1159. this.app.searchBox.resizeFacets();
  1160. return false;
  1161. }
  1162. }
  1163. },
  1164. // Handles all keyboard inputs when in the input field. This checks
  1165. // for movement between facets and inputs, entering a new value that needs
  1166. // to be autocompleted, as well as stepping between facets with backspace.
  1167. keydown : function(e) {
  1168. var key = VS.app.hotkeys.key(e);
  1169. if (key == 'left') {
  1170. if (this.box.getCursorPosition() == 0) {
  1171. e.preventDefault();
  1172. this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
  1173. }
  1174. } else if (key == 'right') {
  1175. if (this.box.getCursorPosition() == this.box.val().length) {
  1176. e.preventDefault();
  1177. this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true});
  1178. }
  1179. } else if (VS.app.hotkeys.shift && key == 'tab') {
  1180. e.preventDefault();
  1181. this.app.searchBox.focusNextFacet(this, -1, {selectText: true});
  1182. } else if (key == 'tab') {
  1183. var value = this.box.val();
  1184. if (value.length) {
  1185. e.preventDefault();
  1186. var remainder = this.addTextFacetRemainder(value);
  1187. var position = this.options.position + (remainder?1:0);
  1188. if (value != remainder) {
  1189. this.app.searchBox.addFacet(value, '', position);
  1190. }
  1191. } else {
  1192. var foundFacet = this.app.searchBox.focusNextFacet(this, 0, {
  1193. skipToFacet: true,
  1194. selectText: true
  1195. });
  1196. if (foundFacet) {
  1197. e.preventDefault();
  1198. }
  1199. }
  1200. } else if (VS.app.hotkeys.command &&
  1201. String.fromCharCode(e.which).toLowerCase() == 'a') {
  1202. e.preventDefault();
  1203. this.app.searchBox.selectAllFacets();
  1204. return false;
  1205. } else if (key == 'backspace' && !this.app.searchBox.allSelected()) {
  1206. if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
  1207. e.preventDefault();
  1208. this.app.searchBox.focusNextFacet(this, -1, {backspace: true});
  1209. return false;
  1210. }
  1211. } else if (key == 'end') {
  1212. var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1];
  1213. view.setCursorAtEnd(-1);
  1214. } else if (key == 'home') {
  1215. var view = this.app.searchBox.inputViews[0];
  1216. view.setCursorAtEnd(-1);
  1217. }
  1218. },
  1219. // We should get the value of an input should be done
  1220. // on keyup since keydown gets the previous value and not the current one
  1221. keyup : function(e) {
  1222. this.box.trigger('resize.autogrow', e);
  1223. }
  1224. });
  1225. })();
  1226. (function(){
  1227. var $ = jQuery; // Handle namespaced jQuery
  1228. // Makes the view enter a mode. Modes have both a 'mode' and a 'group',
  1229. // and are mutually exclusive with any other modes in the same group.
  1230. // Setting will update the view's modes hash, as well as set an HTML class
  1231. // of *[mode]_[group]* on the view's element. Convenient way to swap styles
  1232. // and behavior.
  1233. Backbone.View.prototype.setMode = function(mode, group) {
  1234. this.modes || (this.modes = {});
  1235. if (this.modes[group] === mode) return;
  1236. $(this.el).setMode(mode, group);
  1237. this.modes[group] = mode;
  1238. };
  1239. })();
  1240. (function() {
  1241. var $ = jQuery; // Handle namespaced jQuery
  1242. // DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
  1243. // just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
  1244. // on `keydown`.
  1245. //
  1246. // For the most headache-free way to use this utility, check modifier keys,
  1247. // like shift and command, with `VS.app.hotkeys.shift`, and check every other
  1248. // key with `VS.app.hotkeys.key(e) == 'key_name'`.
  1249. VS.app.hotkeys = {
  1250. // Keys that will be mapped to the `hotkeys` namespace.
  1251. KEYS: {
  1252. '16': 'shift',
  1253. '17': 'command',
  1254. '91': 'command',
  1255. '93': 'command',
  1256. '224': 'command',
  1257. '13': 'enter',
  1258. '37': 'left',
  1259. '38': 'upArrow',
  1260. '39': 'right',
  1261. '40': 'downArrow',
  1262. '46': 'delete',
  1263. '8': 'backspace',
  1264. '35': 'end',
  1265. '36': 'home',
  1266. '9': 'tab',
  1267. '188': 'comma'
  1268. },
  1269. // Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
  1270. initialize : function() {
  1271. _.bindAll(this, 'down', 'up', 'blur');
  1272. $(document).bind('keydown', this.down);
  1273. $(document).bind('keyup', this.up);
  1274. $(window).bind('blur', this.blur);
  1275. },
  1276. // On `keydown`, turn on all keys that match.
  1277. down : function(e) {
  1278. var key = this.KEYS[e.which];
  1279. if (key) this[key] = true;
  1280. },
  1281. // On `keyup`, turn off all keys that match.
  1282. up : function(e) {
  1283. var key = this.KEYS[e.which];
  1284. if (key) this[key] = false;
  1285. },
  1286. // If an input is blurred, all keys need to be turned off, since they are no longer
  1287. // able to modify the document.
  1288. blur : function(e) {
  1289. for (var key in this.KEYS) this[this.KEYS[key]] = false;
  1290. },
  1291. // Check a key from an event and return the common english name.
  1292. key : function(e) {
  1293. return this.KEYS[e.which];
  1294. },
  1295. // Colon is special, since the value is different between browsers.
  1296. colon : function(e) {
  1297. var charCode = e.which;
  1298. return charCode && String.fromCharCode(charCode) == ":";
  1299. },
  1300. // Check a key from an event and match it against any known characters.
  1301. // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
  1302. //
  1303. // These were determined by looping through every `keyCode` and `charCode` that
  1304. // resulted from `keydown` and `keypress` events and counting what was printable.
  1305. printable : function(e) {
  1306. var code = e.which;
  1307. if (e.type == 'keydown') {
  1308. if (code == 32 || // space
  1309. (code >= 48 && code <= 90) || // 0-1a-z
  1310. (code >= 96 && code <= 111) || // 0-9+-/*.
  1311. (code >= 186 && code <= 192) || // ;=,-./^
  1312. (code >= 219 && code <= 222)) { // (\)'
  1313. return true;
  1314. }
  1315. } else {
  1316. // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
  1317. if ((code >= 32 && code <= 126) ||
  1318. (code >= 160 && code <= 500) ||
  1319. (String.fromCharCode(code) == ":")) {
  1320. return true;
  1321. }
  1322. }
  1323. return false;
  1324. }
  1325. };
  1326. })();
  1327. (function() {
  1328. var $ = jQuery; // Handle namespaced jQuery
  1329. // Naive English transformations on words. Only used for a few transformations
  1330. // in VisualSearch.js.
  1331. VS.utils.inflector = {
  1332. // Delegate to the ECMA5 String.prototype.trim function, if available.
  1333. trim : function(s) {
  1334. return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
  1335. },
  1336. // Escape strings that are going to be used in a regex. Escapes punctuation
  1337. // that would be incorrect in a regex.
  1338. escapeRegExp : function(s) {
  1339. return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
  1340. }
  1341. };
  1342. })();
  1343. (function() {
  1344. var $ = jQuery; // Handle namespaced jQuery
  1345. $.fn.extend({
  1346. // Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
  1347. // and are mutually exclusive with any other modes in the same group.
  1348. // Setting will update the view's modes hash, as well as set an HTML class
  1349. // of *[mode]_[group]* on the view's element. Convenient way to swap styles
  1350. // and behavior.
  1351. setMode : function(state, group) {
  1352. group = group || 'mode';
  1353. var re = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
  1354. var mode = (state === null) ? "" : state + "_" + group;
  1355. this.each(function() {
  1356. this.className = (this.className.replace(re, '')+' '+mode)
  1357. .replace(/\s\s/g, ' ');
  1358. });
  1359. return mode;
  1360. },
  1361. // When attached to an input element, this will cause the width of the input
  1362. // to match its contents. This calculates the width of the contents of the input
  1363. // by measuring a hidden shadow div that should match the styling of the input.
  1364. autoGrowInput: function() {
  1365. return this.each(function() {
  1366. var $input = $(this);
  1367. var $tester = $('<div />').css({
  1368. opacity : 0,
  1369. top : -9999,
  1370. left : -9999,
  1371. position : 'absolute',
  1372. whiteSpace : 'nowrap'
  1373. }).addClass('VS-input-width-tester').addClass('VS-interface');
  1374. // Watch for input value changes on all of these events. `resize`
  1375. // event is called explicitly when the input has been changed without
  1376. // a single keypress.
  1377. var events = 'keydown.autogrow keypress.autogrow ' +
  1378. 'resize.autogrow change.autogrow';
  1379. $input.next('.VS-input-width-tester').remove();
  1380. $input.after($tester);
  1381. $input.unbind(events).bind(events, function(e, realEvent) {
  1382. if (realEvent) e = realEvent;
  1383. var value = $input.val();
  1384. // Watching for the backspace key is tricky because it may not
  1385. // actually be deleting the character, but instead the key gets
  1386. // redirected to move the cursor from facet to facet.
  1387. if (VS.app.hotkeys.key(e) == 'backspace') {
  1388. var position = $input.getCursorPosition();
  1389. if (position > 0) value = value.slice(0, position-1) +
  1390. value.slice(position, value.length);
  1391. } else if (VS.app.hotkeys.printable(e) &&
  1392. !VS.app.hotkeys.command) {
  1393. value += String.fromCharCode(e.which);
  1394. }
  1395. value = value.replace(/&/g, '&amp;')
  1396. .replace(/\s/g,'&nbsp;')
  1397. .replace(/</g, '&lt;')
  1398. .replace(/>/g, '&gt;');
  1399. $tester.html(value);
  1400. $input.width($tester.width() + 3 + parseInt($input.css('min-width')));
  1401. $input.trigger('updated.autogrow');
  1402. });
  1403. // Sets the width of the input on initialization.
  1404. $input.trigger('resize.autogrow');
  1405. });
  1406. },
  1407. // Cross-browser method used for calculating where the cursor is in an
  1408. // input field.
  1409. getCursorPosition: function() {
  1410. var position = 0;
  1411. var input = this.get(0);
  1412. if (document.selection) { // IE
  1413. input.focus();
  1414. var sel = document.selection.createRange();
  1415. var selLen = document.selection.createRange().text.length;
  1416. sel.moveStart('character', -input.value.length);
  1417. position = sel.text.length - selLen;
  1418. } else if (input && $(input).is(':visible') &&
  1419. input.selectionStart != null) { // Firefox/Safari
  1420. position = input.selectionStart;
  1421. }
  1422. return position;
  1423. },
  1424. // A simple proxy for `selectRange` that sets the cursor position in an
  1425. // input field.
  1426. setCursorPosition: function(position) {
  1427. return this.each(function() {
  1428. return $(this).selectRange(position, position);
  1429. });
  1430. },
  1431. // Cross-browser way to select text in an input field.
  1432. selectRange: function(start, end) {
  1433. return this.filter(':visible').each(function() {
  1434. if (this.setSelectionRange) { // FF/Webkit
  1435. this.focus();
  1436. this.setSelectionRange(start, end);
  1437. } else if (this.createTextRange) { // IE
  1438. var range = this.createTextRange();
  1439. range.collapse(true);
  1440. range.moveEnd('character', end);
  1441. range.moveStart('character', start);
  1442. if (end - start >= 0) range.select();
  1443. }
  1444. });
  1445. },
  1446. // Returns an object that contains the text selection range values for
  1447. // an input field.
  1448. getSelection: function() {
  1449. var input = this[0];
  1450. if (input.selectionStart != null) { // FF/Webkit
  1451. var start = input.selectionStart;
  1452. var end = input.selectionEnd;
  1453. return {
  1454. start : start,
  1455. end : end,
  1456. length : end-start,
  1457. text : input.value.substr(start, end-start)
  1458. };
  1459. } else if (document.selection) { // IE
  1460. var range = document.selection.createRange();
  1461. if (range) {
  1462. var textRange = input.createTextRange();
  1463. var copyRange = textRange.duplicate();
  1464. textRange.moveToBookmark(range.getBookmark());
  1465. copyRange.setEndPoint('EndToStart', textRange);
  1466. var start = copyRange.text.length;
  1467. var end = start + range.text.length;
  1468. return {
  1469. start : start,
  1470. end : end,
  1471. length : end-start,
  1472. text : range.text
  1473. };
  1474. }
  1475. }
  1476. return {start: 0, end: 0, length: 0};
  1477. }
  1478. });
  1479. // Debugging in Internet Explorer. This allows you to use
  1480. // `console.log(['message', var1, var2, ...])`. Just remove the `false` and
  1481. // add your console.logs. This will automatically stringify objects using
  1482. // `JSON.stringify', so you can read what's going out. Think of this as a
  1483. // *Diet Firebug Lite Zero with Lemon*.
  1484. if (false) {
  1485. window.console = {};
  1486. var _$ied;
  1487. window.console.log = function(msg) {
  1488. if (_.isArray(msg)) {
  1489. var message = msg[0];
  1490. var vars = _.map(msg.slice(1), function(arg) {
  1491. return JSON.stringify(arg);
  1492. }).join(' - ');
  1493. }
  1494. if(!_$ied){
  1495. _$ied = $('<div><ol></ol></div>').css({
  1496. 'position': 'fixed',
  1497. 'bottom': 10,
  1498. 'left': 10,
  1499. 'zIndex': 20000,
  1500. 'width': $('body').width() - 80,
  1501. 'border': '1px solid #000',
  1502. 'padding': '10px',
  1503. 'backgroundColor': '#fff',
  1504. 'fontFamily': 'arial,helvetica,sans-serif',
  1505. 'fontSize': '11px'
  1506. });
  1507. $('body').append(_$ied);
  1508. }
  1509. var $message = $('<li>'+message+' - '+vars+'</li>').css({
  1510. 'borderBottom': '1px solid #999999'
  1511. });
  1512. _$ied.find('ol').append($message);
  1513. _.delay(function() {
  1514. $message.fadeOut(500);
  1515. }, 5000);
  1516. };
  1517. }
  1518. })();
  1519. (function() {
  1520. var $ = jQuery; // Handle namespaced jQuery
  1521. // Used to extract keywords and facets from the free text search.
  1522. var QUOTES_RE = "('[^']+'|\"[^\"]+\")";
  1523. var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
  1524. var CATEGORY_RE = FREETEXT_RE + ':\\s*';
  1525. VS.app.SearchParser = {
  1526. // Matches `category: "free text"`, with and without quotes.
  1527. ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),
  1528. // Matches a single category without the text. Used to correctly extract facets.
  1529. CATEGORY : new RegExp(CATEGORY_RE),
  1530. // Called to parse a query into a collection of `SearchFacet` models.
  1531. parse : function(instance, query) {
  1532. var searchFacets = this._extractAllFacets(instance, query);
  1533. instance.searchQuery.reset(searchFacets);
  1534. return searchFacets;
  1535. },
  1536. // Walks the query and extracts facets, categories, and free text.
  1537. _extractAllFacets : function(instance, query) {
  1538. var facets = [];
  1539. var originalQuery = query;
  1540. while (query) {
  1541. var category, value;
  1542. originalQuery = query;
  1543. var field = this._extractNextField(query);
  1544. if (!field) {
  1545. category = instance.options.remainder;
  1546. value = this._extractSearchText(query);
  1547. query = VS.utils.inflector.trim(query.replace(value, ''));
  1548. } else if (field.indexOf(':') != -1) {
  1549. category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
  1550. value = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
  1551. query = VS.utils.inflector.trim(query.replace(field, ''));
  1552. } else if (field.indexOf(':') == -1) {
  1553. category = instance.options.remainder;
  1554. value = field;
  1555. query = VS.utils.inflector.trim(query.replace(value, ''));
  1556. }
  1557. if (category && value) {
  1558. var searchFacet = new VS.model.SearchFacet({
  1559. category : category,
  1560. value : VS.utils.inflector.trim(value),
  1561. app : instance
  1562. });
  1563. facets.push(searchFacet);
  1564. }
  1565. if (originalQuery == query) break;
  1566. }
  1567. return facets;
  1568. },
  1569. // Extracts the first field found, capturing any free text that comes
  1570. // before the category.
  1571. _extractNextField : function(query) {
  1572. var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
  1573. var textMatch = query.match(textRe);
  1574. if (textMatch && textMatch.length >= 1) {
  1575. return textMatch[1];
  1576. } else {
  1577. return this._extractFirstField(query);
  1578. }
  1579. },
  1580. // If there is no free text before the facet, extract the category and value.
  1581. _extractFirstField : function(query) {
  1582. var fields = query.match(this.ALL_FIELDS);
  1583. return fields && fields.length && fields[0];
  1584. },
  1585. // If the found match is not a category and facet, extract the trimmed free text.
  1586. _extractSearchText : function(query) {
  1587. query = query || '';
  1588. var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
  1589. return text;
  1590. }
  1591. };
  1592. })();
  1593. (function() {
  1594. var $ = jQuery; // Handle namespaced jQuery
  1595. // The model that holds individual search facets and their categories.
  1596. // Held in a collection by `VS.app.searchQuery`.
  1597. VS.model.SearchFacet = Backbone.Model.extend({
  1598. // Extract the category and value and serialize it in preparation for
  1599. // turning the entire searchBox into a search query that can be sent
  1600. // to the server for parsing and searching.
  1601. serialize : function() {
  1602. var category = this.quoteCategory(this.get('category'));
  1603. var value = VS.utils.inflector.trim(this.get('value'));
  1604. var remainder = this.get("app").options.remainder;
  1605. if (!value) return '';
  1606. if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
  1607. value = this.quoteValue(value);
  1608. }
  1609. if (category != remainder) {
  1610. category = category + ': ';
  1611. } else {
  1612. category = "";
  1613. }
  1614. return category + value;
  1615. },
  1616. // Wrap categories that have spaces or any kind of quote with opposite matching
  1617. // quotes to preserve the complex category during serialization.
  1618. quoteCategory : function(category) {
  1619. var hasDoubleQuote = (/"/).test(category);
  1620. var hasSingleQuote = (/'/).test(category);
  1621. var hasSpace = (/\s/).test(category);
  1622. if (hasDoubleQuote && !hasSingleQuote) {
  1623. return "'" + category + "'";
  1624. } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
  1625. return '"' + category + '"';
  1626. } else {
  1627. return category;
  1628. }
  1629. },
  1630. // Wrap values that have quotes in opposite matching quotes. If a value has
  1631. // both single and double quotes, just use the double quotes.
  1632. quoteValue : function(value) {
  1633. var hasDoubleQuote = (/"/).test(value);
  1634. var hasSingleQuote = (/'/).test(value);
  1635. if (hasDoubleQuote && !hasSingleQuote) {
  1636. return "'" + value + "'";
  1637. } else {
  1638. return '"' + value + '"';
  1639. }
  1640. },
  1641. // If provided, use a custom label instead of the raw value.
  1642. label : function() {
  1643. return this.get('label') || this.get('value');
  1644. }
  1645. });
  1646. })();
  1647. (function() {
  1648. var $ = jQuery; // Handle namespaced jQuery
  1649. // Collection which holds all of the individual facets (category: value).
  1650. // Used for finding and removing specific facets.
  1651. VS.model.SearchQuery = Backbone.Collection.extend({
  1652. // Model holds the category and value of the facet.
  1653. model : VS.model.SearchFacet,
  1654. // Turns all of the facets into a single serialized string.
  1655. serialize : function() {
  1656. return this.map(function(facet){ return facet.serialize(); }).join(' ');
  1657. },
  1658. facets : function() {
  1659. return this.map(function(facet) {
  1660. var value = {};
  1661. value[facet.get('category')] = facet.get('value');
  1662. return value;
  1663. });
  1664. },
  1665. // Find a facet by its category. Multiple facets with the same category
  1666. // is fine, but only the first is returned.
  1667. find : function(category) {
  1668. var facet = this.detect(function(facet) {
  1669. return facet.get('category').toLowerCase() == category.toLowerCase();
  1670. });
  1671. return facet && facet.get('value');
  1672. },
  1673. // Counts the number of times a specific category is in the search query.
  1674. count : function(category) {
  1675. return this.select(function(facet) {
  1676. return facet.get('category').toLowerCase() == category.toLowerCase();
  1677. }).length;
  1678. },
  1679. // Returns an array of extracted values from each facet in a category.
  1680. values : function(category) {
  1681. var facets = this.select(function(facet) {
  1682. return facet.get('category').toLowerCase() == category.toLowerCase();
  1683. });
  1684. return _.map(facets, function(facet) { return facet.get('value'); });
  1685. },
  1686. // Checks all facets for matches of either a category or both category and value.
  1687. has : function(category, value) {
  1688. return this.any(function(facet) {
  1689. var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
  1690. if (!value) return categoryMatched;
  1691. return categoryMatched && facet.get('value') == value;
  1692. });
  1693. },
  1694. // Used to temporarily hide specific categories and serialize the search query.
  1695. withoutCategory : function() {
  1696. var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
  1697. return this.map(function(facet) {
  1698. if (!_.include(categories, facet.get('category').toLowerCase())) {
  1699. return facet.serialize();
  1700. };
  1701. }).join(' ');
  1702. }
  1703. });
  1704. })();
  1705. (function(){
  1706. window.JST = window.JST || {};
  1707. window.JST['search_box'] = _.template('<div class="VS-search <% if (readOnly) { %>VS-readonly<% } %>">\n <div class="VS-search-box-wrapper VS-search-box">\n <div class="VS-icon VS-icon-search"></div>\n <div class="VS-placeholder"></div>\n <div class="VS-search-inner"></div>\n <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n </div>\n</div>');
  1708. window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n <input type="text" class="search_facet_input ui-menu VS-interface" value="" <% if (readOnly) { %>disabled="disabled"<% } %> />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
  1709. window.JST['search_input'] = _.template('<input type="text" class="ui-menu" <% if (readOnly) { %>disabled="disabled"<% } %> />');
  1710. })();