はじめに
さて、前回サイドバーにサブメニューを追加したところ、気になる動作があったので、今回はその対処です。
今回実施する内容
今回実施するのは、サイドバーのサブメニューの表示状態の保持です。
前回作成したサイドバーでは、サブメニューをいくつか開いた状態でいずれかのリンクを押下し別のHTMLへ遷移すると、サイドバーの状態はリセットされ、すべて閉じられた状態になります。
この動作が気になりました。
サブメニューの状態は、このサイドバーを使用するリンク内では保持したほうが自然かなと感じましたので、対処します。
前回の動作
新たなページ(例:マイプレイリスト)を開くと、マイページのサブメニュー部分は表示されない。

今回の動作
新たなページ(例:マイプレイリスト)を開くと、マイページのサブメニュー部分は表示される。
※少しサブメニューの内容は前回と変わりましたが、本質ではありません。

ソースコード(Git Hub)
環境
OS: Windows 11 JP (64bit)
Edge:バージョン 148.0.3967.83 (公式ビルド) (64 ビット)
参考
第21回 JavaScript モーダルサイドバーのサブメニュー追加
用語
なし
今回使う機能
ハイパーリンクを押下して別のHTMLを開く場合、現在の状態を次のHTMLへ引きわして、その状態を維持する必要があります。
そのため、URLパラメータを使用します。
URLパラメータとは
URLの後に付与するパラメータで、次のURLへ情報を引き渡すことができます。
http://xxxx/index.html ?yyyyyy
上記の「?」以降の部分がURLパラメータです。
よく「?」以降にSQL文やScriptが書かれていることをみかけたことがあるかと思います。
SQLインジェクションとかOSコマンドとかの攻撃要因になるため、URLパラメータの内容をそのまま実行するのはよくない実装です。暗号化やURLパラメータの使用前にきちんとしたチェックなどが必要とされます。
今回はそういうのはおいておき、状態保持するための手段として実施していきたいと思います。
なお、?以降(?含む)の値は、location.searchという変数に保存されます。
サブメニューの状態保持の考え方
サブメニューの状態を保持して、次のHTMLに渡すときにサブメニューの状態をどう表現するかについてです。
サブメニューの状態は、<div id="${item.id}-group" class="subMenuList">のclass部分です。
idは、例えば、home-groupやmypage-groupなどの名称です。
classはsubMenuListであり、このclassが設定される場合、サブメニューは非表示で、設定されない場合、サブメニューは表示されます。
このclassの有無がサブメニューの状態を示すため、classの有無をビット配列で最下位ビット(右端のビット)から最上位ビット(左端のビット)に向けて保持するようにします。subMenuListが設定される場合「1」で、設定されない場合、「0」とします。
図の例では、ホームのサブメニューは非表示であり、マイページのサブメニューは表示状態です。
ホームのサブメニューは非表示⇒subMenuListは設定される⇒ビットは「1」
マイページのサブメニューは表示⇒subMenuListは設定されていない⇒ビットは「0」
従って、例のビットの値は、
0 1
です。
サブメニューの状態保持と次のURLでの適用タイミング
サブメニューの状態を取得・保持するタイミングは、ハイパーリンクを押下して次のURLへ飛ぶ直前です。
その状態をURLパラメータとして渡して、次のURLでURLパラメータを取得して、サブメニューの状態を設定します。
サブメニュー状態保持、次のURLでの適用の実装
追加する変数
let parentMenuIdList = []; //サブメニューをもつ親メニューのIDを格納する配列
let subMenuToggleStateBits = 0; //サブメニューの表示・非表示の状態を管理するビット列
-
parentMenuList配列:サブメニューをもつ親メニューのIDを格納する配列。 -
subMenuToggleStateBits:サブメニューの表示・非表示の状態を管理するビット列。
parentMenuList配列は、サブメニューのdiv要素のid「${item.id}-group」を検索するために使用します。
parentMenuList配列へ親メニューのIDを格納
サブメニューをもつ親メニューのIDをparentMenuList配列に持ちます。HTMLを読み込んでDOMContentsをロードするときに、この配列を設定します。
window.addEventListener("DOMContentLoaded", () => {
...
parentMenuIdList = [...new Set(objSubMenuItems.map(subItem => subItem.parentId))];
objSubMenuItemsのparentIdの値をmap関数で新しい配列を作成し、set関数で重複を排除して(...new Setで新しい配列を...で追加して)parentMenuIdListに追加します。

subMenuToggleStateBitsの取得・保持
サブメニューの状態を取得・保持するタイミングは、ハイパーリンクを押下して次のHTMLへ飛ぶ直前です。
これは、メニューを押下した場合、および、サブメニューを押下した場合に発生します。
objMenuItems.forEach((item) => {
document.getElementById(item.id).addEventListener("click", async () => {
let objTmpSubMenuItems = objSubMenuItems.filter(subMenuItems => subMenuItems.parentId === item.id);
// サブメニューがある場合はサブメニューの表示・非表示を切り替える
if (objTmpSubMenuItems.length > 0) {
// サブメニューを開く、もしくは、閉じる
document.getElementById(`${item.id}-allow`).classList.toggle("rotate1");
toggleSubMenu(item.id);
} else {
// サイドバーを閉じて、リンクを開く
await toggleSideBar();
subMenuToggleStateBits = getCurrentSubMenuToggleStates();
location.href = item.href + "?" + subMenuToggleStateBits;
}
});
});
objSubMenuItems.forEach((subItem) => {
document.getElementById(subItem.id).addEventListener("click", async () => {
// サイドバーを閉じて、リンクを開く
await toggleSideBar();
subMenuToggleStateBits = getCurrentSubMenuToggleStates();
location.href = subItem.href + "?" + subMenuToggleStateBits;
});
});
上記のsubMenuToggleStateBits = getCurrentSubMenuToggleStates();でサブメニューの状態を取得・保持します。
/**
* クラスにsubMenuListが表示されているかどうかを確認し、subMenuToggleStateBitsに格納する関数。
* サブメニューの表示・非表示の状態をビット列で管理する。
*
* @returns 格納したsubMenuToggleStateBitsの値。
*/
const getCurrentSubMenuToggleStates = () => {
let localSubMenuToggleStateBits = 0;
let number = 0;
parentMenuIdList.forEach((parentId) => {
number++;
//parentId-groupのクラスリストにsubMenuListが含まれているかどうかで、サブメニューの表示・非表示を判定する。
//含まれる場合非表示で、含まれない場合表示されているとみなす。
//含まない場合(表示されている場合)ビットを立て、含む場合(表示されていない場合)ビットを立てない。
if (!document.getElementById(`${parentId}-group`).classList.contains("subMenuList")) {
localSubMenuToggleStateBits += 2** number;
}
});
return localSubMenuToggleStateBits;
}
localSubMenuToggleStateBitsは本関数内でのみ使用するサブメニューのビットの数値ですが、これがそのままreturnしますので、グローバル変数のsubMenuToggleStateBitsに設定します。
parentMenuIdListをforEach文で回して、idが${parentId}-groupであるdiv要素にclass="subMenuList"を含む場合、localSubMenuToglleStateBitsのビットを立てるという動作です。
numberはビット位置を示しており、最下位ビット0から、forEach文のループごとに1インクリメントして、2の階乗で足すという動作です。
図で示していた例(マイページのサブメニューは表示、ホームのサブメニューは非表示)では「01」でしたので、10進数としての値は1です。
- localSubMenuToggleStateBits = 2^0 ✕ 1 + 2^1 ✕ 0 = 1
subMenuToggleStateBitsをURLパラメータに設定
サブメニューの状態をURLパラメータに設定するタイミングは、ハイパーリンクを押下して次のURLへ飛ぶときです。
取得・保持とと同じタイミングであり、メニューを押下した場合、および、サブメニューを押下した場合に発生します。
ソースは上記の「subMenuToggleStateBitsの取得・保持」に含まれていますが、そこから抜粋すると以下です。
location.href = subItem.href + "?" + subMenuToggleStateBits;
?の後にそのままsubMenuToggleStateBitsの数値を付与します。
今回はシンプルにというコンセプトなのでこうしましたが、?subMenuToggleStateBits=値のようにしてもよいとは思います。パラメータ名を暗号化して隠蔽するのも一つの手段とは思います。
次のURLでのURLパラメータ取得とsubMenuToggleStateBitsへ設定
サブメニューの状態は、HTMLを読み込んでDOMContentsをロードするときに、URLパラメータの値をsubMenuToggleStateBitsに設定します。
window.addEventListener("DOMContentLoaded", () => {
// locaiton.hrefから値をうけとり、submenuToggleStatesBitsに格納する。
subMenuToggleStateBits = getLocationSearchValue(location.search);
// サブメニューをもつ親メニューのIDをparentMenuIdListに格納する。
parentMenuIdList = [...new Set(objSubMenuItems.map(subItem => subItem.parentId))];
subMenuToggleStateBits = getLocationSearchValue(location.search);で、getLocationSearchValue関数でURLパラメータ(location.search)からsubMenuToggleStartBits`を取得します。
/**
* location.searchの値から先頭の"?"を取り除いて返す関数。
*
* @param {string} search
* @return 先頭の"?"を取り除いた文字列。searchが空文字の場合は空文字を返す。
*/
const getLocationSearchValue = (search) => {
if (search !== "") {
return search.substring(1);
}
return 0;
};
今回は単純に、location.searchの値を引数searchに代入し、searchの文字が存在していれば、?(0文字目)を除いて、1文字目以降の文字列を応答して取得します。
次のURLのサブメニューに設定
取得したsubMenuToggleStartBitsの値を使用して、ファイルにサブメニューの状態を設定します。
このサブメニューの状態は、HTMLを読み込んでDOMContentsをロードするときに設定します。
menuSideBar.insertAdjacentHTML("beforeend", strHtml);
setSubMenuToggleStates(subMenuToggleStateBits);
/**
* subMenuToggleStateBitsに従って、サブメニューの表示・非表示状態を設定する。
*
* @param {number} subMenuToggleStateBits
*/
const setSubMenuToggleStates = (subMenuToggleStateBits) => {
for (let i = 0 ; i< parentMenuIdList.length; i++) {
if (hasBit(subMenuToggleStateBits, i + 1)) {
// サブメニューを表示する
document.getElementById(`${parentMenuIdList[i]}-group`).classList.remove("subMenuList");
} else {
// サブメニューを非表示する
document.getElementById(`${parentMenuIdList[i]}-group`).classList.add("subMenuList");
}
}
};
/**
* 指定されたビットインデックスにビットが立っているかどうかを確認する。
*
* @param {string} value
* @param {number} bitIndex
* @return 指定されたビットインデックスのビットの値。
*/
const hasBit = (value, bitIndex) => {
return (value & (1 << bitIndex)) !== 0;
}
setSubMenuToggleState関数は、parentMenuIdList数でループし、subMenuToggleStateBitsから各ビットの値(サブメニューが開いているかどうか)を確認し、サブメニューを表示するか否か(subMenuListを追加するか削除するか)を設定します。 hasBit関数は、引数はvalueとbitIndexでvalueはsubMenuToggleStateBitsの値を設定し、bitIndex`には各ビットのビットインデックスで、指定のビットインデックスにビットがたっているかを確認し、ビットの値を応答します。
おわりに
今回はここまでです。
色々作りこみを始めると気になる点が発生しますね。
