Created
January 16, 2014 12:31
-
-
Save oyiptong/8454254 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
/** | |
* Adds the ability to navigate through a ribbon on a page. | |
* | |
* <p><b>Require Path:</b> shared/ribbon/views/ribbon</p> | |
* | |
* @module Shared | |
* @submodule Shared.Ribbon | |
* @namespace Ribbon | |
* @class View | |
* @constructor | |
* @extends foundation/views/base-view | |
**/ | |
define('shared/ribbon/views/ribbon',[ | |
'jquery/nyt', | |
'underscore/nyt', | |
'foundation/views/base-view', | |
'shared/ribbon/templates', | |
'shared/ribbon/views/ribbon-page-navigation', | |
'shared/ribbon/instances/ribbon-data', | |
'shared/ribbon/views/helpers/mixin' | |
], function ($, _, BaseView, Templates, RibbonPageNavigation, feed, RibbonMixin) { | |
'use strict'; | |
var RibbonView = BaseView.registerView('ribbon').extend( | |
_.extend({}, RibbonMixin, { | |
el: '#ribbon', | |
collection: feed, | |
template: Templates.storyCollection, | |
articleTemplate: Templates.article, | |
adTemplate: Templates.ad, | |
isRibbonVisible: false, | |
firstLoad: true, | |
toggleDisabled: 0, | |
oldScrollTop: 0, | |
animationDistance: 100, | |
minDownDistance: 100, | |
minUpDistance: 300, | |
speed: 200, | |
hammerSettings: { | |
drag_block_vertical: true, | |
swipe_velocity: 0.7, | |
drag_min_distance: 3 | |
}, | |
events: { | |
'click .collection-menu li a': 'handleArticleClick', | |
'click .ribbon-navigation-container .next': 'handleNextArrow', | |
'click .ribbon-navigation-container .previous': 'handlePreviousArrow', | |
'mouseenter': 'handleRibbonMouseEnter', | |
'mouseleave': 'handleRibbonMouseOut', | |
// touch events | |
'touch': 'handleTouch', | |
'tap': 'handleArticleClick', | |
'hold': 'handleTouchHold', | |
'dragstart': 'handleTouchDragStart', | |
'drag': 'handleRibbonDrag', | |
'swipe': 'handleRibbonSwipe' | |
}, | |
nytEvents: { | |
'nyt:page-resize': 'resizeRibbon', | |
'nyt:messaging-critical-alerts-move-furniture': 'moveRibbonForAlerts', | |
'nyt:messaging-suggestions-move-furniture': 'moveRibbonForAlerts', | |
'nyt:messaging-message-critical-alerts-closed': 'enableRibbonToggle', | |
'nyt:messaging-message-suggestions-closed': 'enableRibbonToggle', | |
'nyt:comments-panel-opened': 'disableRibbonToggle', | |
'nyt:comments-panel-closed': 'enableRibbonToggle' | |
}, | |
/** | |
* Initialize the ribbon | |
* | |
* @public | |
* @method constructor | |
**/ | |
initialize: function () { | |
_.bindAll(this, | |
'handleMouseMove', | |
'handleArticleClick', | |
'handleRibbonAdClick', | |
'hideCollectionMarkers', | |
'pollHiddenCollections', | |
'pollShowingTabs', | |
'revertRibbon', | |
'handleRibbonSwipe', | |
'handleRibbonDrag', | |
'handleTouch', | |
'handleTouchHold', | |
'handleTouchDragStart', | |
'applyTranslateToRibbon', | |
'assignListenersAndLoad' | |
); | |
this.isDesktop = this.pageManager.isDesktop(); | |
this.canonical = this.pageManager.getCanonical(); | |
this.trackingBaseData = { | |
'module': 'Ribbon', | |
'version': this.collection.originalLoadType, | |
'region': 'Header' | |
}; | |
this.listenToOnce(this.collection, 'sync', _.bind(function () { | |
this.trackingTriggerImpression('ribbon-first-load', { | |
'eventName': 'impression', | |
'action': 'impression', | |
'contentCollection': this.collection.collectionLabels[0].title | |
}); | |
}, this)); | |
}, | |
/** | |
* Renders the ribbon on dom ready | |
* | |
* @private | |
* @method handlePageReady | |
**/ | |
handleDomReady: function () { | |
this.$loader = this.$('.ribbon-loader'); | |
//set ribbon variables | |
this.ribbonMarginTop = parseInt(this.$el.css('margin-top'), 10); | |
this.ribbonMarginBottom = parseInt(this.$el.css('margin-bottom'), 10); | |
this.ribbonHeight = this.$el.height(); | |
this.mastheadHeight = $('#masthead').height() - 1; | |
this.toggleDisabled = false; | |
//assign ribbon selectors | |
this.$ribbonMenu = this.$el.find('.ribbon-menu'); | |
this.$ribbonNavigation = this.$el.find('.ribbon-navigation-container'); | |
this.$previousArrow = this.$ribbonNavigation.find('.previous'); | |
this.$nextArrow = this.$ribbonNavigation.find('.next'); | |
//prime the visibility check | |
this.isRibbonVisible = this.pageManager.isComponentVisible(this.$el); | |
//throw an event when the ribbon comes in and out of view | |
this.listenTo(this.pageManager, 'nyt:page-scroll', this.handleScroll); | |
this.createAdsDeferral(this.assignListenersAndLoad); | |
}, | |
/** collection of actions that allow rendering and data init | |
* | |
* @private | |
* @method assignListenersAndLoad | |
**/ | |
assignListenersAndLoad: function () { | |
window.clearTimeout(this.adxTimeout); | |
this.stopSubscribing('nyt:ads-rendered', this.assignListenersAndLoad); | |
//fire on initial load | |
this.listenTo(this.collection, 'sync', this.render); | |
this.listenToOnce(this.collection, 'sync', this.renderFurniture); | |
this.listenTo(this.collection, 'nyt:ribbon-custom-collection-loaded', this.render); | |
this.collection.loadData(); | |
//create the page navigation arrows so they sit in the story body | |
new RibbonPageNavigation(); | |
}, | |
/** | |
* Renders new content for the Ribbon each time we hear the collection has updated. | |
* | |
* @private | |
* @method render | |
**/ | |
render: function () { | |
var modelsToProcess, collectionLabel, $html, containerWidth, adIndex, adData, $ribbonAd, ribbonLink; | |
var initialUnitWidth = this.collectionStoryWidth + this.animationDistance; | |
var ribbonAdFromMeta = _.indexOf(this.pageManager.getMeta('ads_adNames'), 'Ribbon') >= 0; | |
//Render the items in the collection that are not in display | |
modelsToProcess = this.collection.where({processed: false}); | |
// leave render if there are no new models to process | |
if (modelsToProcess.length === 0) { | |
return; | |
} | |
if ((ribbonAdFromMeta || this.pageManager.getUrlParam('ribbon-ad-idx')) && this.firstLoad === true) { | |
adData = this.returnRibbonAdData(modelsToProcess, ribbonAdFromMeta); | |
adIndex = adData.index; | |
if (adData.model) { | |
modelsToProcess.splice(adData.index, 0, adData.model); | |
} | |
} | |
//Use the first model in the collection to derive the collection label information | |
collectionLabel = this.collection.collectionLabels[modelsToProcess[0].get('collectionId')]; | |
//if the collection label doesn't exist. Exit | |
if (!collectionLabel) { | |
return; | |
} | |
//generate a list for an individual collection | |
$html = $(this.template({ | |
firstLoad: this.firstLoad, | |
canonical: this.canonical, | |
articles: modelsToProcess, | |
adTemplate: this.adTemplate, | |
articleTemplate: this.articleTemplate, | |
collectionLabel: collectionLabel, | |
adPosition: adIndex, | |
sectionId: this.collection.getIdentifier() | |
})); | |
//set the entire menu's width based on the number of collection items, adding the number of collections to account their border | |
containerWidth = (this.collection.length * initialUnitWidth) + this.$loader.width() + this.collection.collectionLabels.length; | |
this.$ribbonMenu.css('width', containerWidth); | |
//append the collection to the ribbon | |
this.$loader.before($html); | |
// always animate on a secondary load | |
// only animate on a primary load if the referrer is not an article | |
if ((this.firstLoad && !this.referredFromArticle()) || !this.firstLoad) { | |
this.animateRibbonStories($html); | |
} | |
// if there is a Ribbon placehoder in the dom, fire a new ads placement event | |
$ribbonAd = this.$el.find('#Ribbon'); | |
if ($ribbonAd.length > 0 && this.firstLoad === true) { | |
this.broadcast('nyt:ads-new-placement', 'Ribbon'); | |
//add the ribbon reference to all branded content ribbon links | |
//so that the page will use the collection of origin if there is a ribbon | |
ribbonLink = $ribbonAd.find('> a'); | |
if (ribbonLink.length) { | |
ribbonLink.attr('href', ribbonLink.attr('href') + '?' + this.collection.getIdentifier()); | |
} | |
this.assignHandlerToIframeClick($ribbonAd.find('iframe'), this.handleRibbonAdClick); | |
} | |
this.assignSyncedHtmlToView(); | |
//manipulate the ribbon data so it fits properly in the view | |
this.updateCollectionValues(); | |
this.firstLoad = false; | |
this.broadcast('nyt:ribbon-rendered'); | |
}, | |
/** | |
* Allow clicks inside an iFrame to be detected in the parent document | |
* | |
* @private | |
* @method assignHandlerToIframeClick | |
* @param $element {Object} iframe wrapped in jQuery object | |
* @param handler {Function} the method to assign to the click | |
**/ | |
assignHandlerToIframeClick: function ($element, handler) { | |
var iframe, iframeDoc; | |
if (!$element.length) { | |
return; | |
} | |
iframe = $element.get(0); | |
iframeDoc = iframe.contentDocument || iframe.contentWindow.document; | |
if (typeof iframeDoc.addEventListener !== 'undefined') { | |
iframeDoc.addEventListener('click', handler, false); | |
} else if (typeof iframeDoc.attachEvent !== 'undefined') { | |
iframeDoc.attachEvent ('onclick', handler); | |
} | |
}, | |
/** | |
* Insert ad models into the ribbon collection and return the modified collection | |
* | |
* @private | |
* @method returnRibbonAdData | |
* @param modelsToProcess {Object} the original collection of story models | |
* @return {Object} the index of the ad and an ad model | |
**/ | |
returnRibbonAdData: function (modelsToProcess, adIndexFromMeta) { | |
var ribbonView = this; | |
var smallScreenBreakpoint = 1030; | |
var adPosition, adModel, activeStoryIndex; | |
activeStoryIndex = this.getStoryIndex(modelsToProcess); | |
adPosition = this.getAdIndex(activeStoryIndex); | |
if (adIndexFromMeta) { | |
adModel = this.collection.getAdModel(); | |
} | |
return { | |
index: adPosition, | |
model: adModel | |
}; | |
}, | |
/** | |
* tests whether the user came from an article page | |
* | |
* @private | |
* @method referredFromArticle | |
* @return {Boolean} the result of the test. defaults to false if no document.referrer is found | |
**/ | |
referredFromArticle: function () { | |
var anchorTag = document.createElement('a'); | |
var referrerPathname; | |
if (document.referrer) { | |
anchorTag.href = document.referrer; | |
referrerPathname = anchorTag.pathname; | |
return anchorTag.hostname.indexOf('nytimes.com') > -1 && /^(\/\w+)?\/\d+/.test(referrerPathname); | |
} | |
return false; | |
}, | |
/** | |
* On the initial load of the collection, render the supporting Ribbon items | |
* | |
* @private | |
* @method renderFurniture | |
**/ | |
renderFurniture: function () { | |
var index = -this.$el.find('.active').index(); | |
this.ribbonAnimation(index, true); | |
// if the initial collection end is visible, get more stories | |
if (this.testForCollectionEnd()) { | |
this.collection.loadFeed(); | |
} | |
// show/hide arrows in desktop mode | |
if (!Modernizr.touch) { | |
this.collectionMarkerTimeout = setTimeout(this.hideCollectionMarkers, 2000); | |
} else { | |
this.assignCustomEasing(); | |
} | |
}, | |
/** | |
* give a custom easing method to jQuery easing for the swipes | |
* | |
* @private | |
* @method assignCustomEasing | |
* @return {Object} the ribbon view object for chaining purposes | |
**/ | |
assignCustomEasing: function () { | |
$.easing.easeOutCirc = function (x, t, b, c, d) { | |
return c * Math.sqrt(1 - (t=t/d-1)*t) + b; | |
}; | |
return this; | |
}, | |
/** | |
* return an Array of matrix values | |
* | |
* @private | |
* @method matrixToArray | |
* @param {String} matrix the matrix to transform | |
* @return {Array} the matrix values in array format | |
**/ | |
matrixToArray: function (matrix) { | |
return matrix.substr(7, matrix.length - 8).split(', '); | |
}, | |
/** | |
* return the transform x value if available. if not fall back to position.left | |
* | |
* @method returnStartingXForTransfrom | |
* @return {String} the left or transformX position | |
**/ | |
returnXForTransform: function () { | |
var transformProperty = this.$ribbonMenu.css('-webkit-transform'); | |
return (transformProperty === 'none') ? this.$ribbonMenu.position().left : parseInt(this.matrixToArray(transformProperty)[4], 10); | |
}, | |
/** | |
* put the translate3d CSS propery on the ribbonMenu | |
* | |
* @private | |
* @method applyTranslateToRibbon | |
* @param {Number} xValue the value to apply | |
* @return {Object} jQuery | |
**/ | |
applyTranslateToRibbon: function (xValue) { | |
var translateProperty; | |
if (!isNaN(xValue)) { | |
translateProperty = 'translate3d(' + xValue + 'px, 0, 0)'; | |
return this.$ribbonMenu.css({ | |
'transform': translateProperty, | |
'-webkit-transform': translateProperty, | |
'msTransform': translateProperty | |
}); | |
} | |
}, | |
/** | |
* actions for the ribbon drag event | |
* @private | |
* @method handleRibbonDrag | |
* @param event {Object} the touch event | |
**/ | |
handleRibbonDrag: function (e) { | |
if (!e.gesture) { | |
return; | |
} | |
e.gesture.preventDefault(); | |
var totalDistanceTraveled, changeToApply, pixelsToShift, endVisibleUponEvent, newXPosition, translateProperty; | |
var currentXPosition = this.returnXForTransform(); | |
// find the distance covered in previous iterations | |
totalDistanceTraveled = this.startDragLocation - currentXPosition; | |
pixelsToShift = (totalDistanceTraveled === 0) ? e.gesture.deltaX : e.gesture.deltaX + totalDistanceTraveled; | |
newXPosition = this.handleRibbonEdges(currentXPosition + pixelsToShift); | |
endVisibleUponEvent = this.testForCollectionEnd(this.startDragLocation); | |
this.applyTranslateToRibbon(newXPosition); | |
// if the new position has exposed the end of the collections, get more stories | |
if (this.testForCollectionEnd() && !endVisibleUponEvent) { | |
this.collection.loadFeed(); | |
} | |
if (e.gesture.deltaX < 0) { | |
this.pollShowingTabs(); | |
} else { | |
this.pollHiddenCollections(); | |
} | |
}, | |
/** | |
* return the transform x value if available. if not fall back to position.left | |
* | |
* @method returnStartingXForTransfrom | |
* @return {String} the left or transformX position | |
**/ | |
returnStartingXForTransform: function () { | |
var transformProperty = this.$ribbonMenu.css('transform'); | |
return (transformProperty === 'none') ? this.$ribbonMenu.position().left : parseInt(this.matrixToArray(transformProperty)[4], 10); | |
}, | |
/** | |
* actions for the ribbon swipe event | |
* @private | |
* @method handleRibbonSwipe | |
* @param {Object} the touch event | |
**/ | |
handleRibbonSwipe: function (event) { | |
var ribbonView = this; | |
var coreDuration = 2000; | |
var ribbonSwipe, distanceMultiplier, deltaDistance, checkedLeftValue, swipeDuration, swipeAnimationPolling; | |
event.gesture.preventDefault(); | |
distanceMultiplier = 1 + (event.gesture.distance / ribbonView.$el.width()); | |
deltaDistance = event.gesture.deltaX * distanceMultiplier; | |
swipeDuration = coreDuration / event.gesture.velocityX; | |
checkedLeftValue = ribbonView.handleRibbonEdges(ribbonView.returnXForTransform() + deltaDistance); | |
swipeAnimationPolling = _.throttle(function () { | |
if (event.gesture.direction === 'left') { | |
ribbonView.pollShowingTabs(); | |
} else { | |
ribbonView.pollHiddenCollections(); | |
} | |
}); | |
var options = { | |
duration: swipeDuration, | |
easing: 'easeOutCirc', | |
step: ribbonView.applyTranslateToRibbon, | |
progress: swipeAnimationPolling, | |
always: function () { | |
// fire this even if the animation is stopped by another swipe | |
if (ribbonView.testForCollectionEnd(checkedLeftValue)) { | |
ribbonView.collection.loadFeed(); | |
} | |
swipeAnimationPolling(); | |
} | |
}; | |
this.animateSwipe(checkedLeftValue, options); | |
}, | |
/** | |
* handle touch events | |
* | |
* @private | |
* @method handleTouch | |
* @param {Object} the touch event | |
**/ | |
handleTouch: function (event) { | |
event.gesture.preventDefault(); | |
}, | |
/** | |
* handle hold events | |
* | |
* @private | |
* @method handleTouchHold | |
* @param {Object} the touch event | |
**/ | |
handleTouchHold: function (event) { | |
this.stopSwipeAnimation(); | |
}, | |
/** | |
* handle dragstart events | |
* | |
* @private | |
* @method handleTouchDragStart | |
* @param {Object} the touch event | |
**/ | |
handleTouchDragStart: function (event) { | |
this.stopSwipeAnimation(); | |
this.startDragLocation = this.returnXForTransform(); | |
}, | |
/** | |
* helper to stop swipe animations without erros | |
* | |
* @private | |
* @method stopSwipeAnimation | |
**/ | |
stopSwipeAnimation: function () { | |
if (this.$swipeAnimationElement) { | |
this.$swipeAnimationElement.stop(); | |
} | |
}, | |
/** | |
* Handle the behavior for when the mouse hovers over the ribbon container | |
* | |
* @private | |
* @method handleRibbonMouseOver | |
**/ | |
handleRibbonMouseEnter: function () { | |
if (!Modernizr.touch && typeof this.$ribbonMenu !== 'undefined') { | |
this.$el.on('mousemove', this.handleMouseMove); | |
clearTimeout(this.collectionMarkerTimeout); | |
if (this.$collectionMarkers) { | |
this.$collectionMarkers.show(); | |
} | |
if (this.$firstCollectionMarker) { | |
this.$firstCollectionMarker.show(); | |
} | |
} | |
}, | |
/** | |
* Handles the mouse movement inside of the ribbon and shows the appropriate | |
* navigation buttons depending on where the cursor. | |
* | |
* @private | |
* @method handleMouseMove | |
**/ | |
handleMouseMove: _.throttle(function (e) { | |
this.checkForActiveNavigation(e.clientX, e.clientY); | |
}), | |
/** | |
* Handle the behavior for when the mouse leaves the ribbon container | |
* | |
* @private | |
* @method handleRibbonMouseOut | |
**/ | |
handleRibbonMouseOut: function () { | |
var ribbonView = this; | |
if (!Modernizr.touch) { | |
this.$el.off('mousemove', this.handleMouseMove); | |
this.hideRibbonArrows(); | |
this.hideCollectionMarkers(); | |
} | |
}, | |
/** | |
* Handle the behavior when the previous button is clicked | |
* | |
* @private | |
* @method handleNextArrow | |
**/ | |
handleNextArrow: function (e) { | |
if (!$(e.currentTarget).hasClass('inactive')) { | |
this.trackingTrigger('ribbon-page-right', { | |
'eventName': 'ScrollRight', | |
'contentCollection': this.getSectionInView(), | |
'action': 'click' | |
}); | |
this.$previousArrow.removeClass('inactive'); | |
this.shiftRibbonLeft(); | |
} | |
}, | |
/** | |
* Handle the behavior when the next button is clicked | |
* | |
* @private | |
* @method handlePreviousArrow | |
**/ | |
handlePreviousArrow: function (e) { | |
if (!$(e.currentTarget).hasClass('inactive')) { | |
this.trackingTrigger('ribbon-page-left', { | |
'eventName': 'ScrollLeft', | |
'contentCollection': this.getSectionInView(), | |
'action': 'click' | |
}); | |
this.$nextArrow.removeClass('inactive'); | |
this.shiftRibbonRight(); | |
} | |
}, | |
/** | |
* broadcast an event to open the ribbon interstitial | |
* | |
* @private | |
* @method handleRibbonAdClick | |
* @param e {Object} the event object | |
**/ | |
handleRibbonAdClick: function (e) { | |
var $ribbonAdContainer; | |
this.broadcast('nyt:ads-fire-ribbon-interstitial'); | |
$ribbonAdContainer = this.$el.find('.ribbon-ad-container'); | |
this.animateRibbon($ribbonAdContainer); | |
// Keep preventDefault from throwing errors in IE8 | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} else { | |
return false; | |
} | |
}, | |
/** | |
* calculate the distance that the ribbon needs to move to bring active asset into position. | |
* Then move asset into new position | |
* | |
* @private | |
* @method animateRibbon | |
* @param ribbonAsset the ribbon asset to be moved | |
**/ | |
animateRibbon: function ($ribbonAsset) { | |
var unitsToMove, xPosition, animationDeferred; | |
xPosition = !Modernizr.touch ? $ribbonAsset.offset().left : this.returnXForTransform() * -1; | |
unitsToMove = Math.floor(xPosition / this.collectionStoryWidth) * -1; | |
animationDeferred = this.ribbonAnimation(unitsToMove); | |
return animationDeferred; | |
}, | |
/** | |
* Animate the ribbon when an article is clicked. | |
* | |
* @private | |
* @method animateRibbonClick | |
**/ | |
handleArticleClick: function (e) { | |
var unitsToMove, animationDeferred, $clickLink, targetHref, xPosition, desiredChange, newLeftValue; | |
var $eventTarget = $(e.target); | |
var $storyParent = $eventTarget.parents('li.collection-item'); | |
if ($storyParent.length > 0) { | |
//safe ribbon ads behave the same as normal stories | |
$clickLink = $storyParent.find('> a, #Ribbon > a'); | |
targetHref = $clickLink.attr('href'); | |
targetHref = this.trackingAppendParams(targetHref, { | |
'action': 'click', | |
'contentCollection': this.getCollectionByArticleElement($storyParent) | |
}); | |
if (e.metaKey !== true) { | |
e.preventDefault(); | |
// remove any active designation and reassign | |
this.$el.find('.collection-item').removeClass('active'); | |
$storyParent.addClass('active'); | |
if (e.type === 'tap') { | |
desiredChange = (this.collectionStoryWidth*0.25) - $storyParent.offset().left; | |
newLeftValue = this.returnXForTransform() + desiredChange; | |
animationDeferred = this.animateSwipe(newLeftValue); | |
} else { | |
xPosition = $storyParent.offset().left; | |
unitsToMove = Math.floor(xPosition / this.collectionStoryWidth); | |
animationDeferred = this.ribbonAnimation(-unitsToMove); | |
} | |
animationDeferred.done(function () { | |
window.location.href = targetHref; | |
}); | |
} | |
else { | |
$clickLink.attr('href', targetHref); | |
} | |
} else if ($eventTarget.parents('.collection-marker').length > 0) { | |
window.location.href = e.target.href; | |
} | |
}, | |
/** | |
* Handles what is triggered when the page is scrolled | |
* | |
* @private | |
* @method handleViewportChange | |
**/ | |
handleScroll: function () { | |
var top = this.pageManager.getViewport().top; | |
this.toggleRibbon(top); | |
this.checkRibbonVisibility(); | |
}, | |
/** | |
* hide all the collection markers | |
* | |
* @private | |
* @method hideCollectionMarkers | |
**/ | |
hideCollectionMarkers: function () { | |
if (this.$collectionMarkers) { | |
this.$collectionMarkers.hide(); | |
} | |
if (this.$firstCollectionMarker) { | |
this.$firstCollectionMarker.hide(); | |
} | |
}, | |
/** | |
* Move the collection markers to the left | |
* | |
* @private | |
* @method slideCollectionMarkers | |
* @return {Object} the promise for the slide | |
**/ | |
slideCollectionMarkers: function () { | |
var collectionMarkerWidth = this.$firstCollectionMarker.outerWidth(); | |
var $markersGroup = this.$el.find('.first-collection-marker'); | |
return $markersGroup.eq(0).animate({marginLeft: -collectionMarkerWidth}, 100).promise(); | |
}, | |
/** | |
* check the collections that are not showing to see if they have slid into view | |
* | |
* @private | |
* @method pollHiddenCollections | |
**/ | |
pollHiddenCollections: function () { | |
var ribbonView = this; | |
var $notShowingMarkers = this.$collectionMarkers.filter('.past-left-border'); | |
var leftCollectionEdge, $currentElement, $markerToClone, $newFirstMarker; | |
$notShowingMarkers.each(function (index, element) { | |
$currentElement = $(element); | |
leftCollectionEdge = $currentElement.offset().left; | |
// -1 to account for the border on the end of each collection | |
if (leftCollectionEdge - (ribbonView.$el.offset().left + 1) > ribbonView.collectionStoryWidth * 0.25) { | |
$currentElement.removeClass('past-left-border'); | |
$markerToClone = $currentElement.closest('.collection').prev().find('.collection-marker'); | |
$newFirstMarker = ribbonView.createFirstCollectionMarker($markerToClone, true); | |
ribbonView.$firstCollectionMarker.last().remove(); | |
ribbonView.$firstCollectionMarker = ribbonView.$el.find('.first-collection-marker'); | |
} | |
}); | |
}, | |
/** | |
* check the tabs that are on the right side of the left ribbon border | |
* | |
* @private | |
* @method pollShowingTabs | |
**/ | |
pollShowingTabs: function () { | |
var ribbonView = this; | |
var $showingTabs = this.$collectionMarkers.not('.past-left-border'); | |
$showingTabs.each(function (index, element) { | |
ribbonView.testForNewMarker($(element)); | |
}); | |
}, | |
/** | |
* check if marker passed has gone beyond the left ribbon border | |
* | |
* @private | |
* @method testForNewMarker | |
**/ | |
testForNewMarker: function ($currentElement) { | |
var $newFirstMarker; | |
var ribbonView = this; | |
var $currentFirstMarker = this.$firstCollectionMarker; | |
// detect if the current element is inside the right border of the partially obscured first story | |
if ($currentElement.offset().left - (ribbonView.$el.offset().left + 1) <= this.collectionStoryWidth * 0.25) { | |
$newFirstMarker = this.createFirstCollectionMarker($currentElement); | |
var slideDeferred = this.slideCollectionMarkers(); | |
slideDeferred.done(function () { | |
$currentFirstMarker.remove(); | |
ribbonView.$firstCollectionMarker = ribbonView.$el.find('.first-collection-marker'); | |
}); | |
} | |
}, | |
/** | |
* make a new first collection marker | |
* | |
* @private | |
* @method createFirstCollectionMarker | |
* @param $markerElement {Object} jquery Object to clone and place | |
* @param previous {Boolean} whether the ribbon moving to previous collections | |
* @return $markerElement {Object} the modified marker element | |
**/ | |
createFirstCollectionMarker: function ($markerElement, previous) { | |
var $newFirstMarker = $markerElement | |
.clone() | |
.addClass('first-collection-marker'); | |
if (previous === true) { | |
$newFirstMarker.removeClass('past-left-border'); | |
this.$firstCollectionMarker.before($newFirstMarker); | |
} else { | |
$markerElement.addClass('past-left-border'); | |
$newFirstMarker.appendTo(this.$el); | |
} | |
return $markerElement; | |
}, | |
/** | |
* Handle moving the ribbon by units and animate | |
* | |
* @private | |
* @method ribbonAnimation | |
* @param unitsToMove {Integer} the number of units for shifting | |
**/ | |
ribbonAnimation: function (unitsToMove, noAnimation) { | |
var ribbonView = this; | |
var currentLeftValue = this.$ribbonMenu.position().left; | |
var newLeftValue, translateProperty, checkForMarkerActions; | |
if (currentLeftValue === 0) { | |
currentLeftValue = this.collectionStoryWidth * 0.25; | |
} | |
newLeftValue = currentLeftValue + (unitsToMove * this.collectionStoryWidth); | |
newLeftValue = this.handleRibbonEdges(newLeftValue); | |
this.checkArrowsAgainstRibbonBoundaries(newLeftValue); | |
if (noAnimation) { | |
if (!Modernizr.touch) { | |
return this.$ribbonMenu.css({left: newLeftValue}); | |
} else { | |
return this.applyTranslateToRibbon(newLeftValue); | |
} | |
} else { | |
if (!Modernizr.touch) { | |
checkForMarkerActions = _.throttle(function () { | |
if (unitsToMove < 0) { | |
ribbonView.pollShowingTabs(); | |
} else { | |
ribbonView.pollHiddenCollections(); | |
} | |
}, 75); | |
return this.$ribbonMenu | |
.stop() | |
.animate({ | |
left: newLeftValue | |
}, { | |
step: checkForMarkerActions | |
}) | |
.promise(); | |
} else { | |
return this.animateSwipe(newLeftValue); | |
} | |
} | |
}, | |
/** | |
* encapsulates the animation for swiping | |
* | |
* @private | |
* @method animateSwipe | |
* @param newLeftValue {Integer} | |
* @return {Object} a jquery promise for the animation | |
**/ | |
animateSwipe: function (newLeftValue, options) { | |
var ribbonView = this; | |
if (!options) { | |
options = { | |
step: this.applyTranslateToRibbon | |
}; | |
} | |
this.stopSwipeAnimation(); | |
this.$swipeAnimationElement = $({animateDummyProperty: ribbonView.returnXForTransform()}); | |
return this.$swipeAnimationElement.animate({ | |
animateDummyProperty: newLeftValue | |
}, options) | |
.promise(); | |
}, | |
/** | |
* see if the border contents are flush with the ribbon boundaries and turn off the appropriate arrow | |
* | |
* @private | |
* @method checkArrowsAgainstRibbonBoundaries | |
* @param ribbonLeftValue {Integer} the current or proposed value for $ribbonMenu's left position | |
**/ | |
checkArrowsAgainstRibbonBoundaries: function (ribbonLeftValue) { | |
if (ribbonLeftValue === this.$el.width() - this.getCollectionsWidth()) { | |
this.$nextArrow.addClass('inactive'); | |
} else if (ribbonLeftValue === 0) { | |
this.$previousArrow.addClass('inactive'); | |
} | |
}, | |
/** | |
* check if the end of the collection will be visible given a specific left value | |
* | |
* @private | |
* @method testForCollectionEnd | |
* @leftValueToTest {Number} desired left value | |
**/ | |
testForCollectionEnd: function (leftValueToTest) { | |
if (typeof leftValueToTest === 'undefined') { | |
leftValueToTest = this.$ribbonMenu.position().left; | |
} | |
return leftValueToTest + this.getCollectionsWidth() < this.$el.width(); | |
}, | |
/** | |
* handle any actions the ribbon needs to take when it runs of out stories on either side | |
* | |
* @private | |
* @method handleRibbonEdges | |
* @param newLeftValue {Integer} the new proposed value of the collectionMenu's left property | |
* @returns newLeftValue {Integer} any changes to the proposed value | |
**/ | |
handleRibbonEdges: function (newLeftValue) { | |
var leftInCollection; | |
// make adjustments if it's scrolled back to the first item | |
if (newLeftValue > 0) { | |
newLeftValue = 0; | |
} | |
// the total collection Width - the element width | |
// how much is to the right of the left border | |
if (this.testForCollectionEnd(newLeftValue)) { | |
if (this.collection.feedSource.length === 0) { | |
newLeftValue = this.$el.width() - this.getCollectionsWidth(); | |
} | |
} | |
this.$el.toggleClass('ribbon-start', newLeftValue === 0); | |
return newLeftValue; | |
}, | |
/** | |
* Determine how to position the ribbon when moving to the right | |
* | |
* @private | |
* @method shiftRibbonRight | |
**/ | |
shiftRibbonRight: function () { | |
var ribbonView = this; | |
var storyUnitsAvailable = this.storyUnitsInView(); | |
var ribbonDeferred = ribbonView.ribbonAnimation(storyUnitsAvailable); | |
ribbonDeferred.done(function () { | |
//check one last time | |
ribbonView.pollHiddenCollections(); | |
ribbonView.broadcast('nyt:ribbon-animation-finished'); | |
}); | |
}, | |
/** | |
* Determine how to position the ribbon when moving to the left | |
* | |
* @private | |
* @method shiftRibbonLeft | |
**/ | |
shiftRibbonLeft: function () { | |
var ribbonView = this; | |
var storyUnitsAvailable = this.storyUnitsInView(); | |
var ribbonDeferred = this.ribbonAnimation(-storyUnitsAvailable); | |
ribbonDeferred.done(function () { | |
if (ribbonView.testForCollectionEnd()) { | |
ribbonView.collection.loadFeed(); | |
} | |
ribbonView.pollShowingTabs(); | |
ribbonView.broadcast('nyt:ribbon-animation-finished'); | |
}); | |
}, | |
/** | |
* Assign new HTML to the Object | |
* | |
* @private | |
* @method assignSyncedHtmlToView | |
**/ | |
assignSyncedHtmlToView: function () { | |
this.$collectionMarkers = this.$ribbonMenu.find('.collection-marker'); | |
// create a first collection marker if there is none | |
if (this.$el.find('.first-collection-marker').length === 0) { | |
this.createFirstCollectionMarker(this.$collectionMarkers.eq(0)); | |
} | |
this.$firstCollectionMarker = this.$el.find('.first-collection-marker').eq(0); | |
}, | |
/** | |
* Update the content in the ribbon | |
* | |
* @private | |
* @method updateCollectionValues | |
**/ | |
updateCollectionValues: function () { | |
var ribbonView = this; | |
//truncate the headlines so the headline is only 48px tall | |
this.$ribbonMenu.find('.story').each(function () { | |
var $el = $(this); | |
var $headline = $el.find('.story-heading'); | |
//when there is no kicker, show 4 lines of text. | |
ribbonView.truncateText($headline, $el.find('.kicker').length ? 48 : 64); | |
//add a class when there are 4 lines of headline | |
$headline.toggleClass('long-story-heading', $headline.height() > 48); | |
}); | |
}, | |
/** | |
* checks to see if the mouse pointer location warrants a display of the nav arrows | |
* | |
* @private | |
* @method checkForActiveNavigation | |
* @param xPosition {Integer} mouse cursor x position | |
* @param yPosition {Integer} mouse cursor y position | |
**/ | |
checkForActiveNavigation: function (xPosition, yPosition) { | |
var ribbonWidth = this.$el.width(); | |
var deadZoneBorderLeft = Math.ceil(0.4 * ribbonWidth); | |
var deadZoneBorderRight = Math.ceil(0.6 * ribbonWidth); | |
var leftIsActive = xPosition <= deadZoneBorderLeft; | |
var rightIsActive = xPosition >= deadZoneBorderRight; | |
var menuTop = this.$ribbonMenu.offset().top; | |
var menuBottom = this.$ribbonMenu.height() + menuTop; | |
var navigationContainerCss; | |
if (leftIsActive) { | |
navigationContainerCss = { | |
left: '10px', | |
right: 'auto' | |
}; | |
} else if (rightIsActive) { | |
navigationContainerCss = { | |
left: 'auto', | |
right: '10px' | |
}; | |
} | |
if (leftIsActive || rightIsActive) { | |
this.$ribbonNavigation.css(navigationContainerCss); | |
this.showRibbonArrows(); | |
} else { | |
this.hideRibbonArrows(); | |
} | |
}, | |
/** | |
* Resizes the ribbon based on a width calculated | |
* in the getWidth method | |
* | |
* @private | |
* @method resizeRibbon | |
**/ | |
resizeRibbon: function () { | |
this.$el.css('width', this.getWidth()); | |
}, | |
/** | |
* Get the width that the ribbon should be set to | |
* which is a combination of the shell width minus | |
* the left margin of the ribbon | |
* | |
* @private | |
* @method getWidth | |
* @return {Number} the width value to set the ribbon to | |
**/ | |
getWidth: function () { | |
var marginLeft = parseInt(this.$el.css('margin-left'), 10); | |
return this.$shell.width() - marginLeft; | |
}, | |
/** | |
* show the ribbon arrows | |
* | |
* @private | |
* @method showRibbonArrows | |
**/ | |
showRibbonArrows: function () { | |
this.$ribbonNavigation.show(); | |
}, | |
/** | |
* hide the ribbon arrows | |
* | |
* @private | |
* @method hideRibbonArrows | |
**/ | |
hideRibbonArrows: function () { | |
if (this.$ribbonNavigation) { | |
this.$ribbonNavigation.hide(); | |
} | |
}, | |
/** | |
* return the width of all the collections that have been returned | |
* | |
* @private | |
* @method getCollectionsWidth | |
**/ | |
getCollectionsWidth: function () { | |
return this.collection.length * this.collectionStoryWidth; | |
}, | |
/** | |
* This is an event handler that passes in a method | |
* to animate the ribbon | |
* | |
* @private | |
* @method moveRibbonForAlerts | |
* @param animate {Function} A method to execute that will animate an element passed in | |
**/ | |
moveRibbonForAlerts: function (animate) { | |
this.disableRibbonToggle(); | |
animate(this.$el); | |
}, | |
/** | |
* Disable the ribbon toggling functionality | |
* | |
* @private | |
* @method disableRibbonToggle | |
**/ | |
disableRibbonToggle: function () { | |
this.toggleDisabled += 1; | |
this.revertRibbon(); | |
}, | |
/** | |
* Sets the toggleDisabled property to false | |
* | |
* @private | |
* @method enableRibbonToggle | |
**/ | |
enableRibbonToggle: function () { | |
this.toggleDisabled -= 1; | |
}, | |
/** | |
* Animate the ribbon stories on page load | |
* | |
* @private | |
* @method animateRibbonStories | |
**/ | |
animateRibbonStories: function ($collection) { | |
var time = 200; | |
var index = $collection.find('.active').index(); | |
index = index < 0 ? 0 : index; | |
$collection.find('.collection-item') | |
.slice(index) | |
.css({ | |
'opacity': 0, | |
'margin-left': this.animationDistance | |
}).each(function (index, element) { | |
time += 200; | |
$(this).animate({ | |
'opacity': 1, | |
'margin-left': 0 | |
}, time); | |
}); | |
}, | |
/** | |
* Checks if the ribbon is visible and triggers an event | |
* if the visibility of the ribbon has changed | |
* | |
* @private | |
* @method checkRibbonVisibility | |
**/ | |
checkRibbonVisibility: function () { | |
var visible = this.pageManager.isComponentVisible(this.$el); | |
if (visible !== this.isRibbonVisible) { | |
this.isRibbonVisible = visible; | |
/** | |
* Fired when there is a change in the ribbon view when scrolling | |
* | |
* @event nyt:ribbon-visiblility | |
**/ | |
this.broadcast('nyt:ribbon-visiblility', visible); | |
} | |
}, | |
/** | |
* Revert the ribbon to its original state | |
* | |
* @private | |
* @method revertRibbon | |
**/ | |
revertRibbon: function () { | |
// return if the ribbon is already reverted | |
if (!this.ribbonFixed) { | |
return; | |
} | |
this.ribbonFixed = false; | |
this.$shell.css('padding-top', ''); | |
this.$el | |
.stop(true) | |
.css({ | |
'position': '', | |
'margin-top': '', | |
'width': '', | |
'top': '' | |
}); | |
}, | |
/** | |
* Slides the ribbon down in a fixed position and adds padding to | |
* the shell element to fill the empty space | |
* | |
* @private | |
* @method slideRibbonDown | |
**/ | |
slideRibbonDown: function () { | |
var ribbonDisplacement = this.ribbonHeight + this.ribbonMarginTop + this.ribbonMarginBottom; | |
this.ribbonFixed = true; | |
this.$shell.css('padding-top', ribbonDisplacement); | |
this.$el.stop(true) | |
.css({ | |
'position': 'fixed', | |
'margin-top': 0, | |
'width': this.getWidth(), | |
'top': -this.ribbonHeight | |
}) | |
.animate({ | |
'top' : this.mastheadHeight | |
}, this.speed); | |
}, | |
/** | |
* Slides the ribbon up and returns it to its original position | |
* | |
* @private | |
* @method slideRibbonUp | |
**/ | |
slideRibbonUp: function () { | |
this.$el.stop(true).animate({ | |
'top': -this.ribbonHeight | |
}, this.speed, this.revertRibbon); | |
}, | |
/** | |
* Show or hide the ribbon depending on the speed of the scroll | |
* | |
* @private | |
* @method toggleRibbon | |
* @param top {Number} number of px between top of viewport and top of document | |
**/ | |
toggleRibbon: function (scrollTop) { | |
var styles; | |
var atTop = scrollTop <= 0; | |
var movingDown = scrollTop > this.oldScrollTop; | |
var movingUp = scrollTop < this.oldScrollTop; | |
var distance = Math.abs(scrollTop - this.oldScrollTop); | |
// check if ribbon toggle behavior has been disabled | |
if (this.toggleDisabled > 0) { return; } | |
// revert the ribbon if the user | |
// has reached the top of the page | |
if (atTop) { | |
this.oldScrollTop = scrollTop; | |
this.revertRibbon(); | |
return; | |
} | |
// check if user has moved down and the | |
// ribbon is fixed to the top of the browser | |
if (movingDown && this.ribbonFixed) { | |
// only hide ribbon if user has scrolled down a certain distance | |
if (distance < this.minDownDistance) { return; } | |
this.slideRibbonUp(); | |
this.broadcast('nyt:ribbon-visiblility', false); | |
// check if the user has moved up and | |
// if the ribbon is not fixed to the browser | |
} else if (movingUp && !this.ribbonFixed) { | |
// only hide ribbon if user has scrolled up at a certain speed | |
if (distance < this.minUpDistance) { | |
this.oldScrollTop = scrollTop; | |
return; | |
} | |
this.slideRibbonDown(); | |
this.broadcast('nyt:ribbon-visiblility', true); | |
} | |
// store the scroll value to check in later | |
// executions of this method | |
this.oldScrollTop = scrollTop; | |
}, | |
/** | |
* Function to get the section currently in view in the ribbon | |
* | |
* @method getSectionInView | |
* @return {String} the title of the section in view in the ribbon | |
*/ | |
getSectionInView: function () { | |
return $.trim(this.$el.find('.first-collection-marker').text()); | |
}, | |
getCollectionByArticleElement: function ($elem) { | |
return $.trim($elem.parents('.collection-menu').prev('.collection-marker').text()); | |
} | |
}) | |
); | |
return RibbonView; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment