├── 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 [![Build Status](https://travis-ci.org/cferdinandi/smooth-scroll.svg)](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 | --------------------------------------------------------------------------------