mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-18 19:38:20 -07:00
348 lines
10 KiB
JavaScript
348 lines
10 KiB
JavaScript
/*!
|
|
* headroom.js v0.7.0 - Give your page some headroom. Hide your header until you need it
|
|
* Copyright (c) 2014 Nick Williams - http://wicky.nillia.ms/headroom.js
|
|
* License: MIT
|
|
*/
|
|
|
|
(function (window, document) {
|
|
|
|
'use strict';
|
|
|
|
/* exported features */
|
|
|
|
var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
|
|
|
|
/**
|
|
* Handles debouncing of events via requestAnimationFrame
|
|
* @see http://www.html5rocks.com/en/tutorials/speed/animations/
|
|
* @param {Function} callback The callback to handle whichever event
|
|
*/
|
|
function Debouncer(callback) {
|
|
this.callback = callback;
|
|
this.ticking = false;
|
|
}
|
|
Debouncer.prototype = {
|
|
constructor: Debouncer,
|
|
|
|
/**
|
|
* dispatches the event to the supplied callback
|
|
* @private
|
|
*/
|
|
update: function () {
|
|
this.callback && this.callback();
|
|
this.ticking = false;
|
|
},
|
|
|
|
/**
|
|
* ensures events don't get stacked
|
|
* @private
|
|
*/
|
|
requestTick: function () {
|
|
if (!this.ticking) {
|
|
requestAnimationFrame(this.rafCallback || (this.rafCallback = this.update.bind(this)));
|
|
this.ticking = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Attach this as the event listeners
|
|
*/
|
|
handleEvent: function () {
|
|
this.requestTick();
|
|
}
|
|
};
|
|
/**
|
|
* Check if object is part of the DOM
|
|
* @constructor
|
|
* @param {Object} obj element to check
|
|
*/
|
|
function isDOMElement(obj) {
|
|
return obj && typeof window !== 'undefined' && (obj === window || obj.nodeType);
|
|
}
|
|
|
|
/**
|
|
* Helper function for extending objects
|
|
*/
|
|
function extend(object /*, objectN ... */) {
|
|
if (arguments.length <= 0) {
|
|
throw new Error('Missing arguments in extend function');
|
|
}
|
|
|
|
var result = object || {},
|
|
key,
|
|
i;
|
|
|
|
for (i = 1; i < arguments.length; i++) {
|
|
var replacement = arguments[i] || {};
|
|
|
|
for (key in replacement) {
|
|
// Recurse into object except if the object is a DOM element
|
|
if (typeof result[key] === 'object' && !isDOMElement(result[key])) {
|
|
result[key] = extend(result[key], replacement[key]);
|
|
}
|
|
else {
|
|
result[key] = result[key] || replacement[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Helper function for normalizing tolerance option to object format
|
|
*/
|
|
function normalizeTolerance(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);
|
|
}
|
|
|
|
/**
|
|
* UI enhancement for fixed headers.
|
|
* Hides header when scrolling down
|
|
* Shows header when scrolling up
|
|
* @constructor
|
|
* @param {DOMElement} elem the header element
|
|
* @param {Object} options options for the widget
|
|
*/
|
|
function Headroom(elems, options) {
|
|
options = extend(options, Headroom.options);
|
|
|
|
this.lastKnownScrollY = 0;
|
|
this.elems = elems;
|
|
this.debouncer = new Debouncer(this.update.bind(this));
|
|
this.tolerance = normalizeTolerance(options.tolerance);
|
|
this.classes = options.classes;
|
|
this.offset = options.offset;
|
|
this.scroller = options.scroller;
|
|
this.initialised = false;
|
|
this.onPin = options.onPin;
|
|
this.onUnpin = options.onUnpin;
|
|
}
|
|
Headroom.prototype = {
|
|
constructor: Headroom,
|
|
|
|
/**
|
|
* Initialises the widget
|
|
*/
|
|
init: function () {
|
|
|
|
for (var i = 0, length = this.elems.length; i < length; i++) {
|
|
this.elems[i].classList.add(this.classes.initial);
|
|
}
|
|
|
|
this.attachEvent();
|
|
|
|
return this;
|
|
},
|
|
|
|
add: function (elem) {
|
|
elem.classList.add(this.classes.initial);
|
|
this.elems.push(elem);
|
|
},
|
|
|
|
remove: function (elem) {
|
|
|
|
var classes = this.classes;
|
|
elem.classList.remove(classes.unpinned, classes.pinned, classes.initial);
|
|
var i = this.elems.indexOf(elem);
|
|
if (i != -1) {
|
|
this.elems.splice(i, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unattaches events and removes any classes that were added
|
|
*/
|
|
destroy: function () {
|
|
var classes = this.classes;
|
|
|
|
this.initialised = false;
|
|
|
|
for (var i = 0, length = this.elems.length; i < length; i++) {
|
|
this.elems[i].classList.remove(classes.unpinned, classes.pinned, classes.initial);
|
|
}
|
|
|
|
removeEventListenerWithOptions(this.scroller, 'scroll', this.debouncer, {
|
|
capture: false,
|
|
passive: true
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Attaches the scroll event
|
|
* @private
|
|
*/
|
|
attachEvent: function () {
|
|
if (!this.initialised) {
|
|
this.lastKnownScrollY = this.getScrollY();
|
|
this.initialised = true;
|
|
addEventListenerWithOptions(this.scroller, 'scroll', this.debouncer, {
|
|
capture: false,
|
|
passive: true
|
|
});
|
|
|
|
this.debouncer.handleEvent();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unpins the header if it's currently pinned
|
|
*/
|
|
unpin: function () {
|
|
|
|
var classes = this.classes;
|
|
|
|
for (var i = 0, length = this.elems.length; i < length; i++) {
|
|
var classList = this.elems[i].classList;
|
|
|
|
if (classList.contains(classes.pinned) || !classList.contains(classes.unpinned)) {
|
|
classList.add(classes.unpinned);
|
|
classList.remove(classes.pinned);
|
|
this.onUnpin && this.onUnpin.call(this);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pins the header if it's currently unpinned
|
|
*/
|
|
pin: function () {
|
|
|
|
var classes = this.classes;
|
|
|
|
for (var i = 0, length = this.elems.length; i < length; i++) {
|
|
var classList = this.elems[i].classList;
|
|
|
|
if (classList.contains(classes.unpinned)) {
|
|
classList.remove(classes.unpinned);
|
|
classList.add(classes.pinned);
|
|
this.onPin && this.onPin.call(this);
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* Gets the Y scroll position
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY
|
|
* @return {Number} pixels the page has scrolled along the Y-axis
|
|
*/
|
|
getScrollY: function () {
|
|
|
|
var pageYOffset = this.scroller.pageYOffset;
|
|
if (pageYOffset !== undefined) {
|
|
return pageYOffset;
|
|
}
|
|
|
|
var scrollTop = this.scroller.scrollTop;
|
|
if (scrollTop !== undefined) {
|
|
return scrollTop;
|
|
}
|
|
|
|
return (document.documentElement || document.body).scrollTop;
|
|
},
|
|
|
|
/**
|
|
* determines if the tolerance has been exceeded
|
|
* @param {int} currentScrollY the current scroll y position
|
|
* @return {bool} true if tolerance exceeded, false otherwise
|
|
*/
|
|
toleranceExceeded: function (currentScrollY, direction) {
|
|
return Math.abs(currentScrollY - this.lastKnownScrollY) >= this.tolerance[direction];
|
|
},
|
|
|
|
/**
|
|
* determine if it is appropriate to unpin
|
|
* @param {int} currentScrollY the current y scroll position
|
|
* @param {bool} toleranceExceeded has the tolerance been exceeded?
|
|
* @return {bool} true if should unpin, false otherwise
|
|
*/
|
|
shouldUnpin: function (currentScrollY, toleranceExceeded) {
|
|
var scrollingDown = currentScrollY > this.lastKnownScrollY,
|
|
pastOffset = currentScrollY >= this.offset;
|
|
|
|
return scrollingDown && pastOffset && toleranceExceeded;
|
|
},
|
|
|
|
/**
|
|
* determine if it is appropriate to pin
|
|
* @param {int} currentScrollY the current y scroll position
|
|
* @param {bool} toleranceExceeded has the tolerance been exceeded?
|
|
* @return {bool} true if should pin, false otherwise
|
|
*/
|
|
shouldPin: function (currentScrollY, toleranceExceeded) {
|
|
var scrollingUp = currentScrollY < this.lastKnownScrollY,
|
|
pastOffset = currentScrollY <= this.offset;
|
|
|
|
return (scrollingUp && toleranceExceeded) || pastOffset;
|
|
},
|
|
|
|
/**
|
|
* Handles updating the state of the widget
|
|
*/
|
|
update: function () {
|
|
var currentScrollY = this.getScrollY(),
|
|
scrollDirection = currentScrollY > this.lastKnownScrollY ? 'down' : 'up',
|
|
toleranceExceeded = this.toleranceExceeded(currentScrollY, scrollDirection);
|
|
|
|
if (currentScrollY < 0) { // Ignore bouncy scrolling in OSX
|
|
return;
|
|
}
|
|
|
|
if (this.shouldUnpin(currentScrollY, toleranceExceeded)) {
|
|
this.unpin();
|
|
}
|
|
else if (this.shouldPin(currentScrollY, toleranceExceeded)) {
|
|
this.pin();
|
|
}
|
|
|
|
this.lastKnownScrollY = currentScrollY;
|
|
}
|
|
};
|
|
/**
|
|
* Default options
|
|
* @type {Object}
|
|
*/
|
|
Headroom.options = {
|
|
tolerance: {
|
|
up: 0,
|
|
down: 0
|
|
},
|
|
offset: 0,
|
|
scroller: window,
|
|
classes: {
|
|
pinned: 'headroom--pinned',
|
|
unpinned: 'headroom--unpinned',
|
|
initial: 'headroom'
|
|
}
|
|
};
|
|
|
|
window.Headroom = Headroom;
|
|
|
|
}(window, document)); |