/pancake-web/pancake/web/static/js/models/searchcollections.js
JavaScript | 343 lines | 189 code | 43 blank | 111 comment | 15 complexity | 5b69741e88e4d9e2e479c63b9faa7a28 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([
- 'underscore',
- 'backbone',
- 'models/postcollection',
- 'models/stackcollection',
- 'models/livecollection',
- 'models/thumbnails',
- 'models/basemodel',
- 'lib/objecttools',
- 'lib/lazily',
- 'lib/promise',
- 'lib/errors'
- ],
- function (
- util,
- Backbone,
- PostCollection,
- StackCollection,
- LiveCollection,
- thumbnails,
- BaseModel,
- objectTools,
- lazily,
- Promise,
- errors
- ) {
- // A shared method used by augmented versions of `StackCollection`
- // and `PostCollection` below.
- var terms = function (terms) {
- if (terms) {
- this._terms = terms;
- // Update internal URL.`
- this.url(null, { q: terms });
- }
- this.state('invalid');
- return this._terms;
- };
- // Bing gets a custom update handler that doesn't diff (because search
- // results don't have IDs).
- var updateAndReset = function (terms) {
- // Update terms -- this should happen every time.
- this.terms(terms);
- // Diff the results of this collection when it comes back.
- return this.fetchDebounced();
- };
- // Alternative to underscore's debounce, this also creates and returns a promise
- function promisedDebounce(func, wait) {
- var timeout;
- return function() {
- var context = this, args = arguments;
- // eventualValue promise represents eventual outcome of func.apply
- // this is what we return and resolve
- var eventualValue = new Promise();
- var funcPromise = null;
-
- // later is called after our wait period
- // and effectively cancels previous calls
- var later = function() {
- timeout = null;
- // if there's a call already in-flight, cancel it
- if(funcPromise && funcPromise.cancel){
- // cancelling in-flight;
- funcPromise.cancel();
- funcPromise = null;
- }
- // make a promise for each call to func.
- // (to get a canceller, we have to have a promise to resolve a promise)
- funcPromise = new Promise(function(){
- // canceller
- // return a special Error so we can treat cancelling differently (its not really an error)
- return new errors.CancelledError();
- });
- // add callbacks for this call of func
- funcPromise.then(
- // success, we resolve the eventualValue promise we returned
- function(){
- // console.log("func call was successful, resolving eventualValue with: ", arguments[0]);
- eventualValue.resolve.apply(eventualValue, arguments);
- },
- function(err){
- // "failure", might be this call was cancelled
- if(err instanceof errors.CancelledError){
- // not an error, do nothing
- } else {
- // legit failure, reject the eventualValue
- eventualValue.reject.apply(eventualValue, arguments);
- }
- }
- );
-
- // trigger call to func. the outcome will result in funcPromise being either resolved or rejected
- // and providing we're not cancelled first, will resolve the eventualValue
- Promise.when(
- func.apply(context, args),
- function(){
- funcPromise.resolve.apply(funcPromise, arguments);
- funcPromise = null;
- },
- function(err){
- funcPromise.reject.apply(funcPromise, arguments);
- funcPromise = null;
- }
- );
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- return eventualValue;
- };
- }
-
- // Define a specialized `StackCollection` that gives us a few affordances
- // for dealing with searches.
- var __StackCollection = StackCollection.prototype;
- var StackSearchCollection = StackCollection.extend({
- terms: terms,
- // Create a debounced fetch method intented to be called from the
- // controller's `update` method as many times per second as you like.
- //
- // TODO: `_.debounce` doesn't return a value. If in future we find
- // it more useful to return a promise for the fetch, we should change
- // the way fetchDebounce is defined, or submit a patch to Underscore.js.
- fetchDebounced: function () {
- // Create the debounced function.
- var fetchDebounced = promisedDebounce(__StackCollection.fetch, 50);
- // Overshadow this function by setting the debounced function on the
- // object. Doing this means only one debounced function will be created
- // per instance.
- this.fetchDebounced = fetchDebounced;
- // ...and invoke this debounced function. Subsequent hits to
- // `fetchDebounced` will hit our debounced function directly.
- return fetchDebounced.apply(this, arguments);
- },
- // Define a custom parse method that adds the search terms
- // to each model. This property is **required** for
- // `PUT /lattice/:username/stack/search` calls.
- parseMatch: function (place) {
- place = __StackCollection.parseMatch(place);
- return util.extend(place, { search_terms: this.terms() });
- },
- // This method should be called from the controller's `update` method.
- // It is indended to be safe to call many times per second (e.g. it handles
- // debouncing fetches, etc).
- update: function (terms) {
- // Update terms -- this should happen every time.
- this.terms(terms);
- this.state('invalid');
- // Diff the results of this collection when it comes back.
- return this.fetchDebounced({ diff: true });
- }
- });
- // A shared method for Bing and Twitter searches. Added to the prototpe below.
- var fetchDebounced1000 = function () {
- // Create the debounced function.
- var fetchDebounced = promisedDebounce(__StackCollection.fetch, 1000);
- // Overshadow this function by setting the debounced function on the
- // object. Doing this means only one debounced function will be created
- // per instance.
- this.fetchDebounced = fetchDebounced;
- // ...and invoke this debounced function. Subsequent hits to
- // `fetchDebounced` will hit our debounced function directly.
- return fetchDebounced.apply(this, arguments);
- };
- // Define a special collection for collections. It is only allowed to have
- // up to 5 items.
- var __LiveCollection = LiveCollection.prototype;
- var SuggestionCollection = LiveCollection.extend({
- maxlength: 5,
- add: function (models, options) {
- var maxlength = this.maxlength;
- // Convert to array, if not array.
- if (!util.isArray(models)) models = [models];
- if (models.length > maxlength)
- models.splice(0, models.length - maxlength);
- var togetherLength = models.length + this.length;
- if (togetherLength > maxlength) {
- var difference = togetherLength - maxlength;
- // Remove items from the end of the stack. Since the Backbone.js
- // version we use now does not implement `pop()`, we use reduce
- // to accomplish the same thing.
- //
- // TODO: replace with `Collection pop()` when we upgrade Backbone.
- this.reduceRight(function (removed, model) {
- if (removed < difference) this.remove(model, options);
- return removed + 1;
- }, 0, this);
- }
- __LiveCollection.add.call(this, models, options);
- },
- parse: function (resp) { return resp.related || []; }
- });
- var __PostCollection = PostCollection.prototype;
- // BingSearchCollection is an ordinary mild-mannered PostCollection, but
- // has a terms method and a debounced fetch method.
- var BingSearchCollection = PostCollection.extend({
- initialize: function (models, options) {
- __PostCollection.initialize.call(this, models, options);
- this.bind('success', this.onSuccess, this);
- },
- // Default handler for processing images and suggestions.
- onSuccess: function (resp, options) {
- thumbnails.parse.call(this, resp);
- this.images().process(resp, options);
- this.suggestions().process(resp, options);
- },
- terms: terms,
- // Create a debounced fetch method intented to be called from the
- // controller's `update` method as many times per second as you like.
- fetchDebounced: fetchDebounced1000,
- update: updateAndReset,
- images: BaseModel.subcollection('images', function () {
- var collection = new LiveCollection();
- // Create a custom parse function for this instance.
- collection.parse = function (resp) { return resp.images || []; };
- return collection;
- }),
- suggestions: BaseModel.subcollection('suggestions', function () {
- return new SuggestionCollection();
- })
- });
- var TwitterSearchCollection = PostCollection.extend({
- terms: terms,
- // Create a debounced fetch method intented to be called from the
- // controller's `update` method as many times per second as you like.
- fetchDebounced: fetchDebounced1000,
- update: updateAndReset,
- // `url: ...` inherited from SiteCollection. Wraps `this.UrlHelper.url`.
- // A custom parse method for Twitter results.
- // Twitter results look like this:
- //
- // {
- // "popular": [
- // {
- // "friend_name": "...",
- // "friend_username": "...",
- // "summary": "...",
- // "friend_img": "...",
- // "time": "Thu, 03 May 2012 18:51:34 +0000",
- // "id": 198122645315788800
- // }
- // ],
- // "recent": [
- // {
- // "friend_name": "...",
- // "friend_username": "...",
- // "summary": "...",
- // "friend_img": "...",
- // "time": "Thu, 03 May 2012 18:51:34 +0000",
- // "id": 198122645315788800
- // }
- // ]
- parse: function (resp) {
- var results = resp.recent;
- // nothing to do here
- if(!results.length) return [];
- // Translate response array using curried translator function.
- results = util.map(results, function (tweet) {
- var urls = tweet.urls;
- // Grab the first URL in the urls array. We'll use that.
- tweet.url = (urls && urls.length > 0) ? urls[0] : '';
- return this.translateKeys(tweet);
- }, this);
- return results;
- },
- translateKeys: util.bind(objectTools.translate, null, {
- 'id': 'place_id',
- 'title': 'place_title',
- 'url': 'place_url',
- 'friend_img': 'avatar',
- // Results may vary re: author vs friend_name. We're passing back `author`
- // for news via results.
- 'author': 'author',
- 'friend_name': 'author',
- 'friend_username': 'username',
- // The search API returns time as an absolute value under this key.
- 'time': 'published',
- 'summary': 'summary'
- })
- });
-
- var prepareUrlModelFromUrl = function(url) {
- return { 'place_url': url.url || url, 'place_title': url.title || url };
- };
- var UrlCollection = LiveCollection.extend({
- model: Backbone.Model.extend({
- // UrlModels expect a single URL string value as input
- // ..though maybe we want to track the matched string and extrapolated url as 2 properties?
- parse: prepareUrlModelFromUrl
- })
- });
- // install static helpers
- UrlCollection.prepareModelFromUrl = prepareUrlModelFromUrl;
- // Export modules. We may as well keep the names short and sweet, since it
- // will be clear what they are from their reference object
- // (`searchCollections.Bing`, etc).
- return {
- Url: UrlCollection,
- Stack: StackSearchCollection,
- Bing: BingSearchCollection,
- Tweets: TwitterSearchCollection
- };
- });