Repository: WickyNilliams/headroom.js Branch: master Commit: 0f53fc75f41b Files: 21 Total size: 41.9 KB Directory structure: gitextract_56e1gft5/ ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cypress/ │ ├── .eslintrc │ ├── fixtures/ │ │ └── index.html │ ├── integration/ │ │ └── headroom.spec.js │ ├── plugins/ │ │ └── index.js │ └── support/ │ ├── commands.js │ └── index.js ├── cypress.json ├── package.json ├── rollup.config.js └── src/ ├── .eslintrc ├── Headroom.js ├── angular.headroom.js ├── features.js ├── jQuery.headroom.js ├── scroller.js └── trackScroll.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "env": { "browser": true, "node": true }, "extends": ["eslint:recommended"], "rules": { "no-unused-expressions": "error", "no-bitwise": "error", "camelcase": "error", "curly": "error", "eqeqeq": "error", "wrap-iife": ["error", "any"], "no-use-before-define": [ "error", { "functions": false } ], "linebreak-style": "error", "new-cap": "error", "no-caller": "error", "quotes": ["error", "double"], "indent": [ "error", 2, { "SwitchCase": 1 } ] } } ================================================ FILE: .gitignore ================================================ node_modules dist/ .vscode ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "10" addons: apt: packages: # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves - libgconf-2-4 cache: # Caches $HOME/.npm when npm ci is default script command # Caches node_modules in all other cases npm: true directories: # we also need to cache folder with Cypress binary - ~/.cache ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2013 Nick Williams Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Headroom.js Headroom.js is a lightweight, high-performance JS widget (with no dependencies) that allows you to react to the user's scroll. The header on [this site](http://wicky.nillia.ms/headroom.js) is a living example, it slides out of view when scrolling down and slides back in when scrolling up. ## Installation Headroom.js is available on npm. To install: ```bash npm install headroom.js --save # or... yarn add headroom.js ``` A a universal build (suitable for script tags, CommonJS, and AMD) is available from unpkg.com: [https://unpkg.com/headroom.js](https://unpkg.com/headroom.js) ## Documentation For complete documentation please visit the [headroom.js website](http://wicky.nillia.ms/headroom.js). ## Quick start After installing `headroom.js`. The following JS will create and initialise a headroom instance: ```js import Headroom from "headroom.js"; // select your header or whatever element you wish const header = document.querySelector("header"); const headroom = new Headroom(header); headroom.init(); ``` Then you can add the following CSS to your page: ```css .headroom { will-change: transform; transition: transform 200ms linear; } .headroom--pinned { transform: translateY(0%); } .headroom--unpinned { transform: translateY(-100%); } ``` You should now see your header slide in and out in response to the user's scroll. ## Contributions & Issues Contributions are welcome. Please clearly explain the purpose of the PR and follow the current code style. Issues can be resolved quickest if they are descriptive and include both a reduced test case and a set of steps to reproduce. ## Contributing Guide ### Setup The following steps will get you setup to contribute changes to this repo: 1. Fork the repo (click the Fork button at the top right of [this page](https://github.com/WickyNilliams/headroom.js)) 2. Clone your fork locally ```bash git clone https://github.com//headroom.js.git cd headroom.js ``` 3. Install dependencies. This repo uses `npm`, so you should too. ```bash npm install ``` ### Building To build the project: ```bash npm run build ``` To start a watcher for building the project and running tests: ```bash npm start ``` ### Testing To run the test suite in headless mode: ```bash npm test ``` ## License Licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php). ================================================ FILE: cypress/.eslintrc ================================================ { "extends": ["plugin:cypress/recommended"] } ================================================ FILE: cypress/fixtures/index.html ================================================ Document

Lorem ipsum

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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 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


#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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 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


#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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 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


#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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 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


#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

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. 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); } }; }