├── src
├── smooth-scroll.js
├── smooth-scroll.polyfills.js
├── _closest.polyfill.js
├── _customEvent.polyfill.js
├── _requestAnimationFrame.polyfill.js
└── core.js
├── .travis.yml
├── .gitignore
├── .github
├── issue_template.md
├── workflows
│ └── push.yml
└── contributing.md
├── package.json
├── LICENSE.md
├── jump-to-top.html
├── rollup.config.js
├── speed.html
├── dist
├── smooth-scroll.min.js
├── smooth-scroll.polyfills.min.js
├── smooth-scroll.js
└── smooth-scroll.polyfills.js
├── index.html
├── gulpfile.js
└── README.md
/src/smooth-scroll.js:
--------------------------------------------------------------------------------
1 | import SmoothScroll from './core.js';
2 |
3 | export default SmoothScroll;
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 | before_script:
5 | - npm install -g gulp
6 | script: gulp
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node
2 | node_modules
3 | test/results
4 | test/coverage
5 |
6 | ## OS X
7 | .DS_Store
8 | ._*
9 | .Spotlight-V100
10 | .Trashes
--------------------------------------------------------------------------------
/src/smooth-scroll.polyfills.js:
--------------------------------------------------------------------------------
1 | import './_closest.polyfill.js';
2 | import './_customEvent.polyfill.js';
3 | import './_requestAnimationFrame.polyfill.js';
4 | import SmoothScroll from './core.js';
5 |
6 | export default SmoothScroll;
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | **Test case:** https://codepen.io/cferdinandi/pen/RqGLpz
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | on: push
2 | name: publish on release
3 | jobs:
4 | publish:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@master
8 | - name: publish
9 | uses: actions/npm@master
10 | env:
11 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
12 | with:
13 | args: publish
14 |
--------------------------------------------------------------------------------
/src/_closest.polyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * closest() polyfill
3 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
4 | */
5 | if (window.Element && !Element.prototype.closest) {
6 | Element.prototype.closest = function(s) {
7 | var matches = (this.document || this.ownerDocument).querySelectorAll(s),
8 | i,
9 | el = this;
10 | do {
11 | i = matches.length;
12 | while (--i >= 0 && matches.item(i) !== el) {}
13 | } while ((i < 0) && (el = el.parentElement));
14 | return el;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/_customEvent.polyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * CustomEvent() polyfill
3 | * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
4 | */
5 | (function () {
6 |
7 | if (typeof window.CustomEvent === "function") return false;
8 |
9 | function CustomEvent(event, params) {
10 | params = params || { bubbles: false, cancelable: false, detail: undefined };
11 | var evt = document.createEvent('CustomEvent');
12 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
13 | return evt;
14 | }
15 |
16 | CustomEvent.prototype = window.Event.prototype;
17 |
18 | window.CustomEvent = CustomEvent;
19 | })();
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smooth-scroll",
3 | "version": "16.1.4",
4 | "description": "Animate scrolling to anchor links",
5 | "main": "./dist/smooth-scroll.polyfills.min.js",
6 | "author": {
7 | "name": "Chris Ferdinandi",
8 | "url": "http://gomakethings.com"
9 | },
10 | "license": "MIT",
11 | "repository": {
12 | "type": "git",
13 | "url": "http://github.com/cferdinandi/smooth-scroll"
14 | },
15 | "scripts": {
16 | "js": "rollup --config",
17 | "build": "npm run js"
18 | },
19 | "devDependencies": {
20 | "rollup": "^2.6.1",
21 | "rollup-plugin-terser": "^5.3.0",
22 | "sass": "^1.26.5",
23 | "imagemin-cli": "^5.1.0",
24 | "imagemin-mozjpeg": "^8.0.0",
25 | "imagemin-pngcrush": "^6.0.0",
26 | "imagemin-pngquant": "^8.0.0",
27 | "imagemin-zopfli": "^6.0.0",
28 | "svgo": "^1.3.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.github/contributing.md:
--------------------------------------------------------------------------------
1 | # Bugs, Questions, and Feature Requests
2 |
3 | Report bugs, ask questions, and request features using [GitHub Issues](https://github.com/cferdinandi/smooth-scroll/issues).
4 |
5 | **Before posting, do a search to make sure your issue or question hasn't already been reported or discussed.** If no matching issue exists, go ahead and create one.
6 |
7 | **Please be sure to include all of the following:**
8 |
9 | 1. A clear, descriptive title (ie. "A bug" is not a good title).
10 | 2. [A reduced test case.](https://css-tricks.com/reduced-test-cases/)
11 | - Clearly demonstrate the bug or issue.
12 | - Include the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug.
13 | - A link to your production site is **not** a reduced test case.
14 | - You can create one by [forking this CodePen](https://codepen.io/cferdinandi/pen/RqGLpz).
15 | 3. The browser and OS that you're using.
16 |
17 | Duplicates and issues without a reduced test case may be closed without comment.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright (c) Go Make Things, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/_requestAnimationFrame.polyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * requestAnimationFrame() polyfill
3 | * By Erik Möller. Fixes from Paul Irish and Tino Zijdel.
4 | * @link http://paulirish.com/2011/requestanimationframe-for-smart-animating/
5 | * @link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
6 | * @license MIT
7 | */
8 | (function() {
9 | var lastTime = 0;
10 | var vendors = ['ms', 'moz', 'webkit', 'o'];
11 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
12 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
13 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] ||
14 | window[vendors[x]+'CancelRequestAnimationFrame'];
15 | }
16 |
17 | if (!window.requestAnimationFrame) {
18 | window.requestAnimationFrame = function(callback, element) {
19 | var currTime = new Date().getTime();
20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
21 | var id = window.setTimeout(function() { callback(currTime + timeToCall); },
22 | timeToCall);
23 | lastTime = currTime + timeToCall;
24 | return id;
25 | };
26 | }
27 |
28 | if (!window.cancelAnimationFrame) {
29 | window.cancelAnimationFrame = function(id) {
30 | clearTimeout(id);
31 | };
32 | }
33 | }());
34 |
--------------------------------------------------------------------------------
/jump-to-top.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Smooth Scroll
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 | .
.
.
.
.
.
.
.
.
.
.
.
.
36 | .
.
.
.
.
.
.
.
.
.
.
.
.
37 | .
.
.
.
.
.
.
.
.
.
.
.
.
38 | .
.
.
.
.
.
.
.
.
.
.
.
.
39 | .
.
.
.
.
.
.
.
.
.
.
.
.
40 | .
.
.
.
.
.
.
.
.
.
.
.
.
41 | .
.
.
.
.
.
.
.
.
.
.
.
.
42 | .
.
.
.
.
.
.
.
.
.
.
.
.
43 |
44 |
45 | Back to the top
46 |
47 |
48 |
49 |
50 |
51 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // Plugins
2 | import { terser } from 'rollup-plugin-terser';
3 | import pkg from './package.json';
4 |
5 |
6 | // Configs
7 | var configs = {
8 | name: 'SmoothScroll',
9 | files: ['smooth-scroll.js', 'smooth-scroll.polyfills.js'],
10 | formats: ['umd'],
11 | default: 'umd',
12 | pathIn: 'src/',
13 | pathOut: 'dist/',
14 | minify: true
15 | };
16 |
17 | // Banner
18 | var banner = `/*! ${configs.name ? configs.name : pkg.name} v${pkg.version} | (c) ${new Date().getFullYear()} ${pkg.author.name} | ${pkg.license} License | ${pkg.repository.url} */`;
19 |
20 | var createOutput = function (filename, minify) {
21 | return configs.formats.map(function (format) {
22 | var output = {
23 | file: `${configs.pathOut}/${filename}${format === configs.default ? '' : `.${format}`}${minify ? '.min' : ''}.js`,
24 | format: format,
25 | banner: banner
26 | };
27 | if (format === 'iife' || format === 'umd') {
28 | output.name = configs.name ? configs.name : pkg.name;
29 | }
30 | if (minify) {
31 | output.plugins = [terser()];
32 | }
33 | return output;
34 | });
35 | };
36 |
37 | /**
38 | * Create output formats
39 | * @param {String} filename The filename
40 | * @return {Array} The outputs array
41 | */
42 | var createOutputs = function (filename) {
43 |
44 | // Create base outputs
45 | var outputs = createOutput(filename);
46 |
47 | // If not minifying, return outputs
48 | if (!configs.minify) return outputs;
49 |
50 | // Otherwise, ceate second set of outputs
51 | var outputsMin = createOutput(filename, true);
52 |
53 | // Merge and return the two arrays
54 | return outputs.concat(outputsMin);
55 |
56 | };
57 |
58 | /**
59 | * Create export object
60 | * @return {Array} The export object
61 | */
62 | var createExport = function (file) {
63 | return configs.files.map(function (file) {
64 | var filename = file.replace('.js', '');
65 | return {
66 | input: `${configs.pathIn}/${file}`,
67 | output: createOutputs(filename)
68 | };
69 | });
70 | };
71 |
72 | export default createExport();
--------------------------------------------------------------------------------
/speed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Smooth Scroll
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 | Click Me
35 | Or me
36 |
37 |
38 |
39 | .
.
.
.
.
.
.
.
.
.
.
.
.
40 | .
.
.
.
.
.
.
.
.
.
.
.
.
41 | .
.
.
.
.
.
.
.
.
.
.
.
.
42 |
43 |
44 | Bazinga!
45 |
46 |
47 | .
.
.
.
.
.
.
.
.
.
.
.
.
48 | .
.
.
.
.
.
.
.
.
.
.
.
.
49 | .
.
.
.
.
.
.
.
.
.
.
.
.
50 | .
.
.
.
.
.
.
.
.
.
.
.
.
51 | .
.
.
.
.
.
.
.
.
.
.
.
.
52 | .
.
.
.
.
.
.
.
.
.
.
.
.
53 |
54 |
55 | Back to the top
56 |
57 | .
.
.
.
.
58 |
59 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/dist/smooth-scroll.min.js:
--------------------------------------------------------------------------------
1 | /*! SmoothScroll v16.1.4 | (c) 2020 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */
2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).SmoothScroll=t()}(this,(function(){"use strict";var e={ignore:"[data-scroll-ignore]",header:null,topOnEmptyHash:!0,speed:500,speedAsDuration:!1,durationMax:null,durationMin:null,clip:!0,offset:0,easing:"easeInOutCubic",customEasing:null,updateURL:!0,popstate:!0,emitEvents:!0},t=function(){var e={};return Array.prototype.forEach.call(arguments,(function(t){for(var n in t){if(!t.hasOwnProperty(n))return;e[n]=t[n]}})),e},n=function(e){"#"===e.charAt(0)&&(e=e.substr(1));for(var t,n=String(e),o=n.length,a=-1,i="",r=n.charCodeAt(0);++a=1&&t<=31||127==t||0===a&&t>=48&&t<=57||1===a&&t>=48&&t<=57&&45===r?i+="\\"+t.toString(16)+" ":i+=t>=128||45===t||95===t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?n.charAt(a):"\\"+n.charAt(a)}return"#"+i},o=function(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},a=function(e){return e?(t=e,parseInt(window.getComputedStyle(t).height,10)+e.offsetTop):0;var t},i=function(e,t,n){0===e&&document.body.focus(),n||(e.focus(),document.activeElement!==e&&(e.setAttribute("tabindex","-1"),e.focus(),e.style.outline="none"),window.scrollTo(0,t))},r=function(e,t,n,o){if(t.emitEvents&&"function"==typeof window.CustomEvent){var a=new CustomEvent(e,{bubbles:!0,detail:{anchor:n,toggle:o}});document.dispatchEvent(a)}};return function(s,c){var u,l,d,f,m={};m.cancelScroll=function(e){cancelAnimationFrame(f),f=null,e||r("scrollCancel",u)},m.animateScroll=function(n,s,c){m.cancelScroll();var l=t(u||e,c||{}),h="[object Number]"===Object.prototype.toString.call(n),p=h||!n.tagName?null:n;if(h||p){var w=window.pageYOffset;l.header&&!d&&(d=document.querySelector(l.header));var g,y,v,S=a(d),E=h?n:function(e,t,n,a){var i=0;if(e.offsetParent)do{i+=e.offsetTop,e=e.offsetParent}while(e);return i=Math.max(i-t-n,0),a&&(i=Math.min(i,o()-window.innerHeight)),i}(p,S,parseInt("function"==typeof l.offset?l.offset(n,s):l.offset,10),l.clip),b=E-w,O=o(),I=0,M=function(e,t){var n=t.speedAsDuration?t.speed:Math.abs(e/1e3*t.speed);return t.durationMax&&n>t.durationMax?t.durationMax:t.durationMin&&n1?1:y),window.scrollTo(0,Math.floor(v)),function(e,t){var o=window.pageYOffset;if(e==t||o==t||(w=O)return m.cancelScroll(!0),i(n,t,h),r("scrollStop",l,n,s),g=null,f=null,!0}(v,E)||(f=window.requestAnimationFrame(A),g=e)};0===window.pageYOffset&&window.scrollTo(0,0),function(e,t,n){t||history.pushState&&n.updateURL&&history.pushState({smoothScroll:JSON.stringify(n),anchor:e.id},document.title,e===document.documentElement?"#top":"#"+e.id)}(n,h,l),"matchMedia"in window&&window.matchMedia("(prefers-reduced-motion)").matches?i(n,Math.floor(E),!1):(r("scrollStart",l,n,s),m.cancelScroll(!0),window.requestAnimationFrame(A))}};var h=function(e){if(!e.defaultPrevented&&!(0!==e.button||e.metaKey||e.ctrlKey||e.shiftKey)&&"closest"in e.target&&(l=e.target.closest(s))&&"a"===l.tagName.toLowerCase()&&!e.target.closest(u.ignore)&&l.hostname===window.location.hostname&&l.pathname===window.location.pathname&&/#/.test(l.href)){var t,o;try{t=n(decodeURIComponent(l.hash))}catch(e){t=n(l.hash)}if("#"===t){if(!u.topOnEmptyHash)return;o=document.documentElement}else o=document.querySelector(t);(o=o||"#top"!==t?o:document.documentElement)&&(e.preventDefault(),function(e){if(history.replaceState&&e.updateURL&&!history.state){var t=window.location.hash;t=t||"",history.replaceState({smoothScroll:JSON.stringify(e),anchor:t||window.pageYOffset},document.title,t||window.location.href)}}(u),m.animateScroll(o,l))}},p=function(){if(null!==history.state&&history.state.smoothScroll&&history.state.smoothScroll===JSON.stringify(u)){var e=history.state.anchor;"string"==typeof e&&e&&!(e=document.querySelector(n(history.state.anchor)))||m.animateScroll(e,null,{updateURL:!1})}};m.destroy=function(){u&&(document.removeEventListener("click",h,!1),window.removeEventListener("popstate",p,!1),m.cancelScroll(),u=null,l=null,d=null,f=null)};return function(){if(!("querySelector"in document&&"addEventListener"in window&&"requestAnimationFrame"in window&&"closest"in window.Element.prototype))throw"Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.";m.destroy(),u=t(e,c||{}),d=u.header?document.querySelector(u.header):null,document.addEventListener("click",h,!1),u.updateURL&&u.popstate&&window.addEventListener("popstate",p,!1)}(),m}}));
3 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Smooth Scroll
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 | Linear
35 | Linear (no other options)
36 |
37 |
38 |
39 | Ease-In
40 | Quad
41 | Cubic
42 | Quart
43 | Quint
44 |
45 |
46 |
47 | Ease-In-Out
48 | Quad
49 | Cubic
50 | Quart
51 | Quint
52 |
53 |
54 |
55 | Ease-Out
56 | Quad
57 | Cubic
58 | Quart
59 | Quint
60 |
61 |
62 |
63 | .
.
.
.
.
.
.
.
.
.
.
.
.
64 | .
.
.
.
.
.
.
.
.
.
.
.
.
65 | .
.
.
.
.
.
.
.
.
.
.
.
.
66 |
67 |
68 |
69 | Non-ASCII Characters
70 | 中文
71 |
72 |
73 |
74 | .
.
.
.
.
.
.
.
.
.
.
.
.
75 | .
.
.
.
.
.
.
.
.
.
.
.
.
76 | .
.
.
.
.
.
.
.
.
.
.
.
.
77 |
78 |
79 |
80 | 中文
81 | More Special Characters
82 |
83 |
84 |
85 | .
.
.
.
.
.
.
.
.
.
.
.
.
86 | .
.
.
.
.
.
.
.
.
.
.
.
.
87 | .
.
.
.
.
.
.
.
.
.
.
.
.
88 |
89 |
90 | myymälä
91 |
92 |
93 | .
.
.
.
.
.
.
.
.
.
.
.
.
94 | .
.
.
.
.
.
.
.
.
.
.
.
.
95 | .
.
.
.
.
.
.
.
.
.
.
.
.
96 |
97 |
98 | Bazinga!
99 |
100 |
101 | .
.
.
.
.
.
.
.
.
.
.
.
.
102 | .
.
.
.
.
.
.
.
.
.
.
.
.
103 | .
.
.
.
.
.
.
.
.
.
.
.
.
104 |
105 |
106 | Back to the top
107 |
108 |
109 |
110 |
111 |
112 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/dist/smooth-scroll.polyfills.min.js:
--------------------------------------------------------------------------------
1 | /*! SmoothScroll v16.1.4 | (c) 2020 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */
2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).SmoothScroll=t()}(this,(function(){"use strict";window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(e){var t,n=(this.document||this.ownerDocument).querySelectorAll(e),o=this;do{for(t=n.length;--t>=0&&n.item(t)!==o;);}while(t<0&&(o=o.parentElement));return o}),function(){if("function"==typeof window.CustomEvent)return!1;function e(e,t){t=t||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),n}e.prototype=window.Event.prototype,window.CustomEvent=e}(),
3 | /**
4 | * requestAnimationFrame() polyfill
5 | * By Erik Möller. Fixes from Paul Irish and Tino Zijdel.
6 | * @link http://paulirish.com/2011/requestanimationframe-for-smart-animating/
7 | * @link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
8 | * @license MIT
9 | */
10 | function(){for(var e=0,t=["ms","moz","webkit","o"],n=0;n=1&&t<=31||127==t||0===i&&t>=48&&t<=57||1===i&&t>=48&&t<=57&&45===r?a+="\\"+t.toString(16)+" ":a+=t>=128||45===t||95===t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?n.charAt(i):"\\"+n.charAt(i)}return"#"+a},o=function(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},i=function(e){return e?(t=e,parseInt(window.getComputedStyle(t).height,10)+e.offsetTop):0;var t},a=function(e,t,n){0===e&&document.body.focus(),n||(e.focus(),document.activeElement!==e&&(e.setAttribute("tabindex","-1"),e.focus(),e.style.outline="none"),window.scrollTo(0,t))},r=function(e,t,n,o){if(t.emitEvents&&"function"==typeof window.CustomEvent){var i=new CustomEvent(e,{bubbles:!0,detail:{anchor:n,toggle:o}});document.dispatchEvent(i)}};return function(c,u){var s,l,d,m,f={};f.cancelScroll=function(e){cancelAnimationFrame(m),m=null,e||r("scrollCancel",s)},f.animateScroll=function(n,c,u){f.cancelScroll();var l=t(s||e,u||{}),w="[object Number]"===Object.prototype.toString.call(n),h=w||!n.tagName?null:n;if(w||h){var p=window.pageYOffset;l.header&&!d&&(d=document.querySelector(l.header));var g,y,v,S=i(d),E=w?n:function(e,t,n,i){var a=0;if(e.offsetParent)do{a+=e.offsetTop,e=e.offsetParent}while(e);return a=Math.max(a-t-n,0),i&&(a=Math.min(a,o()-window.innerHeight)),a}(h,S,parseInt("function"==typeof l.offset?l.offset(n,c):l.offset,10),l.clip),b=E-p,A=o(),O=0,C=function(e,t){var n=t.speedAsDuration?t.speed:Math.abs(e/1e3*t.speed);return t.durationMax&&n>t.durationMax?t.durationMax:t.durationMin&&n1?1:y),window.scrollTo(0,Math.floor(v)),function(e,t){var o=window.pageYOffset;if(e==t||o==t||(p=A)return f.cancelScroll(!0),a(n,t,w),r("scrollStop",l,n,c),g=null,m=null,!0}(v,E)||(m=window.requestAnimationFrame(M),g=e)};0===window.pageYOffset&&window.scrollTo(0,0),function(e,t,n){t||history.pushState&&n.updateURL&&history.pushState({smoothScroll:JSON.stringify(n),anchor:e.id},document.title,e===document.documentElement?"#top":"#"+e.id)}(n,w,l),"matchMedia"in window&&window.matchMedia("(prefers-reduced-motion)").matches?a(n,Math.floor(E),!1):(r("scrollStart",l,n,c),f.cancelScroll(!0),window.requestAnimationFrame(M))}};var w=function(e){if(!e.defaultPrevented&&!(0!==e.button||e.metaKey||e.ctrlKey||e.shiftKey)&&"closest"in e.target&&(l=e.target.closest(c))&&"a"===l.tagName.toLowerCase()&&!e.target.closest(s.ignore)&&l.hostname===window.location.hostname&&l.pathname===window.location.pathname&&/#/.test(l.href)){var t,o;try{t=n(decodeURIComponent(l.hash))}catch(e){t=n(l.hash)}if("#"===t){if(!s.topOnEmptyHash)return;o=document.documentElement}else o=document.querySelector(t);(o=o||"#top"!==t?o:document.documentElement)&&(e.preventDefault(),function(e){if(history.replaceState&&e.updateURL&&!history.state){var t=window.location.hash;t=t||"",history.replaceState({smoothScroll:JSON.stringify(e),anchor:t||window.pageYOffset},document.title,t||window.location.href)}}(s),f.animateScroll(o,l))}},h=function(){if(null!==history.state&&history.state.smoothScroll&&history.state.smoothScroll===JSON.stringify(s)){var e=history.state.anchor;"string"==typeof e&&e&&!(e=document.querySelector(n(history.state.anchor)))||f.animateScroll(e,null,{updateURL:!1})}};f.destroy=function(){s&&(document.removeEventListener("click",w,!1),window.removeEventListener("popstate",h,!1),f.cancelScroll(),s=null,l=null,d=null,m=null)};return function(){if(!("querySelector"in document&&"addEventListener"in window&&"requestAnimationFrame"in window&&"closest"in window.Element.prototype))throw"Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.";f.destroy(),s=t(e,u||{}),d=s.header?document.querySelector(s.header):null,document.addEventListener("click",w,!1),s.updateURL&&s.popstate&&window.addEventListener("popstate",h,!1)}(),f}}));
11 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Settings
3 | * Turn on/off build features
4 | */
5 |
6 | var settings = {
7 | clean: true,
8 | scripts: true,
9 | polyfills: true,
10 | styles: false,
11 | svgs: false,
12 | copy: true,
13 | reload: true
14 | };
15 |
16 |
17 | /**
18 | * Paths to project folders
19 | */
20 |
21 | var paths = {
22 | input: 'src/',
23 | output: 'dist/',
24 | scripts: {
25 | input: 'src/js/*',
26 | polyfills: '.polyfill.js',
27 | output: 'dist/'
28 | },
29 | styles: {
30 | input: 'src/sass/**/*.{scss,sass}',
31 | output: 'dist/css/'
32 | },
33 | svgs: {
34 | input: 'src/svg/*.svg',
35 | output: 'dist/svg/'
36 | },
37 | copy: {
38 | input: 'src/copy/*',
39 | output: './'
40 | },
41 | reload: './'
42 | };
43 |
44 |
45 | /**
46 | * Template for banner to add to file headers
47 | */
48 |
49 | var banner = {
50 | full:
51 | '/*!\n' +
52 | ' * <%= package.name %> v<%= package.version %>\n' +
53 | ' * <%= package.description %>\n' +
54 | ' * (c) ' + new Date().getFullYear() + ' <%= package.author.name %>\n' +
55 | ' * <%= package.license %> License\n' +
56 | ' * <%= package.repository.url %>\n' +
57 | ' */\n\n',
58 | min:
59 | '/*!' +
60 | ' <%= package.name %> v<%= package.version %>' +
61 | ' | (c) ' + new Date().getFullYear() + ' <%= package.author.name %>' +
62 | ' | <%= package.license %> License' +
63 | ' | <%= package.repository.url %>' +
64 | ' */\n'
65 | };
66 |
67 |
68 | /**
69 | * Gulp Packages
70 | */
71 |
72 | // General
73 | var {gulp, src, dest, watch, series, parallel} = require('gulp');
74 | var del = require('del');
75 | var flatmap = require('gulp-flatmap');
76 | var lazypipe = require('lazypipe');
77 | var rename = require('gulp-rename');
78 | var header = require('gulp-header');
79 | var package = require('./package.json');
80 |
81 | // Scripts
82 | var jshint = require('gulp-jshint');
83 | var stylish = require('jshint-stylish');
84 | var concat = require('gulp-concat');
85 | var uglify = require('gulp-uglify');
86 | var optimizejs = require('gulp-optimize-js');
87 |
88 | // Styles
89 | var sass = require('gulp-sass');
90 | var prefix = require('gulp-autoprefixer');
91 | var minify = require('gulp-cssnano');
92 |
93 | // SVGs
94 | var svgmin = require('gulp-svgmin');
95 |
96 | // BrowserSync
97 | var browserSync = require('browser-sync');
98 |
99 |
100 | /**
101 | * Gulp Tasks
102 | */
103 |
104 | // Remove pre-existing content from output folders
105 | var cleanDist = function (done) {
106 |
107 | // Make sure this feature is activated before running
108 | if (!settings.clean) return done();
109 |
110 | // Clean the dist folder
111 | del.sync([
112 | paths.output
113 | ]);
114 |
115 | // Signal completion
116 | return done();
117 |
118 | };
119 |
120 | // Repeated JavaScript tasks
121 | var jsTasks = lazypipe()
122 | .pipe(header, banner.full, {package: package})
123 | .pipe(optimizejs)
124 | .pipe(dest, paths.scripts.output)
125 | .pipe(rename, {suffix: '.min'})
126 | .pipe(uglify)
127 | .pipe(optimizejs)
128 | .pipe(header, banner.min, {package: package})
129 | .pipe(dest, paths.scripts.output);
130 |
131 | // Lint, minify, and concatenate scripts
132 | var buildScripts = function (done) {
133 |
134 | // Make sure this feature is activated before running
135 | if (!settings.scripts) return done();
136 |
137 | // Run tasks on script files
138 | src(paths.scripts.input)
139 | .pipe(flatmap(function(stream, file) {
140 |
141 | // If the file is a directory
142 | if (file.isDirectory()) {
143 |
144 | // Setup a suffix variable
145 | var suffix = '';
146 |
147 | // If separate polyfill files enabled
148 | if (settings.polyfills) {
149 |
150 | // Update the suffix
151 | suffix = '.polyfills';
152 |
153 | // Grab files that aren't polyfills, concatenate them, and process them
154 | src([file.path + '/*.js', '!' + file.path + '/*' + paths.scripts.polyfills])
155 | .pipe(concat(file.relative + '.js'))
156 | .pipe(jsTasks());
157 |
158 | }
159 |
160 | // Grab all files and concatenate them
161 | // If separate polyfills enabled, this will have .polyfills in the filename
162 | src(file.path + '/*.js')
163 | .pipe(concat(file.relative + suffix + '.js'))
164 | .pipe(jsTasks());
165 |
166 | return stream;
167 |
168 | }
169 |
170 | // Otherwise, process the file
171 | return stream.pipe(jsTasks());
172 |
173 | }));
174 |
175 | // Signal completion
176 | done();
177 |
178 | };
179 |
180 | // Lint scripts
181 | var lintScripts = function (done) {
182 |
183 | // Make sure this feature is activated before running
184 | if (!settings.scripts) return done();
185 |
186 | // Lint scripts
187 | src(paths.scripts.input)
188 | .pipe(jshint())
189 | .pipe(jshint.reporter('jshint-stylish'));
190 |
191 | // Signal completion
192 | done();
193 |
194 | };
195 |
196 | // Process, lint, and minify Sass files
197 | var buildStyles = function (done) {
198 |
199 | // Make sure this feature is activated before running
200 | if (!settings.styles) return done();
201 |
202 | // Run tasks on all Sass files
203 | src(paths.styles.input)
204 | .pipe(sass({
205 | outputStyle: 'expanded',
206 | sourceComments: true
207 | }))
208 | .pipe(prefix({
209 | browsers: ['last 2 version', '> 0.25%'],
210 | cascade: true,
211 | remove: true
212 | }))
213 | .pipe(header(banner.full, { package : package }))
214 | .pipe(dest(paths.styles.output))
215 | .pipe(rename({suffix: '.min'}))
216 | .pipe(minify({
217 | discardComments: {
218 | removeAll: true
219 | }
220 | }))
221 | .pipe(header(banner.min, { package : package }))
222 | .pipe(dest(paths.styles.output));
223 |
224 | // Signal completion
225 | done();
226 |
227 | };
228 |
229 | // Optimize SVG files
230 | var buildSVGs = function (done) {
231 |
232 | // Make sure this feature is activated before running
233 | if (!settings.svgs) return done();
234 |
235 | // Optimize SVG files
236 | src(paths.svgs.input)
237 | .pipe(svgmin())
238 | .pipe(dest(paths.svgs.output));
239 |
240 | // Signal completion
241 | done();
242 |
243 | };
244 |
245 | // Copy static files into output folder
246 | var copyFiles = function (done) {
247 |
248 | // Make sure this feature is activated before running
249 | if (!settings.copy) return done();
250 |
251 | // Copy static files
252 | src(paths.copy.input)
253 | .pipe(dest(paths.copy.output));
254 |
255 | // Signal completion
256 | done();
257 |
258 | };
259 |
260 | // Watch for changes to the src directory
261 | var startServer = function (done) {
262 |
263 | // Make sure this feature is activated before running
264 | if (!settings.reload) return done();
265 |
266 | // Initialize BrowserSync
267 | browserSync.init({
268 | server: {
269 | baseDir: paths.reload
270 | }
271 | });
272 |
273 | // Signal completion
274 | done();
275 |
276 | };
277 |
278 | // Reload the browser when files change
279 | var reloadBrowser = function (done) {
280 | if (!settings.reload) return done();
281 | browserSync.reload();
282 | done();
283 | };
284 |
285 | // Watch for changes
286 | var watchSource = function (done) {
287 | watch(paths.input, series(exports.default, reloadBrowser));
288 | done();
289 | };
290 |
291 |
292 | /**
293 | * Export Tasks
294 | */
295 |
296 | // Default task
297 | // gulp
298 | exports.default = series(
299 | cleanDist,
300 | parallel(
301 | buildScripts,
302 | lintScripts,
303 | buildStyles,
304 | buildSVGs,
305 | copyFiles
306 | )
307 | );
308 |
309 | // Watch and reload
310 | // gulp watch
311 | exports.watch = series(
312 | exports.default,
313 | startServer,
314 | watchSource
315 | );
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **DEPRECATION NOTICE:**
2 |
3 | Smooth Scroll is, without a doubt, my most popular and widely used plugin.
4 |
5 | But in the time since I created it, a CSS-only method for smooth scrolling has emerged, and now has fantastic browser support. It can do things this plugin can't (like scrolling to anchor links from another page), and addresses bugs and limitations in the plugin that I have never gotten around to fixing.
6 |
7 | This plugin has run its course, and the browser now offers a better, more feature rich and resilient solution out-of-the-box.
8 |
9 | Learn [how to animate scrolling to anchor links with one line of CSS](https://gomakethings.com/how-to-animate-scrolling-to-anchor-links-with-one-line-of-css/), and [how to prevent anchor links from scrolling behind fixed or sticky headers](https://gomakethings.com/how-to-prevent-anchor-links-from-scrolling-behind-a-sticky-header-with-one-line-of-css/).
10 |
11 | Thanks for the years of support!
12 |
13 | ---
14 |
15 | # Smooth Scroll [](https://travis-ci.org/cferdinandi/smooth-scroll)
16 | A lightweight script to animate scrolling to anchor links. Smooth Scroll works great with [Gumshoe](https://github.com/cferdinandi/gumshoe).
17 |
18 | **[View the Demo on CodePen →](https://codepen.io/cferdinandi/pen/wQzrdM)**
19 |
20 | [Getting Started](#getting-started) | [Scroll Speed](#scroll-speed) | [Easing Options](#easing-options) | [API](#api) | [What's new?](#whats-new) | [Known Issues](#known-issues) | [Browser Compatibility](#browser-compatibility) | [License](#license)
21 |
22 | *__Quick aside:__ you might not need this library. There's [a native CSS way to handle smooth scrolling](https://gomakethings.com/smooth-scrolling-links-with-only-css/) that might fit your needs.*
23 |
24 |
25 |
26 |
27 | ### Want to learn how to write your own vanilla JS plugins? Check out my [Vanilla JS Pocket Guides](https://vanillajsguides.com/) or join the [Vanilla JS Academy](https://vanillajsacademy.com) and level-up as a web developer. 🚀
28 |
29 |
30 |
31 |
32 | ## Getting Started
33 |
34 | Compiled and production-ready code can be found in the `dist` directory. The `src` directory contains development code.
35 |
36 | ### 1. Include Smooth Scroll on your site.
37 |
38 | There are two versions of Smooth Scroll: the standalone version, and one that comes preloaded with polyfills for `closest()`, `requestAnimationFrame()`, and `CustomEvent()`, which are only supported in newer browsers.
39 |
40 | If you're including your own polyfills or don't want to enable this feature for older browsers, use the standalone version. Otherwise, use the version with polyfills.
41 |
42 | **Direct Download**
43 |
44 | You can [download the files directly from GitHub](https://github.com/cferdinandi/smooth-scroll/archive/master.zip).
45 |
46 | ```html
47 |
48 | ```
49 |
50 | **CDN**
51 |
52 | You can also use the [jsDelivr CDN](https://cdn.jsdelivr.net/gh/cferdinandi/smooth-scroll/dist/). I recommend linking to a specific version number or version range to prevent major updates from breaking your site. Smooth Scroll uses semantic versioning.
53 |
54 | ```html
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | ```
68 |
69 | **NPM**
70 |
71 | You can also use NPM (or your favorite package manager).
72 |
73 | ```bash
74 | npm install smooth-scroll
75 | ```
76 |
77 | ### 2. Add the markup to your HTML.
78 |
79 | No special markup needed—just standard anchor links. Give the anchor location an ID just like you normally would.
80 |
81 | ```html
82 | Anchor Link
83 | ...
84 | Bazinga!
85 | ```
86 |
87 | ***Note:*** *Smooth Scroll does not work with `` style anchors. It requires IDs.*
88 |
89 | ### 3. Initialize Smooth Scroll.
90 |
91 | In the footer of your page, after the content, initialize Smooth Scroll by passing in a selector for the anchor links that should be animated. And that's it, you're done. Nice work!
92 |
93 | ```html
94 |
97 | ```
98 |
99 | ***Note:*** *The `a[href*="#"]` selector will apply Smooth Scroll to all anchor links. You can selectively target links using any other selector(s) you'd like. Smooth Scroll accepts multiple selectors as a comma separated list. Example: `'.js-scroll, [data-scroll], #some-link'`.*
100 |
101 |
102 |
103 | ## Scroll Speed
104 |
105 | Smooth Scroll allows you to adjust the speed of your animations with the `speed` option.
106 |
107 | This a number representing the amount of time in milliseconds that it should take to scroll 1000px. Scroll distances shorter than that will take less time, and scroll distances longer than that will take more time. The default is 300ms.
108 |
109 | ```js
110 | var scroll = new SmoothScroll('a[href*="#"]', {
111 | speed: 300
112 | });
113 | ```
114 |
115 | If you want all of your animations to take exactly the same amount of time (the value you set for `speed`), set the `speedAsDuration` option to `true`.
116 |
117 | ```js
118 | // All animations will take exactly 500ms
119 | var scroll = new SmoothScroll('a[href*="#"]', {
120 | speed: 500,
121 | speedAsDuration: true
122 | });
123 | ```
124 |
125 |
126 | ## Easing Options
127 |
128 | Smooth Scroll comes with about a dozen common easing patterns. [Here's a demo of the different patterns.](https://codepen.io/cferdinandi/pen/jQMGaB)
129 |
130 | **Linear**
131 | *Moves at the same speed from start to finish.*
132 |
133 | - `Linear`
134 |
135 |
136 | **Ease-In**
137 | *Gradually increases in speed.*
138 |
139 | - `easeInQuad`
140 | - `easeInCubic`
141 | - `easeInQuart`
142 | - `easeInQuint`
143 |
144 |
145 | **Ease-In-Out**
146 | *Gradually increases in speed, peaks, and then gradually slows down.*
147 |
148 | - `easeInOutQuad`
149 | - `easeInOutCubic`
150 | - `easeInOutQuart`
151 | - `easeInOutQuint`
152 |
153 |
154 | **Ease-Out**
155 | *Gradually decreases in speed.*
156 |
157 | - `easeOutQuad`
158 | - `easeOutCubic`
159 | - `easeOutQuart`
160 | - `easeOutQuint`
161 |
162 |
163 | You can also pass in your own custom easing pattern [using the `customEasing` option](#global-settings).
164 |
165 | ```js
166 | var scroll = new SmoothScroll('a[href*="#"]', {
167 | // Function. Custom easing pattern
168 | // If this is set to anything other than null, will override the easing option above
169 | customEasing: function (time) {
170 |
171 | // return
172 |
173 | // Example: easeInOut Quad
174 | return time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time;
175 |
176 | }
177 | });
178 | ```
179 |
180 |
181 |
182 | ## API
183 |
184 | Smooth Scroll includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings.
185 |
186 | ### Options and Settings
187 |
188 | You can pass options and callbacks into Smooth Scroll when instantiating.
189 |
190 | ```javascript
191 | var scroll = new SmoothScroll('a[href*="#"]', {
192 |
193 | // Selectors
194 | ignore: '[data-scroll-ignore]', // Selector for links to ignore (must be a valid CSS selector)
195 | header: null, // Selector for fixed headers (must be a valid CSS selector)
196 | topOnEmptyHash: true, // Scroll to the top of the page for links with href="#"
197 |
198 | // Speed & Duration
199 | speed: 500, // Integer. Amount of time in milliseconds it should take to scroll 1000px
200 | speedAsDuration: false, // If true, use speed as the total duration of the scroll animation
201 | durationMax: null, // Integer. The maximum amount of time the scroll animation should take
202 | durationMin: null, // Integer. The minimum amount of time the scroll animation should take
203 | clip: true, // If true, adjust scroll distance to prevent abrupt stops near the bottom of the page
204 | offset: function (anchor, toggle) {
205 |
206 | // Integer or Function returning an integer. How far to offset the scrolling anchor location in pixels
207 | // This example is a function, but you could do something as simple as `offset: 25`
208 |
209 | // An example returning different values based on whether the clicked link was in the header nav or not
210 | if (toggle.classList.closest('.my-header-nav')) {
211 | return 25;
212 | } else {
213 | return 50;
214 | }
215 |
216 | },
217 |
218 | // Easing
219 | easing: 'easeInOutCubic', // Easing pattern to use
220 | customEasing: function (time) {
221 |
222 | // Function. Custom easing pattern
223 | // If this is set to anything other than null, will override the easing option above
224 |
225 | // return
226 |
227 | // Example: easeInOut Quad
228 | return time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time;
229 |
230 | },
231 |
232 | // History
233 | updateURL: true, // Update the URL on scroll
234 | popstate: true, // Animate scrolling with the forward/backward browser buttons (requires updateURL to be true)
235 |
236 | // Custom Events
237 | emitEvents: true // Emit custom events
238 |
239 | });
240 | ```
241 |
242 | ### Custom Events
243 |
244 | Smooth Scroll emits three custom events:
245 |
246 | - `scrollStart` is emitted when the scrolling animation starts.
247 | - `scrollStop` is emitted when the scrolling animation stops.
248 | - `scrollCancel` is emitted if the scrolling animation is canceled.
249 |
250 | All three events are emitted on the `document` element and bubble up. You can listen for them with the `addEventListener()` method. The `event.detail` object includes the `anchor` and `toggle` elements for the animation.
251 |
252 | ```js
253 | // Log scroll events
254 | var logScrollEvent = function (event) {
255 |
256 | // The event type
257 | console.log('type:', event.type);
258 |
259 | // The anchor element being scrolled to
260 | console.log('anchor:', event.detail.anchor);
261 |
262 | // The anchor link that triggered the scroll
263 | console.log('toggle:', event.detail.toggle);
264 |
265 | };
266 |
267 | // Listen for scroll events
268 | document.addEventListener('scrollStart', logScrollEvent, false);
269 | document.addEventListener('scrollStop', logScrollEvent, false);
270 | document.addEventListener('scrollCancel', logScrollEvent, false);
271 | ```
272 |
273 | ### Methods
274 |
275 | Smooth Scroll also exposes several public methods.
276 |
277 | #### animateScroll()
278 | Animate scrolling to an anchor.
279 |
280 | ```javascript
281 | var scroll = new SmoothScroll();
282 | scroll.animateScroll(
283 | anchor, // Node to scroll to. ex. document.querySelector('#bazinga')
284 | toggle, // Node that toggles the animation, OR an integer. ex. document.querySelector('#toggle')
285 | options // Classes and callbacks. Same options as those passed into the init() function.
286 | );
287 | ```
288 |
289 | **Example 1**
290 |
291 | ```javascript
292 | var scroll = new SmoothScroll();
293 | var anchor = document.querySelector('#bazinga');
294 | scroll.animateScroll(anchor);
295 | ```
296 |
297 | **Example 2**
298 |
299 | ```javascript
300 | var scroll = new SmoothScroll();
301 | var anchor = document.querySelector('#bazinga');
302 | var toggle = document.querySelector('#toggle');
303 | var options = { speed: 1000, easing: 'easeOutCubic' };
304 | scroll.animateScroll(anchor, toggle, options);
305 | ```
306 |
307 | **Example 3**
308 |
309 | ```javascript
310 | // You can optionally pass in a y-position to scroll to as an integer
311 | var scroll = new SmoothScroll();
312 | scroll.animateScroll(750);
313 | ```
314 |
315 | #### cancelScroll()
316 | Cancel a scroll-in-progress.
317 |
318 | ```javascript
319 | var scroll = new SmoothScroll();
320 | scroll.cancelScroll();
321 | ```
322 |
323 | ***Note:*** *This does not handle focus management. The user will stop in place, and focus will remain on the anchor link that triggered the scroll.*
324 |
325 | #### destroy()
326 | Destroy the current initialization. This is called automatically in the `init` method to remove any existing initializations.
327 |
328 | ```javascript
329 | var scroll = new SmoothScroll();
330 | scroll.destroy();
331 | ```
332 |
333 |
334 | ### Fixed Headers
335 |
336 | If you're using a fixed header, Smooth Scroll will automatically offset scroll distances by the header height. Pass in a valid CSS selector for your fixed header as an option to the `init`.
337 |
338 | If you have multiple fixed headers, pass in the last one in the markup.
339 |
340 | ```html
341 |
344 | ...
345 |
350 | ```
351 |
352 |
353 |
354 | ## What's new?
355 |
356 | Scroll duration now varies based on distance traveled. If you want to maintain the old scroll animation duration behavior, set the `speedAsDuration` option to `true`.
357 |
358 |
359 |
360 | ## Known Issues
361 |
362 | ### Reduce Motion Settings
363 |
364 | This isn't really an "issue" so-much as a question I get a lot.
365 |
366 | Smooth Scroll respects [the `Reduce Motion` setting](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) available in certain operating systems. In browsers that surface that setting, Smooth Scroll will not run and will revert to the default "jump to location" anchor link behavior.
367 |
368 | I've decided to respect user preferences of developer desires here. This is *not* a configurable setting.
369 |
370 | ### `` styling
371 |
372 | If the `` element has been assigned a height of `100%` or `overflow: hidden`, Smooth Scroll is unable to properly calculate page distances and will not scroll to the right location. The `` element can have a fixed, non-percentage based height (ex. `500px`), or a height of `auto`, and an `overflow` of `visible`.
373 |
374 | ### Animating from the bottom
375 |
376 | Animated scrolling links at the very bottom of the page (example: a "scroll to top" link) will stop animated almost immediately after they start when using certain easing patterns. This is an issue that's been around for a while and I've yet to find a good fix for it. I've found that `easeOut*` easing patterns work as expected, but other patterns can cause issues. [See this discussion for more details.](https://github.com/cferdinandi/smooth-scroll/issues/49)
377 |
378 | ### Scrolling to an anchor link on another page
379 |
380 | This, unfortunately, cannot be done well.
381 |
382 | Most browsers instantly jump you to the anchor location when you load a page. You could use `scrollTo(0, 0)` to pull users back up to the top, and then manually use the `animateScroll()` method, but in my experience, it results in a visible jump on the page that's a worse experience than the default browser behavior.
383 |
384 |
385 |
386 | ## Browser Compatibility
387 |
388 | Smooth Scroll works in all modern browsers, and IE 9 and above.
389 |
390 | Smooth Scroll is built with modern JavaScript APIs, and uses progressive enhancement. If the JavaScript file fails to load, or if your site is viewed on older and less capable browsers, anchor links will jump the way they normally would.
391 |
392 | *__Note:__ Smooth Scroll will not run—even in supported browsers—if users have `Reduce Motion` enabled. [Learn more in the "Known Issues" section.](#reduce-motion-settings)*
393 |
394 | ### Polyfills
395 |
396 | Support back to IE9 requires polyfills for `closest()`, `requestAnimationFrame()`, and `CustomEvent()`. Without them, support starts with Edge.
397 |
398 | Use the included polyfills version of Smooth Scroll, or include your own.
399 |
400 |
401 |
402 | ## License
403 |
404 | The code is available under the [MIT License](LICENSE.md).
--------------------------------------------------------------------------------
/src/core.js:
--------------------------------------------------------------------------------
1 | //
2 | // Default settings
3 | //
4 |
5 | var defaults = {
6 |
7 | // Selectors
8 | ignore: '[data-scroll-ignore]',
9 | header: null,
10 | topOnEmptyHash: true,
11 |
12 | // Speed & Duration
13 | speed: 500,
14 | speedAsDuration: false,
15 | durationMax: null,
16 | durationMin: null,
17 | clip: true,
18 | offset: 0,
19 |
20 | // Easing
21 | easing: 'easeInOutCubic',
22 | customEasing: null,
23 |
24 | // History
25 | updateURL: true,
26 | popstate: true,
27 |
28 | // Custom Events
29 | emitEvents: true
30 |
31 | };
32 |
33 |
34 | //
35 | // Utility Methods
36 | //
37 |
38 | /**
39 | * Check if browser supports required methods
40 | * @return {Boolean} Returns true if all required methods are supported
41 | */
42 | var supports = function () {
43 | return (
44 | 'querySelector' in document &&
45 | 'addEventListener' in window &&
46 | 'requestAnimationFrame' in window &&
47 | 'closest' in window.Element.prototype
48 | );
49 | };
50 |
51 | /**
52 | * Merge two or more objects together.
53 | * @param {Object} objects The objects to merge together
54 | * @returns {Object} Merged values of defaults and options
55 | */
56 | var extend = function () {
57 | var merged = {};
58 | Array.prototype.forEach.call(arguments, function (obj) {
59 | for (var key in obj) {
60 | if (!obj.hasOwnProperty(key)) return;
61 | merged[key] = obj[key];
62 | }
63 | });
64 | return merged;
65 | };
66 |
67 | /**
68 | * Check to see if user prefers reduced motion
69 | * @param {Object} settings Script settings
70 | */
71 | var reduceMotion = function () {
72 | if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) {
73 | return true;
74 | }
75 | return false;
76 | };
77 |
78 | /**
79 | * Get the height of an element.
80 | * @param {Node} elem The element to get the height of
81 | * @return {Number} The element's height in pixels
82 | */
83 | var getHeight = function (elem) {
84 | return parseInt(window.getComputedStyle(elem).height, 10);
85 | };
86 |
87 | /**
88 | * Escape special characters for use with querySelector
89 | * @author Mathias Bynens
90 | * @link https://github.com/mathiasbynens/CSS.escape
91 | * @param {String} id The anchor ID to escape
92 | */
93 | var escapeCharacters = function (id) {
94 |
95 | // Remove leading hash
96 | if (id.charAt(0) === '#') {
97 | id = id.substr(1);
98 | }
99 |
100 | var string = String(id);
101 | var length = string.length;
102 | var index = -1;
103 | var codeUnit;
104 | var result = '';
105 | var firstCodeUnit = string.charCodeAt(0);
106 | while (++index < length) {
107 | codeUnit = string.charCodeAt(index);
108 | // Note: there’s no need to special-case astral symbols, surrogate
109 | // pairs, or lone surrogates.
110 |
111 | // If the character is NULL (U+0000), then throw an
112 | // `InvalidCharacterError` exception and terminate these steps.
113 | if (codeUnit === 0x0000) {
114 | throw new InvalidCharacterError(
115 | 'Invalid character: the input contains U+0000.'
116 | );
117 | }
118 |
119 | if (
120 | // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
121 | // U+007F, […]
122 | (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
123 | // If the character is the first character and is in the range [0-9]
124 | // (U+0030 to U+0039), […]
125 | (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
126 | // If the character is the second character and is in the range [0-9]
127 | // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
128 | (
129 | index === 1 &&
130 | codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
131 | firstCodeUnit === 0x002D
132 | )
133 | ) {
134 | // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
135 | result += '\\' + codeUnit.toString(16) + ' ';
136 | continue;
137 | }
138 |
139 | // If the character is not handled by one of the above rules and is
140 | // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
141 | // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
142 | // U+005A), or [a-z] (U+0061 to U+007A), […]
143 | if (
144 | codeUnit >= 0x0080 ||
145 | codeUnit === 0x002D ||
146 | codeUnit === 0x005F ||
147 | codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
148 | codeUnit >= 0x0041 && codeUnit <= 0x005A ||
149 | codeUnit >= 0x0061 && codeUnit <= 0x007A
150 | ) {
151 | // the character itself
152 | result += string.charAt(index);
153 | continue;
154 | }
155 |
156 | // Otherwise, the escaped character.
157 | // http://dev.w3.org/csswg/cssom/#escape-a-character
158 | result += '\\' + string.charAt(index);
159 |
160 | }
161 |
162 | // Return sanitized hash
163 | return '#' + result;
164 |
165 | };
166 |
167 | /**
168 | * Calculate the easing pattern
169 | * @link https://gist.github.com/gre/1650294
170 | * @param {Object} settings Easing pattern
171 | * @param {Number} time Time animation should take to complete
172 | * @returns {Number}
173 | */
174 | var easingPattern = function (settings, time) {
175 | var pattern;
176 |
177 | // Default Easing Patterns
178 | if (settings.easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity
179 | if (settings.easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity
180 | if (settings.easing === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
181 | if (settings.easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity
182 | if (settings.easing === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity
183 | if (settings.easing === 'easeInOutCubic') pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
184 | if (settings.easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity
185 | if (settings.easing === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
186 | if (settings.easing === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
187 | if (settings.easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity
188 | if (settings.easing === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
189 | if (settings.easing === 'easeInOutQuint') pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
190 |
191 | // Custom Easing Patterns
192 | if (!!settings.customEasing) pattern = settings.customEasing(time);
193 |
194 | return pattern || time; // no easing, no acceleration
195 | };
196 |
197 | /**
198 | * Determine the document's height
199 | * @returns {Number}
200 | */
201 | var getDocumentHeight = function () {
202 | return Math.max(
203 | document.body.scrollHeight, document.documentElement.scrollHeight,
204 | document.body.offsetHeight, document.documentElement.offsetHeight,
205 | document.body.clientHeight, document.documentElement.clientHeight
206 | );
207 | };
208 |
209 | /**
210 | * Calculate how far to scroll
211 | * Clip support added by robjtede - https://github.com/cferdinandi/smooth-scroll/issues/405
212 | * @param {Element} anchor The anchor element to scroll to
213 | * @param {Number} headerHeight Height of a fixed header, if any
214 | * @param {Number} offset Number of pixels by which to offset scroll
215 | * @param {Boolean} clip If true, adjust scroll distance to prevent abrupt stops near the bottom of the page
216 | * @returns {Number}
217 | */
218 | var getEndLocation = function (anchor, headerHeight, offset, clip) {
219 | var location = 0;
220 | if (anchor.offsetParent) {
221 | do {
222 | location += anchor.offsetTop;
223 | anchor = anchor.offsetParent;
224 | } while (anchor);
225 | }
226 | location = Math.max(location - headerHeight - offset, 0);
227 | if (clip) {
228 | location = Math.min(location, getDocumentHeight() - window.innerHeight);
229 | }
230 | return location;
231 | };
232 |
233 | /**
234 | * Get the height of the fixed header
235 | * @param {Node} header The header
236 | * @return {Number} The height of the header
237 | */
238 | var getHeaderHeight = function (header) {
239 | return !header ? 0 : (getHeight(header) + header.offsetTop);
240 | };
241 |
242 | /**
243 | * Calculate the speed to use for the animation
244 | * @param {Number} distance The distance to travel
245 | * @param {Object} settings The plugin settings
246 | * @return {Number} How fast to animate
247 | */
248 | var getSpeed = function (distance, settings) {
249 | var speed = settings.speedAsDuration ? settings.speed : Math.abs(distance / 1000 * settings.speed);
250 | if (settings.durationMax && speed > settings.durationMax) return settings.durationMax;
251 | if (settings.durationMin && speed < settings.durationMin) return settings.durationMin;
252 | return parseInt(speed, 10);
253 | };
254 |
255 | var setHistory = function (options) {
256 |
257 | // Make sure this should run
258 | if (!history.replaceState || !options.updateURL || history.state) return;
259 |
260 | // Get the hash to use
261 | var hash = window.location.hash;
262 | hash = hash ? hash : '';
263 |
264 | // Set a default history
265 | history.replaceState(
266 | {
267 | smoothScroll: JSON.stringify(options),
268 | anchor: hash ? hash : window.pageYOffset
269 | },
270 | document.title,
271 | hash ? hash : window.location.href
272 | );
273 |
274 | };
275 |
276 | /**
277 | * Update the URL
278 | * @param {Node} anchor The anchor that was scrolled to
279 | * @param {Boolean} isNum If true, anchor is a number
280 | * @param {Object} options Settings for Smooth Scroll
281 | */
282 | var updateURL = function (anchor, isNum, options) {
283 |
284 | // Bail if the anchor is a number
285 | if (isNum) return;
286 |
287 | // Verify that pushState is supported and the updateURL option is enabled
288 | if (!history.pushState || !options.updateURL) return;
289 |
290 | // Update URL
291 | history.pushState(
292 | {
293 | smoothScroll: JSON.stringify(options),
294 | anchor: anchor.id
295 | },
296 | document.title,
297 | anchor === document.documentElement ? '#top' : '#' + anchor.id
298 | );
299 |
300 | };
301 |
302 | /**
303 | * Bring the anchored element into focus
304 | * @param {Node} anchor The anchor element
305 | * @param {Number} endLocation The end location to scroll to
306 | * @param {Boolean} isNum If true, scroll is to a position rather than an element
307 | */
308 | var adjustFocus = function (anchor, endLocation, isNum) {
309 |
310 | // Is scrolling to top of page, blur
311 | if (anchor === 0) {
312 | document.body.focus();
313 | }
314 |
315 | // Don't run if scrolling to a number on the page
316 | if (isNum) return;
317 |
318 | // Otherwise, bring anchor element into focus
319 | anchor.focus();
320 | if (document.activeElement !== anchor) {
321 | anchor.setAttribute('tabindex', '-1');
322 | anchor.focus();
323 | anchor.style.outline = 'none';
324 | }
325 | window.scrollTo(0 , endLocation);
326 |
327 | };
328 |
329 | /**
330 | * Emit a custom event
331 | * @param {String} type The event type
332 | * @param {Object} options The settings object
333 | * @param {Node} anchor The anchor element
334 | * @param {Node} toggle The toggle element
335 | */
336 | var emitEvent = function (type, options, anchor, toggle) {
337 | if (!options.emitEvents || typeof window.CustomEvent !== 'function') return;
338 | var event = new CustomEvent(type, {
339 | bubbles: true,
340 | detail: {
341 | anchor: anchor,
342 | toggle: toggle
343 | }
344 | });
345 | document.dispatchEvent(event);
346 | };
347 |
348 |
349 | //
350 | // SmoothScroll Constructor
351 | //
352 |
353 | var SmoothScroll = function (selector, options) {
354 |
355 | //
356 | // Variables
357 | //
358 |
359 | var smoothScroll = {}; // Object for public APIs
360 | var settings, anchor, toggle, fixedHeader, eventTimeout, animationInterval;
361 |
362 |
363 | //
364 | // Methods
365 | //
366 |
367 | /**
368 | * Cancel a scroll-in-progress
369 | */
370 | smoothScroll.cancelScroll = function (noEvent) {
371 | cancelAnimationFrame(animationInterval);
372 | animationInterval = null;
373 | if (noEvent) return;
374 | emitEvent('scrollCancel', settings);
375 | };
376 |
377 | /**
378 | * Start/stop the scrolling animation
379 | * @param {Node|Number} anchor The element or position to scroll to
380 | * @param {Element} toggle The element that toggled the scroll event
381 | * @param {Object} options
382 | */
383 | smoothScroll.animateScroll = function (anchor, toggle, options) {
384 |
385 | // Cancel any in progress scrolls
386 | smoothScroll.cancelScroll();
387 |
388 | // Local settings
389 | var _settings = extend(settings || defaults, options || {}); // Merge user options with defaults
390 |
391 | // Selectors and variables
392 | var isNum = Object.prototype.toString.call(anchor) === '[object Number]' ? true : false;
393 | var anchorElem = isNum || !anchor.tagName ? null : anchor;
394 | if (!isNum && !anchorElem) return;
395 | var startLocation = window.pageYOffset; // Current location on the page
396 | if (_settings.header && !fixedHeader) {
397 | // Get the fixed header if not already set
398 | fixedHeader = document.querySelector(_settings.header);
399 | }
400 | var headerHeight = getHeaderHeight(fixedHeader);
401 | var endLocation = isNum ? anchor : getEndLocation(anchorElem, headerHeight, parseInt((typeof _settings.offset === 'function' ? _settings.offset(anchor, toggle) : _settings.offset), 10), _settings.clip); // Location to scroll to
402 | var distance = endLocation - startLocation; // distance to travel
403 | var documentHeight = getDocumentHeight();
404 | var timeLapsed = 0;
405 | var speed = getSpeed(distance, _settings);
406 | var start, percentage, position;
407 |
408 | /**
409 | * Stop the scroll animation when it reaches its target (or the bottom/top of page)
410 | * @param {Number} position Current position on the page
411 | * @param {Number} endLocation Scroll to location
412 | * @param {Number} animationInterval How much to scroll on this loop
413 | */
414 | var stopAnimateScroll = function (position, endLocation) {
415 |
416 | // Get the current location
417 | var currentLocation = window.pageYOffset;
418 |
419 | // Check if the end location has been reached yet (or we've hit the end of the document)
420 | if (position == endLocation || currentLocation == endLocation || ((startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight)) {
421 |
422 | // Clear the animation timer
423 | smoothScroll.cancelScroll(true);
424 |
425 | // Bring the anchored element into focus
426 | adjustFocus(anchor, endLocation, isNum);
427 |
428 | // Emit a custom event
429 | emitEvent('scrollStop', _settings, anchor, toggle);
430 |
431 | // Reset start
432 | start = null;
433 | animationInterval = null;
434 |
435 | return true;
436 |
437 | }
438 | };
439 |
440 | /**
441 | * Loop scrolling animation
442 | */
443 | var loopAnimateScroll = function (timestamp) {
444 | if (!start) { start = timestamp; }
445 | timeLapsed += timestamp - start;
446 | percentage = speed === 0 ? 0 : (timeLapsed / speed);
447 | percentage = (percentage > 1) ? 1 : percentage;
448 | position = startLocation + (distance * easingPattern(_settings, percentage));
449 | window.scrollTo(0, Math.floor(position));
450 | if (!stopAnimateScroll(position, endLocation)) {
451 | animationInterval = window.requestAnimationFrame(loopAnimateScroll);
452 | start = timestamp;
453 | }
454 | };
455 |
456 | /**
457 | * Reset position to fix weird iOS bug
458 | * @link https://github.com/cferdinandi/smooth-scroll/issues/45
459 | */
460 | if (window.pageYOffset === 0) {
461 | window.scrollTo(0, 0);
462 | }
463 |
464 | // Update the URL
465 | updateURL(anchor, isNum, _settings);
466 |
467 | // If the user prefers reduced motion, jump to location
468 | if (reduceMotion()) {
469 | adjustFocus(anchor, Math.floor(endLocation), false);
470 | return;
471 | }
472 |
473 | // Emit a custom event
474 | emitEvent('scrollStart', _settings, anchor, toggle);
475 |
476 | // Start scrolling animation
477 | smoothScroll.cancelScroll(true);
478 | window.requestAnimationFrame(loopAnimateScroll);
479 |
480 | };
481 |
482 | /**
483 | * If smooth scroll element clicked, animate scroll
484 | */
485 | var clickHandler = function (event) {
486 |
487 | // Don't run if event was canceled but still bubbled up
488 | // By @mgreter - https://github.com/cferdinandi/smooth-scroll/pull/462/
489 | if (event.defaultPrevented) return;
490 |
491 | // Don't run if right-click or command/control + click or shift + click
492 | if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
493 |
494 | // Check if event.target has closest() method
495 | // By @totegi - https://github.com/cferdinandi/smooth-scroll/pull/401/
496 | if (!('closest' in event.target)) return;
497 |
498 | // Check if a smooth scroll link was clicked
499 | toggle = event.target.closest(selector);
500 | if (!toggle || toggle.tagName.toLowerCase() !== 'a' || event.target.closest(settings.ignore)) return;
501 |
502 | // Only run if link is an anchor and points to the current page
503 | if (toggle.hostname !== window.location.hostname || toggle.pathname !== window.location.pathname || !/#/.test(toggle.href)) return;
504 |
505 | // Get an escaped version of the hash
506 | var hash;
507 | try {
508 | hash = escapeCharacters(decodeURIComponent(toggle.hash));
509 | } catch(e) {
510 | hash = escapeCharacters(toggle.hash);
511 | }
512 |
513 | // Get the anchored element
514 | var anchor;
515 | if (hash === '#') {
516 | if (!settings.topOnEmptyHash) return;
517 | anchor = document.documentElement;
518 | } else {
519 | anchor = document.querySelector(hash);
520 | }
521 | anchor = !anchor && hash === '#top' ? document.documentElement : anchor;
522 |
523 | // If anchored element exists, scroll to it
524 | if (!anchor) return;
525 | event.preventDefault();
526 | setHistory(settings);
527 | smoothScroll.animateScroll(anchor, toggle);
528 |
529 | };
530 |
531 | /**
532 | * Animate scroll on popstate events
533 | */
534 | var popstateHandler = function () {
535 |
536 | // Stop if history.state doesn't exist (ex. if clicking on a broken anchor link).
537 | // fixes `Cannot read property 'smoothScroll' of null` error getting thrown.
538 | if (history.state === null) return;
539 |
540 | // Only run if state is a popstate record for this instantiation
541 | if (!history.state.smoothScroll || history.state.smoothScroll !== JSON.stringify(settings)) return;
542 |
543 | // Get the anchor
544 | var anchor = history.state.anchor;
545 | if (typeof anchor === 'string' && anchor) {
546 | anchor = document.querySelector(escapeCharacters(history.state.anchor));
547 | if (!anchor) return;
548 | }
549 |
550 | // Animate scroll to anchor link
551 | smoothScroll.animateScroll(anchor, null, {updateURL: false});
552 |
553 | };
554 |
555 | /**
556 | * Destroy the current initialization.
557 | */
558 | smoothScroll.destroy = function () {
559 |
560 | // If plugin isn't already initialized, stop
561 | if (!settings) return;
562 |
563 | // Remove event listeners
564 | document.removeEventListener('click', clickHandler, false);
565 | window.removeEventListener('popstate', popstateHandler, false);
566 |
567 | // Cancel any scrolls-in-progress
568 | smoothScroll.cancelScroll();
569 |
570 | // Reset variables
571 | settings = null;
572 | anchor = null;
573 | toggle = null;
574 | fixedHeader = null;
575 | eventTimeout = null;
576 | animationInterval = null;
577 |
578 | };
579 |
580 | /**
581 | * Initialize Smooth Scroll
582 | * @param {Object} options User settings
583 | */
584 | var init = function () {
585 |
586 | // feature test
587 | if (!supports()) throw 'Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.';
588 |
589 | // Destroy any existing initializations
590 | smoothScroll.destroy();
591 |
592 | // Selectors and variables
593 | settings = extend(defaults, options || {}); // Merge user options with defaults
594 | fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header
595 |
596 | // When a toggle is clicked, run the click handler
597 | document.addEventListener('click', clickHandler, false);
598 |
599 | // If updateURL and popState are enabled, listen for pop events
600 | if (settings.updateURL && settings.popstate) {
601 | window.addEventListener('popstate', popstateHandler, false);
602 | }
603 |
604 | };
605 |
606 |
607 | //
608 | // Initialize plugin
609 | //
610 |
611 | init();
612 |
613 |
614 | //
615 | // Public APIs
616 | //
617 |
618 | return smoothScroll;
619 |
620 | };
621 |
622 | export default SmoothScroll;
--------------------------------------------------------------------------------
/dist/smooth-scroll.js:
--------------------------------------------------------------------------------
1 | /*! SmoothScroll v16.1.4 | (c) 2020 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */
2 | (function (global, factory) {
3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4 | typeof define === 'function' && define.amd ? define(factory) :
5 | (global = global || self, global.SmoothScroll = factory());
6 | }(this, (function () { 'use strict';
7 |
8 | //
9 | // Default settings
10 | //
11 |
12 | var defaults = {
13 |
14 | // Selectors
15 | ignore: '[data-scroll-ignore]',
16 | header: null,
17 | topOnEmptyHash: true,
18 |
19 | // Speed & Duration
20 | speed: 500,
21 | speedAsDuration: false,
22 | durationMax: null,
23 | durationMin: null,
24 | clip: true,
25 | offset: 0,
26 |
27 | // Easing
28 | easing: 'easeInOutCubic',
29 | customEasing: null,
30 |
31 | // History
32 | updateURL: true,
33 | popstate: true,
34 |
35 | // Custom Events
36 | emitEvents: true
37 |
38 | };
39 |
40 |
41 | //
42 | // Utility Methods
43 | //
44 |
45 | /**
46 | * Check if browser supports required methods
47 | * @return {Boolean} Returns true if all required methods are supported
48 | */
49 | var supports = function () {
50 | return (
51 | 'querySelector' in document &&
52 | 'addEventListener' in window &&
53 | 'requestAnimationFrame' in window &&
54 | 'closest' in window.Element.prototype
55 | );
56 | };
57 |
58 | /**
59 | * Merge two or more objects together.
60 | * @param {Object} objects The objects to merge together
61 | * @returns {Object} Merged values of defaults and options
62 | */
63 | var extend = function () {
64 | var merged = {};
65 | Array.prototype.forEach.call(arguments, function (obj) {
66 | for (var key in obj) {
67 | if (!obj.hasOwnProperty(key)) return;
68 | merged[key] = obj[key];
69 | }
70 | });
71 | return merged;
72 | };
73 |
74 | /**
75 | * Check to see if user prefers reduced motion
76 | * @param {Object} settings Script settings
77 | */
78 | var reduceMotion = function () {
79 | if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) {
80 | return true;
81 | }
82 | return false;
83 | };
84 |
85 | /**
86 | * Get the height of an element.
87 | * @param {Node} elem The element to get the height of
88 | * @return {Number} The element's height in pixels
89 | */
90 | var getHeight = function (elem) {
91 | return parseInt(window.getComputedStyle(elem).height, 10);
92 | };
93 |
94 | /**
95 | * Escape special characters for use with querySelector
96 | * @author Mathias Bynens
97 | * @link https://github.com/mathiasbynens/CSS.escape
98 | * @param {String} id The anchor ID to escape
99 | */
100 | var escapeCharacters = function (id) {
101 |
102 | // Remove leading hash
103 | if (id.charAt(0) === '#') {
104 | id = id.substr(1);
105 | }
106 |
107 | var string = String(id);
108 | var length = string.length;
109 | var index = -1;
110 | var codeUnit;
111 | var result = '';
112 | var firstCodeUnit = string.charCodeAt(0);
113 | while (++index < length) {
114 | codeUnit = string.charCodeAt(index);
115 | // Note: there’s no need to special-case astral symbols, surrogate
116 | // pairs, or lone surrogates.
117 |
118 | // If the character is NULL (U+0000), then throw an
119 | // `InvalidCharacterError` exception and terminate these steps.
120 | if (codeUnit === 0x0000) {
121 | throw new InvalidCharacterError(
122 | 'Invalid character: the input contains U+0000.'
123 | );
124 | }
125 |
126 | if (
127 | // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
128 | // U+007F, […]
129 | (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
130 | // If the character is the first character and is in the range [0-9]
131 | // (U+0030 to U+0039), […]
132 | (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
133 | // If the character is the second character and is in the range [0-9]
134 | // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
135 | (
136 | index === 1 &&
137 | codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
138 | firstCodeUnit === 0x002D
139 | )
140 | ) {
141 | // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
142 | result += '\\' + codeUnit.toString(16) + ' ';
143 | continue;
144 | }
145 |
146 | // If the character is not handled by one of the above rules and is
147 | // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
148 | // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
149 | // U+005A), or [a-z] (U+0061 to U+007A), […]
150 | if (
151 | codeUnit >= 0x0080 ||
152 | codeUnit === 0x002D ||
153 | codeUnit === 0x005F ||
154 | codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
155 | codeUnit >= 0x0041 && codeUnit <= 0x005A ||
156 | codeUnit >= 0x0061 && codeUnit <= 0x007A
157 | ) {
158 | // the character itself
159 | result += string.charAt(index);
160 | continue;
161 | }
162 |
163 | // Otherwise, the escaped character.
164 | // http://dev.w3.org/csswg/cssom/#escape-a-character
165 | result += '\\' + string.charAt(index);
166 |
167 | }
168 |
169 | // Return sanitized hash
170 | return '#' + result;
171 |
172 | };
173 |
174 | /**
175 | * Calculate the easing pattern
176 | * @link https://gist.github.com/gre/1650294
177 | * @param {Object} settings Easing pattern
178 | * @param {Number} time Time animation should take to complete
179 | * @returns {Number}
180 | */
181 | var easingPattern = function (settings, time) {
182 | var pattern;
183 |
184 | // Default Easing Patterns
185 | if (settings.easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity
186 | if (settings.easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity
187 | if (settings.easing === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
188 | if (settings.easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity
189 | if (settings.easing === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity
190 | if (settings.easing === 'easeInOutCubic') pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
191 | if (settings.easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity
192 | if (settings.easing === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
193 | if (settings.easing === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
194 | if (settings.easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity
195 | if (settings.easing === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
196 | if (settings.easing === 'easeInOutQuint') pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
197 |
198 | // Custom Easing Patterns
199 | if (!!settings.customEasing) pattern = settings.customEasing(time);
200 |
201 | return pattern || time; // no easing, no acceleration
202 | };
203 |
204 | /**
205 | * Determine the document's height
206 | * @returns {Number}
207 | */
208 | var getDocumentHeight = function () {
209 | return Math.max(
210 | document.body.scrollHeight, document.documentElement.scrollHeight,
211 | document.body.offsetHeight, document.documentElement.offsetHeight,
212 | document.body.clientHeight, document.documentElement.clientHeight
213 | );
214 | };
215 |
216 | /**
217 | * Calculate how far to scroll
218 | * Clip support added by robjtede - https://github.com/cferdinandi/smooth-scroll/issues/405
219 | * @param {Element} anchor The anchor element to scroll to
220 | * @param {Number} headerHeight Height of a fixed header, if any
221 | * @param {Number} offset Number of pixels by which to offset scroll
222 | * @param {Boolean} clip If true, adjust scroll distance to prevent abrupt stops near the bottom of the page
223 | * @returns {Number}
224 | */
225 | var getEndLocation = function (anchor, headerHeight, offset, clip) {
226 | var location = 0;
227 | if (anchor.offsetParent) {
228 | do {
229 | location += anchor.offsetTop;
230 | anchor = anchor.offsetParent;
231 | } while (anchor);
232 | }
233 | location = Math.max(location - headerHeight - offset, 0);
234 | if (clip) {
235 | location = Math.min(location, getDocumentHeight() - window.innerHeight);
236 | }
237 | return location;
238 | };
239 |
240 | /**
241 | * Get the height of the fixed header
242 | * @param {Node} header The header
243 | * @return {Number} The height of the header
244 | */
245 | var getHeaderHeight = function (header) {
246 | return !header ? 0 : (getHeight(header) + header.offsetTop);
247 | };
248 |
249 | /**
250 | * Calculate the speed to use for the animation
251 | * @param {Number} distance The distance to travel
252 | * @param {Object} settings The plugin settings
253 | * @return {Number} How fast to animate
254 | */
255 | var getSpeed = function (distance, settings) {
256 | var speed = settings.speedAsDuration ? settings.speed : Math.abs(distance / 1000 * settings.speed);
257 | if (settings.durationMax && speed > settings.durationMax) return settings.durationMax;
258 | if (settings.durationMin && speed < settings.durationMin) return settings.durationMin;
259 | return parseInt(speed, 10);
260 | };
261 |
262 | var setHistory = function (options) {
263 |
264 | // Make sure this should run
265 | if (!history.replaceState || !options.updateURL || history.state) return;
266 |
267 | // Get the hash to use
268 | var hash = window.location.hash;
269 | hash = hash ? hash : '';
270 |
271 | // Set a default history
272 | history.replaceState(
273 | {
274 | smoothScroll: JSON.stringify(options),
275 | anchor: hash ? hash : window.pageYOffset
276 | },
277 | document.title,
278 | hash ? hash : window.location.href
279 | );
280 |
281 | };
282 |
283 | /**
284 | * Update the URL
285 | * @param {Node} anchor The anchor that was scrolled to
286 | * @param {Boolean} isNum If true, anchor is a number
287 | * @param {Object} options Settings for Smooth Scroll
288 | */
289 | var updateURL = function (anchor, isNum, options) {
290 |
291 | // Bail if the anchor is a number
292 | if (isNum) return;
293 |
294 | // Verify that pushState is supported and the updateURL option is enabled
295 | if (!history.pushState || !options.updateURL) return;
296 |
297 | // Update URL
298 | history.pushState(
299 | {
300 | smoothScroll: JSON.stringify(options),
301 | anchor: anchor.id
302 | },
303 | document.title,
304 | anchor === document.documentElement ? '#top' : '#' + anchor.id
305 | );
306 |
307 | };
308 |
309 | /**
310 | * Bring the anchored element into focus
311 | * @param {Node} anchor The anchor element
312 | * @param {Number} endLocation The end location to scroll to
313 | * @param {Boolean} isNum If true, scroll is to a position rather than an element
314 | */
315 | var adjustFocus = function (anchor, endLocation, isNum) {
316 |
317 | // Is scrolling to top of page, blur
318 | if (anchor === 0) {
319 | document.body.focus();
320 | }
321 |
322 | // Don't run if scrolling to a number on the page
323 | if (isNum) return;
324 |
325 | // Otherwise, bring anchor element into focus
326 | anchor.focus();
327 | if (document.activeElement !== anchor) {
328 | anchor.setAttribute('tabindex', '-1');
329 | anchor.focus();
330 | anchor.style.outline = 'none';
331 | }
332 | window.scrollTo(0 , endLocation);
333 |
334 | };
335 |
336 | /**
337 | * Emit a custom event
338 | * @param {String} type The event type
339 | * @param {Object} options The settings object
340 | * @param {Node} anchor The anchor element
341 | * @param {Node} toggle The toggle element
342 | */
343 | var emitEvent = function (type, options, anchor, toggle) {
344 | if (!options.emitEvents || typeof window.CustomEvent !== 'function') return;
345 | var event = new CustomEvent(type, {
346 | bubbles: true,
347 | detail: {
348 | anchor: anchor,
349 | toggle: toggle
350 | }
351 | });
352 | document.dispatchEvent(event);
353 | };
354 |
355 |
356 | //
357 | // SmoothScroll Constructor
358 | //
359 |
360 | var SmoothScroll = function (selector, options) {
361 |
362 | //
363 | // Variables
364 | //
365 |
366 | var smoothScroll = {}; // Object for public APIs
367 | var settings, toggle, fixedHeader, animationInterval;
368 |
369 |
370 | //
371 | // Methods
372 | //
373 |
374 | /**
375 | * Cancel a scroll-in-progress
376 | */
377 | smoothScroll.cancelScroll = function (noEvent) {
378 | cancelAnimationFrame(animationInterval);
379 | animationInterval = null;
380 | if (noEvent) return;
381 | emitEvent('scrollCancel', settings);
382 | };
383 |
384 | /**
385 | * Start/stop the scrolling animation
386 | * @param {Node|Number} anchor The element or position to scroll to
387 | * @param {Element} toggle The element that toggled the scroll event
388 | * @param {Object} options
389 | */
390 | smoothScroll.animateScroll = function (anchor, toggle, options) {
391 |
392 | // Cancel any in progress scrolls
393 | smoothScroll.cancelScroll();
394 |
395 | // Local settings
396 | var _settings = extend(settings || defaults, options || {}); // Merge user options with defaults
397 |
398 | // Selectors and variables
399 | var isNum = Object.prototype.toString.call(anchor) === '[object Number]' ? true : false;
400 | var anchorElem = isNum || !anchor.tagName ? null : anchor;
401 | if (!isNum && !anchorElem) return;
402 | var startLocation = window.pageYOffset; // Current location on the page
403 | if (_settings.header && !fixedHeader) {
404 | // Get the fixed header if not already set
405 | fixedHeader = document.querySelector(_settings.header);
406 | }
407 | var headerHeight = getHeaderHeight(fixedHeader);
408 | var endLocation = isNum ? anchor : getEndLocation(anchorElem, headerHeight, parseInt((typeof _settings.offset === 'function' ? _settings.offset(anchor, toggle) : _settings.offset), 10), _settings.clip); // Location to scroll to
409 | var distance = endLocation - startLocation; // distance to travel
410 | var documentHeight = getDocumentHeight();
411 | var timeLapsed = 0;
412 | var speed = getSpeed(distance, _settings);
413 | var start, percentage, position;
414 |
415 | /**
416 | * Stop the scroll animation when it reaches its target (or the bottom/top of page)
417 | * @param {Number} position Current position on the page
418 | * @param {Number} endLocation Scroll to location
419 | * @param {Number} animationInterval How much to scroll on this loop
420 | */
421 | var stopAnimateScroll = function (position, endLocation) {
422 |
423 | // Get the current location
424 | var currentLocation = window.pageYOffset;
425 |
426 | // Check if the end location has been reached yet (or we've hit the end of the document)
427 | if (position == endLocation || currentLocation == endLocation || ((startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight)) {
428 |
429 | // Clear the animation timer
430 | smoothScroll.cancelScroll(true);
431 |
432 | // Bring the anchored element into focus
433 | adjustFocus(anchor, endLocation, isNum);
434 |
435 | // Emit a custom event
436 | emitEvent('scrollStop', _settings, anchor, toggle);
437 |
438 | // Reset start
439 | start = null;
440 | animationInterval = null;
441 |
442 | return true;
443 |
444 | }
445 | };
446 |
447 | /**
448 | * Loop scrolling animation
449 | */
450 | var loopAnimateScroll = function (timestamp) {
451 | if (!start) { start = timestamp; }
452 | timeLapsed += timestamp - start;
453 | percentage = speed === 0 ? 0 : (timeLapsed / speed);
454 | percentage = (percentage > 1) ? 1 : percentage;
455 | position = startLocation + (distance * easingPattern(_settings, percentage));
456 | window.scrollTo(0, Math.floor(position));
457 | if (!stopAnimateScroll(position, endLocation)) {
458 | animationInterval = window.requestAnimationFrame(loopAnimateScroll);
459 | start = timestamp;
460 | }
461 | };
462 |
463 | /**
464 | * Reset position to fix weird iOS bug
465 | * @link https://github.com/cferdinandi/smooth-scroll/issues/45
466 | */
467 | if (window.pageYOffset === 0) {
468 | window.scrollTo(0, 0);
469 | }
470 |
471 | // Update the URL
472 | updateURL(anchor, isNum, _settings);
473 |
474 | // If the user prefers reduced motion, jump to location
475 | if (reduceMotion()) {
476 | adjustFocus(anchor, Math.floor(endLocation), false);
477 | return;
478 | }
479 |
480 | // Emit a custom event
481 | emitEvent('scrollStart', _settings, anchor, toggle);
482 |
483 | // Start scrolling animation
484 | smoothScroll.cancelScroll(true);
485 | window.requestAnimationFrame(loopAnimateScroll);
486 |
487 | };
488 |
489 | /**
490 | * If smooth scroll element clicked, animate scroll
491 | */
492 | var clickHandler = function (event) {
493 |
494 | // Don't run if event was canceled but still bubbled up
495 | // By @mgreter - https://github.com/cferdinandi/smooth-scroll/pull/462/
496 | if (event.defaultPrevented) return;
497 |
498 | // Don't run if right-click or command/control + click or shift + click
499 | if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
500 |
501 | // Check if event.target has closest() method
502 | // By @totegi - https://github.com/cferdinandi/smooth-scroll/pull/401/
503 | if (!('closest' in event.target)) return;
504 |
505 | // Check if a smooth scroll link was clicked
506 | toggle = event.target.closest(selector);
507 | if (!toggle || toggle.tagName.toLowerCase() !== 'a' || event.target.closest(settings.ignore)) return;
508 |
509 | // Only run if link is an anchor and points to the current page
510 | if (toggle.hostname !== window.location.hostname || toggle.pathname !== window.location.pathname || !/#/.test(toggle.href)) return;
511 |
512 | // Get an escaped version of the hash
513 | var hash;
514 | try {
515 | hash = escapeCharacters(decodeURIComponent(toggle.hash));
516 | } catch(e) {
517 | hash = escapeCharacters(toggle.hash);
518 | }
519 |
520 | // Get the anchored element
521 | var anchor;
522 | if (hash === '#') {
523 | if (!settings.topOnEmptyHash) return;
524 | anchor = document.documentElement;
525 | } else {
526 | anchor = document.querySelector(hash);
527 | }
528 | anchor = !anchor && hash === '#top' ? document.documentElement : anchor;
529 |
530 | // If anchored element exists, scroll to it
531 | if (!anchor) return;
532 | event.preventDefault();
533 | setHistory(settings);
534 | smoothScroll.animateScroll(anchor, toggle);
535 |
536 | };
537 |
538 | /**
539 | * Animate scroll on popstate events
540 | */
541 | var popstateHandler = function () {
542 |
543 | // Stop if history.state doesn't exist (ex. if clicking on a broken anchor link).
544 | // fixes `Cannot read property 'smoothScroll' of null` error getting thrown.
545 | if (history.state === null) return;
546 |
547 | // Only run if state is a popstate record for this instantiation
548 | if (!history.state.smoothScroll || history.state.smoothScroll !== JSON.stringify(settings)) return;
549 |
550 | // Get the anchor
551 | var anchor = history.state.anchor;
552 | if (typeof anchor === 'string' && anchor) {
553 | anchor = document.querySelector(escapeCharacters(history.state.anchor));
554 | if (!anchor) return;
555 | }
556 |
557 | // Animate scroll to anchor link
558 | smoothScroll.animateScroll(anchor, null, {updateURL: false});
559 |
560 | };
561 |
562 | /**
563 | * Destroy the current initialization.
564 | */
565 | smoothScroll.destroy = function () {
566 |
567 | // If plugin isn't already initialized, stop
568 | if (!settings) return;
569 |
570 | // Remove event listeners
571 | document.removeEventListener('click', clickHandler, false);
572 | window.removeEventListener('popstate', popstateHandler, false);
573 |
574 | // Cancel any scrolls-in-progress
575 | smoothScroll.cancelScroll();
576 |
577 | // Reset variables
578 | settings = null;
579 | toggle = null;
580 | fixedHeader = null;
581 | animationInterval = null;
582 |
583 | };
584 |
585 | /**
586 | * Initialize Smooth Scroll
587 | * @param {Object} options User settings
588 | */
589 | var init = function () {
590 |
591 | // feature test
592 | if (!supports()) throw 'Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.';
593 |
594 | // Destroy any existing initializations
595 | smoothScroll.destroy();
596 |
597 | // Selectors and variables
598 | settings = extend(defaults, options || {}); // Merge user options with defaults
599 | fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header
600 |
601 | // When a toggle is clicked, run the click handler
602 | document.addEventListener('click', clickHandler, false);
603 |
604 | // If updateURL and popState are enabled, listen for pop events
605 | if (settings.updateURL && settings.popstate) {
606 | window.addEventListener('popstate', popstateHandler, false);
607 | }
608 |
609 | };
610 |
611 |
612 | //
613 | // Initialize plugin
614 | //
615 |
616 | init();
617 |
618 |
619 | //
620 | // Public APIs
621 | //
622 |
623 | return smoothScroll;
624 |
625 | };
626 |
627 | return SmoothScroll;
628 |
629 | })));
630 |
--------------------------------------------------------------------------------
/dist/smooth-scroll.polyfills.js:
--------------------------------------------------------------------------------
1 | /*! SmoothScroll v16.1.4 | (c) 2020 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */
2 | (function (global, factory) {
3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4 | typeof define === 'function' && define.amd ? define(factory) :
5 | (global = global || self, global.SmoothScroll = factory());
6 | }(this, (function () { 'use strict';
7 |
8 | /**
9 | * closest() polyfill
10 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
11 | */
12 | if (window.Element && !Element.prototype.closest) {
13 | Element.prototype.closest = function(s) {
14 | var matches = (this.document || this.ownerDocument).querySelectorAll(s),
15 | i,
16 | el = this;
17 | do {
18 | i = matches.length;
19 | while (--i >= 0 && matches.item(i) !== el) {}
20 | } while ((i < 0) && (el = el.parentElement));
21 | return el;
22 | };
23 | }
24 |
25 | /**
26 | * CustomEvent() polyfill
27 | * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
28 | */
29 | (function () {
30 |
31 | if (typeof window.CustomEvent === "function") return false;
32 |
33 | function CustomEvent(event, params) {
34 | params = params || { bubbles: false, cancelable: false, detail: undefined };
35 | var evt = document.createEvent('CustomEvent');
36 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
37 | return evt;
38 | }
39 |
40 | CustomEvent.prototype = window.Event.prototype;
41 |
42 | window.CustomEvent = CustomEvent;
43 | })();
44 |
45 | /**
46 | * requestAnimationFrame() polyfill
47 | * By Erik Möller. Fixes from Paul Irish and Tino Zijdel.
48 | * @link http://paulirish.com/2011/requestanimationframe-for-smart-animating/
49 | * @link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
50 | * @license MIT
51 | */
52 | (function() {
53 | var lastTime = 0;
54 | var vendors = ['ms', 'moz', 'webkit', 'o'];
55 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
56 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
57 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] ||
58 | window[vendors[x]+'CancelRequestAnimationFrame'];
59 | }
60 |
61 | if (!window.requestAnimationFrame) {
62 | window.requestAnimationFrame = function(callback, element) {
63 | var currTime = new Date().getTime();
64 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
65 | var id = window.setTimeout(function() { callback(currTime + timeToCall); },
66 | timeToCall);
67 | lastTime = currTime + timeToCall;
68 | return id;
69 | };
70 | }
71 |
72 | if (!window.cancelAnimationFrame) {
73 | window.cancelAnimationFrame = function(id) {
74 | clearTimeout(id);
75 | };
76 | }
77 | }());
78 |
79 | //
80 | // Default settings
81 | //
82 |
83 | var defaults = {
84 |
85 | // Selectors
86 | ignore: '[data-scroll-ignore]',
87 | header: null,
88 | topOnEmptyHash: true,
89 |
90 | // Speed & Duration
91 | speed: 500,
92 | speedAsDuration: false,
93 | durationMax: null,
94 | durationMin: null,
95 | clip: true,
96 | offset: 0,
97 |
98 | // Easing
99 | easing: 'easeInOutCubic',
100 | customEasing: null,
101 |
102 | // History
103 | updateURL: true,
104 | popstate: true,
105 |
106 | // Custom Events
107 | emitEvents: true
108 |
109 | };
110 |
111 |
112 | //
113 | // Utility Methods
114 | //
115 |
116 | /**
117 | * Check if browser supports required methods
118 | * @return {Boolean} Returns true if all required methods are supported
119 | */
120 | var supports = function () {
121 | return (
122 | 'querySelector' in document &&
123 | 'addEventListener' in window &&
124 | 'requestAnimationFrame' in window &&
125 | 'closest' in window.Element.prototype
126 | );
127 | };
128 |
129 | /**
130 | * Merge two or more objects together.
131 | * @param {Object} objects The objects to merge together
132 | * @returns {Object} Merged values of defaults and options
133 | */
134 | var extend = function () {
135 | var merged = {};
136 | Array.prototype.forEach.call(arguments, function (obj) {
137 | for (var key in obj) {
138 | if (!obj.hasOwnProperty(key)) return;
139 | merged[key] = obj[key];
140 | }
141 | });
142 | return merged;
143 | };
144 |
145 | /**
146 | * Check to see if user prefers reduced motion
147 | * @param {Object} settings Script settings
148 | */
149 | var reduceMotion = function () {
150 | if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) {
151 | return true;
152 | }
153 | return false;
154 | };
155 |
156 | /**
157 | * Get the height of an element.
158 | * @param {Node} elem The element to get the height of
159 | * @return {Number} The element's height in pixels
160 | */
161 | var getHeight = function (elem) {
162 | return parseInt(window.getComputedStyle(elem).height, 10);
163 | };
164 |
165 | /**
166 | * Escape special characters for use with querySelector
167 | * @author Mathias Bynens
168 | * @link https://github.com/mathiasbynens/CSS.escape
169 | * @param {String} id The anchor ID to escape
170 | */
171 | var escapeCharacters = function (id) {
172 |
173 | // Remove leading hash
174 | if (id.charAt(0) === '#') {
175 | id = id.substr(1);
176 | }
177 |
178 | var string = String(id);
179 | var length = string.length;
180 | var index = -1;
181 | var codeUnit;
182 | var result = '';
183 | var firstCodeUnit = string.charCodeAt(0);
184 | while (++index < length) {
185 | codeUnit = string.charCodeAt(index);
186 | // Note: there’s no need to special-case astral symbols, surrogate
187 | // pairs, or lone surrogates.
188 |
189 | // If the character is NULL (U+0000), then throw an
190 | // `InvalidCharacterError` exception and terminate these steps.
191 | if (codeUnit === 0x0000) {
192 | throw new InvalidCharacterError(
193 | 'Invalid character: the input contains U+0000.'
194 | );
195 | }
196 |
197 | if (
198 | // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
199 | // U+007F, […]
200 | (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
201 | // If the character is the first character and is in the range [0-9]
202 | // (U+0030 to U+0039), […]
203 | (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
204 | // If the character is the second character and is in the range [0-9]
205 | // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
206 | (
207 | index === 1 &&
208 | codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
209 | firstCodeUnit === 0x002D
210 | )
211 | ) {
212 | // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
213 | result += '\\' + codeUnit.toString(16) + ' ';
214 | continue;
215 | }
216 |
217 | // If the character is not handled by one of the above rules and is
218 | // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
219 | // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
220 | // U+005A), or [a-z] (U+0061 to U+007A), […]
221 | if (
222 | codeUnit >= 0x0080 ||
223 | codeUnit === 0x002D ||
224 | codeUnit === 0x005F ||
225 | codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
226 | codeUnit >= 0x0041 && codeUnit <= 0x005A ||
227 | codeUnit >= 0x0061 && codeUnit <= 0x007A
228 | ) {
229 | // the character itself
230 | result += string.charAt(index);
231 | continue;
232 | }
233 |
234 | // Otherwise, the escaped character.
235 | // http://dev.w3.org/csswg/cssom/#escape-a-character
236 | result += '\\' + string.charAt(index);
237 |
238 | }
239 |
240 | // Return sanitized hash
241 | return '#' + result;
242 |
243 | };
244 |
245 | /**
246 | * Calculate the easing pattern
247 | * @link https://gist.github.com/gre/1650294
248 | * @param {Object} settings Easing pattern
249 | * @param {Number} time Time animation should take to complete
250 | * @returns {Number}
251 | */
252 | var easingPattern = function (settings, time) {
253 | var pattern;
254 |
255 | // Default Easing Patterns
256 | if (settings.easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity
257 | if (settings.easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity
258 | if (settings.easing === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
259 | if (settings.easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity
260 | if (settings.easing === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity
261 | if (settings.easing === 'easeInOutCubic') pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
262 | if (settings.easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity
263 | if (settings.easing === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
264 | if (settings.easing === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
265 | if (settings.easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity
266 | if (settings.easing === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
267 | if (settings.easing === 'easeInOutQuint') pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
268 |
269 | // Custom Easing Patterns
270 | if (!!settings.customEasing) pattern = settings.customEasing(time);
271 |
272 | return pattern || time; // no easing, no acceleration
273 | };
274 |
275 | /**
276 | * Determine the document's height
277 | * @returns {Number}
278 | */
279 | var getDocumentHeight = function () {
280 | return Math.max(
281 | document.body.scrollHeight, document.documentElement.scrollHeight,
282 | document.body.offsetHeight, document.documentElement.offsetHeight,
283 | document.body.clientHeight, document.documentElement.clientHeight
284 | );
285 | };
286 |
287 | /**
288 | * Calculate how far to scroll
289 | * Clip support added by robjtede - https://github.com/cferdinandi/smooth-scroll/issues/405
290 | * @param {Element} anchor The anchor element to scroll to
291 | * @param {Number} headerHeight Height of a fixed header, if any
292 | * @param {Number} offset Number of pixels by which to offset scroll
293 | * @param {Boolean} clip If true, adjust scroll distance to prevent abrupt stops near the bottom of the page
294 | * @returns {Number}
295 | */
296 | var getEndLocation = function (anchor, headerHeight, offset, clip) {
297 | var location = 0;
298 | if (anchor.offsetParent) {
299 | do {
300 | location += anchor.offsetTop;
301 | anchor = anchor.offsetParent;
302 | } while (anchor);
303 | }
304 | location = Math.max(location - headerHeight - offset, 0);
305 | if (clip) {
306 | location = Math.min(location, getDocumentHeight() - window.innerHeight);
307 | }
308 | return location;
309 | };
310 |
311 | /**
312 | * Get the height of the fixed header
313 | * @param {Node} header The header
314 | * @return {Number} The height of the header
315 | */
316 | var getHeaderHeight = function (header) {
317 | return !header ? 0 : (getHeight(header) + header.offsetTop);
318 | };
319 |
320 | /**
321 | * Calculate the speed to use for the animation
322 | * @param {Number} distance The distance to travel
323 | * @param {Object} settings The plugin settings
324 | * @return {Number} How fast to animate
325 | */
326 | var getSpeed = function (distance, settings) {
327 | var speed = settings.speedAsDuration ? settings.speed : Math.abs(distance / 1000 * settings.speed);
328 | if (settings.durationMax && speed > settings.durationMax) return settings.durationMax;
329 | if (settings.durationMin && speed < settings.durationMin) return settings.durationMin;
330 | return parseInt(speed, 10);
331 | };
332 |
333 | var setHistory = function (options) {
334 |
335 | // Make sure this should run
336 | if (!history.replaceState || !options.updateURL || history.state) return;
337 |
338 | // Get the hash to use
339 | var hash = window.location.hash;
340 | hash = hash ? hash : '';
341 |
342 | // Set a default history
343 | history.replaceState(
344 | {
345 | smoothScroll: JSON.stringify(options),
346 | anchor: hash ? hash : window.pageYOffset
347 | },
348 | document.title,
349 | hash ? hash : window.location.href
350 | );
351 |
352 | };
353 |
354 | /**
355 | * Update the URL
356 | * @param {Node} anchor The anchor that was scrolled to
357 | * @param {Boolean} isNum If true, anchor is a number
358 | * @param {Object} options Settings for Smooth Scroll
359 | */
360 | var updateURL = function (anchor, isNum, options) {
361 |
362 | // Bail if the anchor is a number
363 | if (isNum) return;
364 |
365 | // Verify that pushState is supported and the updateURL option is enabled
366 | if (!history.pushState || !options.updateURL) return;
367 |
368 | // Update URL
369 | history.pushState(
370 | {
371 | smoothScroll: JSON.stringify(options),
372 | anchor: anchor.id
373 | },
374 | document.title,
375 | anchor === document.documentElement ? '#top' : '#' + anchor.id
376 | );
377 |
378 | };
379 |
380 | /**
381 | * Bring the anchored element into focus
382 | * @param {Node} anchor The anchor element
383 | * @param {Number} endLocation The end location to scroll to
384 | * @param {Boolean} isNum If true, scroll is to a position rather than an element
385 | */
386 | var adjustFocus = function (anchor, endLocation, isNum) {
387 |
388 | // Is scrolling to top of page, blur
389 | if (anchor === 0) {
390 | document.body.focus();
391 | }
392 |
393 | // Don't run if scrolling to a number on the page
394 | if (isNum) return;
395 |
396 | // Otherwise, bring anchor element into focus
397 | anchor.focus();
398 | if (document.activeElement !== anchor) {
399 | anchor.setAttribute('tabindex', '-1');
400 | anchor.focus();
401 | anchor.style.outline = 'none';
402 | }
403 | window.scrollTo(0 , endLocation);
404 |
405 | };
406 |
407 | /**
408 | * Emit a custom event
409 | * @param {String} type The event type
410 | * @param {Object} options The settings object
411 | * @param {Node} anchor The anchor element
412 | * @param {Node} toggle The toggle element
413 | */
414 | var emitEvent = function (type, options, anchor, toggle) {
415 | if (!options.emitEvents || typeof window.CustomEvent !== 'function') return;
416 | var event = new CustomEvent(type, {
417 | bubbles: true,
418 | detail: {
419 | anchor: anchor,
420 | toggle: toggle
421 | }
422 | });
423 | document.dispatchEvent(event);
424 | };
425 |
426 |
427 | //
428 | // SmoothScroll Constructor
429 | //
430 |
431 | var SmoothScroll = function (selector, options) {
432 |
433 | //
434 | // Variables
435 | //
436 |
437 | var smoothScroll = {}; // Object for public APIs
438 | var settings, toggle, fixedHeader, animationInterval;
439 |
440 |
441 | //
442 | // Methods
443 | //
444 |
445 | /**
446 | * Cancel a scroll-in-progress
447 | */
448 | smoothScroll.cancelScroll = function (noEvent) {
449 | cancelAnimationFrame(animationInterval);
450 | animationInterval = null;
451 | if (noEvent) return;
452 | emitEvent('scrollCancel', settings);
453 | };
454 |
455 | /**
456 | * Start/stop the scrolling animation
457 | * @param {Node|Number} anchor The element or position to scroll to
458 | * @param {Element} toggle The element that toggled the scroll event
459 | * @param {Object} options
460 | */
461 | smoothScroll.animateScroll = function (anchor, toggle, options) {
462 |
463 | // Cancel any in progress scrolls
464 | smoothScroll.cancelScroll();
465 |
466 | // Local settings
467 | var _settings = extend(settings || defaults, options || {}); // Merge user options with defaults
468 |
469 | // Selectors and variables
470 | var isNum = Object.prototype.toString.call(anchor) === '[object Number]' ? true : false;
471 | var anchorElem = isNum || !anchor.tagName ? null : anchor;
472 | if (!isNum && !anchorElem) return;
473 | var startLocation = window.pageYOffset; // Current location on the page
474 | if (_settings.header && !fixedHeader) {
475 | // Get the fixed header if not already set
476 | fixedHeader = document.querySelector(_settings.header);
477 | }
478 | var headerHeight = getHeaderHeight(fixedHeader);
479 | var endLocation = isNum ? anchor : getEndLocation(anchorElem, headerHeight, parseInt((typeof _settings.offset === 'function' ? _settings.offset(anchor, toggle) : _settings.offset), 10), _settings.clip); // Location to scroll to
480 | var distance = endLocation - startLocation; // distance to travel
481 | var documentHeight = getDocumentHeight();
482 | var timeLapsed = 0;
483 | var speed = getSpeed(distance, _settings);
484 | var start, percentage, position;
485 |
486 | /**
487 | * Stop the scroll animation when it reaches its target (or the bottom/top of page)
488 | * @param {Number} position Current position on the page
489 | * @param {Number} endLocation Scroll to location
490 | * @param {Number} animationInterval How much to scroll on this loop
491 | */
492 | var stopAnimateScroll = function (position, endLocation) {
493 |
494 | // Get the current location
495 | var currentLocation = window.pageYOffset;
496 |
497 | // Check if the end location has been reached yet (or we've hit the end of the document)
498 | if (position == endLocation || currentLocation == endLocation || ((startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight)) {
499 |
500 | // Clear the animation timer
501 | smoothScroll.cancelScroll(true);
502 |
503 | // Bring the anchored element into focus
504 | adjustFocus(anchor, endLocation, isNum);
505 |
506 | // Emit a custom event
507 | emitEvent('scrollStop', _settings, anchor, toggle);
508 |
509 | // Reset start
510 | start = null;
511 | animationInterval = null;
512 |
513 | return true;
514 |
515 | }
516 | };
517 |
518 | /**
519 | * Loop scrolling animation
520 | */
521 | var loopAnimateScroll = function (timestamp) {
522 | if (!start) { start = timestamp; }
523 | timeLapsed += timestamp - start;
524 | percentage = speed === 0 ? 0 : (timeLapsed / speed);
525 | percentage = (percentage > 1) ? 1 : percentage;
526 | position = startLocation + (distance * easingPattern(_settings, percentage));
527 | window.scrollTo(0, Math.floor(position));
528 | if (!stopAnimateScroll(position, endLocation)) {
529 | animationInterval = window.requestAnimationFrame(loopAnimateScroll);
530 | start = timestamp;
531 | }
532 | };
533 |
534 | /**
535 | * Reset position to fix weird iOS bug
536 | * @link https://github.com/cferdinandi/smooth-scroll/issues/45
537 | */
538 | if (window.pageYOffset === 0) {
539 | window.scrollTo(0, 0);
540 | }
541 |
542 | // Update the URL
543 | updateURL(anchor, isNum, _settings);
544 |
545 | // If the user prefers reduced motion, jump to location
546 | if (reduceMotion()) {
547 | adjustFocus(anchor, Math.floor(endLocation), false);
548 | return;
549 | }
550 |
551 | // Emit a custom event
552 | emitEvent('scrollStart', _settings, anchor, toggle);
553 |
554 | // Start scrolling animation
555 | smoothScroll.cancelScroll(true);
556 | window.requestAnimationFrame(loopAnimateScroll);
557 |
558 | };
559 |
560 | /**
561 | * If smooth scroll element clicked, animate scroll
562 | */
563 | var clickHandler = function (event) {
564 |
565 | // Don't run if event was canceled but still bubbled up
566 | // By @mgreter - https://github.com/cferdinandi/smooth-scroll/pull/462/
567 | if (event.defaultPrevented) return;
568 |
569 | // Don't run if right-click or command/control + click or shift + click
570 | if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
571 |
572 | // Check if event.target has closest() method
573 | // By @totegi - https://github.com/cferdinandi/smooth-scroll/pull/401/
574 | if (!('closest' in event.target)) return;
575 |
576 | // Check if a smooth scroll link was clicked
577 | toggle = event.target.closest(selector);
578 | if (!toggle || toggle.tagName.toLowerCase() !== 'a' || event.target.closest(settings.ignore)) return;
579 |
580 | // Only run if link is an anchor and points to the current page
581 | if (toggle.hostname !== window.location.hostname || toggle.pathname !== window.location.pathname || !/#/.test(toggle.href)) return;
582 |
583 | // Get an escaped version of the hash
584 | var hash;
585 | try {
586 | hash = escapeCharacters(decodeURIComponent(toggle.hash));
587 | } catch(e) {
588 | hash = escapeCharacters(toggle.hash);
589 | }
590 |
591 | // Get the anchored element
592 | var anchor;
593 | if (hash === '#') {
594 | if (!settings.topOnEmptyHash) return;
595 | anchor = document.documentElement;
596 | } else {
597 | anchor = document.querySelector(hash);
598 | }
599 | anchor = !anchor && hash === '#top' ? document.documentElement : anchor;
600 |
601 | // If anchored element exists, scroll to it
602 | if (!anchor) return;
603 | event.preventDefault();
604 | setHistory(settings);
605 | smoothScroll.animateScroll(anchor, toggle);
606 |
607 | };
608 |
609 | /**
610 | * Animate scroll on popstate events
611 | */
612 | var popstateHandler = function () {
613 |
614 | // Stop if history.state doesn't exist (ex. if clicking on a broken anchor link).
615 | // fixes `Cannot read property 'smoothScroll' of null` error getting thrown.
616 | if (history.state === null) return;
617 |
618 | // Only run if state is a popstate record for this instantiation
619 | if (!history.state.smoothScroll || history.state.smoothScroll !== JSON.stringify(settings)) return;
620 |
621 | // Get the anchor
622 | var anchor = history.state.anchor;
623 | if (typeof anchor === 'string' && anchor) {
624 | anchor = document.querySelector(escapeCharacters(history.state.anchor));
625 | if (!anchor) return;
626 | }
627 |
628 | // Animate scroll to anchor link
629 | smoothScroll.animateScroll(anchor, null, {updateURL: false});
630 |
631 | };
632 |
633 | /**
634 | * Destroy the current initialization.
635 | */
636 | smoothScroll.destroy = function () {
637 |
638 | // If plugin isn't already initialized, stop
639 | if (!settings) return;
640 |
641 | // Remove event listeners
642 | document.removeEventListener('click', clickHandler, false);
643 | window.removeEventListener('popstate', popstateHandler, false);
644 |
645 | // Cancel any scrolls-in-progress
646 | smoothScroll.cancelScroll();
647 |
648 | // Reset variables
649 | settings = null;
650 | toggle = null;
651 | fixedHeader = null;
652 | animationInterval = null;
653 |
654 | };
655 |
656 | /**
657 | * Initialize Smooth Scroll
658 | * @param {Object} options User settings
659 | */
660 | var init = function () {
661 |
662 | // feature test
663 | if (!supports()) throw 'Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.';
664 |
665 | // Destroy any existing initializations
666 | smoothScroll.destroy();
667 |
668 | // Selectors and variables
669 | settings = extend(defaults, options || {}); // Merge user options with defaults
670 | fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header
671 |
672 | // When a toggle is clicked, run the click handler
673 | document.addEventListener('click', clickHandler, false);
674 |
675 | // If updateURL and popState are enabled, listen for pop events
676 | if (settings.updateURL && settings.popstate) {
677 | window.addEventListener('popstate', popstateHandler, false);
678 | }
679 |
680 | };
681 |
682 |
683 | //
684 | // Initialize plugin
685 | //
686 |
687 | init();
688 |
689 |
690 | //
691 | // Public APIs
692 | //
693 |
694 | return smoothScroll;
695 |
696 | };
697 |
698 | return SmoothScroll;
699 |
700 | })));
701 |
--------------------------------------------------------------------------------