/pancake-web/pancake/web/static/js/controllers/searchcontroller.js
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
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- define([
- 'config',
- 'config/searchProviders',
- 'underscore',
- 'backbone',
- 'lib/template',
- 'lib/routecontroller',
- 'models/searchcollections',
- 'views/stackheadinglistview',
- 'lib/promise',
- 'lib/bridge',
- 'lib/url',
- 'views/widgetview',
- 'views/searchviews',
- 'views/emptysearchresult',
- 'logger'
- ], function(
- config,
- searchProviders,
- util,
- Backbone,
- template,
- RouteController,
- searchCollections,
- StackHeadingListView,
- Promise,
- Bridge,
- Url,
- WidgetView,
- searchViews,
- EmptySearchResultView,
- logger
- ) {
- // We need to pass an empty array into empty collections that need options
- // (null gets interpreted as a value and wrapped). It's a bit fussy, but it
- // should be faster to share a single object than create one.
- var emptyArray = [];
- var bridge = new Bridge();
-
- var onStackVisit = function(view, model, e) {
- e.preventDefault();
- // Make a PUT request to the server to get a new session_id.
- // The model will come back as the first argument of success callback,
- // with a session_id added.
- model.save(null, {
- // Change the URL hit with this call to a PUT against
- // `lattice/:username/stack/search`. Instead of hard-coding, crawl
- // back up to the stack and grab its URL, just in case.
- // SiteModel.PlaceCollection.StackModel.StackCollection.
- url: model.collection.stack().collection.url(),
- success: function (model) {
- var data = util.extend(model.toJSON(), { origin: config.appName });
- bridge.changeStack(data).showViewer();
- },
- error: function(err){
- logger.error("Error encountered when handling stack launch", err);
- }
- });
- };
- var SearchController = RouteController.extend({
- initialize: function (options) {
- // Initialize collections object for storing search collections.
- var collections = this.collections = {};
- var termsModel = this.termsModel = new Backbone.Model({ terms: '' });
-
- // Create an empty `StackSearchCollection` to be shared for
- // the lifetime of this controller. Views are relied on to
- // adapt to the changing contents of this controller.
- collections.stacks = new searchCollections.Stack(emptyArray, {
- urlTokens: { method: 'search' },
- urlQuery: {
- // Set the `limit` param to 4 -- the maximum number of stacks
- // that can be returned with the search.
- l: 4,
- // `q: query.terms,` should be manually configured in the `enter`
- // method using `terms` method.
- //
- // We want to use the `v3` version of this API -- it has the
- // `matches` property for searches.
- v: 3,
- // Set the maximum number of places to be returned by this API.
- p: 3
- }
- });
- // > Depending on how many providers we end up with, we may want to
- // > figure out a way to lazily create collections based on what we
- // > actually need. Prototypal objects are fast, though, so it might
- // > not actually matter -GB.
- collections.bings = new searchCollections.Bing(emptyArray, {
- urlTokens: {
- base: config.searchRoot,
- path: 'bing'
- },
- urlQuery: {
- // `q: query.terms,` should be manually configured in the `enter`
- // method using `terms` method.
- format: 'json'
- }
- });
- collections.tweets = new searchCollections.Tweets(emptyArray, {
- urlTokens: {
- base: config.searchRoot,
- path: 'twitter'
- },
- urlQuery: {
- // `q: query.terms,` should be manually configured in the `enter`
- // method using `terms` method.
- format: 'json'
- }
- });
-
- // create a collection for any URLs we match in the query terms
- collections.matchedUrls = new searchCollections.Url([]);
- this.mainView = new searchViews.SearchTabsView();
- var emptyResultsView = this.emptyResultsView = new EmptySearchResultView({ model: termsModel });
- emptyResultsView.toggle(false); // hide initially
- // bind to the 'goto' events that empty view triggers when its buttons are clicked
- emptyResultsView.bind('goto', util.bind(function (path, e) {
- e.preventDefault();
- var appModel = this.appView.model;
-
- switch(path) {
- case 'newsearch':
- // place focus in the search bar
- var searchBar = this.appView.widget('#searchbar');
- if(searchBar)
- searchBar.clear().focus();
- break;
- default:
- setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
- bridge.navigate(path, true);
- }
- this.termsModel.set({ terms: '' });
- emptyResultsView.toggle(false);
- }, this));
-
-
- var stackListView = this.stacks(collections.stacks);
- var urlListView = this.urls(collections.matchedUrls);
- var bingListView = this.bings(collections.bings);
- var tweetListView = this.tweets(collections.tweets);
- this.mainView.widgets({
- '#url-results': urlListView,
- '#stack-results': stackListView,
- '#bing-results': bingListView,
- '#twitter-results': tweetListView,
- '#empty-results': emptyResultsView
- });
-
- this.mainView.bind('activate', function (view) {
- var activeView = view.widget(view.active);
- var collection = activeView.collection;
- if (collection.state() === 'invalid') {
- activeView.render();
- collection.fetchDebounced();
- }
- });
- this.mainView.activateTab('#bing-results');
- },
- enter: function (query, options) {
- util.extend(this, options || {});
- // Set this view as active in the app.
- this.appView.widget(
- '#main',
- this.mainView
- );
- },
- update: function (query, options) {
- // Update instance from options.
- //
- // TODO: I'm not wild about the way this works -- it's too brittle -GB
- util.extend(this, options || {});
- var terms = this.parseQuery(query).terms;
- var collections = this.collections;
- var termsModel = this.termsModel;
- termsModel.set({ terms: terms });
- var emptyResultsView = this.emptyResultsView;
- emptyResultsView.toggle(false); // hide initially
- var stacks = collections.stacks;
- // Update terms on all collections.
- for(var key in collections){
- if(collections[key].terms) collections[key].terms(terms);
- }
- var urls = this.parseQuery(query).urls;
- if(urls && urls.length){
- urls = urls.map( searchCollections.Url.prepareModelFromUrl );
- collections.matchedUrls.reset(urls);
- } else {
- collections.matchedUrls.reset([]);
- }
-
- var activeCollection = this.mainView.widget(this.mainView.active).collection;
- // Fetch active search collection.
- var searchPromise = activeCollection.fetchDebounced();
-
- // Fetch stacks.
- var stacksPromise = stacks.fetchDebounced();
- // Execute in parallel, but callback when both are complete:
- Promise.all([
- searchPromise,
- stacksPromise
- ]).then(function(){
- var totalResults = collections.matchedUrls.length +
- stacks.length +
- activeCollection.length;
-
- if(!totalResults){
- // only show the empty results view when we have 0 results
- emptyResultsView.toggle(true);
- }
-
- },
- function(){
- logger.log("errback from search and stack debounced fetches: ", arguments);
- }
- );
-
- },
- reset: function(){
- // flush out all the collections
- util.each(this.collections, function(collection, name, collections){
- if(collection.reset){
- collection.reset([]);
- }
- });
- },
- exit: function () {
- // Empty collections on exit.
- var collections = this.collections;
- for (var key in collections) collections[key].reset();
- // These memoized collections are available as properties on the
- // bings collection.
- this.collections.bings.images().reset();
- this.collections.bings.suggestions().reset();
- },
- // Centralizing the scratch logic it takes to create and configure a
- // stackListView for the search controller.
- stacks: function (collection) {
- var stackListView = new StackHeadingListView({
- className: 'headinglist headinglist--with-divider',
- collection: collection
- });
- stackListView.bind('visit', onStackVisit);
- return stackListView;
- },
- // Factory function for creating url-match result view with configured
- // click handlers.
- urls: function(collection){
- var searchUrlsView = new searchViews.SearchUrlListView({
- collection: collection
- });
- // Define a handler for `visit` events on the `searchBingView`.
- searchUrlsView.bind('visit', function (placeView, urlModel, e) {
- e.preventDefault();
- logger.log('searchcontroller: visit to URL query match: ', urlModel.get('place_url'), urlModel.toJSON());
-
- var msg = util.extend(urlModel.toJSON(), {
- title: urlModel.get('place_title'),
- url: urlModel.get('place_url'),
- origin: config.appName
- });
-
- // Clear search field. Reset to home.
- var appModel = this.appView.model;
- setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
-
- // Send message up.
- bridge.createDirectStack(msg);
- }, this);
- return searchUrlsView;
- },
-
- // Factory function for creating tweet result view with configured
- // click handlers. This is all a little bit obtuse, and could probably
- // be cleaner, but I want to avoid binding handlers in the view constructor.
- tweets: function (collection) {
- var searchTweetsView = new searchViews.SearchTweetsView({
- collection: collection
- });
- searchTweetsView.bind('visit', function (placeView, siteModel, e) {
- e.preventDefault();
- var terms = siteModel.collection.terms();
- var msg = util.extend(siteModel.toJSON(), {
- search_terms: terms,
- // TODO: GB: hard-coded. This should be pulled from a list of registered
- // search providers -- probably from an API somewhere. The client should
- // be dumb.
- search_provider: 'twitter',
- // TODO: GB: hard-coded. This should be pulled from a list of registered
- // search providers -- probably from an API somewhere. The client should
- // be dumb.
- search_url: template(config.searchResults)({
- query: terms,
- provider: 'twitter'
- }),
- origin: config.appName
- });
- // Clear search field. Reset to home.
- var appModel = this.appView.model;
- setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
- // Send message up.
- bridge.createStack(msg);
- }, this);
- return searchTweetsView;
- },
- // Factory function for creating bing result view with configured
- // click handlers. This is all a little bit obtuse, and could probably
- // be cleaner, but I want to avoid binding handlers in the view constructor.
- bings: function (collection) {
- var bingView = new searchViews.SearchBingView({
- collection: collection
- });
- // Define a handler for `visit` events on the `view`.
- bingView.bind('visit', function (placeView, siteModel, e) {
- e.preventDefault();
- // Capture search terms by calling the custom `terms` method on the
- // this collection.
- // Capture terms from collection via closure, at event-time.
- var terms = collection.terms();
- var msg = util.extend(siteModel.toJSON(), {
- search_terms: terms,
- // TODO: GB: hard-coded. This should be pulled from a list of registered
- // search providers -- probably from an API somewhere. The client should
- // be dumb.
- search_provider: 'bing',
-
- search_url: template(config.searchResults)({
- query: terms,
- provider: 'bing'
- }),
- origin: config.appName
- });
- // We have to access this property at runtime, rather than via closure
- // because `bings` is called at initialization, but `this.appView` isn't
- // part of the object until it is mixed in via `enter` method.
- //
- // TODO: this is confusing and brittle. We should handle requirements
- // during construction.
- var appModel = this.appView.model;
- // Clear search field. Reset to home.
- setTimeout(util.bind(appModel.set, appModel, { search: '' }), 1000);
- // Send message up.
- bridge.createStack(msg);
- }, this)
- .bind('suggest', function (view, model, e) {
- e.preventDefault();
- // We have to access this property at runtime, rather than via closure
- // because `bings` is called at initialization, but `this.appView` isn't
- // part of the object until it is mixed in via `enter` method.
- //
- // TODO: this is confusing and brittle. We should handle requirements
- // during construction.
- var appModel = this.appView.model;
- appModel.set({ search: model.get('place_title') });
- }, this);
- return bingView;
- },
- parseQuery: function (query) {
- // parse out the query - it can be something like twitter/term1/term2
- // or just 'term',
- // or http://google.com/?q=blah
- // turn out '/' delimited resource paths into space delimited keywords
- if(query.match(/^\w+\/\w+/)){
- query = query.replace(/\//g, ' ');
- }
- var terms = query.split(/\s/),
- // a master list of search providers we support, with name: prefix.
- // define in config maybe?
- providers = searchProviders;
- var parsed = {};
- // We consider the first term to be a provider if it matches one of the
- // providers in our list.
- if(terms.length > 1 && providers[terms[0]]) {
- parsed.provider = terms.shift();
- }
- parsed.terms = terms.join(' ');
-
- var urlMatches = parsed.urls = Url.parseForUrls(query);
- return parsed;
- }
- });
- return new SearchController();
- });