source: src/main/resources/static/js/smooth-scroll.js@ d3cf3a1

Last change on this file since d3cf3a1 was d3cf3a1, checked in by Marija Micevska <marija_micevska@…>, 2 years ago

Initial commit

  • Property mode set to 100644
File size: 18.8 KB
Line 
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})));
Note: See TracBrowser for help on using the repository browser.