💑 ~ 

JavaScriptでHoverと相互Hoverを実装する

2023.04.15
RELATED CATEGORY
TABLE OF CONTENTS

CodePen

HTML

// 単体Hover
<button class="js-hover">ボタン</button>

// 相互リンクHover
<button class="js-hoverLink" id="btn1" data-target-id="btn2">ボタン1</button>
<button class="js-hoverLink" id="btn2" data-target-id="btn1">ボタン2</button>
※ `data-target-id`にリンクさせるターゲットの`id`を指定する

CSS

.js-hover,
.js-hoverLink{
  cursor: pointer;
}

@media (hover: hover) {
  .js-hover.is-hover {
    // 単体Hoverのアニメーション処理
  }
  .js-hover.is-hoverLink {
    // 相互リンク時のアニメーション処理
  }
}

JavaScript

export class Hover {
  constructor() {
    this.isPageEnter = true;
    this.isMatchMediaHover = window.matchMedia("(hover: hover)").matches;
    this.interval = 400; // `HoverEnterイベント`と`HoverLeaveイベント`のdom更新の処理が被らないようにするため

    this.className = {
      hover: "is-hover",
      hoverLink: "is-hoverLink",
    };

    this.toHoverEnter = this.toHoverEnter.bind(this);
    this.toHoverLeave = this.toHoverLeave.bind(this);
    this.toHoverLinkEnter = this.toHoverLinkEnter.bind(this);
    this.toHoverLinkLeave = this.toHoverLinkLeave.bind(this);
  }

  /**
   * @param {number} time 遅延時間
   * @returns
   */
  delay(time) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, time);
    });
  }

  reset() {
    this.isPageEnter = false;

    if (this.hoverList.length > 0) {
      this.hoverList.forEach((ele, i) => {
        ele.removeEventListener("mouseenter", this.toHoverEnter);
        ele.removeEventListener("mouseleave", this.toHoverLeave);
        if (this.hoverList.length - 1 === i) {
          this.hoverList = [];
        }
      });
    }
    if (this.linkList.length > 0) {
      this.linkList.forEach((ele, i) => {
        ele.removeEventListener("mouseenter", this.toHoverLinkEnter);
        ele.removeEventListener("mouseleave", this.toHoverLinkLeave);
        if (this.linkList.length - 1 === i) {
          this.linkList = [];
        }
      });
    }
  }

  /**
   * @param {event} e // `mouseenter`イベント
   */
  toHoverEnter(e) {
    const ele = e.target;
    let isHover = ele.getAttribute("data-hover") === "true";
    if (isHover && this.isPageEnter) {
      ele.classList.add(this.className.hover);
      ele.setAttribute("data-hover", "false");

      setTimeout(() => {
        isHover = ele.getAttribute("data-hover");
        if (isHover === "out") {
          // `toHoverEnter`の処理完了を待たずにHoverLeaveした時の処理
          ele.setAttribute("data-hover", "leave");
          this.toHoverLeave(e);
        } else if (isHover === "false") {
          // Hover中の処理
          ele.setAttribute("data-hover", "leave");
        } else {
          // それ以外
        }
      }, this.interval);
    }
  }

  /**
   * @param {event} e // `mouseleave`イベント
   */
  toHoverLeave(e) {
    if (this.isPageEnter) {
      const ele = e.target,
        isHover = ele.getAttribute("data-hover");
      if (isHover === "leave") {
        !(async () => {
          await this.delay(0);
          ele.setAttribute("data-hover", "run");

          await this.delay(100);
          ele.classList.remove(this.className.hover);

          await this.delay(this.interval);
          let delay = 0;
          if (ele.classList.contains(this.className.hover)) {
            ele.classList.remove(this.className.hover);
            delay = this.interval;
          }
          setTimeout(() => {
            ele.setAttribute("data-hover", "true");
          }, delay);
        })();
      } else if (isHover === "false") {
        // `toHoverEnter`の処理完了を待たずにHoverLeaveした時の処理
        ele.setAttribute("data-hover", "out");
      } else {
        // それ以外
      }
    }
  }

  /**
   * @param {event} e // `mouseenter`イベント
   */
  toHoverLinkEnter(e) {
    const ele = e.target;
    let isHover = ele.getAttribute("data-hover") === "true";

    if (isHover && this.isPageEnter) {
      // 相互リンク先を取得
      const target = document.getElementById(ele.getAttribute("data-target-id"));

      // ステートを更新
      ele.setAttribute("data-hover", "false");
      ele.classList.add(this.className.hover);
      target.setAttribute("data-hover", "false");
      target.classList.add(this.className.hoverLink);

      setTimeout(() => {
        isHover = ele.getAttribute("data-hover");
        if (isHover === "out") {
          // `toHoverLinkEnter`の処理完了を待たずにHoverLeaveした時の処理
          ele.setAttribute("data-hover", "leave");
          target.setAttribute("data-hover", "leave");
          this.toHoverLinkLeave(e);
        } else if (isHover === "false") {
          // Hover中の処理
          ele.setAttribute("data-hover", "leave");
          target.setAttribute("data-hover", "leave");
        } else {
          // それ以外
        }
      }, this.interval);
    }
  }

  /**
   * @param {event} e // `mouseleave`イベント
   */
  toHoverLinkLeave(e) {
    if (this.isPageEnter) {
      const ele = e.target,
        target = document.getElementById(ele.getAttribute("data-target-id")),
        isHover = ele.getAttribute("data-hover");

      if (isHover === "leave") {
        // ステートを更新
        !(async () => {
          await this.delay(0);
          ele.setAttribute("data-hover", "run");
          target.setAttribute("data-hover", "run");

          await this.delay(100);
          ele.classList.remove(this.className.hover);
          target.classList.remove(this.className.hoverLink);

          await this.delay(this.interval);
          let delay = 0;
          if (ele.classList.contains(this.className.hover)) {
            ele.classList.remove(this.className.hover);
            delay = this.interval;
          }
          if (target.classList.contains(this.className.hover)) {
            target.classList.remove(this.className.hover);
            delay = this.interval;
          }
          setTimeout(() => {
            ele.setAttribute("data-hover", "true");
            target.setAttribute("data-hover", "true");
          }, delay);
        })();
      } else if (isHover === "false") {
        // `toHoverLinkEnter`の処理完了を待たずにHoverLeaveした時の処理
        // ステートを更新
        ele.setAttribute("data-hover", "out");
        target.setAttribute("data-hover", "out");
      } else {
        // それ以外
      }
    }
  }

  init() {
    this.isPageEnter = true;
    this.hoverList = [];
    this.linkList = [];

    if (this.isMatchMediaHover) {
      // --------------------------

      // 相互リンクHover

      // --------------------------
      this.linkList = [...document.querySelectorAll(".js-hoverLink")];
      if (this.linkList.length > 0) {
        this.linkList.forEach((ele) => {
          ele.setAttribute("data-hover", true);
          ele.classList.remove(this.className.hover);

          ele.addEventListener("mouseenter", this.toHoverLinkEnter, {
            passive: true,
          });
          ele.addEventListener("mouseleave", this.toHoverLinkLeave);
        });
      }

      // --------------------------

      // 単体Hover
      // 相互リンクHoverより下に記述すること・・・アニメーションのバッディングが生じるため

      // --------------------------
      this.hoverList = [...document.querySelectorAll(".js-hover")];
      if (this.hoverList.length > 0) {
        this.hoverList.forEach((ele) => {
          ele.setAttribute("data-hover", true);
          ele.classList.remove(this.className.hover);

          ele.addEventListener("mouseenter", this.toHoverEnter, {
            passive: true,
          });
          ele.addEventListener("mouseleave", this.toHoverLeave);
        });
      }
    }
  }
}

PICKUP ARTWORK