1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

add passive event listener to headroom

This commit is contained in:
Luke Pulverenti 2016-08-06 22:09:28 -04:00
parent 85731c7453
commit da25a9cc2a

View file

@ -4,16 +4,16 @@
* License: MIT * License: MIT
*/ */
(function(window, document) { (function (window, document) {
'use strict'; 'use strict';
/* exported features */ /* exported features */
var features = { var features = {
bind : !!(function(){}.bind), bind: !!(function () { }.bind),
classList : 'classList' in document.documentElement, classList: 'classList' in document.documentElement,
rAF : !!(window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame) rAF: !!(window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame)
}; };
window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
@ -22,18 +22,18 @@
* @see http://www.html5rocks.com/en/tutorials/speed/animations/ * @see http://www.html5rocks.com/en/tutorials/speed/animations/
* @param {Function} callback The callback to handle whichever event * @param {Function} callback The callback to handle whichever event
*/ */
function Debouncer (callback) { function Debouncer(callback) {
this.callback = callback; this.callback = callback;
this.ticking = false; this.ticking = false;
} }
Debouncer.prototype = { Debouncer.prototype = {
constructor : Debouncer, constructor: Debouncer,
/** /**
* dispatches the event to the supplied callback * dispatches the event to the supplied callback
* @private * @private
*/ */
update : function() { update: function () {
this.callback && this.callback(); this.callback && this.callback();
this.ticking = false; this.ticking = false;
}, },
@ -42,8 +42,8 @@
* ensures events don't get stacked * ensures events don't get stacked
* @private * @private
*/ */
requestTick : function() { requestTick: function () {
if(!this.ticking) { if (!this.ticking) {
requestAnimationFrame(this.rafCallback || (this.rafCallback = this.update.bind(this))); requestAnimationFrame(this.rafCallback || (this.rafCallback = this.update.bind(this)));
this.ticking = true; this.ticking = true;
} }
@ -52,7 +52,7 @@
/** /**
* Attach this as the event listeners * Attach this as the event listeners
*/ */
handleEvent : function() { handleEvent: function () {
this.requestTick(); this.requestTick();
} }
}; };
@ -68,8 +68,8 @@
/** /**
* Helper function for extending objects * Helper function for extending objects
*/ */
function extend (object /*, objectN ... */) { function extend(object /*, objectN ... */) {
if(arguments.length <= 0) { if (arguments.length <= 0) {
throw new Error('Missing arguments in extend function'); throw new Error('Missing arguments in extend function');
} }
@ -82,7 +82,7 @@
for (key in replacement) { for (key in replacement) {
// Recurse into object except if the object is a DOM element // Recurse into object except if the object is a DOM element
if(typeof result[key] === 'object' && ! isDOMElement(result[key])) { if (typeof result[key] === 'object' && !isDOMElement(result[key])) {
result[key] = extend(result[key], replacement[key]); result[key] = extend(result[key], replacement[key]);
} }
else { else {
@ -97,8 +97,34 @@
/** /**
* Helper function for normalizing tolerance option to object format * Helper function for normalizing tolerance option to object format
*/ */
function normalizeTolerance (t) { function normalizeTolerance(t) {
return t === Object(t) ? t : { down : t, up : t }; return t === Object(t) ? t : { down: t, up: t };
}
var supportsCaptureOption = false;
try {
var opts = Object.defineProperty({}, 'capture', {
get: function () {
supportsCaptureOption = true;
}
});
window.addEventListener("test", null, opts);
} catch (e) { }
function addEventListenerWithOptions(target, type, handler, options) {
var optionsOrCapture = options;
if (!supportsCaptureOption) {
optionsOrCapture = options.capture;
}
target.addEventListener(type, handler, optionsOrCapture);
}
function removeEventListenerWithOptions(target, type, handler, options) {
var optionsOrCapture = options;
if (!supportsCaptureOption) {
optionsOrCapture = options.capture;
}
target.removeEventListener(type, handler, optionsOrCapture);
} }
/** /**
@ -109,7 +135,7 @@
* @param {DOMElement} elem the header element * @param {DOMElement} elem the header element
* @param {Object} options options for the widget * @param {Object} options options for the widget
*/ */
function Headroom (elem, options) { function Headroom(elem, options) {
options = extend(options, Headroom.options); options = extend(options, Headroom.options);
this.lastKnownScrollY = 0; this.lastKnownScrollY = 0;
@ -126,21 +152,19 @@
this.onNotTop = options.onNotTop; this.onNotTop = options.onNotTop;
} }
Headroom.prototype = { Headroom.prototype = {
constructor : Headroom, constructor: Headroom,
/** /**
* Initialises the widget * Initialises the widget
*/ */
init : function() { init: function () {
if(!Headroom.cutsTheMustard) { if (!Headroom.cutsTheMustard) {
return; return;
} }
this.elem.classList.add(this.classes.initial); this.elem.classList.add(this.classes.initial);
// defer event registration to handle browser this.attachEvent();
// potentially restoring previous scroll position
setTimeout(this.attachEvent.bind(this), 100);
return this; return this;
}, },
@ -148,23 +172,29 @@
/** /**
* Unattaches events and removes any classes that were added * Unattaches events and removes any classes that were added
*/ */
destroy : function() { destroy: function () {
var classes = this.classes; var classes = this.classes;
this.initialised = false; this.initialised = false;
this.elem.classList.remove(classes.unpinned, classes.pinned, classes.top, classes.initial); this.elem.classList.remove(classes.unpinned, classes.pinned, classes.top, classes.initial);
this.scroller.removeEventListener('scroll', this.debouncer, false); removeEventListenerWithOptions(this.scroller, 'scroll', this.debouncer, {
capture: false,
passive: true
});
}, },
/** /**
* Attaches the scroll event * Attaches the scroll event
* @private * @private
*/ */
attachEvent : function() { attachEvent: function () {
if(!this.initialised){ if (!this.initialised) {
this.lastKnownScrollY = this.getScrollY(); this.lastKnownScrollY = this.getScrollY();
this.initialised = true; this.initialised = true;
this.scroller.addEventListener('scroll', this.debouncer, false); addEventListenerWithOptions(this.scroller, 'scroll', this.debouncer, {
capture: false,
passive: true
});
this.debouncer.handleEvent(); this.debouncer.handleEvent();
} }
@ -173,11 +203,11 @@
/** /**
* Unpins the header if it's currently pinned * Unpins the header if it's currently pinned
*/ */
unpin : function() { unpin: function () {
var classList = this.elem.classList, var classList = this.elem.classList,
classes = this.classes; classes = this.classes;
if(classList.contains(classes.pinned) || !classList.contains(classes.unpinned)) { if (classList.contains(classes.pinned) || !classList.contains(classes.unpinned)) {
classList.add(classes.unpinned); classList.add(classes.unpinned);
classList.remove(classes.pinned); classList.remove(classes.pinned);
this.onUnpin && this.onUnpin.call(this); this.onUnpin && this.onUnpin.call(this);
@ -187,11 +217,11 @@
/** /**
* Pins the header if it's currently unpinned * Pins the header if it's currently unpinned
*/ */
pin : function() { pin: function () {
var classList = this.elem.classList, var classList = this.elem.classList,
classes = this.classes; classes = this.classes;
if(classList.contains(classes.unpinned)) { if (classList.contains(classes.unpinned)) {
classList.remove(classes.unpinned); classList.remove(classes.unpinned);
classList.add(classes.pinned); classList.add(classes.pinned);
this.onPin && this.onPin.call(this); this.onPin && this.onPin.call(this);
@ -201,11 +231,11 @@
/** /**
* Handles the top states * Handles the top states
*/ */
top : function() { top: function () {
var classList = this.elem.classList, var classList = this.elem.classList,
classes = this.classes; classes = this.classes;
if(!classList.contains(classes.top)) { if (!classList.contains(classes.top)) {
classList.add(classes.top); classList.add(classes.top);
classList.remove(classes.notTop); classList.remove(classes.notTop);
this.onTop && this.onTop.call(this); this.onTop && this.onTop.call(this);
@ -215,11 +245,11 @@
/** /**
* Handles the not top state * Handles the not top state
*/ */
notTop : function() { notTop: function () {
var classList = this.elem.classList, var classList = this.elem.classList,
classes = this.classes; classes = this.classes;
if(!classList.contains(classes.notTop)) { if (!classList.contains(classes.notTop)) {
classList.add(classes.notTop); classList.add(classes.notTop);
classList.remove(classes.top); classList.remove(classes.top);
this.onNotTop && this.onNotTop.call(this); this.onNotTop && this.onNotTop.call(this);
@ -231,7 +261,7 @@
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY
* @return {Number} pixels the page has scrolled along the Y-axis * @return {Number} pixels the page has scrolled along the Y-axis
*/ */
getScrollY : function() { getScrollY: function () {
return (this.scroller.pageYOffset !== undefined) return (this.scroller.pageYOffset !== undefined)
? this.scroller.pageYOffset ? this.scroller.pageYOffset
: (this.scroller.scrollTop !== undefined) : (this.scroller.scrollTop !== undefined)
@ -244,7 +274,7 @@
* @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript * @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript
* @return {int} the height of the viewport in pixels * @return {int} the height of the viewport in pixels
*/ */
getViewportHeight : function () { getViewportHeight: function () {
return window.innerHeight return window.innerHeight
|| document.documentElement.clientHeight || document.documentElement.clientHeight
|| document.body.clientHeight; || document.body.clientHeight;
@ -255,7 +285,7 @@
* @see http://james.padolsey.com/javascript/get-document-height-cross-browser/ * @see http://james.padolsey.com/javascript/get-document-height-cross-browser/
* @return {int} the height of the document in pixels * @return {int} the height of the document in pixels
*/ */
getDocumentHeight : function () { getDocumentHeight: function () {
var body = document.body, var body = document.body,
documentElement = document.documentElement; documentElement = document.documentElement;
@ -271,7 +301,7 @@
* @param {Object} elm the element to calculate the height of which * @param {Object} elm the element to calculate the height of which
* @return {int} the height of the element in pixels * @return {int} the height of the element in pixels
*/ */
getElementHeight : function (elm) { getElementHeight: function (elm) {
return Math.max( return Math.max(
elm.scrollHeight, elm.scrollHeight,
elm.offsetHeight, elm.offsetHeight,
@ -283,7 +313,7 @@
* Gets the height of the scroller element * Gets the height of the scroller element
* @return {int} the height of the scroller element in pixels * @return {int} the height of the scroller element in pixels
*/ */
getScrollerHeight : function () { getScrollerHeight: function () {
return (this.scroller === window || this.scroller === document.body) return (this.scroller === window || this.scroller === document.body)
? this.getDocumentHeight() ? this.getDocumentHeight()
: this.getElementHeight(this.scroller); : this.getElementHeight(this.scroller);
@ -294,11 +324,15 @@
* @param {int} currentScrollY the current y scroll position * @param {int} currentScrollY the current y scroll position
* @return {bool} true if out of bounds, false otherwise * @return {bool} true if out of bounds, false otherwise
*/ */
isOutOfBounds : function (currentScrollY) { isOutOfBounds: function (currentScrollY) {
var pastTop = currentScrollY < 0, var pastTop = currentScrollY < 0;
pastBottom = currentScrollY + this.getViewportHeight() > this.getScrollerHeight();
return pastTop || pastBottom; if (pastTop) {
return true;
}
var pastBottom = currentScrollY + this.getViewportHeight() > this.getScrollerHeight();
return pastBottom;
}, },
/** /**
@ -306,8 +340,8 @@
* @param {int} currentScrollY the current scroll y position * @param {int} currentScrollY the current scroll y position
* @return {bool} true if tolerance exceeded, false otherwise * @return {bool} true if tolerance exceeded, false otherwise
*/ */
toleranceExceeded : function (currentScrollY, direction) { toleranceExceeded: function (currentScrollY, direction) {
return Math.abs(currentScrollY-this.lastKnownScrollY) >= this.tolerance[direction]; return Math.abs(currentScrollY - this.lastKnownScrollY) >= this.tolerance[direction];
}, },
/** /**
@ -316,7 +350,7 @@
* @param {bool} toleranceExceeded has the tolerance been exceeded? * @param {bool} toleranceExceeded has the tolerance been exceeded?
* @return {bool} true if should unpin, false otherwise * @return {bool} true if should unpin, false otherwise
*/ */
shouldUnpin : function (currentScrollY, toleranceExceeded) { shouldUnpin: function (currentScrollY, toleranceExceeded) {
var scrollingDown = currentScrollY > this.lastKnownScrollY, var scrollingDown = currentScrollY > this.lastKnownScrollY,
pastOffset = currentScrollY >= this.offset; pastOffset = currentScrollY >= this.offset;
@ -329,7 +363,7 @@
* @param {bool} toleranceExceeded has the tolerance been exceeded? * @param {bool} toleranceExceeded has the tolerance been exceeded?
* @return {bool} true if should pin, false otherwise * @return {bool} true if should pin, false otherwise
*/ */
shouldPin : function (currentScrollY, toleranceExceeded) { shouldPin: function (currentScrollY, toleranceExceeded) {
var scrollingUp = currentScrollY < this.lastKnownScrollY, var scrollingUp = currentScrollY < this.lastKnownScrollY,
pastOffset = currentScrollY <= this.offset; pastOffset = currentScrollY <= this.offset;
@ -339,25 +373,25 @@
/** /**
* Handles updating the state of the widget * Handles updating the state of the widget
*/ */
update : function() { update: function () {
var currentScrollY = this.getScrollY(), var currentScrollY = this.getScrollY(),
scrollDirection = currentScrollY > this.lastKnownScrollY ? 'down' : 'up', scrollDirection = currentScrollY > this.lastKnownScrollY ? 'down' : 'up',
toleranceExceeded = this.toleranceExceeded(currentScrollY, scrollDirection); toleranceExceeded = this.toleranceExceeded(currentScrollY, scrollDirection);
if(this.isOutOfBounds(currentScrollY)) { // Ignore bouncy scrolling in OSX if (this.isOutOfBounds(currentScrollY)) { // Ignore bouncy scrolling in OSX
return; return;
} }
if (currentScrollY <= this.offset ) { if (currentScrollY <= this.offset) {
this.top(); this.top();
} else { } else {
this.notTop(); this.notTop();
} }
if(this.shouldUnpin(currentScrollY, toleranceExceeded)) { if (this.shouldUnpin(currentScrollY, toleranceExceeded)) {
this.unpin(); this.unpin();
} }
else if(this.shouldPin(currentScrollY, toleranceExceeded)) { else if (this.shouldPin(currentScrollY, toleranceExceeded)) {
this.pin(); this.pin();
} }
@ -369,18 +403,18 @@
* @type {Object} * @type {Object}
*/ */
Headroom.options = { Headroom.options = {
tolerance : { tolerance: {
up : 0, up: 0,
down : 0 down: 0
}, },
offset : 0, offset: 0,
scroller: window, scroller: window,
classes : { classes: {
pinned : 'headroom--pinned', pinned: 'headroom--pinned',
unpinned : 'headroom--unpinned', unpinned: 'headroom--unpinned',
top : 'headroom--top', top: 'headroom--top',
notTop : 'headroom--not-top', notTop: 'headroom--not-top',
initial : 'headroom' initial: 'headroom'
} }
}; };
Headroom.cutsTheMustard = typeof features !== 'undefined' && features.rAF && features.bind && features.classList; Headroom.cutsTheMustard = typeof features !== 'undefined' && features.rAF && features.bind && features.classList;