Skip to content

Instantly share code, notes, and snippets.

@oyiptong
Last active January 3, 2016 11:09
Show Gist options
  • Save oyiptong/8454169 to your computer and use it in GitHub Desktop.
Save oyiptong/8454169 to your computer and use it in GitHub Desktop.
/**
* Creates an Article Collection that will be used to sync data for the Ribbon and the
* Arrow buttons
*
* <p><b>Require Path:</b> shared/ribbon/collections/ribbon</p>
*
* @module Shared
* @submodule Shared.Ribbon
* @class Collection
* @constructor
* @extends BaseCollection
**/
define('shared/ribbon/collections/ribbon',[
'backbone/nyt',
'underscore/nyt',
'foundation/collections/base-collection',
'shared/data/models/article',
'foundation/hosts',
'foundation/models/user-data',
'shared/data/instances/most-emailed',
'shared/data/instances/recommendations',
'shared/data/instances/context',
'shared/data/instances/top-news',
'shared/data/instances/section-origin'
], function (Backbone, _, BaseCollection, Article, Hosts, userData, mostEmailed, recommendations, context, topNews, sectionOrigin) {
'use strict';
var RibbonCollection = BaseCollection.extend({
url: function () {
if (_.isFunction(this.feedUrl)) {
this.handleFeedAsFunction(this.feedUrl);
}
return this.feedUrl || '';
},
model: Article,
initialize: function (settings) {
_.bindAll(this, 'getMostEmailed', 'getContext', 'getTopNews', 'getRecommendations', 'getOrigin');
this.collectionLabels = [];
this.feedSource = [
{ origin: this.getOrigin },
{ context: this.getContext },
{ mostEmailed: this.getMostEmailed },
{ homepage: this.getTopNews },
{ recommendations: this.getRecommendations },
{ world: Hosts.json + '/services/json/sectionfronts/world/index.jsonp' },
{ us: Hosts.json + '/services/json/sectionfronts/national/index.jsonp' },
{ business: Hosts.json + '/services/json/sectionfronts/business/index.jsonp' },
{ opinion: Hosts.json + '/services/json/sectionfronts/opinion/index.jsonp' },
{ technology: Hosts.json + '/services/json/sectionfronts/technology/index.jsonp' },
{ politics: Hosts.json + '/services/json/sectionfronts/politics/index.jsonp' },
{ sports: Hosts.json + '/services/json/sectionfronts/sports/index.jsonp' },
{ science: Hosts.json + '/services/json/sectionfronts/science/index.jsonp' },
{ health: Hosts.json + '/services/json/sectionfronts/health/index.jsonp' },
{ arts: Hosts.json + '/services/json/sectionfronts/arts/index.jsonp' },
{ style: Hosts.json + '/services/json/sectionfronts/style/index.jsonp' },
{ nyregion: Hosts.json + '/services/json/sectionfronts/nyregion/index.jsonp' }
];
this.currentArticleUrl = settings.currentArticleUrl;
this.originalLoadType = 'context';
this.feedUrl = this.setFeedUrl(settings.sectionFeedUrl);
this.subscribe(this, 'sync', this.setCurrentArticle);
},
/**
* Overriding mixin helper function. Called by the ribbon view to initialize our first collection
*
* @private
* @method loadData
**/
loadData: function () {
if (_.isFunction(this.feedUrl)) {
this.handleFeedAsFunction(this.feedUrl);
} else {
this.fetch();
}
},
/**
* Called each time we want to fetch a new set of articles from a particular feed
*
* @private
* @method sync
* @param method {String} Create, Read, Update, or Delete
* @param model {Object} the model to be saved (or collection to be read)
* @param options {Object} jquery ajax request options
* @return {Object} Backbone Sync
**/
sync: function (method, model, options) {
//callbacks are jsonFeedCallback_section_subsection
var callback = this.url().match(/sectionfronts\/(.+)\/index/) || ['', 'homepage'];
options.dataType = 'jsonp';
options.jsonpCallback = 'jsonFeedCallback_' + callback[1].replace('/', '_');
//sets sectionId for use in ?rref=section/subsection
this.sectionId = 'rref=' + callback[1];
return Backbone.sync(method, model, options);
},
/**
* Sets the currentArticle property based on the currentArticleUrl property
*
* @private
* @method setCurrentArticle
**/
setCurrentArticle: function () {
var feedCollection = this;
this.currentArticle = this.find(function (article) {
return article.get('link') === feedCollection.currentArticleUrl;
});
},
/**
* Modifies the format of our article collection results
*
* @private
* @method parse
* @param response {Object} The data returned after a sync is completed
* @return {Object} Revised JSON response to be added to the collection
**/
parse: function (response) {
return this.prepCollection(response.items, response.title.replace('NYT > ', ''), response.link, 'news');
},
/**
* Use a key to find the url of a feed source
*
* @private
* @method getFeedSourceValue
* @param key {String} the key to look up
* @return {String} the url
**/
getFeedSourceValue: function (key) {
for (var i = 0, objectLength = this.feedSource.length; i < objectLength; i++) {
if (this.feedSource[i].hasOwnProperty(key)) {
return this.feedSource[i][key];
}
}
},
/**
* Use a key to remove an object from the feed source
*
* @private
* @method removeFeedByKey
* @param keyToRemove {String}
**/
removeFeedByKey: function (keyToRemove) {
for (var i = 0, objectLength = this.feedSource.length; i < objectLength; i++) {
if (_.keys(this.feedSource[i])[0] === keyToRemove) {
this.feedSource.splice(i, 1);
return;
}
}
},
/**
* Use a url to remove an object from the feed source
*
* @private
* @method removeFeedByUrl
* @param urlToRemove {String}
**/
removeFeedByUrl: function (urlToRemove) {
var feedSourceValue;
var pathname = this.createAnchor(urlToRemove).pathname;
for (var i = 0, objectLength = this.feedSource.length; i < objectLength; i++) {
feedSourceValue = _.values(this.feedSource[i])[0];
if (!_.isFunction(feedSourceValue) && feedSourceValue.indexOf(pathname) > -1) {
this.feedSource.splice(i, 1);
return;
}
}
},
/**
* Create an anchor element for easy url parsing
*
* @private
* @method createAnchor
* @param url {String} the url to apply to the element as href
* @return {Object} the element created by the method
**/
createAnchor: function (url) {
var anchor = document.createElement('a');
anchor.href = url;
return anchor;
},
/**
* Compare a url to the url for the desired section
*
* @private
* @method isUrlForSection
* @param testUrl {String}
* @param section {String} the section to lookup
* @return {Boolean} the result of the test
**/
isUrlForSection: function (testUrl, section) {
var testPath = this.createAnchor(testUrl).pathname;
var lookupPath = this.createAnchor(this.getFeedSourceValue(section)).pathname;
return testPath === lookupPath;
},
/**
* Set the initial feed for the collection
*
* @private
* @method setFeedUrl
* @param sectionFeedUrl {String} the current default section feed
* @return {String} the updated url
**/
setFeedUrl: function (sectionFeedUrl) {
var ref, feedSrc, firstRibbonCollection;
var collectionObj = this;
var source = this.pageManager.getUrlParam('src') || '';
var ribbonReference = this.pageManager.getUrlParam('rref');
var loadType = 'origin';
//user arrives from clicking on an item in the ribbon
if (ribbonReference && ribbonReference !== 'homepage') {
this.removeFeedByKey(ribbonReference);
feedSrc = 'origin';
//user arrives from home page or section front
} else if (this.pageManager.getUrlParam('hp') === '' || ribbonReference === 'homepage') {
feedSrc = 'homepage';
//user arrives from section front
} else if (this.pageManager.getUrlParam('ref')) {
ref = this.createAnchor(document.referrer);
//if nyt and a section front
if (/.nytimes.com$/.test(ref.host) && /^\/pages/.test(ref.pathname)) {
feedSrc = 'origin';
//otherwise load the collection in context
} else {
feedSrc = 'context';
loadType = 'context';
}
//user arrives from most emailed
} else if (source === 'me') {
feedSrc = 'mostEmailed';
//User arrives from rec engine
} else if (source.indexOf('rec') === 0) {
feedSrc = 'recommendations';
//collection in context
} else {
feedSrc = 'context';
loadType = 'context';
}
//The collection that should be loaded
firstRibbonCollection = this.getFeedSourceValue(feedSrc);
//set load type
this.originalLoadType = loadType;
//always remove the origin, context and the feed's src from the list
this.removeFeedByKey('context');
this.removeFeedByKey('origin');
this.removeFeedByKey(feedSrc);
//remove recommendations if the user is anonymous
userData.ready(function () {
if (!userData.isLoggedIn()) {
collectionObj.removeFeedByKey('recommendations');
}
});
return firstRibbonCollection;
},
/**
* Process a feed that is represented by a function
*
* @private
* @method handleFeedAsFunction
* @param feed {Function}
**/
handleFeedAsFunction: function (feed) {
var collection = feed();
//if the response is an array, add it to the collection immediately
if (_.isArray(collection)) {
// reset the collection if there is a single model instead of zero models
this[this.length === 1 ? 'reset' : 'add'](collection);
this.local(this, 'sync');
this.local(this, 'nyt:ribbon-custom-collection-loaded');
//if the response is a callback because the data isn't ready, fire it
} else {
collection();
}
},
/**
* Pulls the next section off the list and loads its content
*
* @method loadSection
**/
loadFeed: function () {
var collection, feed;
//Special collections (most emailed) are added in bulk with JSON
if (this.feedSource.length > 0) {
feed = _.values(this.feedSource.shift())[0];
if (_.isFunction(feed)) {
this.handleFeedAsFunction(feed);
//Normal URL based feeds use Backbone Fetch
} else {
this.feedUrl = feed;
this.fetch({remove: false});
}
return true;
} else {
return false;
}
},
/**
* return an article model tagged as an ad
*
* @public
* @method getAdModel
* @return {Object} an unprocessed article Model with an isAd attribute set to true
**/
getAdModel: function () {
return new Article({ processed: false, isAd: true});
},
/**
* Prepare a collection for use with the ribbon and also associate the ribbon content
* with the appropriate section name and url
*
* @method prepCollection
* @return {Object} A modified JSON collection that has the collectionId
**/
prepCollection: function (items, title, url, type) {
var i, l;
var labelId = this.collectionLabels.length;
for (i = 0, l = items.length; i < l; i += 1) {
items[i].collectionId = labelId;
}
this.collectionLabels.push({
title: title,
url: url,
type: type
});
return items;
},
/**
* By passing this method a model, it will return the next model in the collection
*
* @method next
* @param model {Object} In theory, it should be the current article model data passed to the method
* @return {Object} Returns the next model
**/
next: function (model) {
var index = this.indexOf(model);
var next;
if (index === this.length) {
next = 0;
} else {
next = index + 1;
}
/**
* Fired when the collection moves forward one article.
* @event nyt:ribbon-collection-next
* @param model {Object} The next model in the collection.
**/
this.local(this, 'nyt:ribbon-collection-next', this.models[next]);
return this.models[next];
},
/**
* By passing this method a model, it will return the previous model in the collection
*
* @method previous
* @param model {Object} In theory, it should be the current article model data passed to the method
* @return {Object} The previous model in the collection.
**/
previous: function (model) {
var index = this.indexOf(model);
var prev;
if (index === 0) {
prev = this.models.length;
} else {
prev = index - 1;
}
/**
* Fired when the collection moves back one article.
* @event nyt:ribbon-collection-previous
* @param model {Object} The previous model in the collection.
**/
this.local(this, 'nyt:ribbon-collection-previous', this.models[prev]);
return this.models[prev];
},
/**
* Import a special collection for use in the ribbon
*
* @method importCollection
* @return {Object} A JSON collection for most emailed
**/
importCollection: function (settings) {
var ribbonCollection = this;
var json = settings.collection.toJSON();
//if there is JSON available, return the data immediately
if (json.length > 0) {
this.sectionId = settings.collection.getIdentifier();
return this.prepCollection(json, settings.name, settings.url, settings.type);
//if no JSON is available, add the collection back to the front and wait
} else {
this.feedSource.unshift(settings.callback);
return function () {
//when the collection has data, load it into the ribbon
var collectionCallback = function () {
window.clearTimeout(collectionTimeout);
ribbonCollection.loadFeed();
};
//if nothing after 1 sec, cancel call and ask for next feed
var collectionTimeout = window.setTimeout(function () {
ribbonCollection.stopSubscribing(settings.collection, 'sync', collectionCallback);
ribbonCollection.feedSource.shift();
ribbonCollection.loadFeed();
}, 1500);
//when the collection is available, try the feed again
ribbonCollection.subscribeOnce(settings.collection, 'sync', collectionCallback);
};
}
},
/**
* Gets the collection's identifier to be used to track between ribbon clicks
*
* @private
* @method getIdentifier
**/
getIdentifier: function () {
return this.sectionId;
},
/**
* Get the most emailed collection so it can be added to the ribbon.
*
* @private
* @method getMostEmailed
**/
getMostEmailed: function () {
return this.importCollection({
collection: mostEmailed.loadData(),
name: 'Most Emailed',
url: Hosts.www + '/most-popular-emailed',
type: 'most-emailed',
callback: {mostEmailed: this.getMostEmailed}
});
},
/**
* Get the recommendations collection so it can be added to the ribbon.
*
* @private
* @method getRecommendations
**/
getRecommendations: function () {
return this.importCollection({
collection: recommendations.loadData(),
name: 'Recommended',
url: Hosts.www + '/recommendations',
type: 'news',
callback: {recommendations: this.getRecommendations}
});
},
/**
* Get the home page collection so it can be added to the ribbon.
*
* @private
* @method getTopNews
**/
getTopNews: function () {
return this.importCollection({
collection: topNews.loadData(),
name: 'Home Page',
url: Hosts.www,
type: 'news',
callback: {homepage: this.getTopNews}
});
},
/**
* Get the section front of context collection so it can be added to the ribbon.
*
* @private
* @method getContext
**/
getContext: function () {
var collection = this.pageManager.getMeta('article:collection') || '';
//if collection in context is also in the default set of urls, remove it
this.removeFeedByUrl(collection);
return this.importCollection({
collection: context.loadData(),
name: context.getName(),
url: context.getUrl(),
type: 'news',
callback: {context: this.getContext}
});
},
/**
* Get the section front of origin collection so it can be added to the ribbon.
*
* @private
* @method getContext
**/
getOrigin: function () {
this.removeFeedByKey(this.pageManager.getUrlParam('ref'));
return this.importCollection({
collection: sectionOrigin.loadData(),
name: sectionOrigin.getName(),
url: sectionOrigin.getUrl(),
type: 'news',
callback: {origin: this.getOrigin}
});
}
});
return RibbonCollection;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment