1/*! 2 * gumshoejs v5.1.1 3 * A simple, framework-agnostic scrollspy script. 4 * (c) 2019 Chris Ferdinandi 5 * MIT License 6 * http://github.com/cferdinandi/gumshoe 7 */ 8 9(function (root, factory) { 10 if ( typeof define === 'function' && define.amd ) { 11 define([], (function () { 12 return factory(root); 13 })); 14 } else if ( typeof exports === 'object' ) { 15 module.exports = factory(root); 16 } else { 17 root.Gumshoe = factory(root); 18 } 19})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this, (function (window) { 20 21 'use strict'; 22 23 // 24 // Defaults 25 // 26 27 var defaults = { 28 29 // Active classes 30 navClass: 'active', 31 contentClass: 'active', 32 33 // Nested navigation 34 nested: false, 35 nestedClass: 'active', 36 37 // Offset & reflow 38 offset: 0, 39 reflow: false, 40 41 // Event support 42 events: true 43 44 }; 45 46 47 // 48 // Methods 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 * Emit a custom event 69 * @param {String} type The event type 70 * @param {Node} elem The element to attach the event to 71 * @param {Object} detail Any details to pass along with the event 72 */ 73 var emitEvent = function (type, elem, detail) { 74 75 // Make sure events are enabled 76 if (!detail.settings.events) return; 77 78 // Create a new event 79 var event = new CustomEvent(type, { 80 bubbles: true, 81 cancelable: true, 82 detail: detail 83 }); 84 85 // Dispatch the event 86 elem.dispatchEvent(event); 87 88 }; 89 90 /** 91 * Get an element's distance from the top of the Document. 92 * @param {Node} elem The element 93 * @return {Number} Distance from the top in pixels 94 */ 95 var getOffsetTop = function (elem) { 96 var location = 0; 97 if (elem.offsetParent) { 98 while (elem) { 99 location += elem.offsetTop; 100 elem = elem.offsetParent; 101 } 102 } 103 return location >= 0 ? location : 0; 104 }; 105 106 /** 107 * Sort content from first to last in the DOM 108 * @param {Array} contents The content areas 109 */ 110 var sortContents = function (contents) { 111 if(contents) { 112 contents.sort((function (item1, item2) { 113 var offset1 = getOffsetTop(item1.content); 114 var offset2 = getOffsetTop(item2.content); 115 if (offset1 < offset2) return -1; 116 return 1; 117 })); 118 } 119 }; 120 121 /** 122 * Get the offset to use for calculating position 123 * @param {Object} settings The settings for this instantiation 124 * @return {Float} The number of pixels to offset the calculations 125 */ 126 var getOffset = function (settings) { 127 128 // if the offset is a function run it 129 if (typeof settings.offset === 'function') { 130 return parseFloat(settings.offset()); 131 } 132 133 // Otherwise, return it as-is 134 return parseFloat(settings.offset); 135 136 }; 137 138 /** 139 * Get the document element's height 140 * @private 141 * @returns {Number} 142 */ 143 var getDocumentHeight = function () { 144 return Math.max( 145 document.body.scrollHeight, document.documentElement.scrollHeight, 146 document.body.offsetHeight, document.documentElement.offsetHeight, 147 document.body.clientHeight, document.documentElement.clientHeight 148 ); 149 }; 150 151 /** 152 * Determine if an element is in view 153 * @param {Node} elem The element 154 * @param {Object} settings The settings for this instantiation 155 * @param {Boolean} bottom If true, check if element is above bottom of viewport instead 156 * @return {Boolean} Returns true if element is in the viewport 157 */ 158 var isInView = function (elem, settings, bottom) { 159 var bounds = elem.getBoundingClientRect(); 160 var offset = getOffset(settings); 161 if (bottom) { 162 return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight); 163 } 164 return parseInt(bounds.top, 10) <= offset; 165 }; 166 167 /** 168 * Check if at the bottom of the viewport 169 * @return {Boolean} If true, page is at the bottom of the viewport 170 */ 171 var isAtBottom = function () { 172 if (window.innerHeight + window.pageYOffset >= getDocumentHeight()) return true; 173 return false;
174 }; 175 176 /** 177 * Check if the last item should be used (even if not at the top of the page) 178 * @param {Object} item The last item 179 * @param {Object} settings The settings for this instantiation 180 * @return {Boolean} If true, use the last item 181 */ 182 var useLastItem = function (item, settings) { 183 if (isAtBottom() && isInView(item.content, settings, true)) return true; 184 return false; 185 }; 186 187 /** 188 * Get the active content 189 * @param {Array} contents The content areas 190 * @param {Object} settings The settings for this instantiation 191 * @return {Object} The content area and matching navigation link 192 */ 193 var getActive = function (contents, settings) { 194 var last = contents[contents.length-1]; 195 if (useLastItem(last, settings)) return last; 196 for (var i = contents.length - 1; i >= 0; i--) { 197 if (isInView(contents[i].content, settings)) return contents[i]; 198 } 199 }; 200 201 /** 202 * Deactivate parent navs in a nested navigation 203 * @param {Node} nav The starting navigation element 204 * @param {Object} settings The settings for this instantiation 205 */ 206 var deactivateNested = function (nav, settings) { 207 208 // If nesting isn't activated, bail 209 if (!settings.nested) return; 210 211 // Get the parent navigation 212 var li = nav.parentNode.closest('li'); 213 if (!li) return; 214 215 // Remove the active class 216 li.classList.remove(settings.nestedClass); 217 218 // Apply recursively to any parent navigation elements 219 deactivateNested(li, settings); 220 221 }; 222 223 /** 224 * Deactivate a nav and content area 225 * @param {Object} items The nav item and content to deactivate 226 * @param {Object} settings The settings for this instantiation 227 */ 228 var deactivate = function (items, settings) { 229 230 // Make sure their are items to deactivate 231 if (!items) return; 232 233 // Get the parent list item 234 var li = items.nav.closest('li'); 235 if (!li) return; 236 237 // Remove the active class from the nav and content 238 li.classList.remove(settings.navClass); 239 items.content.classList.remove(settings.contentClass); 240 241 // Deactivate any parent navs in a nested navigation 242 deactivateNested(li, settings); 243 244 // Emit a custom event 245 emitEvent('gumshoeDeactivate', li, { 246 link: items.nav, 247 content: items.content, 248 settings: settings 249 }); 250 251 }; 252 253 254 /** 255 * Activate parent navs in a nested navigation 256 * @param {Node} nav The starting navigation element 257 * @param {Object} settings The settings for this instantiation 258 */ 259 var activateNested = function (nav, settings) { 260 261 // If nesting isn't activated, bail 262 if (!settings.nested) return; 263 264 // Get the parent navigation 265 var li = nav.parentNode.closest('li'); 266 if (!li) return; 267 268 // Add the active class 269 li.classList.add(settings.nestedClass); 270 271 // Apply recursively to any parent navigation elements 272 activateNested(li, settings); 273 274 }; 275 276 /** 277 * Activate a nav and content area 278 * @param {Object} items The nav item and content to activate 279 * @param {Object} settings The settings for this instantiation 280 */ 281 var activate = function (items, settings) { 282 283 // Make sure their are items to activate 284 if (!items) return; 285 286 // Get the parent list item 287 var li = items.nav.closest('li'); 288 if (!li) return; 289 290 // Add the active class to the nav and content 291 li.classList.add(settings.navClass); 292 items.content.classList.add(settings.contentClass); 293 294 // Activate any parent navs in a nested navigation 295 activateNested(li, settings); 296 297 // Emit a custom event 298 emitEvent('gumshoeActivate', li, { 299 link: items.nav, 300 content: items.content, 301 settings: settings 302 }); 303 304 }; 305 306 /** 307 * Create the Constructor object 308 * @param {String} selector The selector to use for navigation items 309 * @param {Object} options User options and settings 310 */ 311 var Constructor = function (selector, options) { 312 313 // 314 // Variables 315 // 316 317 var publicAPIs = {}; 318 var navItems, contents, current, timeout, settings; 319 320 321 // 322 // Methods 323 // 324 325 /** 326 * Set variables from DOM elements 327 */ 328 publicAPIs.setup = function () { 329 330 // Get all nav items 331 navItems = document.querySelectorAll(selector); 332 333 // Create contents array 334 contents = []; 335 336 // Loop through each item, get it's matching content, and push to the array 337 Array.prototype.forEach.call(navItems, (function (item) { 338 339 // Get the content for the nav item 340 var content = document.getElementById(decodeURIComponent(item.hash.substr(1))); 341 if (!content) return; 342 343 // Push to the contents array 344 contents.push({ 345 nav: item, 346 content: content 347 }); 348 349 })); 350 351 // Sort contents by the order they appear in the DOM 352 sortContents(contents); 353 354 }; 355 356 /** 357 * Detect which content is currently active 358 */ 359 publicAPIs.detect = function () { 360 361 // Get the active content 362 var active = getActive(contents, settings); 363 364 // if there's no active content, deactivate and bail 365 if (!active) { 366 if (current) { 367 deactivate(current, settings); 368 current = null; 369 } 370 return; 371 } 372 373 // If the active content is the one currently active, do nothing 374 if (current && active.content === current.content) return; 375 376 // Deactivate the current content and activate the new content 377 deactivate(current, settings); 378 activate(active, settings); 379 380 // Update the currently active content 381 current = active; 382 383 }; 384 385 /** 386 * Detect the active content on scroll 387 * Debounced for performance 388 */ 389 var scrollHandler = function (event) { 390 391 // If there's a timer, cancel it 392 if (timeout) { 393 window.cancelAnimationFrame(timeout); 394 } 395 396 // Setup debounce callback 397 timeout = window.requestAnimationFrame(publicAPIs.detect); 398 399 }; 400 401 /** 402 * Update content sorting on resize 403 * Debounced for performance 404 */ 405 var resizeHandler = function (event) { 406 407 // If there's a timer, cancel it 408 if (timeout) { 409 window.cancelAnimationFrame(timeout); 410 } 411 412 // Setup debounce callback 413 timeout = window.requestAnimationFrame((function () { 414 sortContents(contents); 415 publicAPIs.detect(); 416 })); 417 418 }; 419 420 /** 421 * Destroy the current instantiation 422 */ 423 publicAPIs.destroy = function () { 424 425 // Undo DOM changes 426 if (current) { 427 deactivate(current, settings); 428 } 429 430 // Remove event listeners 431 window.removeEventListener('scroll', scrollHandler, false); 432 if (settings.reflow) { 433 window.removeEventListener('resize', resizeHandler, false); 434 } 435 436 // Reset variables 437 contents = null; 438 navItems = null; 439 current = null; 440 timeout = null; 441 settings = null; 442 443 }; 444 445 /** 446 * Initialize the current instantiation 447 */ 448 var init = function () { 449 450 // Merge user options into defaults 451 settings = extend(defaults, options || {}); 452 453 // Setup variables based on the current DOM 454 publicAPIs.setup(); 455 456 // Find the currently active content 457 publicAPIs.detect(); 458 459 // Setup event listeners 460 window.addEventListener('scroll', scrollHandler, false); 461 if (settings.reflow) { 462 window.addEventListener('resize', resizeHandler, false); 463 } 464 465 }; 466 467 468 // 469 // Initialize and return the public APIs 470 // 471 472 init(); 473 return publicAPIs; 474 475 }; 476 477 478 //
479 // Return the Constructor 480 // 481 482 return Constructor; 483 484}));
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.