PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/pancake-web/pancake/web/static/js/controllers/searchcontroller.js

https://bitbucket.org/mozillapancake/pancake
JavaScript | 444 lines | 269 code | 76 blank | 99 comment | 15 complexity | d825f0cd582be067078ed162d85c6dd9 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, MIT, Apache-2.0
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define([
  5. 'config',
  6. 'config/searchProviders',
  7. 'underscore',
  8. 'backbone',
  9. 'lib/template',
  10. 'lib/routecontroller',
  11. 'models/searchcollections',
  12. 'views/stackheadinglistview',
  13. 'lib/promise',
  14. 'lib/bridge',
  15. 'lib/url',
  16. 'views/widgetview',
  17. 'views/searchviews',
  18. 'views/emptysearchresult',
  19. 'logger'
  20. ], function(
  21. config,
  22. searchProviders,
  23. util,
  24. Backbone,
  25. template,
  26. RouteController,
  27. searchCollections,
  28. StackHeadingListView,
  29. Promise,
  30. Bridge,
  31. Url,
  32. WidgetView,
  33. searchViews,
  34. EmptySearchResultView,
  35. logger
  36. ) {
  37. // We need to pass an empty array into empty collections that need options
  38. // (null gets interpreted as a value and wrapped). It's a bit fussy, but it
  39. // should be faster to share a single object than create one.
  40. var emptyArray = [];
  41. var bridge = new Bridge();
  42. var onStackVisit = function(view, model, e) {
  43. e.preventDefault();
  44. // Make a PUT request to the server to get a new session_id.
  45. // The model will come back as the first argument of success callback,
  46. // with a session_id added.
  47. model.save(null, {
  48. // Change the URL hit with this call to a PUT against
  49. // `lattice/:username/stack/search`. Instead of hard-coding, crawl
  50. // back up to the stack and grab its URL, just in case.
  51. // SiteModel.PlaceCollection.StackModel.StackCollection.
  52. url: model.collection.stack().collection.url(),
  53. success: function (model) {
  54. var data = util.extend(model.toJSON(), { origin: config.appName });
  55. bridge.changeStack(data).showViewer();
  56. },
  57. error: function(err){
  58. logger.error("Error encountered when handling stack launch", err);
  59. }
  60. });
  61. };
  62. var SearchController = RouteController.extend({
  63. initialize: function (options) {
  64. // Initialize collections object for storing search collections.
  65. var collections = this.collections = {};
  66. var termsModel = this.termsModel = new Backbone.Model({ terms: '' });
  67. // Create an empty `StackSearchCollection` to be shared for
  68. // the lifetime of this controller. Views are relied on to
  69. // adapt to the changing contents of this controller.
  70. collections.stacks = new searchCollections.Stack(emptyArray, {
  71. urlTokens: { method: 'search' },
  72. urlQuery: {
  73. // Set the `limit` param to 4 -- the maximum number of stacks
  74. // that can be returned with the search.
  75. l: 4,
  76. // `q: query.terms,` should be manually configured in the `enter`
  77. // method using `terms` method.
  78. //
  79. // We want to use the `v3` version of this API -- it has the
  80. // `matches` property for searches.
  81. v: 3,
  82. // Set the maximum number of places to be returned by this API.
  83. p: 3
  84. }
  85. });
  86. // > Depending on how many providers we end up with, we may want to
  87. // > figure out a way to lazily create collections based on what we
  88. // > actually need. Prototypal objects are fast, though, so it might
  89. // > not actually matter -GB.
  90. collections.bings = new searchCollections.Bing(emptyArray, {
  91. urlTokens: {
  92. base: config.searchRoot,
  93. path: 'bing'
  94. },
  95. urlQuery: {
  96. // `q: query.terms,` should be manually configured in the `enter`
  97. // method using `terms` method.
  98. format: 'json'
  99. }
  100. });
  101. collections.tweets = new searchCollections.Tweets(emptyArray, {
  102. urlTokens: {
  103. base: config.searchRoot,
  104. path: 'twitter'
  105. },
  106. urlQuery: {
  107. // `q: query.terms,` should be manually configured in the `enter`
  108. // method using `terms` method.
  109. format: 'json'
  110. }
  111. });
  112. // create a collection for any URLs we match in the query terms
  113. collections.matchedUrls = new searchCollections.Url([]);
  114. this.mainView = new searchViews.SearchTabsView();
  115. var emptyResultsView = this.emptyResultsView = new EmptySearchResultView({ model: termsModel });
  116. emptyResultsView.toggle(false); // hide initially
  117. // bind to the 'goto' events that empty view triggers when its buttons are clicked
  118. emptyResultsView.bind('goto', util.bind(function (path, e) {
  119. e.preventDefault();
  120. var appModel = this.appView.model;
  121. switch(path) {
  122. case 'newsearch':
  123. // place focus in the search bar
  124. var searchBar = this.appView.widget('#searchbar');
  125. if(searchBar)
  126. searchBar.clear().focus();
  127. break;
  128. default:
  129. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  130. bridge.navigate(path, true);
  131. }
  132. this.termsModel.set({ terms: '' });
  133. emptyResultsView.toggle(false);
  134. }, this));
  135. var stackListView = this.stacks(collections.stacks);
  136. var urlListView = this.urls(collections.matchedUrls);
  137. var bingListView = this.bings(collections.bings);
  138. var tweetListView = this.tweets(collections.tweets);
  139. this.mainView.widgets({
  140. '#url-results': urlListView,
  141. '#stack-results': stackListView,
  142. '#bing-results': bingListView,
  143. '#twitter-results': tweetListView,
  144. '#empty-results': emptyResultsView
  145. });
  146. this.mainView.bind('activate', function (view) {
  147. var activeView = view.widget(view.active);
  148. var collection = activeView.collection;
  149. if (collection.state() === 'invalid') {
  150. activeView.render();
  151. collection.fetchDebounced();
  152. }
  153. });
  154. this.mainView.activateTab('#bing-results');
  155. },
  156. enter: function (query, options) {
  157. util.extend(this, options || {});
  158. // Set this view as active in the app.
  159. this.appView.widget(
  160. '#main',
  161. this.mainView
  162. );
  163. },
  164. update: function (query, options) {
  165. // Update instance from options.
  166. //
  167. // TODO: I'm not wild about the way this works -- it's too brittle -GB
  168. util.extend(this, options || {});
  169. var terms = this.parseQuery(query).terms;
  170. var collections = this.collections;
  171. var termsModel = this.termsModel;
  172. termsModel.set({ terms: terms });
  173. var emptyResultsView = this.emptyResultsView;
  174. emptyResultsView.toggle(false); // hide initially
  175. var stacks = collections.stacks;
  176. // Update terms on all collections.
  177. for(var key in collections){
  178. if(collections[key].terms) collections[key].terms(terms);
  179. }
  180. var urls = this.parseQuery(query).urls;
  181. if(urls && urls.length){
  182. urls = urls.map( searchCollections.Url.prepareModelFromUrl );
  183. collections.matchedUrls.reset(urls);
  184. } else {
  185. collections.matchedUrls.reset([]);
  186. }
  187. var activeCollection = this.mainView.widget(this.mainView.active).collection;
  188. // Fetch active search collection.
  189. var searchPromise = activeCollection.fetchDebounced();
  190. // Fetch stacks.
  191. var stacksPromise = stacks.fetchDebounced();
  192. // Execute in parallel, but callback when both are complete:
  193. Promise.all([
  194. searchPromise,
  195. stacksPromise
  196. ]).then(function(){
  197. var totalResults = collections.matchedUrls.length +
  198. stacks.length +
  199. activeCollection.length;
  200. if(!totalResults){
  201. // only show the empty results view when we have 0 results
  202. emptyResultsView.toggle(true);
  203. }
  204. },
  205. function(){
  206. logger.log("errback from search and stack debounced fetches: ", arguments);
  207. }
  208. );
  209. },
  210. reset: function(){
  211. // flush out all the collections
  212. util.each(this.collections, function(collection, name, collections){
  213. if(collection.reset){
  214. collection.reset([]);
  215. }
  216. });
  217. },
  218. exit: function () {
  219. // Empty collections on exit.
  220. var collections = this.collections;
  221. for (var key in collections) collections[key].reset();
  222. // These memoized collections are available as properties on the
  223. // bings collection.
  224. this.collections.bings.images().reset();
  225. this.collections.bings.suggestions().reset();
  226. },
  227. // Centralizing the scratch logic it takes to create and configure a
  228. // stackListView for the search controller.
  229. stacks: function (collection) {
  230. var stackListView = new StackHeadingListView({
  231. className: 'headinglist headinglist--with-divider',
  232. collection: collection
  233. });
  234. stackListView.bind('visit', onStackVisit);
  235. return stackListView;
  236. },
  237. // Factory function for creating url-match result view with configured
  238. // click handlers.
  239. urls: function(collection){
  240. var searchUrlsView = new searchViews.SearchUrlListView({
  241. collection: collection
  242. });
  243. // Define a handler for `visit` events on the `searchBingView`.
  244. searchUrlsView.bind('visit', function (placeView, urlModel, e) {
  245. e.preventDefault();
  246. logger.log('searchcontroller: visit to URL query match: ', urlModel.get('place_url'), urlModel.toJSON());
  247. var msg = util.extend(urlModel.toJSON(), {
  248. title: urlModel.get('place_title'),
  249. url: urlModel.get('place_url'),
  250. origin: config.appName
  251. });
  252. // Clear search field. Reset to home.
  253. var appModel = this.appView.model;
  254. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  255. // Send message up.
  256. bridge.createDirectStack(msg);
  257. }, this);
  258. return searchUrlsView;
  259. },
  260. // Factory function for creating tweet result view with configured
  261. // click handlers. This is all a little bit obtuse, and could probably
  262. // be cleaner, but I want to avoid binding handlers in the view constructor.
  263. tweets: function (collection) {
  264. var searchTweetsView = new searchViews.SearchTweetsView({
  265. collection: collection
  266. });
  267. searchTweetsView.bind('visit', function (placeView, siteModel, e) {
  268. e.preventDefault();
  269. var terms = siteModel.collection.terms();
  270. var msg = util.extend(siteModel.toJSON(), {
  271. search_terms: terms,
  272. // TODO: GB: hard-coded. This should be pulled from a list of registered
  273. // search providers -- probably from an API somewhere. The client should
  274. // be dumb.
  275. search_provider: 'twitter',
  276. // TODO: GB: hard-coded. This should be pulled from a list of registered
  277. // search providers -- probably from an API somewhere. The client should
  278. // be dumb.
  279. search_url: template(config.searchResults)({
  280. query: terms,
  281. provider: 'twitter'
  282. }),
  283. origin: config.appName
  284. });
  285. // Clear search field. Reset to home.
  286. var appModel = this.appView.model;
  287. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  288. // Send message up.
  289. bridge.createStack(msg);
  290. }, this);
  291. return searchTweetsView;
  292. },
  293. // Factory function for creating bing result view with configured
  294. // click handlers. This is all a little bit obtuse, and could probably
  295. // be cleaner, but I want to avoid binding handlers in the view constructor.
  296. bings: function (collection) {
  297. var bingView = new searchViews.SearchBingView({
  298. collection: collection
  299. });
  300. // Define a handler for `visit` events on the `view`.
  301. bingView.bind('visit', function (placeView, siteModel, e) {
  302. e.preventDefault();
  303. // Capture search terms by calling the custom `terms` method on the
  304. // this collection.
  305. // Capture terms from collection via closure, at event-time.
  306. var terms = collection.terms();
  307. var msg = util.extend(siteModel.toJSON(), {
  308. search_terms: terms,
  309. // TODO: GB: hard-coded. This should be pulled from a list of registered
  310. // search providers -- probably from an API somewhere. The client should
  311. // be dumb.
  312. search_provider: 'bing',
  313. search_url: template(config.searchResults)({
  314. query: terms,
  315. provider: 'bing'
  316. }),
  317. origin: config.appName
  318. });
  319. // We have to access this property at runtime, rather than via closure
  320. // because `bings` is called at initialization, but `this.appView` isn't
  321. // part of the object until it is mixed in via `enter` method.
  322. //
  323. // TODO: this is confusing and brittle. We should handle requirements
  324. // during construction.
  325. var appModel = this.appView.model;
  326. // Clear search field. Reset to home.
  327. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  328. // Send message up.
  329. bridge.createStack(msg);
  330. }, this)
  331. .bind('suggest', function (view, model, e) {
  332. e.preventDefault();
  333. // We have to access this property at runtime, rather than via closure
  334. // because `bings` is called at initialization, but `this.appView` isn't
  335. // part of the object until it is mixed in via `enter` method.
  336. //
  337. // TODO: this is confusing and brittle. We should handle requirements
  338. // during construction.
  339. var appModel = this.appView.model;
  340. appModel.set({ search: model.get('place_title') });
  341. }, this);
  342. return bingView;
  343. },
  344. parseQuery: function (query) {
  345. // parse out the query - it can be something like twitter/term1/term2
  346. // or just 'term',
  347. // or http://google.com/?q=blah
  348. // turn out '/' delimited resource paths into space delimited keywords
  349. if(query.match(/^\w+\/\w+/)){
  350. query = query.replace(/\//g, ' ');
  351. }
  352. var terms = query.split(/\s/),
  353. // a master list of search providers we support, with name: prefix.
  354. // define in config maybe?
  355. providers = searchProviders;
  356. var parsed = {};
  357. // We consider the first term to be a provider if it matches one of the
  358. // providers in our list.
  359. if(terms.length > 1 && providers[terms[0]]) {
  360. parsed.provider = terms.shift();
  361. }
  362. parsed.terms = terms.join(' ');
  363. var urlMatches = parsed.urls = Url.parseForUrls(query);
  364. return parsed;
  365. }
  366. });
  367. return new SearchController();
  368. });