Javascript 滚动侦测导航

一个以前的小项目要改,新增一个根据时间线显示不同月份绩效的需求。
没有设计稿,想了下怎么做出来好看。就有了以下这个想法

左侧显示时间线,滑动会显示不同的年份和月份,
右侧显示不同月份的统计数据。

view

为了显示这个想法不是很蠢,所以需要做的很 酷炫
其实就是交互友好些,左右块都有相应的交互动作。


预想的效果

设想的是,点击左侧月份之后右侧会快速滚动到相应的月份,方便用户操作。
当然右侧下滑操作的时候左侧的时间线也会对应的滚动。

那么就要用到滚动侦测了,以前都是用的 UI 框架里边的,
所以这次也是直接去翻阅了这个项目所用到的 UI,MuseUI 的文档,
当然里边没有这个组件,不然也不会有这篇笔记了

去翻阅了一下 BootStrapScrollspy 源码,其实就是用到了内容元素的 offsetTop 和滚动条的监听


后端给的数据格式是类似这样的

{
  date:'2020-01',
  detail:{
    a:100,
    b:78,
    total:178,
    machines:967
  },
  note:'some text of 2020-01'
},

所以可以直接使用 computed 返回所有的月份,对,我使用的 Vue 作为框架。

computed: {
  timeline() {
    let list = [];
    const data = this.sourceData;
    if (!data) return list;
    data.forEach(item => {
      const date = item.date.split("-");
      const year = list.find(item => item.year === date[0]);
      if (year) {
        year.months.push(date[1]);
      } else {
        list.push({
          year: date[0],
          months: [date[1]]
        });
      }
    });
    return list;
  },
},

尝试过直接使用对象,用年份作为字段名,但是使用 v-for 循环的时候会按照升序打印出来,
折腾了有一会放弃了,还是使用数组,记不得前段时间自己是怎么直接用对象实现的时间分组的了 😂 -> Js 对象 调整属性排序是否有意义
其实差不多只是匹配的时候稍微麻烦点需要用到 find() 方法。

然后也根据后台返回的数据来计算右侧内容部分每一个月份的 offsetTop

computed:{
  offsetList() {
    const list = this.$refs["month-detail"];
    let data = list.map(el => {
      return {
        date: el.getAttribute("date"),
        offset: el.offsetTop - 100
      };
    });
    return data;
  }
}

这块其实很简单,直接在循环输出的时候注册 ref 即可,然后直接遍历 DOM 元素数组保存 offsetTop 。(但是如果是动态改变的DOM就不能使用 computed 来计算了,具体查看 使用VueJS的计算属性监听DOM元素属性的问题

然后是点击左侧时间轴右侧内容部分滚动到对应的月份,

methods:{
  // 跳转到对应月份
  toMonth(year, month) {
    this.currTime = `${year}-${month}`;
    const detailItem = this.offsetList.find(
      item => item.date === this.currTime
    );
    this.$refs["wrap"].scrollTo({
      top: detailItem.offset,
      behavior: "smooth"
    });
  },
}

这里有一个 Js 的新东西 behavior: "smooth" 是原生滚动的一个新 API,应该是新 API 哈,以前都没有见到过,这次才发现有这个 Option,也可以考虑使用 CSS 来实现,但是听说兼容性堪忧。
最后加上右侧内容的滚动事件绑定 <div class="wrap" ref="wrap" @scroll="scrollSpyNav">

watch: {
  currTime() {
    this.scrollTimeline();
  }
},
methods:{
  // 滚动侦测导航
  scrollSpyNav(e) {
    clearTimeout(this.timer);
    const offsetTop = e.target.scrollTop;
    const curr = this.offsetList.find(item => item.offset >= offsetTop);
    this.timer = window.setTimeout(() => {
      this.currTime = curr.date;
    }, 300);
  },
  // 滚动左侧时间线
  scrollTimeline() {
    const el = this.$refs["month-block"].find(
      item => item.getAttribute("date") === this.currTime
    );
    this.$refs["timeline"].scrollTo({
      top: el.offsetTop - 50,
      behavior: "smooth"
    });
  },
}

直接使用了 watch 来侦听的了日期的改变,来触发左侧时间线的滚动,也实现了点击时间轴自动置顶当前月份的效果。
顺带写了个计时器,防止抖动….

🌰 DEMO

兼容

  • JavaScript - scrollTo [MDN]
    scrollTo
  • CSS - scroll-behavior:smooth [Can I use]
    scroll-behavior