HTML文档的内容可能非常长,难以通过滚动来访问。 由于这个艰巨的任务,开发人员经常使用内部链接(页面跳转)作为页面周围的另一种传输模式。这个有用的技术已经改进,有了 Javascript 的帮助可以提供更好的体验,我们可以借助 Gumshoe, Smooth Scroll 和 Anime.js 自定义滚动监听脚本。滚动监听主要用于根据滚动位置自动更新导航列表中的链接。
通过这个教程,我们将构建一个自定义滚动监听( Scrollspy )组件。效果如下:
javascriptscrollspy
要完成这个自定义滚动监听( Scrollspy )我们将使用:

  • Gumshoe:简单,与框架无关的滚动监听脚本。
  • Smooth Scroll:轻量脚本动画滚动到锚点链接。
  • Anime.js:灵活而轻巧的 Javascript 动画库。

可以点上面的链接访问它们的 Github ,了解它们的功能和使用方法。

HTML

让我们从我们将要使用的HTML结构开始,描述注释中的关键元素:

<section>
    <!-- Fixed header -->
    <!-- The [data-gumshoe-header] attribute tell Gumshoe that automatically offset it's calculations based on the header's height -->
    <!-- The [data-scroll-header] attribute do the same thing but for Smooth Scroll calculations -->
    <header class="page-header" data-gumshoe-header data-scroll-header>
        <div class="page-nav">
            <!-- Nav and links -->
            <!-- The [data-gumshoe] attribute indicates the navigation list that Gumshoe should watch -->
            <nav data-gumshoe>
                <!-- Turn anchor links into Smooth Scroll links by adding the [data-scroll] data attribute -->
                <a data-scroll href="#eenie">Eenie</a>
                <a data-scroll href="#meanie">Meanie</a>
                <a data-scroll href="#minnie">Minnie</a>
                <a data-scroll href="#moe">Moe</a>
            </nav>
            <!-- Arrows -->
            <a class="nav-arrow nav-arrow-left"><svg class="icon"><use xlink:href="#arrow-up"/></svg></a>
            <a class="nav-arrow nav-arrow-right"><svg class="icon"><use xlink:href="#arrow-down"/></svg></a>
        </div>
    </header>
    <!-- Page content -->
    <main class="page-content">
        <section>
            <h2 id="eenie"><a data-scroll href="#eenie">Eenie</a></h2>
            <p>Lorem ipsum dolor sit amet, has dico eligendi ut.</p>
            <!-- MORE CONTENT HERE -->
        </section>
    </main>
</section>

CSS

为上面的 HTML 添加一些风格。


h2 {
  /* This is to solve the headbutting/padding issue. Read more: https://css-tricks.com/hash-tag-links-padding/ */
  /* 110px = 80px (fixed header) + 30px (additional margin) */
  &:before {
    display: block;
    content: " ";
    margin-top: -110px;
    height: 110px;
    visibility: hidden;
  }
}

/* Fixed header */
.page-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 80px; /* The height of fixed header */
  background-color: #2D353F;
  text-align: center;
  z-index: 2;
}

/* Content container */
.page-content {
  display: inline-block; /* This is for clearing purpose. */
  margin: 80px 50px 30px; /* Margin top = 80px because of fixed header */
}

/* Nav container */
.page-nav {
  display: inline-block;
  position: relative;
  margin-top: 20px;
  height: 40px; /* This is the same height of each link */
  width: 400px;
  max-width: 100%; /* Responsive behavior */
  overflow: hidden; /* Only current link visible */
  background-color: #427BAB;
}

/* Nav and links */
nav {
  position: relative;
  width: 100%;
  line-height: 40px;
  text-align: center;
  background-color: rgba(0, 0, 0, 0.05);

  a {
    display: block;
    font-size: 18px;
    color: #fff;
    outline: none;
  }
}

JAVASCRIPT

开始增加功能和动画,我们需要获得需要此功能的所有元素。此外,我们将声明使用的附加变量。


// Init variables
var navOpen = false;
var pageNav = document.querySelector('.page-nav');
var navEl = document.querySelector('.page-nav nav');
var navLinks = document.querySelectorAll('.page-nav nav a');
var arrowLeft = document.querySelector('.nav-arrow-left');
var arrowRight = document.querySelector('.nav-arrow-right');
var navHeight = 40;
var activeIndex, activeDistance, activeItem, navAnimation, navItemsAnimation;

以下是关键部分。此函数将 nav 元素转换为仅显示所选链接,使用 activeIndex 值。


// This translate the nav element to show the selected item
function translateNav(item) {
    // If animation is defined, pause it
    if (navItemsAnimation) navItemsAnimation.pause();
    // Animate the `translateY` of `nav` to show only the current link
    navItemsAnimation = anime({
        targets: navEl,
        translateY: (item ? -activeIndex * navHeight : 0) + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
    // Update link on arrows, and disable/enable accordingly if first or last link
    updateArrows();
}

然后,我们需要一种方法来打开和关闭 。打开状态应该让我们看到所有的链接,让我们可以直接选择其中之一。关闭状态是默认关闭状态,只看到选定的链接。


// Open the nav, showing all the links
function openNav() {
    // Updating states
    navOpen = !navOpen;
    pageNav.classList.add('nav-open');
    // Moving the nav just like first link is active
    translateNav();
    // Animate the `height` of the nav, letting see all the links
    navAnimation = anime({
        targets: pageNav,
        height: navLinks.length * navHeight + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
}

// Close the nav, showing only the selected link
function closeNav() {
    // Updating states
    navOpen = !navOpen;
    pageNav.classList.remove('nav-open');
    // Moving the nav showing only the active link
    translateNav(activeItem);
    // Animate the `height` of the nav, letting see just the active link
    navAnimation = anime({
        targets: pageNav,
        height: navHeight + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
}

现在让我们来看看如何处理事件。我们需要处理程序来相应地打开或关闭 nav


// Init click events for each nav link
for (var i = 0; i < navLinks.length; i++) {
    navLinks[i].addEventListener('click', function (e) {
        if (navOpen) {
            // Just close the `nav`
            closeNav();
        } else {
            // Prevent scrolling to the active link and instead open the `nav`
            e.preventDefault();
            e.stopPropagation();
            openNav();
        }
    });
}
document.addEventListener('click', function (e) {
    if (navOpen) {
        var isClickInside = pageNav.contains(e.target);
        if (!isClickInside) {
            closeNav();
        }
    }
});

现在,准备让 GumshoeSmooth Scroll 变魔术。看看如何初始化它们:

// Init Smooth Scroll

smoothscroll-init


// Init Gumshoe
gumshoe.init({
    // The callback is triggered after setting the active link, to show it as active in the `nav`
    callback: function (nav) {
        // Check if active link has changed
        if (activeDistance !== nav.distance) {
            // Update states
            activeDistance = nav.distance;
            activeItem = nav.nav;
            activeIndex = getIndex(activeItem);
            // Translate `nav` to show the active link, or close it
            if (navOpen) {
                closeNav();
            } else {
                translateNav(activeItem);
            }
        }
    }
});

完成!