🐉 ~
汎用JavaScriptのまとめ
2023.04.17
RELATED CATEGORY
TABLE OF CONTENTS
筆者自身が汎用的に使用する以下のJavaScript(VanillaJs)をまとめました。
今後も日々アップデートしていきます。
スムーススクロール(アンカーリンク)
CodePen
See the Pen Smooth scroll to Anker link by ShoyaKajita (@ShoyaKajita) on CodePen.
Code
export class AnkerLink {
constructor() {
this.idName = {
header: "header",
scrollTop: "scrollTop",
};
this.distanceFromTop = 0;
}
/**
* スムーススクロール
* @param {number} y
* @param {number} x
*/
scrollTo(y = 0, x = 0) {
window.scrollTo({
top: y,
left: x,
behavior: "smooth", // ページ内ではスムーススクロールを設定
});
}
/**
* 移動先の座標を取得しスムーススクロールをする
* @param {event} e // クリックイベント
*/
setScrollPosition(e) {
e.preventDefault();
const href = e.target.getAttribute("href"),
string = href.slice(0, 1),
header = document.getElementById(this.idName.header),
offset = header
? header.getBoundingClientRect().height + this.distanceFromTop
: this.distanceFromTop;
if (string === "#" && (href != "" || href != "#")) {
const target = document.querySelector(href);
if (target) {
const y =
target.getBoundingClientRect().top - offset + window.pageYOffset;
this.scrollTo(y, 0);
}
}
}
/**
* 初期位置の座標を取得し移動する
*/
setInitPosition() {
let id = null,
y = 0;
const delay = 200,
hash = window.location.hash,
query = this.getParameter("id"),
header = document.getElementById(this.idName.header),
offset = header
? header.getBoundingClientRect().height + this.distanceFromTop
: this.distanceFromTop;
if (hash != "") {
id = document.querySelector(hash);
if (id != null) {
y = id.getBoundingClientRect().top - offset + window.pageYOffset;
setTimeout(() => {
window.scrollTo(0, y);
}, delay);
}
} else if (query != "" && query != null) {
id = document.getElementById(`${query}`);
if (id != null) {
y = id.getBoundingClientRect().top - offset + window.pageYOffset;
setTimeout(() => {
window.scrollTo(0, y);
}, delay);
}
}
}
/**
* GETパラメータのキーから値を取得し返す
* @param {string} name // 取得したいGETパラメータのキー
* @returns GETパラメータ値
*/
getParameter(name) {
// https://www-creators.com/archives/4463
name = name.replace(/[\[\]]/g, "\\$&");
let regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)"),
results = regex.exec(window.location.href);
if (!results) return null;
if (!results[2]) return "";
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
init() {
this.setInitPosition();
const linkList = [...document.querySelectorAll("a[href^='#']")];
if (linkList) {
linkList.forEach((ele) => {
ele.addEventListener("click", this.setScrollPosition.bind(this));
});
}
const scrollTop = document.getElementById(this.idName.scrollTop);
if (scrollTop) {
scrollTop.addEventListener("click", (e) => {
this.scrollTo(0, 0);
});
}
}
}
IntersectionObserver
/*
// --------------------------
// html
// --------------------------
// 監視要素 | 「data-delay」が最優先される
<div class="js-observer" data-delay="0" data-delay-pc="0" data-delay-sp="0"></div>
// --------------------------
// css
// --------------------------
.js-observer.is-cue {
// 発火したスタイルを記述する
}
*/
export class Observer {
constructor() {
this.isMatchMedia = window.matchMedia("(max-width: 768px)").matches;
this.config = {
root: null,
rootMargin: "0% 0%",
threshold: 0,
};
}
reset() {
this.targetList = [];
this.destroy();
this.observer = null;
}
/**
* @param {object} 複数の監視要素の情報が格納されている
*/
entry(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const ele = entry.target;
const dataDelay = ele.getAttribute("data-delay");
let delay = dataDelay ? dataDelay : 0;
if (this.isMatchMedia) {
const dataDelaySp = ele.getAttribute("data-delay-sp");
if (dataDelaySp) delay = dataDelaySp;
} else {
const dataDelayPc = ele.getAttribute("data-delay-pc");
if (dataDelayPc) delay = dataDelayPc;
}
setTimeout(() => {
if (ele) ele.classList.add("is-cue");
}, delay);
}
});
}
destroy() {
this.observer.disconnect(); // 監視を終了する
}
init() {
this.targetList = [];
this.observer = null;
this.observer = new IntersectionObserver(
this.entry.bind(this),
this.config
);
this.targetList = [...document.querySelectorAll(".js-observer")];
this.targetList.forEach((ele) => {
this.observer.observe(ele);
});
}
}
セッションストレージ
export class Session {
clear() {
sessionStorage.clear();
}
/**
* @param {string} key // 削除するキー名
*/
remove(key) {
sessionStorage.removeItem(key);
console.log("session remove", key);
}
/**
* @param {string} key //取得したいキー名
* @returns {string} // 値
*/
get(key) {
console.log("session get", key, sessionStorage.getItem(key));
return sessionStorage.getItem(key);
}
/**
* @param {string} key // セットしたいキー名
* @param {string} value // セットしたい値
*/
set(key, value) {
sessionStorage.setItem(key, value);
console.log("session set", key, value, sessionStorage.getItem(key));
}
}
ユーザーエージェント
現在(2023.01.01)「M1、M2」と「Macbook Pro Air」とのユーザーエージェントの違いを確認できていません...
export class Ua {
constructor(body) {
this.body = body;
this.bodyClassList = this.body.classList;
this.ua = window.navigator.userAgent.toLowerCase();
this.data = {
browser: "",
os: "",
device: "",
iphone: "",
};
this.checkBrowser();
this.checkOs();
this.checkDevice();
this.checkiPhone();
}
checkBrowser() {
if (
this.ua.indexOf("edge") !== -1 ||
this.ua.indexOf("edga") !== -1 ||
this.ua.indexOf("edgios") !== -1
) {
console.log("🚀 ~ This is Edge");
this.data.browser = "edge";
} else if (
this.ua.indexOf("opera") !== -1 ||
this.ua.indexOf("opr") !== -1
) {
console.log("🚀 ~ This is Opera");
this.data.browser = "opera";
} else if (this.ua.indexOf("samsungbrowser") !== -1) {
console.log("🚀 ~ This is SamsungInternetBrowser");
this.data.browser = "samsung";
} else if (this.ua.indexOf("ucbrowser") !== -1) {
console.log("🚀 ~ This is UcBrowser");
this.data.browser = "uc";
} else if (
this.ua.indexOf("chrome") !== -1 ||
this.ua.indexOf("crios") !== -1
) {
console.log("🚀 ~ This is Chrome");
this.data.browser = "chrome";
} else if (
this.ua.indexOf("firefox") !== -1 ||
this.ua.indexOf("fxios") !== -1
) {
console.log("🚀 ~ This is Firefox");
this.data.browser = "firefox";
} else if (this.ua.indexOf("safari") !== -1) {
console.log("🚀 ~ This is Safari");
this.data.browser = "safari";
} else if (
this.ua.indexOf("msie") !== -1 ||
this.ua.indexOf("trident") !== -1
) {
console.log("🚀 ~ This is IE");
this.data.browser = "ie";
alert("このブラウザは現在サポートされておりません。");
} else {
console.log("🚀 ~ This is An unknown browser");
this.data.browser = "";
}
if (this.data.browser != "") this.bodyClassList.add(this.data.browser);
}
checkOs() {
if (this.ua.indexOf("windows nt") !== -1) {
console.log("🚀 ~ This is Windows");
this.data.os = "windows";
} else if (this.ua.indexOf("android") !== -1) {
console.log("🚀 ~ This is Android");
this.data.os = "android";
} else if (
this.ua.indexOf("iphone") !== -1 ||
this.ua.indexOf("ipad") !== -1
) {
console.log("🚀 ~ This is iOS");
this.data.os = "ios";
} else if (this.ua.indexOf("mac os x") !== -1) {
console.log("🚀 ~ This is macOS");
this.data.os = "macos";
} else {
console.log("🚀 ~ This is An unknown OS");
this.data.os = "";
}
if (this.data.os != "") this.bodyClassList.add(this.data.os);
}
checkDevice() {
if (
this.ua.indexOf("iphone") !== -1 ||
(this.ua.indexOf("android") !== -1 && this.ua.indexOf("Mobile") > 0)
) {
console.log("🚀 ~ This is Mobile");
this.data.device = "mobile";
} else if (
this.ua.indexOf("ipad") !== -1 ||
this.ua.indexOf("android") !== -1
) {
console.log("🚀 ~ This is Tablet");
this.data.device = "tablet";
} else if (
this.ua.indexOf("ipad") > -1 ||
(this.ua.indexOf("macintosh") > -1 && "ontouchend" in document)
) {
console.log("🚀 ~ This is iPad");
this.data.device = "tablet";
} else {
console.log("🚀 ~ This is PC");
this.data.device = "pc";
}
if (this.data.device != "") this.bodyClassList.add(this.data.device);
}
checkiPhone() {
if (this.ua.indexOf("iphone") !== -1) {
console.log("🚀 ~ This is iPhone");
this.data.iphone = "iphone";
} else {
this.data.iphone = "";
}
if (this.data.iphone != "") this.bodyClassList.add(this.data.iphone);
}
}
1文字分割
/*
// --------------------------
// html
// --------------------------
// before
<div class="js-splitText">これは文字列です。</div>
// after
<div class="js-splitText">
<span class="t" style="display: inline-block;">こ</span><span class="t" style="display: inline-block;">れ</span><span class="t" style="display: inline-block;">は</span><span class="t" style="display: inline-block;">文</span><span class="t" style="display: inline-block;">字</span><span class="t" style="display: inline-block;">列</span><span class="t" style="display: inline-block;">で</span><span class="t" style="display: inline-block;">す</span><span class="t" style="display: inline-block;">。</span>
</div>
// --------------------------
// scss
// --------------------------
// 遅延設定(1文字アニメーション用)
.t{
@for $i from 1 through 100 {
&:nth-of-type(#{$i}) {
transition-delay: (0.05s * $i) - 0.05s;
}
}
}
*/
export class SplitText {
/**
* 分割した1文字を要素にする
* @param {string} text 1文字
* @returns {element} 要素 | <span class="t" style="display: inline-block;">1文字</span>
*/
createEle(text) {
const ele = document.createElement("span");
ele.className = "t";
ele.setAttribute("style", "display: inline-block;");
ele.innerHTML = text != " " ? text : " ;"; // ⚠️ 目視するために半角スペースを入れている
return ele;
}
/**
* 文字列を要素に分割し追加する
* @param {element} obj.wrap 文字列の格納先
* @param {string} obj.text 分割する文字列
*/
createSplit(obj) {
const wrap = obj.wrap,
text = obj.text;
const array = text.split("");
const fragment = document.createDocumentFragment();
for (let i = 0; i < array.length; i++) {
const ele = this.createEle(array[i]);
fragment.appendChild(ele);
}
wrap.appendChild(fragment);
}
init() {
this.eleList = [...document.querySelectorAll(".js-splitText")];
this.eleList.forEach((ele) => {
const obj = {
wrap: ele,
text: ele.innerText,
};
ele.innerText = "";
if (obj.text != "") this.createSplit(obj);
});
}
}
メニュー
/*
// --------------------------
// html
// --------------------------
// メニューボタン
<div id="menuBtn">メニュー</div>
// 閉じるボタン
<div class="js-menuClose">閉じる</div>
// --------------------------
// css
// --------------------------
body[data-menu="open"] {
overflow: hidden;
}
*/
export class Menu {
constructor(body = document.body) {
this.body = body;
this.body.setAttribute("data-menu", "close");
this.isOpenMenu = false;
this.isAbleToClick = true;
this.interval = 600;
this.menuBtn = document.getElementById("menuBtn");
this.menuBtn.addEventListener("click", (e) => {
if (this.isAbleToClick) {
this.isAbleToClick = false;
this.isOpenMenu ? this.toCloseMenu() : this.toOpenMenu();
setTimeout(() => {
this.isAbleToClick = true;
}, this.interval);
}
});
}
toOpenMenu() {
this.isOpenMenu = true;
this.body.setAttribute("data-menu", "open");
}
toCloseMenu() {
this.isOpenMenu = false;
this.body.setAttribute("data-menu", "close");
}
init() {
this.isAbleToClick = true;
this.isOpenMenu = false;
this.body.setAttribute("data-menu", "close");
}
}
アコーディオン|detailsを使用したもの
CodePen
Code
/*
// HTML
<details class="c-details">
<summary class="c-details__summary">
<h2>Summary</h2>
</summary>
<div class="c-details__content">
<div class="c-details__inner">
コンテンツ // ここで`padding`で脂肪をつける
</div>
</div>
</details>
// css
summary {
display: block; // list-item; → 三角形のアイコンを表示する
cursor: pointer;
}
//Safariの三角形アイコンを非表示にする
summary::-webkit-details-marker {
display: none;
}
*/
export class Accordion {
constructor() {
this.isMatchMedia = window.matchMedia("(max-width: 768px)").matches;
this.config = {
duration: 300,
easing: "ease",
};
}
reset() {}
resizeAfter() {
this.detailsList.forEach((ele) => {
const summary = ele.querySelector("summary"),
content = summary.nextElementSibling;
content.style.height = "auto";
});
}
/**
* @param {element} obj.details // コンテナ
* @param {element} obj.summary // 開閉ボタン
* @param {element} obj.content // 開閉要素
*/
open(obj) {
const details = obj.details,
summary = obj.summary,
content = obj.content;
const openAnimeKeyframes = (content) => [
{
height: 0,
opacity: 0,
},
{
height: content.offsetHeight + "px",
opacity: 1,
},
];
details.setAttribute("open", "true");
details.setAttribute("data-status", "open");
const openingAnime = content.animate(
openAnimeKeyframes(content),
this.config
);
openingAnime.onfinish = () => {
// 終了時の処理
};
}
close(obj) {
const details = obj.details,
summary = obj.summary,
content = obj.content;
const closeAnimeKeyframes = (content) => [
{
height: content.offsetHeight + "px",
opacity: 1,
},
{
height: 0,
opacity: 0,
},
];
const closingAnime = content.animate(
closeAnimeKeyframes(content),
this.config
);
closingAnime.onfinish = () => {
details.removeAttribute("open");
details.setAttribute("data-status", "close");
};
}
init() {
this.detailsList = [];
this.detailsList = [...document.querySelectorAll("details")];
this.detailsList.forEach((ele) => {
const isOpen = ele.getAttribute("data-open");
if (
(isOpen === "pc" && !this.isMatchMedia) ||
(isOpen === "sp" && this.isMatchMedia)
) {
ele.setAttribute("open", "true");
ele.setAttribute("data-status", "open");
} else {
ele.removeAttribute("open");
ele.setAttribute("data-status", "close");
}
const summary = ele.querySelector("summary"),
content = summary.nextElementSibling;
content.setAttribute(
"style",
`overflow: hidden; transition: ${
this.config.duration / 1000
}s ease height;`
);
const obj = {
details: ele,
summary: summary,
content: content,
};
summary.addEventListener("click", (e) => {
e.preventDefault();
if (
ele.getAttribute("data-status") === "open" ||
ele.getAttribute("data-status") === "close"
) {
ele.setAttribute("data-status", "run");
ele.open ? this.close(obj) : this.open(obj);
}
});
});
}
}
モーダル|ID指定(複数設置)
CodePen
Code
/*
// --------------------------
// html
// --------------------------
// モーダルボタン
<button class="js-modalBtn" data-modal-id="modal1">モーダルボタン</button>
// モーダル要素
<div class="l-modal" id="modal1">
<div class="js-modalCloseBtn" data-modal-id="modal1">閉じる</div>
<div class="js-modalPrevBtn" data-modal-id="modal3">前のモーダルへ</div>
<div class="js-modalNextBtn" data-modal-id="modal2">▶次のモーダルへ</div>
</div>
// --------------------------
// css
// --------------------------
body.is-openModalId{
overflow: hidden;
}
.l-modal.is-openModalId {
// モーダルが開く
}
.js-modalCloseBtn,
.js-modalPrevBtn,
.js-modalNextBtn{
cursor: pointer;
}
*/
export class ModalId {
constructor(body = document.body) {
this.body = body;
this.bodyClassList = body.classList;
this.timer = {
modal: null,
};
this.className = {
open: "is-openModalId",
};
this.isOpenModal = false;
this.isAbleToClick = true;
this.interval = 500;
this.currentId = "";
}
removeClassName() {
if (this.bodyClassList.contains(this.className.open)) this.bodyClassList.remove(this.className.open);
}
addClassName() {
this.bodyClassList.add(this.className.open);
}
reset() {
this.isOpenModal = false;
this.isAbleToClick = false;
clearTimeout(this.timer.modal);
this.currentId = "";
}
toCloseModal(target) {
console.log("open");
this.isOpenModal = false;
this.removeClassName();
if (target.classList.contains(this.className.open)) target.classList.remove(this.className.open);
}
toOpenModal(target) {
console.log("close");
this.isOpenModal = true;
this.addClassName();
target.classList.add(this.className.open);
}
toClick(ele) {
console.log("isClick");
if (this.isAbleToClick) {
this.isAbleToClick = false;
const targetId = ele.getAttribute("data-modal-id");
console.log(targetId);
if (targetId) {
this.currentId = targetId;
const target = document.getElementById(targetId);
target.scrollTo(0, 0);
console.log(target);
if (this.isOpenModal) {
this.currentId = "";
this.toCloseModal(target);
} else {
this.currentId = targetId;
this.toOpenModal(target);
}
clearTimeout(this.timer.modal);
this.timer.modal = setTimeout(() => {
this.isAbleToClick = true;
}, this.interval);
} else {
this.isAbleToClick = true;
this.isOpenModal = false;
}
}
}
toChange(ele) {
if (this.isAbleToClick) {
this.isAbleToClick = false;
const currentTarget = document.getElementById(this.currentId);
if (currentTarget) {
this.currentId = "";
this.toCloseModal(currentTarget);
}
const targetId = ele.getAttribute("data-modal-id");
if (targetId) {
this.currentId = targetId;
const target = document.getElementById(targetId);
target.scrollTo(0, 0);
if (this.isOpenModal) {
this.currentId = "";
this.toCloseModal(target);
} else {
this.currentId = targetId;
this.toOpenModal(target);
}
clearTimeout(this.timer.modal);
this.timer.modal = setTimeout(() => {
this.isAbleToClick = true;
}, this.interval);
} else {
this.isAbleToClick = true;
this.isOpenModal = false;
}
}
}
init() {
console.log("🚀 ~ Modal init");
this.isOpenModal = false;
this.isAbleToClick = true;
// 開くボタン
this.modalBtnList = [...document.querySelectorAll(".js-modalBtn")];
if (this.modalBtnList) {
this.modalBtnList.forEach((ele) => {
ele.addEventListener("click", (e) => {
this.toClick(ele);
});
});
}
// 閉じるボタン
this.closeBtnList = [...document.querySelectorAll(".js-modalCloseBtn")];
if (this.closeBtnList) {
this.closeBtnList.forEach((ele) => {
ele.addEventListener("click", (e) => {
this.toClick(ele);
});
});
}
// prevボタン
this.prevBtnList = [...document.querySelectorAll(".js-modalPrevBtn")];
if (this.prevBtnList) {
this.prevBtnList.forEach((ele) => {
ele.addEventListener("click", (e) => {
this.toChange(ele);
});
});
}
// nextボタン
this.nextBtnList = [...document.querySelectorAll(".js-modalNextBtn")];
if (this.nextBtnList) {
this.nextBtnList.forEach((ele) => {
ele.addEventListener("click", (e) => {
this.toChange(ele);
});
});
}
}
}
モーダル|画像
CodePen
Code
/*
// --------------------------
// html
// --------------------------
// ボタン
<div class="js-modalImgBtn">
<img src="#" alt="" />
</div>
// モーダル
<div class="l-modalImg">
<div class="js-modalImgCloseBtn"></div>
<div id="modalImgTarget">
<!-- ここにimgタグがJSの処理によって入ります。 -->
</div>
</div>
// --------------------------
// css
// --------------------------
body.is-openModalImg {
overflow: hidden;
}
.js-modalImgBtn,
.js-modalImgCloseBtn {
cursor: pointer;
}
*/
export class ModalImg {
constructor(body = document.body) {
this.body = body;
this.bodyClassList = this.body.classList;
this.className = {
open: "is-openModalImg",
};
this.isPageEnter = true;
this.isAbleToClick = true;
this.interval = 200;
}
reset() {
this.isPageEnter = false;
this.isAbleToClick = false;
}
removeClassName() {
if (this.bodyClassList.contains(this.className.open))
this.bodyClassList.remove(this.className.open);
}
addClassName() {
this.bodyClassList.add(this.className.open);
}
toCloseModal() {
if (this.isPageEnter) {
this.isAbleToClick = false;
this.removeClassName();
setTimeout(() => {
if (this.isPageEnter) this.isAbleToClick = true;
}, this.interval);
}
}
/**
* @param {event} e // クリックイベント
*/
toOpenModal(e) {
if (this.isPageEnter) {
this.isAbleToClick = false;
const ele = e.target;
const src = ele.querySelector("img").getAttribute("src");
const img = `<img src="${src}" alt="">`;
this.target.innerHTML = img;
this.addClassName();
setTimeout(() => {
if (this.isPageEnter) this.isAbleToClick = true;
}, this.interval);
}
}
init() {
console.log("🚀 ~ Modal Image");
this.isPageEnter = true;
this.isAbleToClick = true;
this.btnList = [...document.querySelectorAll(".js-modalImgBtn")];
this.closeList = [...document.querySelectorAll(".js-modalImgCloseBtn")];
this.target = document.getElementById("modalImgTarget");
// 開くボタン
this.btnList.forEach((ele) => {
ele.addEventListener("click", (e) => {
if (this.isAbleToClick && this.isPageEnter) this.toOpenModal(e);
});
});
// 閉じるボタン
this.closeList.forEach((ele) => {
ele.addEventListener("click", (e) => {
if (this.isAbleToClick && this.isPageEnter) this.toCloseModal();
});
});
}
}
画面サイズをCSS変数として設定
`resize` イベントで使用します。
/**
* @param {number} w // window.innerWidth
* @param {number} h // window.innerHeight
*/
export function SetPropertySize(w, h) {
const vh = h * 0.01,
longer = w > h ? w : h,
shorter = w > h ? h : w;
document.documentElement.style.setProperty("--vh", vh + "px"); // height: calc(var(--vh, 1vh) * 100);
document.documentElement.style.setProperty("--longer", longer + "px");
document.documentElement.style.setProperty("--shorter", shorter + "px");
}
Utility(lerp, delay, getParameter)
export class Utility {
/**
* 線形補間
* リファレンス:https://ja.wikipedia.org/wiki/%E7%B7%9A%E5%BD%A2%E8%A3%9C%E9%96%93
* @param {number} start
* @param {number} end
* @param {number} ease
* @returns {number} startとendを補完した数値
*/
lerp(start, end, ease) {
return start * (1 - ease) + end * ease;
}
/**
* 遅延 (await)
* @param {number} time 遅延時間
* @returns
*/
delay(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
}
/**
* GETパラメータのキーから値を取得し返す
* 参考サイト:https://www-creators.com/archives/4463
* @param {string} name // 取得したいGETパラメータのキー
* @returns {string} // GETパラメータ値
*/
getParameter(name) {
name = name.replace(/[\[\]]/g, "\\$&");
let regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)"),
results = regex.exec(window.location.href);
if (!results) return null;
if (!results[2]) return "";
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
}