Pellentesque habitant morbi tristique senectus et netus
et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat
vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet
quam egestas semper. Aenean ultricies mi vitae est. Mauris
placerat eleifend leo. Quisque sit amet est et sapien ullamcorper
pharetra. Vestibulum erat wisi, condimentum sed,
commodo vitae, ornare sit amet, wisi. Aenean fermentum,
elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus
lacus enim ac dui. Donec non enim in turpis pulvinar
facilisis. Ut felis.
Header Level 2
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
magna. Cras in mi at felis aliquet congue. Ut a est eget ligula
molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis
mollis, tellus est malesuada tellus, at luctus turpis elit sit amet
quam. Vivamus pretium ornare est.
Header Level 3
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
#header h1 a {
display: block;
width: 300px;
height: 80px;
}
Pellentesque habitant morbi tristique senectus et netus
et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat
vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet
quam egestas semper. Aenean ultricies mi vitae est. Mauris
placerat eleifend leo. Quisque sit amet est et sapien ullamcorper
pharetra. Vestibulum erat wisi, condimentum sed,
commodo vitae, ornare sit amet, wisi. Aenean fermentum,
elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus
lacus enim ac dui. Donec non enim in turpis pulvinar
facilisis. Ut felis.
Header Level 2
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
magna. Cras in mi at felis aliquet congue. Ut a est eget ligula
molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis
mollis, tellus est malesuada tellus, at luctus turpis elit sit amet
quam. Vivamus pretium ornare est.
Header Level 3
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
#header h1 a {
display: block;
width: 300px;
height: 80px;
}
Pellentesque habitant morbi tristique senectus et netus
et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat
vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet
quam egestas semper. Aenean ultricies mi vitae est. Mauris
placerat eleifend leo. Quisque sit amet est et sapien ullamcorper
pharetra. Vestibulum erat wisi, condimentum sed,
commodo vitae, ornare sit amet, wisi. Aenean fermentum,
elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus
lacus enim ac dui. Donec non enim in turpis pulvinar
facilisis. Ut felis.
Header Level 2
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
magna. Cras in mi at felis aliquet congue. Ut a est eget ligula
molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis
mollis, tellus est malesuada tellus, at luctus turpis elit sit amet
quam. Vivamus pretium ornare est.
Header Level 3
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
#header h1 a {
display: block;
width: 300px;
height: 80px;
}
Pellentesque habitant morbi tristique senectus et netus
et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat
vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet
quam egestas semper. Aenean ultricies mi vitae est. Mauris
placerat eleifend leo. Quisque sit amet est et sapien ullamcorper
pharetra. Vestibulum erat wisi, condimentum sed,
commodo vitae, ornare sit amet, wisi. Aenean fermentum,
elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus
lacus enim ac dui. Donec non enim in turpis pulvinar
facilisis. Ut felis.
Header Level 2
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
magna. Cras in mi at felis aliquet congue. Ut a est eget ligula
molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis
mollis, tellus est malesuada tellus, at luctus turpis elit sit amet
quam. Vivamus pretium ornare est.
Header Level 3
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
- Aliquam tincidunt mauris eu risus.
#header h1 a {
display: block;
width: 300px;
height: 80px;
}
================================================
FILE: cypress/integration/headroom.spec.js
================================================
import createScroller from "../../src/scroller";
describe("Headroom", function() {
const initialiseHeadroom = options => {
cy.window().then(win => {
win.hr = new win.Headroom(win.document.querySelector("header"), options);
win.hr.init();
});
cy.wait(200); // eslint-disable-line cypress/no-unnecessary-waiting
};
beforeEach(() => {
cy.visit("./cypress/fixtures/index.html");
});
afterEach(() => {
cy.window().then(win => {
win.hr.destroy();
});
cy.get("header").should("be.destroyed");
});
it("works!", function() {
initialiseHeadroom();
cy.get("header")
.should("be.initialised")
.should("be.top")
.should("not.be.bottom");
cy.scrollTo(0, 50);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.scrollTo(0, 25);
cy.get("header")
.should("be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.scrollTo(0, 0);
cy.get("header")
.should("be.pinned")
.should("be.top")
.should("not.be.bottom");
cy.window()
.then(win => createScroller(win.hr.scroller))
.then(scroller => {
const distanceToBottom = scroller.scrollHeight() - scroller.height();
cy.scrollTo(0, distanceToBottom - 1);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.scrollTo(0, distanceToBottom);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("be.bottom");
});
});
it("handles tolerance correctly", () => {
initialiseHeadroom({
tolerance: 10
});
cy.scrollTo(0, 5);
cy.get("header").should("be.initialised");
cy.scrollTo(0, 15);
cy.get("header").should("not.be.pinned");
cy.scrollTo(0, 12);
cy.get("header").should("not.be.pinned");
cy.scrollTo(0, 2);
cy.get("header").should("be.pinned");
});
it("handles offset correctly", () => {
initialiseHeadroom({
offset: 50,
tolerance: 10
});
cy.scrollTo(0, 25);
cy.get("header")
.should("be.initialised")
.should("be.top");
cy.scrollTo(0, 55);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top");
cy.scrollTo(0, 49);
cy.get("header")
.should("be.pinned")
.should("be.top");
});
it("handles up/down offset correctly", () => {
initialiseHeadroom({
offset: {
up: 70,
down: 120,
}
});
cy.scrollTo(0, 119);
cy.get("header")
.should("be.initialised")
.should("be.top");
cy.scrollTo(0, 140);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top");
cy.scrollTo(0, 121);
cy.get("header")
.should("be.pinned")
.should("not.be.top");
cy.scrollTo(0, 69);
cy.get("header")
.should("be.pinned")
.should("be.top");
});
it("can be frozen / unfrozen", () => {
initialiseHeadroom();
cy.scrollTo(0, 20);
cy.get("header").should("not.be.pinned");
cy.window().then(win => {
win.hr.freeze();
});
cy.scrollTo(0, 10);
cy.get("header")
.should("be.froze")
.should("not.be.pinned");
cy.window().then(win => {
win.hr.unfreeze();
});
cy.scrollTo(0, 5);
cy.get("header")
.should("not.be.froze")
.should("be.pinned");
});
it("handles scrollers besides window", () => {
cy.get(".scroller").then(scroller => {
initialiseHeadroom({ scroller: scroller[0] });
});
cy.get("header").should("be.initialised");
cy.get(".scroller").scrollTo(0, 50);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.get(".scroller").scrollTo(0, 25);
cy.get("header")
.should("be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.get(".scroller").scrollTo(0, 0);
cy.get("header")
.should("be.pinned")
.should("be.top")
.should("not.be.bottom");
cy.window()
.then(win => createScroller(win.hr.scroller))
.then(scroller => {
const distanceToBottom = scroller.scrollHeight() - scroller.height();
cy.get(".scroller").scrollTo(0, distanceToBottom - 1);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.get(".scroller").scrollTo(0, distanceToBottom);
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("be.bottom");
});
});
it("handles programmatically pinning/unpinning", () => {
initialiseHeadroom();
cy.window().then(win => {
win.hr.unpin();
});
cy.get("header").should("not.be.pinned");
cy.window().then(win => {
win.hr.pin();
});
cy.get("header").should("be.pinned");
});
it("handles an iframe's window as the scroll source", () => {
cy.get("iframe").then(([iframe]) => {
initialiseHeadroom({
scroller: iframe.contentWindow
});
});
cy.get("header").should("be.initialised");
cy.get("iframe").then(([iframe]) => {
iframe.contentWindow.scroll(0, 50);
});
cy.get("header")
.should("not.be.pinned")
.should("not.be.top")
.should("not.be.bottom");
cy.get("iframe").then(([iframe]) => {
iframe.contentWindow.scroll(0, 25);
});
cy.get("header")
.should("be.pinned")
.should("not.be.top")
.should("not.be.bottom");
});
it("fires callbacks", () => {
let pinStatus, topStatus, bottomStatus;
initialiseHeadroom({
onPin: () => {
pinStatus = "pinned";
},
onUnpin: () => {
pinStatus = "unpinned";
},
onTop: () => {
topStatus = "top";
},
onNotTop: () => {
topStatus = "notTop";
},
onBottom: () => {
bottomStatus = "bottom";
},
onNotBottom: () => {
bottomStatus = "notBottom";
}
});
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(0).should(() => {
expect(topStatus).to.equal("top");
expect(pinStatus).to.equal(undefined);
expect(bottomStatus).to.equal("notBottom");
});
cy.scrollTo(0, 50);
cy.should(() => {
expect(topStatus).to.equal("notTop");
expect(pinStatus).to.equal("unpinned");
expect(bottomStatus).to.equal("notBottom");
});
cy.scrollTo(0, 25);
cy.should(() => {
expect(topStatus).to.equal("notTop");
expect(pinStatus).to.equal("pinned");
expect(bottomStatus).to.equal("notBottom");
});
cy.scrollTo(0, 0);
cy.should(() => {
expect(topStatus).to.equal("top");
expect(pinStatus).to.equal("pinned");
expect(bottomStatus).to.equal("notBottom");
});
cy.scrollTo("bottom");
cy.should(() => {
expect(topStatus).to.equal("notTop");
expect(pinStatus).to.equal("unpinned");
expect(bottomStatus).to.equal("bottom");
});
});
describe("handling options and defaults", () => {
it("merges our own classes and preserves other defaults", () => {
const classes = {
initial: "foo",
pinned: "foo--pinned"
};
initialiseHeadroom({ classes });
cy.window().then(win => {
expect(win.hr.classes).to.deep.contain(classes);
const { initial, pinned, ...defaultClasses } = win.hr.classes; // eslint-disable-line no-unused-vars
expect(win.hr.classes).to.deep.contain(defaultClasses);
});
});
it("assigns default classes if no options supplied", () => {
initialiseHeadroom();
cy.window().then(win => {
expect(win.hr.classes).to.deep.equal(win.Headroom.options.classes);
});
});
it("assigns default classes if no no classes supplied", () => {
initialiseHeadroom({ tolerance: 5 });
cy.window().then(win => {
expect(win.hr.classes).to.deep.equal(win.Headroom.options.classes);
});
});
it("handles multiple classes", () => {
initialiseHeadroom({
classes: {
pinned: "headroom--pinned foo",
unpinned: "headroom--unpinned bar"
}
});
cy.scrollTo(0, 50);
cy.get("header")
.should("not.be.pinned")
.should("have.class", "bar");
cy.scrollTo(0, 25);
cy.get("header")
.should("be.pinned")
.should("have.class", "foo");
});
});
});
================================================
FILE: cypress/plugins/index.js
================================================
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (/*on, config*/) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};
================================================
FILE: cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("pinned", function assertIsPinned() {
const negate = utils.flag(this, "negate");
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
if (negate) {
assertion.to.have
.class("headroom--unpinned")
.to.not.have.class("headroom--pinned");
} else {
assertion.to.have
.class("headroom--pinned")
.to.not.have.class("headroom--unpinned");
}
});
});
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("top", function assertIsTop() {
const negate = utils.flag(this, "negate");
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
if (negate) {
assertion.to.have
.class("headroom--not-top")
.to.not.have.class("headroom--top");
} else {
assertion.to.have
.class("headroom--top")
.to.not.have.class("headroom--not-top");
}
});
});
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("bottom", function assertIsBottom() {
const negate = utils.flag(this, "negate");
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
if (negate) {
assertion.to.have
.class("headroom--not-bottom")
.to.not.have.class("headroom--bottom");
} else {
assertion.to.have
.class("headroom--bottom")
.to.not.have.class("headroom--not-bottom");
}
});
});
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("froze", function assertIsFrozen() {
const negate = utils.flag(this, "negate");
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
if (negate) {
assertion.not.to.have.class("headroom--frozen");
} else {
assertion.to.have.class("headroom--frozen");
}
});
});
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("destroyed", function assertIsDestroyed() {
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
assertion.to.not.have
.class("headroom--pinned")
.to.not.have.class("headroom--unpinned")
.to.not.have.class("headroom--top")
.to.not.have.class("headroom--not-top")
.to.not.have.class("headroom--bottom")
.to.not.have.class("headroom--not-bottom");
});
});
chai.use((_chai, utils) => {
_chai.Assertion.addMethod("initialised", function assertIsInitialised() {
const obj = utils.flag(this, "object");
const assertion = new _chai.Assertion(obj);
assertion.to.have
.class("headroom")
.to.not.have.class("headroom--pinned")
.to.not.have.class("headroom--unpinned");
});
});
================================================
FILE: cypress.json
================================================
{
"video": false
}
================================================
FILE: package.json
================================================
{
"name": "headroom.js",
"version": "0.12.0",
"description": "Give your page some headroom. Hide your header until you need it",
"main": "dist/headroom.js",
"files": [
"dist"
],
"scripts": {
"start": "rollup --config --watch & cypress open",
"test": "npm run build && cypress run",
"build": "rollup --config",
"version": "npm run build",
"postversion": "git push origin master --tags && npm publish"
},
"repository": {
"type": "git",
"url": "https://github.com/WickyNilliams/headroom.js"
},
"keywords": [
"header",
"fixed",
"scroll",
"menu"
],
"author": "Nick Williams",
"homepage": "http://wicky.nillia.ms/headroom.js",
"license": "MIT",
"bugs": {
"url": "https://github.com/WickyNilliams/headroom.js/issues"
},
"devDependencies": {
"cypress": "^3.4.1",
"eslint": "^6.5.0",
"eslint-plugin-cypress": "^2.7.0",
"rollup": "^1.19.4",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-filesize": "^6.2.0",
"rollup-plugin-license": "^0.12.1",
"rollup-plugin-uglify": "^6.0.2"
}
}
================================================
FILE: rollup.config.js
================================================
import license from "rollup-plugin-license";
import { uglify } from "rollup-plugin-uglify";
import filesize from "rollup-plugin-filesize";
import { eslint } from "rollup-plugin-eslint";
const input = "src/Headroom.js";
const output = {
format: "umd",
name: "Headroom"
};
const licensePlugin = license({
banner: {
commentStyle: "ignored",
content: `<%= pkg.name %> v<%= pkg.version %> - <%= pkg.description %>
Copyright (c) <%= moment().format('YYYY') %> <%= pkg.author %> - <%= pkg.homepage %>
License: <%= pkg.license %>`
}
});
const unminified = {
input,
output: {
...output,
file: "dist/headroom.js"
},
plugins: [
eslint(),
licensePlugin,
filesize({
showMinifiedSize: false,
showGzippedSize: false
})
]
};
const minified = {
input,
output: {
...output,
file: "dist/headroom.min.js",
compact: true
},
plugins: [
uglify(),
licensePlugin,
filesize({
showMinifiedSize: false,
showBrotliSize: true
})
]
};
export default [unminified, minified];
================================================
FILE: src/.eslintrc
================================================
{
"parserOptions": {
"ecmaVersion": 6
},
"env": {
"browser": true
}
}
================================================
FILE: src/Headroom.js
================================================
import { isBrowser, isSupported } from "./features";
import trackScroll from "./trackScroll";
function normalizeUpDown(t) {
return t === Object(t) ? t : { down: t, up: t };
}
/**
* 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(elem, options) {
options = options || {};
Object.assign(this, Headroom.options, options);
this.classes = Object.assign({}, Headroom.options.classes, options.classes);
this.elem = elem;
this.tolerance = normalizeUpDown(this.tolerance);
this.offset = normalizeUpDown(this.offset);
this.initialised = false;
this.frozen = false;
}
Headroom.prototype = {
constructor: Headroom,
/**
* Start listening to scrolling
* @public
*/
init: function() {
if (Headroom.cutsTheMustard && !this.initialised) {
this.addClass("initial");
this.initialised = true;
// defer event registration to handle browser
// potentially restoring previous scroll position
setTimeout(
function(self) {
self.scrollTracker = trackScroll(
self.scroller,
{ offset: self.offset, tolerance: self.tolerance },
self.update.bind(self)
);
},
100,
this
);
}
return this;
},
/**
* Destroy the widget, clearing up after itself
* @public
*/
destroy: function() {
this.initialised = false;
Object.keys(this.classes).forEach(this.removeClass, this);
this.scrollTracker.destroy();
},
/**
* Unpin the element
* @public
*/
unpin: function() {
if (this.hasClass("pinned") || !this.hasClass("unpinned")) {
this.addClass("unpinned");
this.removeClass("pinned");
if (this.onUnpin) {
this.onUnpin.call(this);
}
}
},
/**
* Pin the element
* @public
*/
pin: function() {
if (this.hasClass("unpinned")) {
this.addClass("pinned");
this.removeClass("unpinned");
if (this.onPin) {
this.onPin.call(this);
}
}
},
/**
* Freezes the current state of the widget
* @public
*/
freeze: function() {
this.frozen = true;
this.addClass("frozen");
},
/**
* Re-enables the default behaviour of the widget
* @public
*/
unfreeze: function() {
this.frozen = false;
this.removeClass("frozen");
},
top: function() {
if (!this.hasClass("top")) {
this.addClass("top");
this.removeClass("notTop");
if (this.onTop) {
this.onTop.call(this);
}
}
},
notTop: function() {
if (!this.hasClass("notTop")) {
this.addClass("notTop");
this.removeClass("top");
if (this.onNotTop) {
this.onNotTop.call(this);
}
}
},
bottom: function() {
if (!this.hasClass("bottom")) {
this.addClass("bottom");
this.removeClass("notBottom");
if (this.onBottom) {
this.onBottom.call(this);
}
}
},
notBottom: function() {
if (!this.hasClass("notBottom")) {
this.addClass("notBottom");
this.removeClass("bottom");
if (this.onNotBottom) {
this.onNotBottom.call(this);
}
}
},
shouldUnpin: function(details) {
var scrollingDown = details.direction === "down";
return scrollingDown && !details.top && details.toleranceExceeded;
},
shouldPin: function(details) {
var scrollingUp = details.direction === "up";
return (scrollingUp && details.toleranceExceeded) || details.top;
},
addClass: function(className) {
this.elem.classList.add.apply(
this.elem.classList,
this.classes[className].split(" ")
);
},
removeClass: function(className) {
this.elem.classList.remove.apply(
this.elem.classList,
this.classes[className].split(" ")
);
},
hasClass: function(className) {
return this.classes[className].split(" ").every(function(cls) {
return this.classList.contains(cls);
}, this.elem);
},
update: function(details) {
if (details.isOutOfBounds) {
// Ignore bouncy scrolling in OSX
return;
}
if (this.frozen === true) {
return;
}
if (details.top) {
this.top();
} else {
this.notTop();
}
if (details.bottom) {
this.bottom();
} else {
this.notBottom();
}
if (this.shouldUnpin(details)) {
this.unpin();
} else if (this.shouldPin(details)) {
this.pin();
}
}
};
/**
* Default options
* @type {Object}
*/
Headroom.options = {
tolerance: {
up: 0,
down: 0
},
offset: 0,
scroller: isBrowser() ? window : null,
classes: {
frozen: "headroom--frozen",
pinned: "headroom--pinned",
unpinned: "headroom--unpinned",
top: "headroom--top",
notTop: "headroom--not-top",
bottom: "headroom--bottom",
notBottom: "headroom--not-bottom",
initial: "headroom"
}
};
Headroom.cutsTheMustard = isSupported();
export default Headroom;
================================================
FILE: src/angular.headroom.js
================================================
(function (angular, Headroom) {
if(!angular) {
return;
}
function headroom(HeadroomService) {
return {
scope: {
tolerance: '=',
offset: '=',
classes: '=',
scroller: '@'
},
link: function ($scope, $element) {
var options = {};
var opts = HeadroomService.options;
for (var prop in opts) {
options[prop] = $scope[prop] || opts[prop];
}
if ($scope.scroller) {
options.scroller = document.querySelector($scope.scroller);
}
var headroom = new HeadroomService($element[0], options).init();
$scope.$on('$destroy', function(){
headroom.destroy();
});
}
};
}
headroom.$inject = ['HeadroomService'];
function HeadroomService() {
return Headroom;
}
angular
.module('headroom', [])
.directive('headroom', headroom)
.factory('HeadroomService', HeadroomService);
})(window.angular, window.Headroom);
================================================
FILE: src/features.js
================================================
export function isBrowser() {
return typeof window !== "undefined";
}
/**
* Used to detect browser support for adding an event listener with options
* Credit: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
export function passiveEventsSupported() {
var supported = false;
try {
var options = {
// eslint-disable-next-line getter-return
get passive() {
supported = true;
}
};
window.addEventListener("test", options, options);
window.removeEventListener("test", options, options);
} catch (err) {
supported = false;
}
return supported;
}
export function isSupported() {
return !!(
isBrowser() &&
function() {}.bind &&
"classList" in document.documentElement &&
Object.assign &&
Object.keys &&
requestAnimationFrame
);
}
================================================
FILE: src/jQuery.headroom.js
================================================
(function($) {
if(!$) {
return;
}
////////////
// Plugin //
////////////
$.fn.headroom = function(option) {
return this.each(function() {
var $this = $(this),
data = $this.data('headroom'),
options = typeof option === 'object' && option;
options = $.extend(true, {}, Headroom.options, options);
if (!data) {
data = new Headroom(this, options);
data.init();
$this.data('headroom', data);
}
if (typeof option === 'string') {
data[option]();
if(option === 'destroy'){
$this.removeData('headroom');
}
}
});
};
//////////////
// Data API //
//////////////
$('[data-headroom]').each(function() {
var $this = $(this);
$this.headroom($this.data());
});
}(window.Zepto || window.jQuery));
================================================
FILE: src/scroller.js
================================================
function isDocument(obj) {
return obj.nodeType === 9; // Node.DOCUMENT_NODE === 9
}
function isWindow(obj) {
// `obj === window` or `obj instanceof Window` is not sufficient,
// as the obj may be the window of an iframe.
return obj && obj.document && isDocument(obj.document);
}
function windowScroller(win) {
var doc = win.document;
var body = doc.body;
var html = doc.documentElement;
return {
/**
* @see http://james.padolsey.com/javascript/get-document-height-cross-browser/
* @return {Number} the scroll height of the document in pixels
*/
scrollHeight: function() {
return Math.max(
body.scrollHeight,
html.scrollHeight,
body.offsetHeight,
html.offsetHeight,
body.clientHeight,
html.clientHeight
);
},
/**
* @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript
* @return {Number} the height of the viewport in pixels
*/
height: function() {
return win.innerHeight || html.clientHeight || body.clientHeight;
},
/**
* Gets the Y scroll position
* @return {Number} pixels the page has scrolled along the Y-axis
*/
scrollY: function() {
if (win.pageYOffset !== undefined) {
return win.pageYOffset;
}
return (html || body.parentNode || body).scrollTop;
}
};
}
function elementScroller(element) {
return {
/**
* @return {Number} the scroll height of the element in pixels
*/
scrollHeight: function() {
return Math.max(
element.scrollHeight,
element.offsetHeight,
element.clientHeight
);
},
/**
* @return {Number} the height of the element in pixels
*/
height: function() {
return Math.max(element.offsetHeight, element.clientHeight);
},
/**
* Gets the Y scroll position
* @return {Number} pixels the element has scrolled along the Y-axis
*/
scrollY: function() {
return element.scrollTop;
}
};
}
export default function createScroller(element) {
return isWindow(element) ? windowScroller(element) : elementScroller(element);
}
================================================
FILE: src/trackScroll.js
================================================
import createScroller from "./scroller";
import { passiveEventsSupported } from "./features";
/**
* @param element EventTarget
*/
export default function trackScroll(element, options, callback) {
var isPassiveSupported = passiveEventsSupported();
var rafId;
var scrolled = false;
var scroller = createScroller(element);
var lastScrollY = scroller.scrollY();
var details = {};
function update() {
var scrollY = Math.round(scroller.scrollY());
var height = scroller.height();
var scrollHeight = scroller.scrollHeight();
// reuse object for less memory churn
details.scrollY = scrollY;
details.lastScrollY = lastScrollY;
details.direction = scrollY > lastScrollY ? "down" : "up";
details.distance = Math.abs(scrollY - lastScrollY);
details.isOutOfBounds = scrollY < 0 || scrollY + height > scrollHeight;
details.top = scrollY <= options.offset[details.direction];
details.bottom = scrollY + height >= scrollHeight;
details.toleranceExceeded =
details.distance > options.tolerance[details.direction];
callback(details);
lastScrollY = scrollY;
scrolled = false;
}
function handleScroll() {
if (!scrolled) {
scrolled = true;
rafId = requestAnimationFrame(update);
}
}
var eventOptions = isPassiveSupported
? { passive: true, capture: false }
: false;
element.addEventListener("scroll", handleScroll, eventOptions);
update();
return {
destroy: function() {
cancelAnimationFrame(rafId);
element.removeEventListener("scroll", handleScroll, eventOptions);
}
};
}