1/*! 2 * smooth-scroll v10.1.0: Animate scrolling to anchor links 3 * (c) 2016 Chris Ferdinandi 4 * MIT License 5 * http://github.com/cferdinandi/smooth-scroll 6 */ 7 8(function (root, factory) { 9 if ( typeof define === 'function' && define.amd ) { 10 define([], factory(root)); 11 } else if ( typeof exports === 'object' ) { 12 module.exports = factory(root); 13 } else { 14 root.smoothScroll = factory(root); 15 } 16})(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { 17 18 'use strict'; 19 20 // 21 // Variables 22 // 23 24 var smoothScroll = {}; // Object for public APIs 25 var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test 26 var settings, anchor, toggle, fixedHeader, headerHeight, eventTimeout, animationInterval; 27 28 // Default settings 29 var defaults = { 30 selector: '[data-scroll]', 31 selectorHeader: null, 32 speed: 600, 33 easing: 'easeInQuint', 34 offset: 0, 35 callback: function () {} 36 }; 37 38 39 // 40 // Methods 41 // 42 43 /** 44 * Merge two or more objects. Returns a new object. 45 * @private 46 * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 47 * @param {Object} objects The objects to merge together 48 * @returns {Object} Merged values of defaults and options 49 */ 50 var extend = function () { 51 52 // Variables 53 var extended = {}; 54 var deep = false; 55 var i = 0; 56 var length = arguments.length; 57 58 // Check if a deep merge 59 if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 60 deep = arguments[0]; 61 i++; 62 } 63 64 // Merge the object into the extended object 65 var merge = function (obj) { 66 for ( var prop in obj ) { 67 if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 68 // If deep merge and property is an object, merge properties 69 if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 70 extended[prop] = extend( true, extended[prop], obj[prop] ); 71 } else { 72 extended[prop] = obj[prop]; 73 } 74 } 75 } 76 }; 77 78 // Loop through each object and conduct a merge 79 for ( ; i < length; i++ ) { 80 var obj = arguments[i]; 81 merge(obj); 82 } 83 84 return extended; 85 86 }; 87 88 /** 89 * Get the height of an element. 90 * @private 91 * @param {Node} elem The element to get the height of 92 * @return {Number} The element's height in pixels 93 */ 94 var getHeight = function ( elem ) { 95 return Math.max( elem.scrollHeight, elem.offsetHeight, elem.clientHeight ); 96 }; 97 98 /** 99 * Get the closest matching element up the DOM tree. 100 * @private 101 * @param {Element} elem Starting element 102 * @param {String} selector Selector to match against (class, ID, data attribute, or tag) 103 * @return {Boolean|Element} Returns null if not match found 104 */ 105 var getClosest = function ( elem, selector ) { 106 107 // Variables 108 var firstChar = selector.charAt(0); 109 var supports = 'classList' in document.documentElement; 110 var attribute, value; 111 112 // If selector is a data attribute, split attribute from value 113 if ( firstChar === '[' ) { 114 selector = selector.substr(1, selector.length - 2); 115 attribute = selector.split( '=' ); 116 117 if ( attribute.length > 1 ) { 118 value = true; 119 attribute[1] = attribute[1].replace( /"/g, '' ).replace( /'/g, '' ); 120 } 121 } 122 123 // Get closest match 124 for ( ; elem && elem !== document && elem.nodeType === 1; elem = elem.parentNode ) { 125 126 // If selector is a class 127 if ( firstChar === '.' ) { 128 if ( supports ) { 129 if ( elem.classList.contains( selector.substr(1) ) ) { 130 return elem; 131 } 132 } else { 133 if ( new RegExp('(^|\\s)' + selector.substr(1) + '(\\s|$)').test( elem.className ) ) { 134 return elem; 135 } 136 } 137 } 138 139 // If selector is an ID 140 if ( firstChar === '#' ) { 141 if ( elem.id === selector.substr(1) ) { 142 return elem; 143 } 144 } 145 146 // If selector is a data attribute 147 if ( firstChar === '[' ) { 148 if ( elem.hasAttribute( attribute[0] ) ) { 149 if ( value ) { 150 if ( elem.getAttribute( attribute[0] ) === attribute[1] ) { 151 return elem; 152 } 153 } else { 154 return elem; 155 } 156 } 157 } 158 159 // If selector is a tag 160 if ( elem.tagName.toLowerCase() === selector ) { 161 return elem; 162 } 163 164 } 165 166 return null; 167 168 }; 169 170 /** 171 * Escape special characters for use with querySelector 172 * @private 173 * @param {String} id The anchor ID to escape 174 * @author Mathias Bynens 175 * @link https://github.com/mathiasbynens/CSS.escape 176 */ 177 var escapeCharacters = function ( id ) { 178 179 // Remove leading hash 180 if ( id.charAt(0) === '#' ) { 181 id = id.substr(1); 182 } 183 184 var string = String(id); 185 var length = string.length; 186 var index = -1; 187 var codeUnit; 188 var result = ''; 189 var firstCodeUnit = string.charCodeAt(0); 190 while (++index < length) { 191 codeUnit = string.charCodeAt(index); 192 // Note: thereâs no need to special-case astral symbols, surrogate 193 // pairs, or lone surrogates. 194 195 // If the character is NULL (U+0000), then throw an 196 // `InvalidCharacterError` exception and terminate these steps. 197 if (codeUnit === 0x0000) { 198 throw new InvalidCharacterError( 199 'Invalid character: the input contains U+0000.' 200 ); 201 } 202 203 if ( 204 // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is 205 // U+007F, [â¦] 206 (codeUnit >
206= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || 207 // If the character is the first character and is in the range [0-9] 208 // (U+0030 to U+0039), [â¦] 209 (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || 210 // If the character is the second character and is in the range [0-9] 211 // (U+0030 to U+0039) and the first character is a `-` (U+002D), [â¦] 212 ( 213 index === 1 && 214 codeUnit >= 0x0030 && codeUnit <= 0x0039 && 215 firstCodeUnit === 0x002D 216 ) 217 ) { 218 // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point 219 result += '\\' + codeUnit.toString(16) + ' '; 220 continue; 221 } 222 223 // If the character is not handled by one of the above rules and is 224 // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or 225 // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to 226 // U+005A), or [a-z] (U+0061 to U+007A), [â¦] 227 if ( 228 codeUnit >= 0x0080 || 229 codeUnit === 0x002D || 230 codeUnit === 0x005F || 231 codeUnit >= 0x0030 && codeUnit <= 0x0039 || 232 codeUnit >= 0x0041 && codeUnit <= 0x005A || 233 codeUnit >= 0x0061 && codeUnit <= 0x007A 234 ) { 235 // the character itself 236 result += string.charAt(index); 237 continue; 238 } 239 240 // Otherwise, the escaped character. 241 // http://dev.w3.org/csswg/cssom/#escape-a-character 242 result += '\\' + string.charAt(index); 243 244 } 245 246 return '#' + result; 247 248 }; 249 250 /** 251 * Calculate the easing pattern 252 * @private 253 * @link https://gist.github.com/gre/1650294 254 * @param {String} type Easing pattern 255 * @param {Number} time Time animation should take to complete 256 * @returns {Number} 257 */ 258 var easingPattern = function ( type, time ) { 259 var pattern; 260 if ( type === 'easeInQuad' ) pattern = time * time; // accelerating from zero velocity 261 if ( type === 'easeOutQuad' ) pattern = time * (2 - time); // decelerating to zero velocity 262 if ( type === 'easeInOutQuad' ) pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration 263 if ( type === 'easeInCubic' ) pattern = time * time * time; // accelerating from zero velocity 264 if ( type === 'easeOutCubic' ) pattern = (--time) * time * time + 1; // decelerating to zero velocity 265 if ( type === 'easeInOutCubic' ) pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration 266 if ( type === 'easeInQuart' ) pattern = time * time * time * time; // accelerating from zero velocity 267 if ( type === 'easeOutQuart' ) pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity 268 if ( type === 'easeInOutQuart' ) pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration 269 if ( type === 'easeInQuint' ) pattern = time * time * time * time * time; // accelerating from zero velocity 270 if ( type === 'easeOutQuint' ) pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity 271 if ( type === 'easeInOutQuint' ) pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration 272 return pattern || time; // no easing, no acceleration 273 }; 274 275 /** 276 * Calculate how far to scroll 277 * @private 278 * @param {Element} anchor The anchor element to scroll to 279 * @param {Number} headerHeight Height of a fixed header, if any 280 * @param {Number} offset Number of pixels by which to offset scroll 281 * @returns {Number} 282 */ 283 var getEndLocation = function ( anchor, headerHeight, offset ) { 284 var location = 0; 285 if (anchor.offsetParent) { 286 do { 287 location += anchor.offsetTop; 288 anchor = anchor.offsetParent; 289 } while (anchor); 290 } 291 location = Math.max(location - headerHeight - offset, 0); 292 return Math.min(location, getDocumentHeight() - getViewportHeight()); 293 }; 294 295 /** 296 * Determine the viewport's height 297 * @private 298 * @returns {Number} 299 */ 300 var getViewportHeight = function() { 301 return Math.max( document.documentElement.clientHeight, root.innerHe
301ight || 0 ); 302 }; 303 304 /** 305 * Determine the document's height 306 * @private 307 * @returns {Number} 308 */ 309 var getDocumentHeight = function () { 310 return Math.max( 311 document.body.scrollHeight, document.documentElement.scrollHeight, 312 document.body.offsetHeight, document.documentElement.offsetHeight, 313 document.body.clientHeight, document.documentElement.clientHeight 314 ); 315 }; 316 317 /** 318 * Convert data-options attribute into an object of key/value pairs 319 * @private 320 * @param {String} options Link-specific options as a data attribute string 321 * @returns {Object} 322 */ 323 var getDataOptions = function ( options ) { 324 return !options || !(typeof JSON === 'object' && typeof JSON.parse === 'function') ? {} : JSON.parse( options ); 325 }; 326 327 /** 328 * Get the height of the fixed header 329 * @private 330 * @param {Node} header The header 331 * @return {Number} The height of the header 332 */ 333 var getHeaderHeight = function ( header ) { 334 return !header ? 0 : ( getHeight( header ) + header.offsetTop ); 335 }; 336 337 /** 338 * Bring the anchored element into focus 339 * @private 340 */ 341 var adjustFocus = function ( anchor, endLocation, isNum ) { 342 343 // Don't run if scrolling to a number on the page 344 if ( isNum ) return; 345 346 // Otherwise, bring anchor element into focus 347 anchor.focus(); 348 if ( document.activeElement.id !== anchor.id ) { 349 anchor.setAttribute( 'tabindex', '-1' ); 350 anchor.focus(); 351 anchor.style.outline = 'none'; 352 } 353 root.scrollTo( 0 , endLocation ); 354 355 }; 356 357 /** 358 * Start/stop the scrolling animation 359 * @public 360 * @param {Node|Number} anchor The element or position to scroll to 361 * @param {Element} toggle The element that toggled the scroll event 362 * @param {Object} options 363 */ 364 smoothScroll.animateScroll = function ( anchor, toggle, options ) { 365 366 // Options and overrides 367 var overrides = getDataOptions( toggle ? toggle.getAttribute('data-options') : null ); 368 var animateSettings = extend( settings || defaults, options || {}, overrides ); // Merge user options with defaults 369 370 // Selectors and variables 371 var isNum = Object.prototype.toString.call( anchor ) === '[object Number]' ? true : false; 372 var anchorElem = isNum || !anchor.tagName ? null : anchor; 373 if ( !isNum && !anchorElem ) return; 374 var startLocation = root.pageYOffset; // Current location on the page 375 if ( animateSettings.selectorHeader && !fixedHeader ) { 376 // Get the fixed header if not already set 377 fixedHeader = document.querySelector( animateSettings.selectorHeader ); 378 } 379 if ( !headerHeight ) { 380 // Get the height of a fixed header if one exists and not already set 381 headerHeight = getHeaderHeight( fixedHeader ); 382 } 383 var endLocation = isNum ? anchor : getEndLocation( anchorElem, headerHeight, parseInt(animateSettings.offset, 10) ); // Location to scroll to 384 var distance = endLocation - startLocation; // distance to travel 385 var documentHeight = getDocumentHeight(); 386 var timeLapsed = 0; 387 var percentage, position; 388 389 /** 390 * Stop the scroll animation when it reaches its target (or the bottom/top of page) 391 * @private 392 * @param {Number} position Current position on the page 393 * @param {Number} endLocation Scroll to location 394 * @param {Number} animationInterval How much to scroll on this loop 395 */ 396 var stopAnimateScroll = function ( position, endLocation, animationInterval ) { 397 var currentLocation = root.pageYOffset; 398 if ( position == endLocation || currentLocation == endLocation || ( (root.innerHeight + currentLocation) >= documentHeight ) ) { 399 400 // Clear the animation timer 401 clearInterval(animationInterval); 402 403 // Bring the anchored element into focus 404 adjustFocus( anchor, endLocation, isNum ); 405 406 // Run callback after animation complete 407 animateSettings.callback( anchor, toggle ); 408 409 } 410 }; 411 412 /** 413 * Loop scrolling animation 414 * @private 415 */ 416 var loopAnimateScroll = function () { 417 timeLapsed += 16; 418 percentage = ( timeLapsed / parseInt(animateSettings.speed, 10) ); 419 percentage = ( percentage > 1 ) ? 1 : percentage; 420 position = startLocation + ( distance * easingPattern(animateSettings.easing, percentage) ); 421 root.scrollTo( 0, Math.floor(position) ); 422 stopAnimateScroll(position, endLocation, animationInterval); 423 }; 424 425 /** 426 * Set interval timer 427 * @private 428 */ 429 var startAnimateScroll = function () { 430 clearInterval(animationInterval); 431 animationInterval = setInterval(loopAnimateScroll, 16); 432 }; 433 434 /** 435 * Reset position to fix weird iOS bug 436 * @link https://github.com/cferdinandi/smooth-scroll/issues/45 437 */ 438 if ( root.pageYOffset === 0 ) { 439 root.scrollTo( 0, 0 ); 440 } 441 442 // Start scrolling animation 443 startAnimateScroll(); 444 445 }; 446 447 /** 448 * Handle has change event 449 * @private 450 */ 451 var hashChangeHandler = function (event) { 452 453 // Get hash from URL 454 var hash = root.location.hash; 455 456 // Only run if there's an anchor element to scroll to 457 if ( !anchor ) return; 458
459 // Reset the anchor element's ID 460 anchor.id = anchor.getAttribute( 'data-scroll-id' ); 461 462 // Scroll to the anchored content 463 smoothScroll.animateScroll( anchor, toggle ); 464 465 // Reset anchor and toggle 466 anchor = null; 467 toggle = null; 468 469 }; 470 471 /** 472 * If smooth scroll element clicked, animate scroll 473 * @private 474 */ 475 var clickHandler = function (event) { 476 477 // Don't run if right-click or command/control + click 478 if ( event.button !== 0 || event.metaKey || event.ctrlKey ) return; 479 480 // Check if a smooth scroll link was clicked 481 toggle = getClosest( event.target, settings.selector ); 482 if ( !toggle || toggle.tagName.toLowerCase() !== 'a' ) return; 483 484 // Only run if link is an anchor and points to the current page 485 if ( toggle.hostname !== root.location.hostname || toggle.pathname !== root.location.pathname || !/#/.test(toggle.href) ) return; 486 487 // Get the sanitized hash 488 var hash = escapeCharacters( toggle.hash ); 489 490 // If the hash is empty, scroll to the top of the page 491 if ( hash === '#' ) { 492 493 // Prevent default link behavior 494 event.preventDefault(); 495 496 // Set the anchored element 497 anchor = document.body; 498 499 // Save or create the ID as a data attribute and remove it (prevents scroll jump) 500 var id = anchor.id ? anchor.id : 'smooth-scroll-top'; 501 anchor.setAttribute( 'data-scroll-id', id ); 502 anchor.id = ''; 503 504 // If no hash change event will happen, fire manually 505 // Otherwise, update the hash 506 if ( root.location.hash.substring(1) === id ) { 507 hashChangeHandler(); 508 } else { 509 root.location.hash = id; 510 } 511 512 return; 513 514 } 515 516 // Get the anchored element 517 anchor = document.querySelector( hash ); 518 519 // If anchored element exists, save the ID as a data attribute and remove it (prevents scroll jump) 520 if ( !anchor ) return; 521 anchor.setAttribute( 'data-scroll-id', anchor.id ); 522 anchor.id = ''; 523 524 // If no hash change event will happen, fire manually 525 if ( toggle.hash === root.location.hash ) { 526 event.preventDefault(); 527 hashChangeHandler(); 528 } 529 530 }; 531 532 /** 533 * On window scroll and resize, only run events at a rate of 15fps for better performance 534 * @private 535 * @param {Function} eventTimeout Timeout function 536 * @param {Object} settings 537 */ 538 var resizeThrottler = function (event) { 539 if ( !eventTimeout ) { 540 eventTimeout = setTimeout((function() { 541 eventTimeout = null; // Reset timeout 542 headerHeight = getHeaderHeight( fixedHeader ); // Get the height of a fixed header if one exists 543 }), 66); 544 } 545 }; 546 547 /** 548 * Destroy the current initialization. 549 * @public 550 */ 551 smoothScroll.destroy = function () { 552 553 // If plugin isn't already initialized, stop 554 if ( !settings ) return; 555 556 // Remove event listeners 557 document.removeEventListener( 'click', clickHandler, false ); 558 root.removeEventListener( 'resize', resizeThrottler, false ); 559 560 // Reset varaibles 561 settings = null; 562 anchor = null; 563 toggle = null; 564 fixedHeader = null; 565 headerHeight = null; 566 eventTimeout = null; 567 animationInterval = null; 568 }; 569 570 /** 571 * Initialize Smooth Scroll 572 * @public 573 * @param {Object} options User settings 574 */ 575 smoothScroll.init = function ( options ) { 576 577 // feature test 578 if ( !supports ) return; 579 580 // Destroy any existing initializations 581 smoothScroll.destroy(); 582 583 // Selectors and variables 584 settings = extend( defaults, options || {} ); // Merge user options with defaults 585 fixedHeader = settings.selectorHeader ? document.querySelector( settings.selectorHeader ) : null; // Get the fixed header 586 headerHeight = getHeaderHeight( fixedHeader ); 587 588 // When a toggle is clicked, run the click handler 589 document.addEventListener( 'click', clickHandler, false ); 590 591 // Listen for hash changes 592 root.addEventListener('hashchange', hashChangeHandler, false); 593 594 // If window is resized and there's a fixed header, recalculate its size 595 if ( fixedHeader ) { 596 root.addEventListener( 'resize', resizeThrottler, false ); 597 } 598 599 }; 600 601 602 // 603 // Public APIs 604 // 605 606 return smoothScroll; 607 608}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.