导航

-- 定位与高亮

课程概要

本课程主要讲解导航栏常见的定位与高亮,在日常网页中,我们经常可以发现随着页面的滚动,导航栏的位置有时候会发生变化,同时和他对应位置的项目会有高亮效果,当我们点击导航栏时,页面会定位到目标位置。通过本课程,完成导航的定位与高亮交互,大家将对页面的滚动和定位有更深入的了解。

主要包含以下功能:

  • 滚动超过 banner 区域,固定定位在顶部
  • 滚动到与导航内容匹配的区域,导航内容高亮
  • 点击导航内容,定位到内容所匹配的位置

知识点

本课程涉及到的主要知识点有:

  • document.documentElement.scrollTop 浏览器滚动的位置
let scrollTop = document.documentElement.scrollTop;
console.log(scrollTop);
  • scroll 滚动事件
window.addEventListener('scroll',function(){
  console.log(document.documentElement.scrollTop)
});
  • offsetTop 相对父级元素顶部的距离
<div class="list">
  <div class="item"></div>
  <div class="item" id="item-2"></div>
</div>
let offsetTop = document.getElementById('item-2').offsetTop;
console.log(offsetTop);

实现步骤

  1. HTML & CSS
  2. 构建单例对象PAGE
  3. 导航滚动定位
  4. 导航滚动高亮
  5. 导航点击移动

HTML & CSS

  1. HTML
<div class="section-banner h400">SECTION Banner</div>
<div class="navigator-bar" id="navigator-bar">
  <ul>
    <li class="navigator-bar-item" data-nav="section-1">SECTION 1</li>
    <li class="navigator-bar-item" data-nav="section-2">SECTION 2</li>
    <li class="navigator-bar-item" data-nav="section-3">SECTION 3</li>
    <li class="navigator-bar-item" data-nav="section-4">SECTION 4</li>
    <li class="navigator-bar-item" data-nav="section-5">SECTION 5</li>
  </ul>
</div>
<div class="h400" id="section-1">SECTION 1</div>
<div class="h400" id="section-2">SECTION 2</div>
<div class="h400" id="section-3">SECTION 3</div>
<div class="h400" id="section-4">SECTION 4</div>
<div class="h400" id="section-5">SECTION 5</div>
<div class="h800">SECTION Footer</div>
  1. css
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.h400{
  height: 400px;
  border: 1px solid #e4e4e4;
  color: #666;
  padding: 20px;
}
.h800{
  height: 800px;
  border: 1px solid #e4e4e4;
  color: #666;
  padding: 20px;
}
.navigator-bar {
  height: 60px;
}
.navigator-bar ul{
  display: flex;
  list-style-type: none;
  background: #f5f5f5;
}
.navigator-bar.fixed-top ul{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
}
.navigator-bar-item{
  flex: 1;
  line-height: 60px;
  text-align: center;
  cursor: pointer;
  color: #666;
  transition: all .2s ease;
}
.navigator-bar-item:hover{
  background: #666;
  color: #fff;
}
.navigator-bar-item.active{
  background: #333;
  color: #fff;
}

构建单例对象PAGE

  1. PAGE
  2. init、bind
  3. PAGE.init
  4. PAGE.data
  • navigatorBarIdArr 存储导航定位的容器 id
  • navigatorBarActiveId 当前导航高亮 id
  • navigatorBarFixed 导航是否定位在顶部
  • navigatorBarFixedOffset 导航距离顶部的高度
  • navigatorBarHeight 导航自身高度
const PAGE = {
  data: {
    navigatorBarIdArr: ['section-1','section-2','section-3','section-4','section-5'],
    navigatorBarActiveId: '',
    navigatorBarFixed: false,
    navigatorBarFixedOffset: 460,
    navigatorBarHeight: 60,
  },
  init: function() {
    this.bind();
  },
  bind: function() {

  }
}

PAGE.init();

导航滚动定位

  1. 为 window 绑定 scroll 事件 refreshNavigator
  2. 创建 PAGE.refreshNavigator 方法,并调用 PAGE.fixedNavigator 方法
  3. 创建 PAGE.fixedNavigator,用于判断定位导航
  4. 获取当前浏览器滚动的距离
  5. 获取导航需要定位的距离
  6. 比较滚动距离和定位距离,如果滚动距离大于定位距离,就需要定位
  7. 获取当前浏览器是否定位的状态,如果与上一步比较的结构不一样,就执行下述方法
  8. 更新 PAGE.data 中 当前浏览器是否定位的状态
  9. 获取 navigator-bar 导航元素
  10. 判断是否要需要定位,在导航元素 className 中添加 fixed-top
  11. 如果否,就设置导航元素 className 为 navigator-bar
const PAGE = {
  data: {
    navigatorBarIdArr: ['section-1','section-2','section-3','section-4','section-5'],
    navigatorBarActiveId: '',
    navigatorBarFixed: false,
    navigatorBarFixedOffset: 460,
    navigatorBarHeight: 60,
  },
  init: function() {
    this.bind();
  },
  bind: function() {
    // 1. 为 window 绑定 scroll 事件 refreshNavigator
    window.addEventListener('scroll',this.refreshNavigator);
  },
  // 2. 创建 PAGE.refreshNavigator 方法,并调用 PAGE.fixedNavigator 方法
  refreshNavigator: function() {
    PAGE.fixedNavigator();
  },
  // 3. 创建 PAGE.fixedNavigator,用于判断定位导航
  fixedNavigator: function() {
    // 4. 获取当前浏览器滚动的距离
    let scrollTop = document.documentElement.scrollTop;
    // 5. 获取导航需要定位的距离
    let navigatorBarTop = (PAGE.data.navigatorBarFixedOffset + PAGE.data.navigatorBarHeight);
    // 6. 比较滚动距离和定位距离,如果滚动距离大于定位距离,就需要定位
    let navigatorBarFixed = scrollTop >= navigatorBarTop;
    // 7. 获取当前浏览器是否定位的状态,如果与上一步比较的结构不一样,就执行下述方法
    if( PAGE.data.navigatorBarFixed !== navigatorBarFixed){
      // 8. 更新 PAGE.data 中 当前浏览器是否定位的状态
      PAGE.data.navigatorBarFixed = navigatorBarFixed;
      // 9. 获取 navigator-bar 导航元素
      let navigatorBar = document.getElementById('navigator-bar');
      // 10. 判断是否要需要定位,在导航元素 className 中添加 fixed-top
      if(navigatorBarFixed){
        navigatorBar.className = 'navigator-bar fixed-top'
        // 11. 如果否,就设置导航元素 className 为 navigator-bar
      }else{
        navigatorBar.className = 'navigator-bar'
      }
    }
  }
}

PAGE.init();

导航滚动高亮

  1. 在 PAGE.refreshNavigator 中添加调用 PAGE.heightLightNavigator ,在每次滚动都执行
  2. 定义 PAGE.heightLightNavigator 方法
  3. 获取当前浏览器滚动高度
  4. 筛选浏览器滚动过的,需要定位的导航容器 id
  5. 获取导航定位的 id 容器距离文档顶部距离
  6. 判断滚动距离是否大于容器距离文档顶部距离
  7. 定义当前应该高亮的容器 id ,为滚动过的最后一个,如果没有就为空
  8. 判断当前应该高亮的容器和 PAGE.data 存储高亮的容器 id 是否一致
  9. 更新 PAGE.data 存储高亮的容器 id
  10. 获取所有 navigator-bar-item 导航项目元素
  11. 循环导航项目元素
  12. 获取每一个导航元素
  13. 获取导航元素的 data-nav 中对应容器的 id 值
  14. 判断元素对应容器的 id 值是否和当前应该高亮的容器的 id 值是否一致,为其 className 添加 active
  15. 如果不一致,设置回 navigator-bar-item
const PAGE = {
  data: {
    navigatorBarIdArr: ['section-1','section-2','section-3','section-4','section-5'],
    navigatorBarActiveId: '',
    navigatorBarFixed: false,
    navigatorBarFixedOffset: 460,
    navigatorBarHeight: 60,
  },
  init: function() {
    this.bind();
  },
  bind: function() {
    window.addEventListener('scroll',this.refreshNavigator);
  },
  refreshNavigator: function() {
    PAGE.fixedNavigator();
    // 1. 在 PAGE.refreshNavigator 中添加调用 PAGE.heightLightNavigator
    PAGE.heightLightNavigator();
  },
  // 2. 定义 PAGE.heightLightNavigator 方法
  heightLightNavigator: function() {
    // 3. 获取当前浏览器滚动高度
    let scrollTop = document.documentElement.scrollTop;
    // 4. 筛选浏览器滚动过的,需要定位的导航容器 id
    let filterNav = PAGE.data.navigatorBarIdArr.filter( data => {
      // 5. 获取导航定位的 id 容器距离文档顶部距离
      let offsetTop = document.getElementById(data).offsetTop;
      // 6. 判断滚动距离是否大于容器距离文档顶部距离
      return scrollTop >= offsetTop - PAGE.data.navigatorBarHeight
    })
    // 7. 定义当前应该高亮的容器 id ,为滚动过的最后一个,如果没有就为空
    let navigatorBarActiveId = filterNav.length ? filterNav[filterNav.length - 1] : '';
    // 8. 判断当前应该高亮的容器和 PAGE.data 存储高亮的容器 id 是否一致
    if(PAGE.data.navigatorBarActiveId !== navigatorBarActiveId){
      // 9. 更新 PAGE.data 存储高亮的容器 id
      PAGE.data.navigatorBarActiveId = navigatorBarActiveId;
      // 10. 获取所有 navigator-bar-item 导航项目元素
      let navigatorBarItems = document.getElementsByClassName('navigator-bar-item');
      // 11. 循环导航项目元素
      for (let i = 0; i < navigatorBarItems.length; i++) {
        // 12. 获取每一个导航元素
        let navigatorBarItem = navigatorBarItems[i];
        // 13. 获取导航元素的 data-nav 中对应容器的 id 值
        let dataNav = navigatorBarItem.dataset.nav;
        // 14. 判断元素对应容器的 id 值是否和当前应该高亮的容器的 id 值是否一致,为其 className 添加 active
        if(dataNav === navigatorBarActiveId){
          navigatorBarItem.className = 'navigator-bar-item active';
        // 15. 如果不一致,设置回 navigator-bar-item
        }else{
          navigatorBarItem.className = 'navigator-bar-item';
        }
      }
    }
  },
  fixedNavigator: function() {
    let scrollTop = document.documentElement.scrollTop;
    let navigatorBarTop = (PAGE.data.navigatorBarFixedOffset + PAGE.data.navigatorBarHeight);
    let navigatorBarFixed = scrollTop >= navigatorBarTop;
    if( PAGE.data.navigatorBarFixed !== navigatorBarFixed){
      PAGE.data.navigatorBarFixed = navigatorBarFixed;
      let navigatorBar = document.getElementById('navigator-bar');
      if(navigatorBarFixed){
        navigatorBar.className = 'navigator-bar fixed-top'
      }else{
        navigatorBar.className = 'navigator-bar'
      }
    }
  }
}

PAGE.init();

导航点击移动

  1. 添加委托绑定方法 onEventLister
  2. 在 bind 中委托绑定点击 navigator-bar-item 触发 goNavigator 事件
  3. 定义 PAGE.goNavigator 点击导航页面定位事件
  4. 获取点击元素中对应容器 id
  5. 定义该容器 id 于顶部的距离值
  6. 设置文档位置到距离中
const PAGE = {
  ...,
  bind: function() {
    window.addEventListener('scroll',this.refreshNavigator);
    // 2. 在 bind 中委托绑定点击 navigator-bar-item 触发 goNavigator 事件
    this.onEventLister(navigatorBar,'click','navigator-bar-item',this.goNavigator);
  },
  // 1. 添加委托绑定方法 onEventLister
  onEventLister: function(parentNode,action,childClassName,callback) {
    parentNode.addEventListener(action,function(e){
      e.target.className.indexOf(childClassName) >= 0 && callback(e);
    })
  },
  ...,
  // 3. 定义 PAGE.goNavigator 点击导航页面定位事件
  goNavigator: function(e) {
    // 4. 获取点击元素中对应容器 id 
    let id = e.target.dataset.nav;
    // 5. 定义该容器 id 于顶部的距离值
    let offsetTop = document.getElementById(id).offsetTop - PAGE.data.navigatorBarHeight;
    // 6. 设置文档位置到距离中
    document.documentElement.scrollTop = offsetTop;
  }
}

PAGE.init();

代码示例

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>NavigatorBar</title>
  <style type="text/css">
    *{
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    .h400{
      height: 400px;
      border: 1px solid #e4e4e4;
      color: #666;
      padding: 20px;
    }
    .h800{
      height: 800px;
      border: 1px solid #e4e4e4;
      color: #666;
      padding: 20px;
    }
    .navigator-bar {
      height: 60px;
    }
    .navigator-bar ul{
      display: flex;
      list-style-type: none;
      background: #f5f5f5;
    }
    .navigator-bar.fixed-top ul{
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
    }
    .navigator-bar-item{
      flex: 1;
      line-height: 60px;
      text-align: center;
      cursor: pointer;
      color: #666;
      transition: all .2s ease;
    }
    .navigator-bar-item:hover{
      background: #666;
      color: #fff;
    }
    .navigator-bar-item.active{
      background: #333;
      color: #fff;
    }
  </style>
</head>
<body>
  <div class="section-banner h400">SECTION Banner</div>
  <div class="navigator-bar" id="navigator-bar">
    <ul>
      <li class="navigator-bar-item" data-nav="section-1">SECTION 1</li>
      <li class="navigator-bar-item" data-nav="section-2">SECTION 2</li>
      <li class="navigator-bar-item" data-nav="section-3">SECTION 3</li>
      <li class="navigator-bar-item" data-nav="section-4">SECTION 4</li>
      <li class="navigator-bar-item" data-nav="section-5">SECTION 5</li>
    </ul>
  </div>
  <div class="h400" id="section-1">SECTION 1</div>
  <div class="h400" id="section-2">SECTION 2</div>
  <div class="h400" id="section-3">SECTION 3</div>
  <div class="h400" id="section-4">SECTION 4</div>
  <div class="h400" id="section-5">SECTION 5</div>
  <div class="h800">SECTION Footer</div>

  <script type="text/javascript">
    const PAGE = {
      data: {
        navigatorBarIdArr: ['section-1','section-2','section-3','section-4','section-5'],
        navigatorBarActiveId: '',
        navigatorBarFixed: false,
        navigatorBarFixedOffset: 460,
        navigatorBarHeight: 60,
      },
      init: function() {
        this.bind();
      },
      bind: function() {
        window.addEventListener('scroll',this.refreshNavigator);
        let navigatorBar = document.getElementById('navigator-bar');
        this.onEventLister(navigatorBar,'click','navigator-bar-item',this.goNavigator);
      },
      onEventLister: function(parentNode,action,childClassName,callback) {
        parentNode.addEventListener(action,function(e){
          e.target.className.indexOf(childClassName) >= 0 && callback(e);
        })
      },
      refreshNavigator: function(e) {
        PAGE.fixedNavigator();
        PAGE.heightLightNavigator();
      },
      fixedNavigator: function() {
        let scrollTop = document.documentElement.scrollTop;
        let navigatorBarTop = (PAGE.data.navigatorBarFixedOffset + PAGE.data.navigatorBarHeight);
        let navigatorBarFixed = scrollTop >= navigatorBarTop;
        if( PAGE.data.navigatorBarFixed !== navigatorBarFixed){
          PAGE.data.navigatorBarFixed = navigatorBarFixed;
          let navigatorBar = document.getElementById('navigator-bar');
          if(navigatorBarFixed){
            navigatorBar.className = 'navigator-bar fixed-top'
          }else{
            navigatorBar.className = 'navigator-bar'
          }
        }
      },
      heightLightNavigator: function() {
        let scrollTop = document.documentElement.scrollTop;
        let filterNav = PAGE.data.navigatorBarIdArr.filter( data => {
          let offsetTop = document.getElementById(data).offsetTop;
          return scrollTop >= offsetTop - PAGE.data.navigatorBarHeight
        })
        let navigatorBarActiveId = filterNav.length ? filterNav[filterNav.length - 1] : '';
        if(PAGE.data.navigatorBarActiveId !== navigatorBarActiveId){
          PAGE.data.navigatorBarActiveId = navigatorBarActiveId;
          let navigatorBarItems = document.getElementsByClassName('navigator-bar-item');
          for (let i = 0; i < navigatorBarItems.length; i++) {
            let navigatorBarItem = navigatorBarItems[i];
            let dataNav = navigatorBarItem.dataset.nav;
            if(dataNav === navigatorBarActiveId){
              navigatorBarItem.className = 'navigator-bar-item active';
            }else{
              navigatorBarItem.className = 'navigator-bar-item';
            }
          }
        }
      },
      goNavigator: function(e) {
        let id = e.target.dataset.nav;
        let offsetTop = document.getElementById(id).offsetTop - PAGE.data.navigatorBarHeight;
        document.documentElement.scrollTop = offsetTop;
      }
    }

    PAGE.init();
  </script>
</body>
</html>