Skip to content

Instantly share code, notes, and snippets.

@oyiptong
Created January 16, 2014 12:36
Show Gist options
  • Save oyiptong/8454329 to your computer and use it in GitHub Desktop.
Save oyiptong/8454329 to your computer and use it in GitHub Desktop.
/**
* 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