You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

715 lines
21 KiB

5 years ago
  1. /*!
  2. * smooth-scroll v16.1.0
  3. * Animate scrolling to anchor links
  4. * (c) 2019 Chris Ferdinandi
  5. * MIT License
  6. * http://github.com/cferdinandi/smooth-scroll
  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. * CustomEvent() polyfill
  26. * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
  27. */
  28. (function () {
  29. if (typeof window.CustomEvent === "function") return false;
  30. function CustomEvent(event, params) {
  31. params = params || { bubbles: false, cancelable: false, detail: undefined };
  32. var evt = document.createEvent('CustomEvent');
  33. evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
  34. return evt;
  35. }
  36. CustomEvent.prototype = window.Event.prototype;
  37. window.CustomEvent = CustomEvent;
  38. })();
  39. /**
  40. * requestAnimationFrame() polyfill
  41. * By Erik Möller. Fixes from Paul Irish and Tino Zijdel.
  42. * @link http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  43. * @link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
  44. * @license MIT
  45. */
  46. (function() {
  47. var lastTime = 0;
  48. var vendors = ['ms', 'moz', 'webkit', 'o'];
  49. for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
  50. window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
  51. window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] ||
  52. window[vendors[x]+'CancelRequestAnimationFrame'];
  53. }
  54. if (!window.requestAnimationFrame) {
  55. window.requestAnimationFrame = function(callback, element) {
  56. var currTime = new Date().getTime();
  57. var timeToCall = Math.max(0, 16 - (currTime - lastTime));
  58. var id = window.setTimeout((function() { callback(currTime + timeToCall); }),
  59. timeToCall);
  60. lastTime = currTime + timeToCall;
  61. return id;
  62. };
  63. }
  64. if (!window.cancelAnimationFrame) {
  65. window.cancelAnimationFrame = function(id) {
  66. clearTimeout(id);
  67. };
  68. }
  69. }());
  70. (function (root, factory) {
  71. if (typeof define === 'function' && define.amd) {
  72. define([], (function () {
  73. return factory(root);
  74. }));
  75. } else if (typeof exports === 'object') {
  76. module.exports = factory(root);
  77. } else {
  78. root.SmoothScroll = factory(root);
  79. }
  80. })(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this, (function (window) {
  81. 'use strict';
  82. //
  83. // Default settings
  84. //
  85. var defaults = {
  86. // Selectors
  87. ignore: '[data-scroll-ignore]',
  88. header: null,
  89. topOnEmptyHash: true,
  90. // Speed & Duration
  91. speed: 500,
  92. speedAsDuration: false,
  93. durationMax: null,
  94. durationMin: null,
  95. clip: true,
  96. offset: 0,
  97. // Easing
  98. easing: 'easeInOutCubic',
  99. customEasing: null,
  100. // History
  101. updateURL: true,
  102. popstate: true,
  103. // Custom Events
  104. emitEvents: true
  105. };
  106. //
  107. // Utility Methods
  108. //
  109. /**
  110. * Check if browser supports required methods
  111. * @return {Boolean} Returns true if all required methods are supported
  112. */
  113. var supports = function () {
  114. return (
  115. 'querySelector' in document &&
  116. 'addEventListener' in window &&
  117. 'requestAnimationFrame' in window &&
  118. 'closest' in window.Element.prototype
  119. );
  120. };
  121. /**
  122. * Merge two or more objects together.
  123. * @param {Object} objects The objects to merge together
  124. * @returns {Object} Merged values of defaults and options
  125. */
  126. var extend = function () {
  127. var merged = {};
  128. Array.prototype.forEach.call(arguments, (function (obj) {
  129. for (var key in obj) {
  130. if (!obj.hasOwnProperty(key)) return;
  131. merged[key] = obj[key];
  132. }
  133. }));
  134. return merged;
  135. };
  136. /**
  137. * Check to see if user prefers reduced motion
  138. * @param {Object} settings Script settings
  139. */
  140. var reduceMotion = function () {
  141. if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) {
  142. return true;
  143. }
  144. return false;
  145. };
  146. /**
  147. * Get the height of an element.
  148. * @param {Node} elem The element to get the height of
  149. * @return {Number} The element's height in pixels
  150. */
  151. var getHeight = function (elem) {
  152. return parseInt(window.getComputedStyle(elem).height, 10);
  153. };
  154. /**
  155. * Escape special characters for use with querySelector
  156. * @author Mathias Bynens
  157. * @link https://github.com/mathiasbynens/CSS.escape
  158. * @param {String} id The anchor ID to escape
  159. */
  160. var escapeCharacters = function (id) {
  161. // Remove leading hash
  162. if (id.charAt(0) === '#') {
  163. id = id.substr(1);
  164. }
  165. var string = String(id);
  166. var length = string.length;
  167. var index = -1;
  168. var codeUnit;
  169. var result = '';
  170. var firstCodeUnit = string.charCodeAt(0);
  171. while (++index < length) {
  172. codeUnit = string.charCodeAt(index);
  173. // Note: there’s no need to special-case astral symbols, surrogate
  174. // pairs, or lone surrogates.
  175. // If the character is NULL (U+0000), then throw an
  176. // `InvalidCharacterError` exception and terminate these steps.
  177. if (codeUnit === 0x0000) {
  178. throw new InvalidCharacterError(
  179. 'Invalid character: the input contains U+0000.'
  180. );
  181. }
  182. if (
  183. // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
  184. // U+007F, […]
  185. (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
  186. // If the character is the first character and is in the range [0-9]
  187. // (U+0030 to U+0039), […]
  188. (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
  189. // If the character is the second character and is in the range [0-9]
  190. // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
  191. (
  192. index === 1 &&
  193. codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
  194. firstCodeUnit === 0x002D
  195. )
  196. ) {
  197. // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point
  198. result += '\\' + codeUnit.toString(16) + ' ';
  199. continue;
  200. }
  201. // If the character is not handled by one of the above rules and is
  202. // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
  203. // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
  204. // U+005A), or [a-z] (U+0061 to U+007A), […]
  205. if (
  206. codeUnit >= 0x0080 ||
  207. codeUnit === 0x002D ||
  208. codeUnit === 0x005F ||
  209. codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
  210. codeUnit >= 0x0041 && codeUnit <= 0x005A ||
  211. codeUnit >= 0x0061 && codeUnit <= 0x007A
  212. ) {
  213. // the character itself
  214. result += string.charAt(index);
  215. continue;
  216. }
  217. // Otherwise, the escaped character.
  218. // http://dev.w3.org/csswg/cssom/#escape-a-character
  219. result += '\\' + string.charAt(index);
  220. }
  221. // Return sanitized hash
  222. return '#' + result;
  223. };
  224. /**
  225. * Calculate the easing pattern
  226. * @link https://gist.github.com/gre/1650294
  227. * @param {String} type Easing pattern
  228. * @param {Number} time Time animation should take to complete
  229. * @returns {Number}
  230. */
  231. var easingPattern = function (settings, time) {
  232. var pattern;
  233. // Default Easing Patterns
  234. if (settings.easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity
  235. if (settings.easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity
  236. if (settings.easing === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
  237. if (settings.easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity
  238. if (settings.easing === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity
  239. 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
  240. if (settings.easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity
  241. if (settings.easing === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
  242. if (settings.easing === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
  243. if (settings.easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity
  244. if (settings.easing === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
  245. 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
  246. // Custom Easing Patterns
  247. if (!!settings.customEasing) pattern = settings.customEasing(time);
  248. return pattern || time; // no easing, no acceleration
  249. };
  250. /**
  251. * Determine the document's height
  252. * @returns {Number}
  253. */
  254. var getDocumentHeight = function () {
  255. return Math.max(
  256. document.body.scrollHeight, document.documentElement.scrollHeight,
  257. document.body.offsetHeight, document.documentElement.offsetHeight,
  258. document.body.clientHeight, document.documentElement.clientHeight
  259. );
  260. };
  261. /**
  262. * Calculate how far to scroll
  263. * Clip support added by robjtede - https://github.com/cferdinandi/smooth-scroll/issues/405
  264. * @param {Element} anchor The anchor element to scroll to
  265. * @param {Number} headerHeight Height of a fixed header, if any
  266. * @param {Number} offset Number of pixels by which to offset scroll
  267. * @param {Boolean} clip If true, adjust scroll distance to prevent abrupt stops near the bottom of the page
  268. * @returns {Number}
  269. */
  270. var getEndLocation = function (anchor, headerHeight, offset, clip) {
  271. var location = 0;
  272. if (anchor.offsetParent) {
  273. do {
  274. location += anchor.offsetTop;
  275. anchor = anchor.offsetParent;
  276. } while (anchor);
  277. }
  278. location = Math.max(location - headerHeight - offset, 0);
  279. if (clip) {
  280. location = Math.min(location, getDocumentHeight() - window.innerHeight);
  281. }
  282. return location;
  283. };
  284. /**
  285. * Get the height of the fixed header
  286. * @param {Node} header The header
  287. * @return {Number} The height of the header
  288. */
  289. var getHeaderHeight = function (header) {
  290. return !header ? 0 : (getHeight(header) + header.offsetTop);
  291. };
  292. /**
  293. * Calculate the speed to use for the animation
  294. * @param {Number} distance The distance to travel
  295. * @param {Object} settings The plugin settings
  296. * @return {Number} How fast to animate
  297. */
  298. var getSpeed = function (distance, settings) {
  299. var speed = settings.speedAsDuration ? settings.speed : Math.abs(distance / 1000 * settings.speed);
  300. if (settings.durationMax && speed > settings.durationMax) return settings.durationMax;
  301. if (settings.durationMin && speed < settings.durationMin) return settings.durationMin;
  302. return parseInt(speed, 10);
  303. };
  304. var setHistory = function (options) {
  305. // Make sure this should run
  306. if (!history.replaceState || !options.updateURL || history.state) return;
  307. // Get the hash to use
  308. var hash = window.location.hash;
  309. hash = hash ? hash : '';
  310. // Set a default history
  311. history.replaceState(
  312. {
  313. smoothScroll: JSON.stringify(options),
  314. anchor: hash ? hash : window.pageYOffset
  315. },
  316. document.title,
  317. hash ? hash : window.location.href
  318. );
  319. };
  320. /**
  321. * Update the URL
  322. * @param {Node} anchor The anchor that was scrolled to
  323. * @param {Boolean} isNum If true, anchor is a number
  324. * @param {Object} options Settings for Smooth Scroll
  325. */
  326. var updateURL = function (anchor, isNum, options) {
  327. // Bail if the anchor is a number
  328. if (isNum) return;
  329. // Verify that pushState is supported and the updateURL option is enabled
  330. if (!history.pushState || !options.updateURL) return;
  331. // Update URL
  332. history.pushState(
  333. {
  334. smoothScroll: JSON.stringify(options),
  335. anchor: anchor.id
  336. },
  337. document.title,
  338. anchor === document.documentElement ? '#top' : '#' + anchor.id
  339. );
  340. };
  341. /**
  342. * Bring the anchored element into focus
  343. * @param {Node} anchor The anchor element
  344. * @param {Number} endLocation The end location to scroll to
  345. * @param {Boolean} isNum If true, scroll is to a position rather than an element
  346. */
  347. var adjustFocus = function (anchor, endLocation, isNum) {
  348. // Is scrolling to top of page, blur
  349. if (anchor === 0) {
  350. document.body.focus();
  351. }
  352. // Don't run if scrolling to a number on the page
  353. if (isNum) return;
  354. // Otherwise, bring anchor element into focus
  355. anchor.focus();
  356. if (document.activeElement !== anchor) {
  357. anchor.setAttribute('tabindex', '-1');
  358. anchor.focus();
  359. anchor.style.outline = 'none';
  360. }
  361. window.scrollTo(0 , endLocation);
  362. };
  363. /**
  364. * Emit a custom event
  365. * @param {String} type The event type
  366. * @param {Object} options The settings object
  367. * @param {Node} anchor The anchor element
  368. * @param {Node} toggle The toggle element
  369. */
  370. var emitEvent = function (type, options, anchor, toggle) {
  371. if (!options.emitEvents || typeof window.CustomEvent !== 'function') return;
  372. var event = new CustomEvent(type, {
  373. bubbles: true,
  374. detail: {
  375. anchor: anchor,
  376. toggle: toggle
  377. }
  378. });
  379. document.dispatchEvent(event);
  380. };
  381. //
  382. // SmoothScroll Constructor
  383. //
  384. var SmoothScroll = function (selector, options) {
  385. //
  386. // Variables
  387. //
  388. var smoothScroll = {}; // Object for public APIs
  389. var settings, anchor, toggle, fixedHeader, eventTimeout, animationInterval;
  390. //
  391. // Methods
  392. //
  393. /**
  394. * Cancel a scroll-in-progress
  395. */
  396. smoothScroll.cancelScroll = function (noEvent) {
  397. cancelAnimationFrame(animationInterval);
  398. animationInterval = null;
  399. if (noEvent) return;
  400. emitEvent('scrollCancel', settings);
  401. };
  402. /**
  403. * Start/stop the scrolling animation
  404. * @param {Node|Number} anchor The element or position to scroll to
  405. * @param {Element} toggle The element that toggled the scroll event
  406. * @param {Object} options
  407. */
  408. smoothScroll.animateScroll = function (anchor, toggle, options) {
  409. // Cancel any in progress scrolls
  410. smoothScroll.cancelScroll();
  411. // Local settings
  412. var _settings = extend(settings || defaults, options || {}); // Merge user options with defaults
  413. // Selectors and variables
  414. var isNum = Object.prototype.toString.call(anchor) === '[object Number]' ? true : false;
  415. var anchorElem = isNum || !anchor.tagName ? null : anchor;
  416. if (!isNum && !anchorElem) return;
  417. var startLocation = window.pageYOffset; // Current location on the page
  418. if (_settings.header && !fixedHeader) {
  419. // Get the fixed header if not already set
  420. fixedHeader = document.querySelector(_settings.header);
  421. }
  422. var headerHeight = getHeaderHeight(fixedHeader);
  423. 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
  424. var distance = endLocation - startLocation; // distance to travel
  425. var documentHeight = getDocumentHeight();
  426. var timeLapsed = 0;
  427. var speed = getSpeed(distance, _settings);
  428. var start, percentage, position;
  429. /**
  430. * Stop the scroll animation when it reaches its target (or the bottom/top of page)
  431. * @param {Number} position Current position on the page
  432. * @param {Number} endLocation Scroll to location
  433. * @param {Number} animationInterval How much to scroll on this loop
  434. */
  435. var stopAnimateScroll = function (position, endLocation) {
  436. // Get the current location
  437. var currentLocation = window.pageYOffset;
  438. // Check if the end location has been reached yet (or we've hit the end of the document)
  439. if (position == endLocation || currentLocation == endLocation || ((startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight)) {
  440. // Clear the animation timer
  441. smoothScroll.cancelScroll(true);
  442. // Bring the anchored element into focus
  443. adjustFocus(anchor, endLocation, isNum);
  444. // Emit a custom event
  445. emitEvent('scrollStop', _settings, anchor, toggle);
  446. // Reset start
  447. start = null;
  448. animationInterval = null;
  449. return true;
  450. }
  451. };
  452. /**
  453. * Loop scrolling animation
  454. */
  455. var loopAnimateScroll = function (timestamp) {
  456. if (!start) { start = timestamp; }
  457. timeLapsed += timestamp - start;
  458. percentage = speed === 0 ? 0 : (timeLapsed / speed);
  459. percentage = (percentage > 1) ? 1 : percentage;
  460. position = startLocation + (distance * easingPattern(_settings, percentage));
  461. window.scrollTo(0, Math.floor(position));
  462. if (!stopAnimateScroll(position, endLocation)) {
  463. animationInterval = window.requestAnimationFrame(loopAnimateScroll);
  464. start = timestamp;
  465. }
  466. };
  467. /**
  468. * Reset position to fix weird iOS bug
  469. * @link https://github.com/cferdinandi/smooth-scroll/issues/45
  470. */
  471. if (window.pageYOffset === 0) {
  472. window.scrollTo(0, 0);
  473. }
  474. // Update the URL
  475. updateURL(anchor, isNum, _settings);
  476. // If the user prefers reduced motion, jump to location
  477. if (reduceMotion()) {
  478. window.scrollTo(0, Math.floor(endLocation));
  479. return;
  480. }
  481. // Emit a custom event
  482. emitEvent('scrollStart', _settings, anchor, toggle);
  483. // Start scrolling animation
  484. smoothScroll.cancelScroll(true);
  485. window.requestAnimationFrame(loopAnimateScroll);
  486. };
  487. /**
  488. * If smooth scroll element clicked, animate scroll
  489. */
  490. var clickHandler = function (event) {
  491. // Don't run if event was canceled but still bubbled up
  492. // By @mgreter - https://github.com/cferdinandi/smooth-scroll/pull/462/
  493. if (event.defaultPrevented) return;
  494. // Don't run if right-click or command/control + click or shift + click
  495. if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
  496. // Check if event.target has closest() method
  497. // By @totegi - https://github.com/cferdinandi/smooth-scroll/pull/401/
  498. if (!('closest' in event.target)) return;
  499. // Check if a smooth scroll link was clicked
  500. toggle = event.target.closest(selector);
  501. if (!toggle || toggle.tagName.toLowerCase() !== 'a' || event.target.closest(settings.ignore)) return;
  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. // Get an escaped version of the hash
  505. var hash = escapeCharacters(toggle.hash);
  506. // Get the anchored element
  507. var anchor;
  508. if (hash === '#') {
  509. if (!settings.topOnEmptyHash) return;
  510. anchor = document.documentElement;
  511. } else {
  512. anchor = document.querySelector(hash);
  513. }
  514. anchor = !anchor && hash === '#top' ? document.documentElement : anchor;
  515. // If anchored element exists, scroll to it
  516. if (!anchor) return;
  517. event.preventDefault();
  518. setHistory(settings);
  519. smoothScroll.animateScroll(anchor, toggle);
  520. };
  521. /**
  522. * Animate scroll on popstate events
  523. */
  524. var popstateHandler = function (event) {
  525. // Stop if history.state doesn't exist (ex. if clicking on a broken anchor link).
  526. // fixes `Cannot read property 'smoothScroll' of null` error getting thrown.
  527. if (history.state === null) return;
  528. // Only run if state is a popstate record for this instantiation
  529. if (!history.state.smoothScroll || history.state.smoothScroll !== JSON.stringify(settings)) return;
  530. // Only run if state includes an anchor
  531. // if (!history.state.anchor && history.state.anchor !== 0) return;
  532. // Get the anchor
  533. var anchor = history.state.anchor;
  534. if (typeof anchor === 'string' && anchor) {
  535. anchor = document.querySelector(escapeCharacters(history.state.anchor));
  536. if (!anchor) return;
  537. }
  538. // Animate scroll to anchor link
  539. smoothScroll.animateScroll(anchor, null, {updateURL: false});
  540. };
  541. /**
  542. * Destroy the current initialization.
  543. */
  544. smoothScroll.destroy = function () {
  545. // If plugin isn't already initialized, stop
  546. if (!settings) return;
  547. // Remove event listeners
  548. document.removeEventListener('click', clickHandler, false);
  549. window.removeEventListener('popstate', popstateHandler, false);
  550. // Cancel any scrolls-in-progress
  551. smoothScroll.cancelScroll();
  552. // Reset variables
  553. settings = null;
  554. anchor = null;
  555. toggle = null;
  556. fixedHeader = null;
  557. eventTimeout = null;
  558. animationInterval = null;
  559. };
  560. /**
  561. * Initialize Smooth Scroll
  562. * @param {Object} options User settings
  563. */
  564. var init = function () {
  565. // feature test
  566. if (!supports()) throw 'Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.';
  567. // Destroy any existing initializations
  568. smoothScroll.destroy();
  569. // Selectors and variables
  570. settings = extend(defaults, options || {}); // Merge user options with defaults
  571. fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header
  572. // When a toggle is clicked, run the click handler
  573. document.addEventListener('click', clickHandler, false);
  574. // If updateURL and popState are enabled, listen for pop events
  575. if (settings.updateURL && settings.popstate) {
  576. window.addEventListener('popstate', popstateHandler, false);
  577. }
  578. };
  579. //
  580. // Initialize plugin
  581. //
  582. init();
  583. //
  584. // Public APIs
  585. //
  586. return smoothScroll;
  587. };
  588. return SmoothScroll;
  589. }));