Instantly share code, notes, and snippets.
Created
January 16, 2014 12:36
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save oyiptong/8454329 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The ribbon page navigation | |
* | |
* <p><b>Require Path:</b> shared/ribbon/views/ribbon-page-navigation</p> | |
* | |
* @module Shared | |
* @submodule Shared.Ribbon | |
* @namespace Ribbon.PageNavigation | |
* @class View | |
* @constructor | |
* @extends foundation/views/base-view | |
**/ | |
define('shared/ribbon/views/ribbon-page-navigation',[ | |
'jquery/nyt', | |
'underscore/nyt', | |
'foundation/views/base-view', | |
'foundation/views/page-manager', | |
'shared/ribbon/instances/ribbon-data', | |
'shared/ribbon/templates', | |
'shared/modal/views/modal', | |
'foundation/models/page-storage', | |
'shared/ribbon/views/helpers/mixin' | |
], function ($, _, BaseView, pageManager, feed, templates, Modal, pageStorage, RibbonMixin) { | |
'use strict'; | |
var RibbonPageNavigation = BaseView.registerView('arrows').extend( | |
_.extend({}, RibbonMixin, { | |
/** | |
* Dom context for this backbone view, in this case, | |
* the body tag | |
* | |
* @private | |
* @property el | |
* @type {Object} A dom object | |
**/ | |
el: 'body', | |
/** | |
* Amount of time to delay before showing the arrows | |
* | |
* @private | |
* @property delay | |
* @type {Number} | |
**/ | |
delay: 200, | |
/** | |
* Speed to perform the arrow animations | |
* | |
* @private | |
* @property speed | |
* @type {Number} | |
**/ | |
speed: 250, | |
/** | |
* Distance in pixels to expand the arrow | |
* | |
* @private | |
* @property expandWidth | |
* @type {Number} | |
**/ | |
expandWidth: 275, | |
/** | |
* A property describing whether or not the arrows are expanded | |
* | |
* @private | |
* @property expanded | |
* @type {Boolean} | |
**/ | |
expanded: false, | |
/** | |
* An object whose key-value pairs represent dom events and their handlers | |
* | |
* @private | |
* @property events | |
* @type {Object} | |
**/ | |
events: { | |
'click .ribbon-page-navigation': 'changeArticle', | |
'mouseenter .ribbon-page-navigation': 'showArticle', | |
'mouseleave .ribbon-page-navigation': 'hideArticle', | |
'mouseleave #ribbon-page-navigation-modal .modal': 'hideArticle' | |
}, | |
/** | |
* An object whose key-value pairs represent NYT-specific events and their handlers | |
* | |
* @private | |
* @property events | |
* @type {Object} | |
**/ | |
nytEvents: { | |
'nyt:comments-panel-opened': 'hideRightArrow', | |
'nyt:comments-panel-closed': 'showRightArrow', | |
'nyt:page-drag': 'handlePageDrag' | |
}, | |
/** | |
* Initializes the arrows view. | |
* | |
* @private | |
* @method initialize | |
**/ | |
initialize: function () { | |
_.bindAll(this, 'preventScroll', 'checkForFeed'); | |
this.feed = feed; | |
this.subscribe('nyt:ads-fire-ribbon-interstitial', this.ribbonInterstitialFired); | |
if (this.pageManager.isDomReady()) { | |
this.handlePageReady(); | |
} else { | |
this.subscribe('nyt:page-ready', this.handlePageReady); | |
} | |
this.trackingBaseData = { | |
'module': 'ArrowsNav', | |
'contentCollection': this.pageManager.getMeta('article:section') | |
}; | |
}, | |
/** | |
* Executes the ribbon page arrows when the dom is ready | |
* | |
* @private | |
* @method handlePageReady | |
**/ | |
handlePageReady: function () { | |
this.restrict = this.pageManager.isComponentVisible($('#ribbon')); | |
this.createAdsDeferral(this.checkForFeed); | |
}, | |
/** | |
* helper method to segregate feed-dependent actions | |
* | |
* @private | |
* @method @checkForFeed | |
**/ | |
checkForFeed: function () { | |
//If the feed is ready, render the arrows | |
if (this.feed.length > 1) { | |
this.render(); | |
} | |
this.subscribeOnce(feed, 'sync', this.render); | |
//add the tooltip if the user has never seen it before | |
if (pageStorage.get('ribbon_hasViewedTooltip') !== true) { | |
this.addToolTip(); | |
} | |
}, | |
/** | |
* Renders the view once the model is ready | |
* | |
* @private | |
* @method render | |
**/ | |
render: function () { | |
var curArt = this.feed.currentArticle; | |
var prev = this.feed.previous(curArt); | |
var next = this.feed.next(curArt); | |
this.activeStoryIndex = this.getStoryIndex(this.feed.models, this.feed.currentArticle); | |
this.$arrows = $(this.createTemplate('previous', prev) + this.createTemplate('next', next)); | |
this.$shell.append(this.$arrows); | |
this.adjustArrows(); | |
this.adjustText(); | |
this.subscribe('nyt:page-resize', this.adjustArrows); | |
this.subscribe('nyt:ribbon-visiblility', this.restrictArrow); | |
this.subscribe('nyt:ribbon-left', this.handleKeyboardLeftArrow); | |
this.subscribe('nyt:ribbon-right', this.handleKeyboardRightArrow); | |
if (this.pageManager.isMobile()) { | |
this.$arrows.hide(); | |
} | |
}, | |
/** | |
* Adjusts the arrow position at a certain browser width so that the x | |
* position of the arrows is still aligned with the edges of the | |
* story container | |
* | |
* @private | |
* @method adjustArrows | |
**/ | |
adjustArrows: function () { | |
var width, sWidth; | |
var breakpoint = this.pageManager.getCurrentBreakpoint(); | |
var $previous = this.$arrows.filter('.previous'); | |
var $next = this.$arrows.filter('.next'); | |
if (breakpoint >= 10070) { | |
// if we are at viewport-large-92 or greater | |
width = this.$window.width(); | |
sWidth = this.$shell.width(); | |
$previous.css('left', (width - sWidth) / 2); | |
$next.css('right', (width - sWidth) / 2); | |
this.cssControl = false; | |
} else { | |
if (this.cssControl) { | |
return; | |
} | |
$previous.css('left', ''); | |
$next.css('right', ''); | |
this.cssControl = true; | |
} | |
}, | |
/** | |
* Adjusts the text in the container so that it is | |
* vertically aligned | |
* | |
* @private | |
* @method adjustText | |
**/ | |
adjustText: function () { | |
var $summary, $story, $el, paddingTop; | |
var arrowsView = this; | |
this.$arrows.each(function (key, el) { | |
$el = $(el); | |
$story = $el.find('.story'); | |
$summary = $el.find('.story .summary'); | |
paddingTop = parseInt($story.css('padding-top'), 10); | |
$story.css({ | |
'display': 'block', | |
'opacity': 0 | |
}); | |
$summary.css('margin-top', $el.height() / 2 - $summary.height() / 2 - paddingTop); | |
$story.css({ | |
'display': 'none', | |
'opacity': 1 | |
}); | |
}); | |
}, | |
/** | |
* Sets the 'restrict' property to true or false based on whether or not the | |
* ribbon is in the viewport | |
* | |
* @private | |
* @method restrictArrow | |
* @param isRibbonVisible {Boolean} Is the Ribbon in view? | |
**/ | |
restrictArrow: function (isRibbonVisible) { | |
this.restrict = isRibbonVisible; | |
}, | |
/** | |
* Navigates to the Previous Page | |
* | |
* @private | |
* @method previousPage | |
**/ | |
previousPage: function () { | |
var $prev = this.$arrows.filter('.previous'); | |
var href = $prev.data('href'); | |
if (href) { | |
window.location = href; | |
} | |
}, | |
/** | |
* Navigates to the Next Page | |
* | |
* @private | |
* @method nextPage | |
**/ | |
nextPage: function () { | |
var $next = this.$arrows.filter('.next'); | |
var href = $next.data('href'); | |
if (href) { | |
window.location = href; | |
} | |
}, | |
/** | |
* Interpolates the template that is used to create the arrow | |
* markup to be injected into the dom | |
* | |
* @private | |
* @method createTemplate | |
* @param dir {String} left or right arrow | |
* @param article {Object} The article model to get interpolated | |
* @return {String} The interpolated html string to be injected into the dom | |
**/ | |
createTemplate: function (dir, article) { | |
var adRelationship, shouldQueueAd, adPosition; | |
var data = { | |
direction : dir, | |
display : 'none', | |
title : '', | |
image : '', | |
link : '', | |
kicker : '', | |
shouldQueueAd : false | |
}; | |
if (article) { | |
// if there is an ad, find its position and determine whether it should be fired on click / arrow | |
if (_.indexOf(this.pageManager.getMeta('ads_adNames'), 'Ribbon') >= 0) { | |
adPosition = this.getAdIndex(this.activeStoryIndex); | |
adRelationship = adPosition - _.indexOf(this.feed.models, article); | |
shouldQueueAd = ((dir === 'previous' && adRelationship === 1) || (dir === 'next' && adRelationship === 0)); | |
} | |
data = { | |
direction : dir, | |
display : 'block', | |
title : article.get('headline') || article.get('title'), | |
image : article.getCrop('thumbStandard'), | |
link : this.makeLinkRelative(article.get('link'), adPosition), | |
kicker : article.get('kicker'), | |
shouldQueueAd : shouldQueueAd | |
}; | |
} | |
return templates.ribbonPageNavigation(data); | |
}, | |
/** | |
* Takes a url string and removes the nytimes host from it to | |
* make the path relative | |
* | |
* @private | |
* @method makeLinkRelative | |
* @param link {String} The canonical url for an article | |
* @return {String} The adjusted string | |
**/ | |
makeLinkRelative: function (link, adPosition) { | |
var a = document.createElement("a"); | |
a.href = link; | |
if (a.hostname.indexOf('www') === 0 && window.location.hostname.indexOf('www') === 0) { | |
link = a.pathname.indexOf('/') === 0 ? a.pathname : '/' + a.pathname; | |
} | |
if (adPosition) { | |
link += '?ribbon-ad-idx=' + adPosition; | |
} | |
//Add ribbon reference to correct collection | |
link += /\?/.test(link) ? '&' : '?'; | |
link += this.feed.getIdentifier(); | |
return link; | |
}, | |
/** | |
* Used instead of the normal anchor tag click behavior so | |
* we can easily add Ajax functionality if needed | |
* | |
* @private | |
* @method changeArticle | |
* @param event {Object} A click event object | |
**/ | |
changeArticle: function (event) { | |
event.preventDefault(); | |
var $arrow = $(event.currentTarget); | |
var href = this.trackingAppendParams($arrow.data('href'), { | |
'action': 'click', | |
'region': $arrow.is('.next') ? 'FixedRight' : 'FixedLeft' | |
}); | |
if (this.fireQueuedAd($arrow) !== true) { | |
window.location = href; | |
} | |
}, | |
/** | |
* detect, fire the ribbon ad, and return the result | |
* | |
* @private | |
* @method fireQueuedAd | |
* @param $arrow {Object} a jquery object with appropriate data attached | |
* @return {Boolean} whether the ad was fired or not | |
**/ | |
fireQueuedAd: function ($arrow) { | |
var queueAd = $arrow.data('queue-ad'); | |
var $ribbonAd = $('.ribbon-ad'); | |
$('#ribbon').find('.collection-item').removeClass('active'); | |
if (queueAd !== true) { | |
return false; | |
} | |
if ($ribbonAd.find('> iframe').length > 0) { | |
this.broadcast('nyt:ads-fire-ribbon-interstitial'); | |
$arrow.data('queue-ad', false); | |
} else { | |
window.location = $ribbonAd.find('#ribbonAdBodytxt').attr('href'); | |
} | |
return true; | |
}, | |
/** | |
* the ribbon ad has been fired | |
* | |
* @private | |
* @method ribbonInterstitialFired | |
**/ | |
ribbonInterstitialFired: function () { | |
if (this.pageManager.isMobile()) { | |
this.$arrows.show(); | |
} | |
}, | |
/** | |
* Prevents the scrolling behavior on touch when swiping | |
* | |
* @private | |
* @method preventScroll | |
* @param event {Event} the touchmove event | |
**/ | |
preventScroll: function (e) { | |
this.scrollLock = true; | |
e.preventDefault(); | |
}, | |
/** | |
* A left or right swipe will go to the appropriate article | |
* | |
* @private | |
* @method handlePageSwipe | |
* @param direction {Object} The direction in which a user swiped. | |
**/ | |
handlePageDrag: function (e, dir) { | |
//Ignore all page drags when the media viewer is open | |
var mediaViewer = this.pageManager.getMeta('mediaviewer_isVisible') || false; | |
if (mediaViewer || !e.gesture) { | |
return; | |
} | |
var action, $arrow; | |
var maxWidth = this.expandWidth; | |
var minWidth = 0; | |
var angle = e.gesture.distance; | |
var dist = e.gesture.distance * 1.5 - 20; | |
//prevent scrolling on start | |
if (!this.scrollLock) { | |
this.$document.on('touchmove', this.preventScroll); | |
} | |
//set which arrow | |
if (e.gesture.direction === 'left') { | |
$arrow = this.$arrows.filter('.next'); | |
} else if (e.gesture.direction === 'right') { | |
$arrow = this.$arrows.filter('.previous'); | |
} | |
//cancel on no link, short distances, and non horizontal angles | |
if (!$arrow || !$arrow.data('href') || e.gesture.distance < 20) { | |
this.$arrows.hide(); | |
this.$document.off('touchmove', this.preventScroll); | |
this.scrollLock = false; | |
return; | |
} | |
//move arrows left and right | |
if (dir === 'right' || dir === 'left') { | |
dist = dist < maxWidth ? dist : maxWidth; | |
dist = dist > minWidth ? dist : minWidth; | |
action = dist < 100 ? 'hide' : 'show'; | |
$arrow | |
.show() | |
.css('width', dist + 'px') | |
.find('.story')[action](); | |
//truncate text to three lines | |
this.truncateArticleSummary($arrow); | |
//navigate away on end if it's maximized (with a 5px buffer) | |
} else if ($arrow && dir === 'end') { | |
if ($arrow.width() < maxWidth - 5) { | |
//note: use hide instead of aniamte | |
//there is bug with jquery/ipad where animation fails on scroll jumps | |
$arrow.hide(); | |
} else { | |
var href = this.trackingAppendParams($arrow.data('href'), { | |
'action': 'swipe', | |
'region': e.gesture.direction === 'left' ? 'FixedRight' : 'FixedLeft' | |
}); | |
window.location = href; | |
} | |
this.$document.off('touchmove', this.preventScroll); | |
this.scrollLock = false; | |
//anythine else hide the arrow | |
} else if (dir !== 'start') { | |
this.$arrows.hide(); | |
this.$document.off('touchmove', this.preventScroll); | |
this.scrollLock = false; | |
} | |
}, | |
/** | |
* To handle when the event fired from the page indicating that the left arrow key is to | |
* navigate to the next ribbon article | |
* | |
* @method handleKeyboardLeftArrow | |
*/ | |
handleKeyboardLeftArrow: function () { | |
var $arrow = this.$arrows.filter('.previous'); | |
var href = this.trackingAppendParams($arrow.data('href'), { | |
'action': 'keypress', | |
'region': 'FixedLeft' | |
}); | |
if (this.fireQueuedAd($arrow) !== true) { | |
$arrow.data('href', href); | |
this.previousPage(); | |
} | |
}, | |
/** | |
* To handle when the event fired from the page indicating that the right arrow key is to | |
* navigate to the next ribbon article | |
* | |
* @method handleKeyboardRightArrow | |
*/ | |
handleKeyboardRightArrow: function () { | |
var $arrow = this.$arrows.filter('.next'); | |
var href = this.trackingAppendParams($arrow.data('href'), { | |
'action': 'keypress', | |
'region': 'FixedRight' | |
}); | |
if (this.fireQueuedAd($arrow) !== true) { | |
$arrow.data('href', href); | |
this.nextPage(); | |
} | |
}, | |
/** | |
* On the mouseenter event of the visible part of the arrow, | |
* shows the rest of the arrow by animating the width and | |
* then fading in the article content immediately after | |
* | |
* @private | |
* @method showArticle | |
* @param event {Object} A mouseenter event object | |
**/ | |
showArticle: function (event) { | |
var arrowsView = this; | |
var $arrow = $(event.currentTarget); | |
if (this.restrict || this.expanded) { return; } | |
this.origWidth = this.$arrows.width(); | |
this.timeout = window.setTimeout(function () { | |
$arrow | |
.animate({ | |
width: arrowsView.expandWidth | |
}, { | |
duration: arrowsView.speed, | |
complete: function () { | |
if (!arrowsView.expanded) { | |
return false; | |
} | |
$arrow | |
.find('.story') | |
.fadeIn(arrowsView.speed); | |
//truncate the text to only 3 lines | |
arrowsView.truncateArticleSummary($arrow); | |
} | |
}); | |
arrowsView.expanded = true; | |
}, this.delay); | |
}, | |
/** | |
* Truncates text of the article summary to three lines and adjusts margins as necessary | |
* | |
* @private | |
* @method truncateArticleSummary | |
* @param $arrow {Object} the the ribbon page navigation object | |
**/ | |
truncateArticleSummary: function ($arrow) { | |
var maxSummaryHeight = 48; | |
var $summary = $arrow.find('.story .summary'); | |
// Truncate captions that are too long | |
if ($arrow.find('.story-heading').height() > maxSummaryHeight ) { | |
this.truncateText($arrow.find('.story-heading'), maxSummaryHeight); | |
$summary.css('margin-top', $arrow.find('.story-heading').height() / 2 - maxSummaryHeight / 2); | |
} | |
}, | |
/** | |
* On the mouseleave event, contracts the arrow width | |
* so the article content is no longer showing | |
* | |
* @private | |
* @method hideArticle | |
* @param event {Object} A mouseleave event object | |
**/ | |
hideArticle: function (event) { | |
var arrowsView = this; | |
var $tooltip = $('#ribbon-page-navigation-modal').find('.modal'); | |
var $arrow = $tooltip.is(event.currentTarget) ? $('.ribbon-page-navigation.next') : $(event.currentTarget); | |
clearTimeout(this.timeout); | |
//when the nav isn't expanded and not moving on a tooltip exit out | |
if (!this.expanded || $tooltip.has(event.relatedTarget).length > 0) { | |
return; | |
} | |
$arrow | |
.animate({ | |
width: arrowsView.origWidth | |
}, { | |
duration: this.speed, | |
complete: function () { | |
$arrow.css('width', ''); | |
} | |
}) | |
.find('.story').hide(); | |
this.expanded = false; | |
}, | |
/** | |
* Shows right arrow when comment panel is opened. | |
* | |
* @private | |
* @method showRightArrow | |
**/ | |
showRightArrow: function () { | |
if (this.$arrows) { | |
this.$arrows.filter('.next').show(); | |
} | |
}, | |
/** | |
* Hides right arrow when comment panel is opened. | |
* | |
* @private | |
* @method hideRightArrow | |
**/ | |
hideRightArrow: function () { | |
if (this.$arrows) { | |
this.$arrows.filter('.next').hide(); | |
} | |
}, | |
/** | |
* Add a tooltip to the navigation arrows on the first use. | |
* | |
* @private | |
* @method addToolTip | |
**/ | |
addToolTip: function () { | |
var ribbonObj = this; | |
var openTimeout; | |
var ribbonTip = new Modal({ | |
id: 'ribbon-page-navigation-modal', | |
modalContent: templates.ribbonPageNavTip(), | |
binding: '.ribbon-page-navigation.next', | |
tailDirection: 'right', | |
canOpenOnHover: true, | |
width: '322px', | |
mouseEnterDelay: 500, | |
tailTopOffset: -5, | |
tailLeftOffset: 9, | |
closeOnMouseOut: true, | |
openCallback: function () { | |
pageStorage.save('ribbon_hasViewedTooltip', true); | |
openTimeout = window.setTimeout(ribbonTip.close, 20000); | |
ribbonObj.subscribeOnce('nyt:page-scroll', ribbonTip.close); | |
}, | |
closeCallback: function () { | |
ribbonTip.removeFromPage(); | |
window.clearTimeout(openTimeout); | |
} | |
}); | |
//Add modal to page | |
ribbonTip.addToPage(); | |
} | |
}) | |
); | |
return RibbonPageNavigation; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment