JavaScriptのIntersectionObserverでメニューアイテムを動的にハイライトする方法

JavaScript

JavaScriptのIntersectionObserverでメニューアイテムを動的にハイライトする方法を紹介します。

JavaScriptのIntersectionObserverでメニューアイテムを動的にハイライトする手順

  • ScrollObserverクラスを作成してIntersectionObserverを初期化
  • Mainクラスを作成してScrollObserverクラスを活用

ScrollObserverクラスを作成してIntersectionObserverを初期化

class ScrollObserver {
  constructor(els, cb, options) {
    this.els = document.querySelectorAll(els);
    const defaultOptions = {
      root: null,
      rootMargin: '0px',
      threshold: 0,
      once: true,
    };
    this.cb = cb;
    this.options = Object.assign(defaultOptions, options);
    this.once = this.options.once;
    this._init();
  }
  _init() {
    const callback = function (entries, observer) {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.cb(entry.target, true);
          if (this.once) {
            observer.unobserve(entry.target);
          }
        } else {
          this.cb(entry.target, false);
        }
      });
    };

    this.io = new IntersectionObserver(callback.bind(this), this.options);

    this.els.forEach(el => this.io.observe(el));
  }

  destroy() {
    this.io.disconnect();
  }
}

html

<header class="header">
  <nav class="header__nav">
    <img class="header__logo" src="../src/img/logo.png" alt="" />
    <div class="header__nav-container">
      <ul class="header__nav-list">
        <li class="header__nav-item">
          <a href="#home" class="header__nav-link">Home</a>
        </li>
        <li class="header__nav-item">
          <a href="#fields" class="header__nav-link">Science Fields</a>
        </li>
        <li class="header__nav-item">
          <a href="#courses" class="header__nav-link">Course Types</a>
        </li>
        <li class="header__nav-item">
          <a href="#facilities" class="header__nav-link">Facilities</a>
        </li>
        <li class="header__nav-item">
          <a href="#awards" class="header__nav-link">Awards</a>
        </li>
        <li class="header__nav-item">
          <a href="#branches" class="header__nav-link">Branches</a>
        </li>
        <li class="header__nav-item">
          <a href="#contact" class="header__nav-link">Contact Us</a>
        </li>
      </ul>
      <div class="sns">
        <a href="#" class="sns__link"><i class="fab fa-facebook-f"></i></a>
        <a href="#" class="sns__link"><i class="fab fa-twitter"></i></a>
        <a href="#" class="sns__link"><i class="fab fa-github"></i></a>
      </div>
    </div>
    <button class="header__mobile-menu">
      <span class="header__line">&nbsp;</span>
    </button>
  </nav>
</header>
<section class="section-hero" id="home">
...
<section class="section-fields" id="fields">
...

Mainクラスを作成してScrollObserverクラスを活用

document.addEventListener('DOMContentLoaded', () => {
  const main = new Main();
});

class Main {
  constructor() {
    this.sections = [
      '.section-hero',
      '.section-fields',
      '.section-courses',
      '.section-facilities',
      '.section-awards',
      '.section-branches',
      '.section-contact',
    ];
    this.navItems = document.querySelectorAll('.header__nav-item');
    this._observers = [];
    this._init();
  }

  set observers(val) {
    this._observers.push(val);
  }

  get observers() {
    return this._observers;
  }

  _init() {
    this._scrollInit();
  }

  //   ビュー内のsectionのメニューアイテムをハイライト
  _highlightMenuItem(el, inview) {
    //   elはインスタンス化したScrollObserverのsection、inviewはintersectingのboolean
    this.navItems.forEach(item => {
      const itemHref = item.childNodes[0].getAttribute('href');
      const activeSection = el.getAttribute('id');
      if (!inview) return;
      if (inview) {
        if (activeSection === itemHref.slice(1)) {
          item.childNodes[0].classList.add('u-active-item');
        } else {
          item.childNodes[0].classList.remove('u-active-item');
        }
      }
    });
  }

  _destroyObservers() {
    this.observers.forEach(ob => {
      ob.destroy();
    });
  }

  destroy() {
    this._destroyObservers();
  }

  _scrollInit() {

    // メニューの現在のアイテムのハイライト
    this.sections.forEach(section => {
      this.observers = new ScrollObserver(
        section,
        this._highlightMenuItem.bind(this),
        { once: false, rootMargin: '-50% 0px' }
      );
    });
  }
}

constructor内のsectionsのclass名とナビゲーションメニューのアイテムのclass名は、適宜変更します。
highlightMenuItemというメソッドを作成しています。
elはインスタンス化したScrollObserverのsectionが渡されます。
inviewはintersectingのbooleanです。
navタグ内のアイテムをquerySelectorAllですべて選択し、そのhref属性に指定されている飛び先と、sectionのidと合致するものにactiveとわかるクラスを付与します。
交差は、inviewで判定します。
各sectionのidは、elからアクセスすることができます。
そのために、sectionsでそれぞれのクラスなりを格納して、ScrollObserverを初期化するときに、渡せるようにします。