はじめに
前回が2024年9月1日に記事を書いたので、かなり久しぶりですが、モーダルサイドバーの第4回目です。
前回までで、モーダルサイドバーを作り、そこにメニューを追加して、メニューをクリックすると、リンク先を開くものを作りました。
今回実施する内容
サイドバーのメニューとサブメニューを作成し、サブメニューを開いたり閉じたりできるようにします。
基本的に第20回からの差分だけを紹介します。
ソースコード(Git Hub)
環境
OS: Windows 11 JP (64bit)
Microsoft Edge: バージョン 147.0.3912.86 (公式ビルド) (64 ビット)
参考
第18回 JavaScript アニメーションによるサイドバー(translateX)
第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)
第20回 JavaScript モーダルサイドバーの共通化
第3回 SVG ローカル環境のCORS対処
用語
なし
今回実施する内容(もう少し細かく)
- メニューの配下にサブメニューを作る
- サブメニューがあるメニュークリック時はサブメニューを開く、もしくは、閉じる
- サブメニュークリック時はそのリンクを開く
- (おまけ)関数名のリファクタ
- (おまけ)メニューの折り返し対処
- (おまけ)メニューにアイコンの追加
メニューとサブメニューの作成
メニューとサブメニューのリストの作成
メニューとサブメニューの構成は以下の通り。
- ホーム
- マイページ
- 検索
- マイプレイリスト
- マイ履歴
- サイトマップ
該当ソース部分は以下の通り。
const objMenuItems = [
{ id: "home", label: "ホーム", href: "index.html", icon: "homeIcon.svg", symbol: "homeSymbol" },
{ id: "mypage", label: "マイページ", href: "mypage.html", icon: "mypageIcon.svg", symbol: "myPageSymbol" },
{ id: "sitemap", label: "サイトマップ", href: "sitemap.html", icon: "sitemapIcon.svg", symbol: "sitemapSymbol" },
];
const objMenuSubItems = [
{ id: "search", label: "検索", href: "search.html", icon: "searchIcon.svg", symbol: "searchSymbol", parentId: "mypage" },
{ id: "myplaylist", label: "マイプレイリスト", href: "playlist.html", icon: "playlistIcon.svg", symbol: "playlistSymbol", parentId: "mypage"},
{ id: "history", label: "マイ履歴", href: "history.html", icon: "historyIcon.svg", symbol: "historySymbol", parentId: "mypage"}
];
今回はメニューとサブメニューで使用する変数が多いため、オブジェクトで作成しました。
- メニューのオブジェクトの説明
| キー | 説明 |
|---|---|
| id | メニューを識別するID。 |
| label | メニューに表示する名前。 |
| href | メニュークリック時のハイパーリンク先。 |
| icon | メニューに追加するアイコンファイル名(SVG)。 |
| symbol | 上記アイコン(SVG)内のsymbolの値。 |
- サブメニューのオブジェクトの説明
| キー | 説明 |
|---|---|
| id | サブメニューを識別するID。 |
| label | サブメニューに表示する名前。 |
| href | サブメニュークリック時のハイパーリンク先。 |
| icon | サブメニューに追加するアイコンファイル名(SVG)。 |
| symbol | 上記アイコン(SVG)内のsymbolの値。 |
| parentId | サブメニューの元の親のメニューのId。 |
マイページのところのhrefは不要です。今回はサブメニューを開くためだけに使うだけのためです。何かの操作でマイページを開く動作もありかなと思って記載しましたが、今回は使っていません。
メニューとサブメニューの描画
window.addEventListener("DOMContentLoaded", () => {
let strHtml = `\n`;
strHtml += `\t<img src="humbergerIcon.svg" id="menuIcon" class="menuIcon"/>\n`;
strHtml += `\t<div id="sideBar">\n`;
strHtml += `\t\t<img src="humbergerIcon.svg" id="menuIcon2" class="menuIcon"/>\n`;
strHtml += `\t\t<div id="menuList">\n`;
objMenuItems.forEach((item) => {
// サブメニューの有無を確認
let objTmpSubMenuItems = objSubMenuItems.filter(subMenuItems => subMenuItems.parentId === item.id);
if (objTmpSubMenuItems.length > 0) {
//サブメニューがある場合は、サブメニューも含めてHTMLを生成する
strHtml += `\t\t\t<div id="${item.id}" class="menuItem">\n`;
strHtml += `\t\t\t\t<svg width="48px" height="48px"><use xlink:href="${item.icon}#${item.symbol}"></use></svg>${item.label}\n`;
strHtml += `\t\t\t\t<svg id="${item.id}-allow" width="20px" height="20px"><use xlink:href="arrowIcon.svg#arrowSymbol"></use></svg>\n`;
strHtml += `\t\t\t</div>\n`;
strHtml += `\t\t\t<div id="${item.id}-group" class="subMenuList">\n`;
objTmpSubMenuItems.forEach((subItem) => {
strHtml += `\t\t\t\t<div id="${subItem.id}" class="subMenuItem">\n`;
strHtml += `\t\t\t\t\t<svg width="48px" height="48px"><use xlink:href="${subItem.icon}#${subItem.symbol}"></use></svg>${subItem.label}\n`;
strHtml += `\t\t\t\t</div>\n`;
});
strHtml += `\t\t\t</div>\n`;
} else {
// サブメニューがない場合は、通常のメニューアイテムとしてHTMLを生成する
strHtml += `\t\t\t<div id="${item.id}" class="menuItem">\n`;
strHtml += `\t\t\t\t<svg width="48px" height="48px"><use xlink:href="${item.icon}#${item.symbol}"></use></svg>${item.label}\n`;
strHtml += `\t\t\t</div>\n`;
}
});
-
objMenuItems.forEach((item) => {- 第20回の記事ではわかりやすさのためにこの描画部分はべた書きしていましたが、今回は(サブ)メニューをオブジェクトにしたこともあり、forEachメソッドでループを使用しました。
-
let objTmpMenuSubItems = ...let objTmpSubMenuItems = objSubMenuItems.filter(subMenuItems => subMenuItems.parentId === item.id); if (objTmpSubMenuItems.length > 0) {- サブメニューを含む
objSubMenuItemsの中で、subMenuItems.parentIdの値がメニューのID(item.id)と一致するものをフィルターして新たなobjTmpSubMenuItemsに代入。
- サブメニューを含む
-
if (objTmpSubMenuItems.length > 0) {- サブメニューがある場合となる場合を分岐するif文。
- サブメニューがある場合、メニューとサブメニューを描画。
- サブメニューがない場合、メニューを描画。
-
strHtml += `\t\t\t\t<svg id="${item.id}-allow"...strHtml += `\t\t\t\t<svg id="${item.id}-allow" width="20px" height="20px"><use xlink:href="arrowIcon.svg#arrowSymbol"></use></svg>\n`;- メニュー名の右に「>」という矢印のSVGを追加。
- この矢印SVGは、メニューをクリックすると90度回転してサブメニューが開いていることを示すために使用。
-
strHtml += `\t\t\t<div id="${item.id}-group" class="menuSubList">\n`;- サブメニューをグループ化するdiv要素。このグループ単位でメニュークリック時にサブメニューを開く、もしくは、閉じるを実行するために作成。
-
objTmpSubMenuItems.forEach((subItem) => {- サブメニューをforEachメソッドでループして描画。
メニュー、サブメニュークリック時の動作
objMenuItems.forEach((item) => {
document.getElementById(item.id).addEventListener("click", async () => {
let objTmpSubMenuItems = objSubMenuItems.filter(subItems => subItems.parentId === item.id);
// サブメニューがある場合はサブメニューの表示・非表示を切り替える
if (objTmpSubMenuItems.length > 0) {
// サブメニューを開く、もしくは、閉じる
document.getElementById(`${item.id}-allow`).classList.toggle("rotate1");
toggleSubMenu(item.id);
} else {
// サイドバーを閉じて、リンクを開く
await toggleSideBar();
location.href = item.href;
}
});
});
objSubMenuItems.forEach((subItem) => {
document.getElementById(subItem.id).addEventListener("click", async () => {
await toggleSideBar();
location.href = subItem.href;
});
});
-
objMenuItems.forEach((item) => {-
objMenuItemsをforEachメソッドでループ。
-
-
let objTmpSubMenuItems = objSubMenuItems.filter(subItems => subItems.parentId === item.id);- サブメニューを含む
objSubMenuItemsの中で、subItems.parentIdの値がメニューのID(item.id)と一致するものをフィルターして新たなobjTmpSubMenuItemsに代入。
- サブメニューを含む
-
if (objTmpSubMenuItems.length > 0) {- サブメニューがある場合となる場合を分岐するif文。
- サブメニューがある場合、サブメニューを表示、もしくは、閉じる。
- サブメニューがない場合、メニューのリンクを開く。
-
document.getElementById(${item.id}-allow).classList.toggle("rotate1");-
${item.id}-allowの要素(例:mypage-allow)にrotate1のCSSを追加、もしくは、削除する。
-
-
objSubMenuItems.forEach((subItem) => {- サブメニューのリンクを開く。
.rotate1 {
transform: rotate(90deg);
}
- 単純に90度回転するCSS。
サブメニューを開く、閉じる関数とCSS
const toggleSubMenu = (id) => {
document.getElementById(`${id}-group`).classList.toggle("menuSubList");
};
-
document.getElementById(${id}-group).classList.toggle("menuSubList");-
${id}-groupの要素(例:mypage-group)にsubMenuListのCSSを追加、もしくは、削除する。
-
.subMenuList {
display: none;
}
- 単純にサブメニューを消すCSS。
関数名のリファクタ(おまけ)
- 第20回の時に使用していた
showSideBarは、toggleSideBarに変更。
メニューの折り返し対処(おまけ)
- メニュー名が長く、メニュー幅よりも長くなる場合で、かつアルファベットが使われる場合、折り返しされずに、そのまま表示されてしまいます。
- さらに、SVGのアイコンが徐々に小さくなり、下図ではアイコンが消えています。
これをさけるために、.menuItemと.subMenuItemにword-break: break-allを付与します。
単語区切りで折り返すoverflow-warp: break-wordもあります。
今回これも試したのですが、メニューの内容が単語とは限らないと思い、単語区切りよりは改行が必要な時に改行でよいと思いました。
ついでにsvgとメニュー名がセンタリングするようにdisplayとalign-itemsを付与しました。
さらに、SVGのサイズを変更させないように、flex-shrink: 0;を含むクラスを追加します。
.menuItem {
width: 180px;
padding-left: 10px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
/* overflow-wrap: break-word; */
word-break: break-all;
align-items: center;
display: flex;
}
.subMenuItem {
width: 150px;
padding-left: 30px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
/* overflow-wrap: break-word; */
word-break: break-all;
align-items: center;
display: flex;
}
.menuItem svg {
flex-shrink: 0;
}
.subMenuItem svg {
flex-shrink: 0;
}
おわりに
意外と簡単にできるなと思いました。
次は、もう少し進化させて、別のHTMLファイルへ移ったときにサブメニューの状態(開く、もしくは、閉じる)も持ち越せるようにできたらと思います。
今回からGitHub Copilotに手伝ってもらったのですが、なかなか便利ですね。
