PageSourceSearch

https://us-rse.org/assets/js/gumshoe.js

js us-rse.org collected 2026-09-24 09:52:57 UTC 11,364 bytes, 484 lines download raw bytes

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.