PageRenderTime 47ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/static/js/controllers/searchcontroller.js

https://bitbucket.org/gordonbrander/accordion-drawer-prototypes
JavaScript | 321 lines | 185 code | 54 blank | 82 comment | 7 complexity | d2d9f0e8b966953aaca3c6c96f6dc125 MD5 | raw file
Possible License(s): Apache-2.0
  1. define([
  2. 'config',
  3. 'config/searchProviders',
  4. 'underscore',
  5. 'backbone',
  6. 'lib/template',
  7. 'lib/routecontroller',
  8. 'models/searchcollections',
  9. 'views/stackheadinglistview',
  10. 'views/postlistview',
  11. 'lib/bridge',
  12. 'views/widgetview',
  13. 'views/searchviews',
  14. 'logger'
  15. ], function(
  16. config,
  17. searchProviders,
  18. util,
  19. Backbone,
  20. template,
  21. RouteController,
  22. searchCollections,
  23. StackHeadingListView,
  24. PostListView,
  25. Bridge,
  26. WidgetView,
  27. searchViews,
  28. logger
  29. ) {
  30. // We need to pass an empty array into empty collections that need options
  31. // (null gets interpreted as a value and wrapped). It's a bit fussy, but it
  32. // should be faster to share a single object than create one.
  33. var emptyArray = [];
  34. var bridge = new Bridge();
  35. var onStackVisit = function(view, model, e) {
  36. e.preventDefault();
  37. // Make a PUT request to the server to get a new session_id.
  38. // The model will come back as the first argument of success callback,
  39. // with a session_id added.
  40. model.save(null, {
  41. // Change the URL hit with this call to a PUT against
  42. // `lattice/:username/stack/search`. Instead of hard-coding, crawl
  43. // back up to the stack and grab its URL, just in case.
  44. // SiteModel.PlaceCollection.StackModel.StackCollection.
  45. url: model.parent().stack().parent().url(),
  46. success: function (model) {
  47. var data = util.extend(model.toJSON(), { origin: config.appName });
  48. bridge.changeStack(data).showViewer();
  49. },
  50. error: function(err){
  51. logger.error("Error encountered when handling stack launch", err);
  52. }
  53. });
  54. };
  55. var SearchController = RouteController.extend({
  56. initialize: function (options) {
  57. // Initialize collections object for storing search collections.
  58. var collections = this.collections = {};
  59. // Create an empty `StackSearchCollection` to be shared for
  60. // the lifetime of this controller. Views are relied on to
  61. // adapt to the changing contents of this controller.
  62. collections.stacks = new searchCollections.Stack(emptyArray, {
  63. urlTokens: { method: 'search' },
  64. urlQuery: {
  65. // Set the `limit` param to 4 -- the maximum number of stacks
  66. // that can be returned with the search.
  67. l: 4,
  68. // `q: query.terms,` should be manually configured in the `enter`
  69. // method using `terms` method.
  70. //
  71. // We want to use the `v3` version of this API -- it has the
  72. // `matches` property for searches.
  73. v: 3,
  74. // Set the maximum number of places to be returned by this API.
  75. p: 3
  76. }
  77. });
  78. // > Depending on how many providers we end up with, we may want to
  79. // > figure out a way to lazily create collections based on what we
  80. // > actually need. Prototypal objects are fast, though, so it might
  81. // > not actually matter -GB.
  82. collections.bings = new searchCollections.Bing(emptyArray, {
  83. urlTokens: {
  84. base: config.searchRoot,
  85. path: 'bing'
  86. },
  87. urlQuery: {
  88. // `q: query.terms,` should be manually configured in the `enter`
  89. // method using `terms` method.
  90. format: 'json'
  91. }
  92. });
  93. collections.tweets = new searchCollections.Tweets(emptyArray, {
  94. urlTokens: {
  95. base: config.searchRoot,
  96. path: 'twitter'
  97. },
  98. urlQuery: {
  99. // `q: query.terms,` should be manually configured in the `enter`
  100. // method using `terms` method.
  101. format: 'json'
  102. }
  103. });
  104. this.mainView = new searchViews.SearchTabsView();
  105. var stackListView = this.stacks(collections.stacks);
  106. var bingListView = this.bings(collections.bings);
  107. var tweetListView = this.tweets(collections.tweets);
  108. this.mainView.widgets({
  109. '#stack-results': stackListView,
  110. '#bing-results': bingListView,
  111. '#twitter-results': tweetListView
  112. });
  113. this.mainView.bind('activate', function (view) {
  114. var activeView = view.widget(view.active);
  115. var collection = activeView.collection;
  116. if (collection.state() === 'invalid') {
  117. activeView.render();
  118. collection.fetchDebounced();
  119. }
  120. });
  121. this.mainView.activateTab('#bing-results');
  122. },
  123. enter: function (query, options) {
  124. util.extend(this, options || {});
  125. // Set this view as active in the app.
  126. this.appView.widget(
  127. '#main',
  128. this.mainView
  129. );
  130. },
  131. update: function (query, options) {
  132. // Update instance from options.
  133. //
  134. // TODO: I'm not wild about the way this works -- it's too brittle -GB
  135. util.extend(this, options || {});
  136. var terms = this.parseQuery(query).terms;
  137. var collections = this.collections;
  138. var stacks = collections.stacks;
  139. // Update terms on all collections.
  140. for(var key in collections) collections[key].terms(terms);
  141. // Fetch stacks.
  142. stacks.fetchDebounced();
  143. // Fetch active search collection.
  144. this.mainView.widget(this.mainView.active).collection.fetchDebounced();
  145. },
  146. exit: function () {
  147. // Empty collections on exit.
  148. var collections = this.collections;
  149. for (var key in collections) collections[key].reset();
  150. // These memoized collections are available as properties on the
  151. // bings collection.
  152. this.collections.bings.images().reset();
  153. this.collections.bings.suggestions().reset();
  154. },
  155. // Centralizing the scratch logic it takes to create and configure a
  156. // stackListView for the search controller.
  157. stacks: function (collection) {
  158. var stackListView = new StackHeadingListView({
  159. className: 'headinglist headinglist--with-divider',
  160. collection: collection
  161. });
  162. stackListView.bind('visit', onStackVisit);
  163. return stackListView;
  164. },
  165. // Factory function for creating tweet result view with configured
  166. // click handlers. This is all a little bit obtuse, and could probably
  167. // be cleaner, but I want to avoid binding handlers in the view constructor.
  168. tweets: function (collection) {
  169. var searchTweetsView = new searchViews.SearchTweetsView({
  170. collection: collection
  171. });
  172. searchTweetsView.bind('visit', function (placeView, siteModel, e) {
  173. e.preventDefault();
  174. var terms = siteModel.collection.terms();
  175. var msg = util.extend(siteModel.toJSON(), {
  176. search_terms: terms,
  177. // TODO: GB: hard-coded. This should be pulled from a list of registered
  178. // search providers -- probably from an API somewhere. The client should
  179. // be dumb.
  180. search_provider: 'twitter',
  181. // TODO: GB: hard-coded. This should be pulled from a list of registered
  182. // search providers -- probably from an API somewhere. The client should
  183. // be dumb.
  184. search_url: template(config.searchResults)({
  185. query: terms,
  186. provider: 'twitter'
  187. }),
  188. origin: config.appName
  189. });
  190. // Clear search field. Reset to home.
  191. var appModel = this.appView.model;
  192. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  193. // Send message up.
  194. bridge.createStack(msg);
  195. }, this);
  196. return searchTweetsView;
  197. },
  198. // Factory function for creating bing result view with configured
  199. // click handlers. This is all a little bit obtuse, and could probably
  200. // be cleaner, but I want to avoid binding handlers in the view constructor.
  201. bings: function (collection) {
  202. var bingView = new searchViews.SearchBingView({
  203. collection: collection
  204. });
  205. // Define a handler for `visit` events on the `view`.
  206. bingView.bind('visit', function (placeView, siteModel, e) {
  207. e.preventDefault();
  208. // Capture search terms by calling the custom `terms` method on the
  209. // this collection.
  210. // Capture terms from collection via closure, at event-time.
  211. var terms = collection.terms();
  212. var msg = util.extend(siteModel.toJSON(), {
  213. search_terms: terms,
  214. // TODO: GB: hard-coded. This should be pulled from a list of registered
  215. // search providers -- probably from an API somewhere. The client should
  216. // be dumb.
  217. search_provider: 'bing',
  218. search_url: template(config.searchResults)({
  219. query: terms,
  220. provider: 'bing'
  221. }),
  222. origin: config.appName
  223. });
  224. // We have to access this property at runtime, rather than via closure
  225. // because `bings` is called at initialization, but `this.appView` isn't
  226. // part of the object until it is mixed in via `enter` method.
  227. //
  228. // TODO: this is confusing and brittle. We should handle requirements
  229. // during construction.
  230. var appModel = this.appView.model;
  231. // Clear search field. Reset to home.
  232. setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
  233. // Send message up.
  234. bridge.createStack(msg);
  235. }, this)
  236. .bind('suggest', function (view, model, e) {
  237. e.preventDefault();
  238. // We have to access this property at runtime, rather than via closure
  239. // because `bings` is called at initialization, but `this.appView` isn't
  240. // part of the object until it is mixed in via `enter` method.
  241. //
  242. // TODO: this is confusing and brittle. We should handle requirements
  243. // during construction.
  244. var appModel = this.appView.model;
  245. appModel.set({ search: model.get('place_title') });
  246. }, this);
  247. return bingView;
  248. },
  249. parseQuery: function (query) {
  250. var // parse out the query - it can be something like twitter/term1/term2
  251. // or just 'term'
  252. terms = query.split('/'),
  253. // a master list of search providers we support, with name: prefix.
  254. // define in config maybe?
  255. providers = searchProviders;
  256. var parsed = {};
  257. // We consider the first term to be a provider if it matches one of the
  258. // providers in our list.
  259. if(terms.length > 1 && providers[terms[0]]) {
  260. parsed.provider = terms.shift();
  261. }
  262. parsed.terms = terms.join(' ');
  263. return parsed;
  264. }
  265. });
  266. return new SearchController();
  267. });