Giter VIP home page Giter VIP logo

gumshoe's Introduction

Gumshoe Build Status

A simple vanilla JS scrollspy script. Gumshoe works great with Smooth Scroll.

View the Demo on CodePen β†’

Getting Started | Nested Navigation | Reflows | Fixed Headers | API | What's new? | Browser Compatibility | License


Want to learn how to write your own vanilla JS plugins? Check out my Vanilla JS Pocket Guides or join the Vanilla JS Academy and level-up as a web developer. πŸš€


Getting Started

Compiled and production-ready code can be found in the dist directory. The src directory contains development code.

1. Include Gumshoe on your site.

There are two versions of Gumshoe: the standalone version, and one that comes preloaded with polyfills for closest() and CustomEvent(), which are only supported in newer browsers.

If you're including your own polyfills or don't want to enable this feature for older browsers, use the standalone version. Otherwise, use the version with polyfills.

Direct Download

You can download the files directly from GitHub.

<script src="path/to/gumshoe.polyfills.min.js"></script>

CDN

You can also use the jsDelivr CDN. I recommend linking to a specific version number or version range to prevent major updates from breaking your site. Gumshoe uses semantic versioning.

<!-- Always get the latest version -->
<!-- Not recommended for production sites! -->
<script src="https://cdn.jsdelivr.net/gh/cferdinandi/gumshoe/dist/gumshoe.polyfills.min.js"></script>

<!-- Get minor updates and patch fixes within a major version -->
<script src="https://cdn.jsdelivr.net/gh/cferdinandi/gumshoe@4/dist/gumshoe.polyfills.min.js"></script>

<!-- Get patch fixes within a minor version -->
<script src="https://cdn.jsdelivr.net/gh/cferdinandi/[email protected]/dist/gumshoe.polyfills.min.js"></script>

<!-- Get a specific version -->
<script src="https://cdn.jsdelivr.net/gh/cferdinandi/[email protected]/dist/gumshoe.polyfills.min.js"></script>

NPM

You can also use NPM (or your favorite package manager).

npm install gumshoejs

2. Add the markup to your HTML.

The only thing Gumshoe needs to work is a list of anchor links. They can be ordered or unordered, inline or unstyled, or even nested.

<ul id="my-awesome-nav">
	<li><a href="#eenie">Eenie</a></li>
	<li><a href="#meenie">Meenie</a></li>
	<li><a href="#miney">Miney</a></li>
	<li><a href="#mo">Mo</a></li>
</ul>

3. Initialize Gumshoe.

In the footer of your page, after the content, initialize Gumshoe by passing in a selector for the navigation links that should be detected as the user scrolls.

<script>
	var spy = new Gumshoe('#my-awesome-nav a');
</script>

4. Add styling.

Gumshoe adds the .active class to the list item (<li></li>) and content for the active link, but does not include any styling.

Add styles to your CSS as desired. And that's it, you're done. Nice work!

#my-awesome-nav li.active a {
	font-weight: bold;
}

View a Demo on CodePen β†’

Note: you can customize the class names with user options.

Nested navigation

If you have a nested navigation menu with multiple levels, Gumshoe can also apply an .active class to the parent list items of the currently active link.

<ul id="my-awesome-nav">
	<li><a href="#eenie">Eenie</a></li>
	<li>
		<a href="#meenie">Meenie</a>
		<ul>
			<li><a href="#hickory">Hickory</a></li>
			<li><a href="#dickory">Dickory</a></li>
			<li><a href="#doc">Doc</a></li>
		</ul>
	</li>
	<li><a href="#miney">Miney</a></li>
	<li><a href="#mo">Mo</a></li>
</ul>

Set nested to true when instantiating Gumshoe. You can also customize the class name.

var spy = new Gumshoe('#my-awesome-nav a', {
	nested: true,
	nestedClass: 'active-parent'
});

Try nested navigation on CodePen β†’

Catching reflows

If the content that's linked to by your navigation has different layouts at different viewports, Gumshoe will need to detect these changes and update some calculations behind-the-scenes.

Set reflow to true to enable this (it's off by default).

var spy = new Gumshoe('#my-awesome-nav a', {
	reflow: true
});

Accounting for fixed headers

If you have a fixed header on your page, you may want to offset when a piece of content is considered "active."

The offset user setting accepts either a number, or a function that returns a number. If you need to dynamically calculate dimensions, a function is the preferred method.

Here's an example that automatically calculates a header's height and offsets by that amount.

// Get the header
var header = document.querySelector('#my-header');

// Initialize Gumshoe
var spy = new Gumshoe('#my-awesome-nav a', {
	offset: function () {
		return header.getBoundingClientRect().height;
	}
});

Try using an offset on CodePen β†’

API

Gumshoe includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings.

Options and Settings

You can pass options into Gumshoe when instantiating.

var spy = new Gumshoe('#my-awesome-nav a', {

	// Active classes
	navClass: 'active', // applied to the nav list item
	contentClass: 'active', // applied to the content

	// Nested navigation
	nested: false, // if true, add classes to parents of active link
	nestedClass: 'active', // applied to the parent items

	// Offset & reflow
	offset: 0, // how far from the top of the page to activate a content area
	reflow: false, // if true, listen for reflows

	// Event support
	events: true // if true, emit custom events

});

Custom Events

Gumshoe emits two custom events:

  • gumshoeActivate is emitted when a link is activated.
  • gumshoeDeactivate is emitted when a link is deactivated.

Both events are emitted on the list item and bubble up. You can listen for them with the addEventListener() method. The event.detail object includes the link and content elements, and the settings for the current instantiation.

// Listen for activate events
document.addEventListener('gumshoeActivate', function (event) {

	// The list item
	var li = event.target;

	// The link
	var link = event.detail.link;

	// The content
	var content = event.detail.content;

}, false);

Methods

Gumshoe also exposes several public methods.

setup()

Setups all of the calculations Gumshoe needs behind-the-scenes. If you dynamically add navigation items to the DOM after Gumshoe is instantiated, you can run this method to update the calculations.

Example

var spy = new Gumshoe('#my-awesome-nav a');
spy.setup();

detect()

Activate the navigation link that's content is currently in the viewport.

Example

var spy = new Gumshoe('#my-awesome-nav a');
spy.detect();

destroy()

Destroy the current instantiation of Gumshoe.

Example

var spy = new Gumshoe('#my-awesome-nav a');
spy.destroy();

What's new?

Gumshoe 4 is a ground-up rewrite.

New Features

  • Multiple instantiations can be run with different settings for each.
  • An active class is now added to the content as well.
  • Nested navigation is now supported.
  • Offsets can be dynamically calculated instead of set just once at initialization.
  • Special and non-Roman characters can now be used in anchor links and IDs.
  • Custom events provide a more flexible way to react to DOM changes.

Breaking Changes

  • Gumshoe must now be instantiated as a new object (new Gumshoe()) instead of being initialized gumshoe.init().
  • Callback methods have been removed in favor of events.
  • Automatic header offsetting has been removed.
  • The public init() method has been deprecated.

Browser Compatibility

Gumshoe works in all modern browsers, and IE 9 and above.

Polyfills

Support back to IE9 requires polyfills for closest() and CustomEvent(). Without them, support starts with Edge.

Use the included polyfills version of Gumshoe, or include your own.

License

The code is available under the MIT License.

gumshoe's People

Contributors

altano avatar cferdinandi avatar colbin8r avatar davidrapson avatar dougmacknz avatar ezzatron avatar fraserthompson avatar massimocelima avatar matthewmcvickar avatar persianphilosopher avatar strarsis avatar thibaudcolas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gumshoe's Issues

Fixed header on scroll

Hi Chris.

I'm running into some issues with a fixed header for a nav for which becomes fixed when it scrolls past the top of the viewport. It seems to cause the calculations to fail, as they don't seem to include the fixed header height because it's not fixed on page load.

Would the script be easy to modify to support this?

Thanks

Jason

link with class active is skipped when section heights in vh

Hi!

I appreciate much your work. Smooth scrolling works great and gumshoe is the perfect complement. However, I am having the issue that one link is skipped (the problem seems to be similar to #37).

The script does work as intended when I use absolute heights for the sections (which is not an option). But with height: 100vh it skips the second link. Is there a solution for that?

I set up something so you can see yourself servertest.at/senoner.
username: external
pw: ext1

I hope you have an idea.
Thanks in advance!
CS

Used on Swift.org

Thought you might like to know that, and maybe put it in the README.

Install with npm?

It is not published on npm? Why not? Very inconvenient. I doesn't have any script imports in my SPA setup!

Unit without reference to the menu

In the current project, I get the following situation. Any block for which no references in the menu. And it turns out that when you scroll to this block incorrect items are marked on the menu. I would like to at this point, no item was not marked active.

Main script reference in package.json

The "main" property in package.json is set to the dist/ directory rather than the main script within that directory. I created an issue here only because in Webpack and similar tools, the script has to be explicitly defined, or else it has to be manually changed. (I also noticed in your smooth-scroll that you reference the full path to the file.) Thanks!

Script doesn't work in generator-webapp.

Hello. When I'm trying to use gumshoe in scaffold generator-webapp it doesn't work.

Uncaught TypeError: Cannot read property 'window' of undefined
Uncaught ReferenceError: gumshoe is not defined

When I load script from CDN everything works fine. And everything works when I doesn't use generator-webapp, just static files.

What can cause that error?
I tried to place <script> in head, in beginning of the body and at the end. Doesn't work in that scaffold. I'm confused, plz help!

media queries

Hey There,

This isn't really so much a bug as it is a heads-up. I was having trouble with your code which I tracked down to the gumshoe.init() method executing before my media query was kicking in and moving the nav to be fixed position, so all the distances were off.

I was able to fix this with the tiny snippet of code at https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js

Using that snippet, I added this single line to gumshoe.js:

// Listen for events
root.addEventListener('resize', eventThrottler, false);
root.addEventListener('scroll', eventThrottler, false);

// Listen for media query changes and pretend they're a resize
root.mqEvents(eventThrottler.bind(null, {type: 'resize'}));   // <=============

You might either want to integrate this code into gumshoe OR at least mention that gumshoe doesn't listen for media query events, only window resize, so consumer code would have to implement the same thing I did if they're using media queries.

Active class not being applied to link

Hi cferdinandi,

Really great work on this plugin (and smooth-scroll), it's really easy to use! However I have ran into an issue. I have four links in my navigation bar like this:

<div class="links-right" data-gumshoe>
                    <a data-scroll href="#intro">Intro</a>
                    <a data-scroll href="#about">About</a>
                    <a data-scroll href="#services">Services</a>
                    <a data-scroll href="#contact">Contact</a>
                </div>

It applies the classes correctly when I scroll, however it's skipping the #services link for some reason.

The markup for each section is like the following:

<section class="services-section" id="services">
            ...
</section>

To better explain whats happening, i made a short video showing it. Do you have any idea what's causing this? :)
link being skipped.zip

Completely wrong calculations

On a onepager site gumshoe detects sections much later (with much
more vertical offset) than they are actually positioned on the page.

Fails on server side

Hi

I tried to use gumshoe on my nextjs app. Unfortunately sole import of gumshoe end with document is not defined error, since it's running on the server. I believe code shouldn't run anything until init() get's triggered

Script doesn't work with RequireJS

Hi there,

I've been trying to add the script using

require(['libs/gumshoe/dist/js/gumshoe'], function(gumshoe) {
    gumshoe.init();
});

and I kept getting an undefined so I tried and modified the script, like this:

 (function (root, factory) {
     'use strict';
     if (typeof define === 'function' && define.amd) {
         define([], factory(root));
     } else if (typeof exports === 'object') {
         module.exports = factory(root);
     } else {
         root.gumshoe = factory(root);
     }
 }(window || this, function (root) {

    // The whole script goes here

}));

and now it maggically works! πŸ˜„ At least for me...

Is that a thing?

I took the code from another script I was using in my project and as I said, it's working for me, I don't know, hehe!

Do I need both Gumshoe and Smoothscroll

Do I need both Gumshoe and Smoothscroll libraries in order to use both the Gumshoe navigation and the custom scrolling events of Smoothscroll? They both seem to use common functionalities for the actual animated scrolling don't they? Thanks!

Apply multiple classes

It would be great if it was possible to add multiple active classes to the desired element.

My personal site is built with tailwind and since tailwind is about putting classes and not writing CSS, I had to put multiple classes on my element to style it. But in order to get the desired hover effect, I must apply multiple classes on it.

Add optimize js

package.json

"gulp-optimize-js": "^1.0.2",

gulpfile.js

var optimizejs = require('gulp-optimize-js');

    var jsTasks = lazypipe()
        .pipe(header, banner.full, { package : package })
        .pipe(optimizejs)
        .pipe(gulp.dest, paths.scripts.output)
        .pipe(rename, { suffix: '.min' })
        .pipe(uglify)
        .pipe(optimizejs)
        .pipe(header, banner.min, { package : package })
        .pipe(gulp.dest, paths.scripts.output);

Option to apply .active to parent <li> elements as well

On bootstrap's scrollspy, I added some styling that will automatically collapse any lists that are not currently active. The effect is that the menus automatically expand and collapse as we scroll.

li:not(.active) > ul { display: none }

The difference is however, gumshoe only applies .active to the most lower level <li>. So if a second level navigation item becomes active, the entire menu collapses.

Would it be possible to have an option that also marks all of the parent <li> elements as active?

Doesn't adapt to position: fixed headers

Chris, I really love the simplicity of this library. Thanks for making it open source for us all to enjoy! πŸ˜„

Just thought that I would point out, as I found out in one of my projections, the getOffsetTop won't work for sticky headers that are fixed with position: fixed because the offsetTop property of the element itself is never taken into consideration. Consider adding an else statement to the conditional that accounts for it. For example:

var getOffsetTop = function ( elem ) {
    var location = 0;
    if (elem.offsetParent) {
        do {
            location += elem.offsetTop;
            elem = elem.offsetParent;
        } while (elem);
    } else {
        location = elem.offsetTop;
    }
    location = location - headerHeight - settings.offset;
    return location >= 0 ? location : 0;
};

The original function won't work with fixed headers because, in Webkit, elements that are position: fixed have no offsetParent; therefore, the location never has the element's offsetTop property included. With a fixed position header, the offsetTop property is all location needs to be set to before accounting for the headerHeight and settings.offset.

Assigning the "activeClass" to more than one selector

Hi there,

Looking to apply the gumshoe both to a top navigation header and a Dotted Side Navigation.
Either one works fine but when I target both items with the selector param (either unifying them or adding separating by comma) only the second one works. I assume this has to do with removing the active class from all other elements but the current one.
How could I achieve assigning the class to 2 different elements in 2 different navs?
Thanks! :)

NPM Versioning displays Github-Link

Hello
I've noticed that the version number in the package.json is not displayed correctly, instead a github link shows up:
image

"npm outdated" reports that it is at version 3.5.0, while hovering in Visual Studio Code reports "0.2.1".
The newest version (according to "npm outdated") is just "git".

There seems to be something majorly off with the versioning here, could you please look into that?

Allow for throttle/delay in setting active nav item

I am using your Smooth Scroll script alongside Gumshoe. I have a long document with a sidebar containing the table of contents, and when I click on a link in the sidebar, all of the links between the active item and the newly-clicked item get highlighted for a fraction of a second as they are marked as active. It's distracting and I want to avoid it. Is there a way to make it so the active nav item is only updated every x seconds so that scrolling doesn't activate a series of items until it's slowed down?

Don't change current anchor as 2byte language (ex.Korean)

i guess it only support Latin and digit.

ex. head text string is 'κ°€λ‚˜λ‹€λΌ',
ex case more.

κ°€λ‚˜λ‹€λΌ

λ§ˆλ°”μ‚¬

μ•„μžμ°¨ μΉ΄νƒ€νŒŒν•˜

gumshoe.min.js:2 Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '#%EA%B2%8C%EC%9E%84%EC%9D%98-%EC%88%9C%EC%84%9C' is not a valid selector.
    at http://localhost:4000/gitbook/gitbook-plugin-intopic-toc/gumshoe.min.js:2:2974
    at d (http://localhost:4000/gitbook/gitbook-plugin-intopic-toc/gumshoe.min.js:2:581)
    at Object.a.init (http://localhost:4000/gitbook/gitbook-plugin-intopic-toc/gumshoe.min.js:2:2932)
    at <anonymous>:1:9

offset required

Hey there,

I'm testing in Chrome and I find myself having to use {offset: 2} because:

  1. I have <h1 id="someHeader">...</h1>. When I go to #someHeader:
    1. pageYOffset = 8151
    2. nav.distance = 8152
    3. document.getElementById("Testing").getBoundingClientRect().top = 0.859375
      I didn't have time to dig in but I think there is some sub-pixel issue here causing it to round up to 8152.
  2. The code: if ( nav.distance < position ) { Shouldn't this be <= not <?

With an offset of 2, 8152 becomes 8150 and can pass the nav.distance < position check which makes it so that navigating to #someHeader actually highlights the right nav element.

Offset not taken in consideration

In version 2.0.0, in src/js/gumshoe.js at line 238

// Get current position from top of the document
var position = root.pageYOffset;

should be

// Get current position from top of the document
var position = root.pageYOffset + config.offset;

in order to work with the offset param. I suspect this is only a mistake, because config.offset isn't used anywhere in version 2.0.0

Specific element classnames

It would be ideal if, instead of applying the same class to both the list item and the link, you could specify a class for each element - or only apply the class to one element.

Reason being, if you want active links to have a red background and the link has a border radius, you will loose the radius due to both elements inheriting the class!

Great script by the way :)

NPM package

Hi

First of all, good job!

I noticed someone already asked this (#72) but do you mind publishing this in NPM as gumshoejs (or another name since gumshoe is a package already)?

NPM has a few problems in installing packages from git (npm/npm#17379) so it would be great if your package was living there. My colleagues thought in forking it and publishing it but it would be better you do it.

Thanks in advance.

Uncaught ReferenceError: eventHandler is not defined

I had to comment/remove this piece of code:

// Remove event listeners
    document.removeEventListener('resize', eventHandler, false);
    document.removeEventListener('scroll', eventHandler, false);

because eventHandler doesn't exist anywhere.

Did you maybe mean eventThrottler?

Or maybe forgot to create eventHandler? Because I've seen it in your other project smoothScroll that you have them there...

Help installing and configuring?

Hey!

First of all thanks for this project and for Smooth Scroll

I have installed smooth scroll and now I'm trying to install gumshoe in my jekyll page and I'm failing and I don't know why.

Basically I've downloaded gumshoe.min.js and installed in assets/js/plugins/gumshoe.min.js.
Then I've tried to call the function using _main.js and main.min.js located in assets/js/plugins with:

  gumshoe.init();

And them I build the main.min.js with npm run build:js after set in the package.json in the uglyfy part the path assets/js/plugins/gumshoe.min.js.

It didn't work... so I tried a more straight forward method and put in the footer:

<script src="{{ site.url }}/assets/js/plugins/gumshoe.min.js">
	gumshoe.init({
		selector: '[data-gumshoe] a', // Default link selector (must use a valid CSS selector)
		selectorHeader: '[data-gumshoe-header]', // Fixed header selector (must use a valid CSS selector)
		container: window, // The element to spy on scrolling in (must be a valid DOM Node)
		offset: 0, // Distance in pixels to offset calculations
		activeClass: 'active', // Class to apply to active navigation link and its parent list item
		scrollDelay: false, // Wait until scrolling has stopped before updating the navigation
		callback: function (nav) {} // Callback to run after setting active link
	});
</script>

It didn't work also...

what I'm missing?

you can check the repo here: https://github.com/luispuerto/gumshoe-test
The page running with gumshoe here: https://luispuerto.github.io/gumshoe-test/
A good example post here: https://luispuerto.github.io/gumshoe-test/blog/2017/11/16/the-preliminaries-for-bread-and-pizza-making/

With Smooth Scroll was really easy... I don't know what I'm missing here.

Race condition when gumshoe is destroyed

It is possible for gumshoe to be destroyed while the timeout of scrollStop is still in progress.

eventTimeout = setTimeout(function() {
    gumshoe.setDistances();
    gumshoe.getCurrentNav();
}, 66);

As a result, there is a race condition and an error is thrown as gumshoe is undefined.

This happens in React when the component using gumshoe unmounts (and destroys gumshoe), but this may well happen in other environments too.

root not being defined

commit: ff342ce
Changing this back to include window fixed my gumshoe (commonJS), otherwise root is undefined, making the script fail.

Completely refactor

Rewrite with a constructor pattern and tweak to better calculate location on the page

webpack cannot find gumshoe dependency (v4.0.1)

After installing gumshoe v4.0.1 from Github repository
(as that release hadn't been published to npm),
webpack fails to find it:

    "gumshoe": "https://github.com/cferdinandi/gumshoe.git#v4.0.1",
import Gumshoe from 'gumshoe';
This dependency was not found:
* gumshoe in ./resources/assets/scripts/routes/common.js
To install it, you can run: npm install --save gumshoe

In node_modules there is a gumshoe directory for the gumshoe v4.0.1 module.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.