💑 ~
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);
});
}
}
}
}