これはなに?
下記のような、イベントハンドラとなる関数が登録された連想配列を、ツリーメニューに変換し表示・展開できるようにします。
const treeMenu = {}
treeMenu['menu1'] = (event) => outputToLog('menu1')
treeMenu['menu2'] = {}
treeMenu['menu2']['menu2-1'] = (event) => outputToLog('menu2-1')
treeMenu['menu2']['menu2-2'] = (event) => outputToLog('menu2-2')
treeMenu['menu2']['menu2-3'] = (event) => outputToLog('menu2-3')
treeMenu['menu3'] = (event) => outputToLog('menu3')
(event) => outputToLog()
は、暫定的なイベントハンドラです。実際に使用するときは、各メニュー選択時に実行したい関数を登録してください。
実際にツリーメニューが展開されると、下記のように<ul class='menu'>
を使ったHTMLが展開されます。したがって、実用時はこれをCSSで装飾して見栄えを整える必要があると思います。
<!-- '≡'をクリックするまでは折り畳まれている -->
<ul class='menu'>
<li>menu1</li>
<li>menu2</li>
<!-- 'menu2'をクリックするまでは折り畳まれている -->
<ul class='menu'>
<li>menu2-1</li>
<li>menu2-2</li>
<li>menu2-3</li>
</ul>
<li>menu3</li>
</ul>
サンプル
See the Pen easy_tree_menu by THANKS-JP (@thanks-jp) on CodePen.
作り方
上記プログラムの作り方です。
ツリーメニュー表示用のモジュールを作る
ツリーメニューの要件は以下のように定義しました。
- ≡をクリックすると、最上階層のメニューが表示される
- イベントハンドラが登録された項目をクリックすると、イベントハンドラが実行される
- サブメニューの連想配列が登録された項目をクリックすると、サブメニューが展開される
- 展開されたメニューの親項目をクリックすると、そのメニューは収納される
上記要件を実現するために、次のようなモジュールを作ります。
"use strict"
/**
* ツリー型メニューを開くイベントハンドラを作成する関数を作成します。
*
* @param 'string' className (required) ULタグのクラス名
* @return 'function'
*/
const makeMakerOfOpenMenuHandler = (className) => {
/**
* クリックされた要素の子メニューを閉じるイベントハンドラを作成します。
*
* @param 'function' handler (required) 子メニューを閉じた後、再び開くためのイベントハンドラ
* @return 'function'
*/
const makeCloseMenuHandler = (handler) => {
const closeMenu = (event) => {
const element = event.currentTarget
element.parentNode.removeChild(element.nextSibling)
element.removeEventListener('click', closeMenu)
element.addEventListener('click', handler)
}
return closeMenu
}
/**
* クリックされた要素のイベントハンドラを実行して、ツリーメニュー全体を閉じるイベントハンドラを作成します。
*
* @param 'function' handler (required) クリックされた要素のイベントハンドラ
*/
const makeExecuteElementHandler = (handler) => (event) => {
const element = event.currentTarget
let parentNode = element.parentNode
while (parentNode) {
const nextNode = parentNode.parentNode
if (parentNode.nodeName === 'UL' && parentNode.getAttribute('class') === className) {
parentNode.previousSibling.click()
}
parentNode = nextNode
}
handler(event)
}
/**
* ULタグに、イベントハンドラを持つ項目を追加します。
*
* @param 'HTMLElement' ul (required) 項目を追加するULタグ
* @param 'string' name (required) 項目の表示名
* @param 'function' handler (required) 項目に対応したイベントハンドラ
*/
const addItem = (ul, name, handler) => {
const li = document.createElement('li')
li.innerHTML = name
li.addEventListener('click', makeExecuteElementHandler(handler))
ul.appendChild(li)
}
/**
* ULタグに、サブメニューを持つ項目を追加します。
*
* @param 'HTMLElement' ul (required) 項目を追加するULタグ
* @param 'string' name (required) 項目の表示名
* @param 'object' menuStruct (required) 項目に対応したサブメニュー構造連想配列
*/
const addMenuItem = (ul, name, menuStruct) => {
const li = document.createElement('li')
li.innerHTML = name
li.addEventListener('click', makeOpenMenuHandler(menuStruct))
ul.appendChild(li)
}
/**
* 指定したHTMLElement直後に、指定したメニュー構造を持つツリーメニューを展開します。
*
* @param 'HTMLELement' element (required) メニューを展開する直前のHTML要素
* @param 'object' menuStruct (required) 展開するメニューの構造を表す連想配列
*/
const openMenu = (element, menuStruct) => {
const ul = document.createElement('ul')
ul.setAttribute('class', className)
for (const menu in menuStruct) {
const item = menuStruct[menu]
if (typeof item === 'function') {
addItem(ul, menu, item)
} else if (typeof item === 'object') {
addMenuItem(ul, menu, item)
}
}
element.parentNode.insertBefore(ul, element.nextSibling)
}
/**
* 指定したメニュー構造を持つツリーメニューを展開するイベントハンドラを作成します。
*
* @param 'object' menuStruct (required) 展開されるメニューの構造を表す連想配列
* @return 'function'
*/
const makeOpenMenuHandler = (menuStruct) => {
const openMenuHandler = (event) => {
const element = event.currentTarget
element.removeEventListener('click', openMenuHandler)
element.addEventListener('click', makeCloseMenuHandler(openMenuHandler))
openMenu(element, menuStruct)
}
return openMenuHandler
}
return makeOpenMenuHandler
}
// makeMakerOfOpenMenuHandlerをエクスポートします。
export default makeMakerOfOpenMenuHandler
モジュールの使い方
makeMakerOfOpenMenuHandler()
でクラス名を指定して、ツリーメニューを展開するためのイベントハンドラを作成する関数を作成します。ここで指定したクラス名は、<ul class='menu'>
の'menu'
部分に反映されるので、好きな名前を設定してください。
import makeMakerOfOpenMenuHandler from 'menu.js'
const makeOpenMenuHandler = makeMakerOfOpenMenuHandler('menu')
そして、makeOpenMenuHandler()
で展開するツリーメニューの構造を表す連想配列を指定して、ツリーメニューを展開するためのイベントハンドラを作成します。
const openMenu = makeOpenMenuHandler(treeMenu)
そして、作成したイベントハンドラを、<p id='menu'>
の'click'
イベントに登録すると、<p id='menu'>
をクリックしたときに、treeMenu
で指定した構造を持つツリーメニューが展開されるようになります。
const menu = document.getElementById('menu')
menu.addEventListener('click', openMenu)
テストページを作る
では、実際に上記モジュールを使用してテストページを作ってみましょう。menu.js
は、ツリーメニューを実際に表示するためのモジュールです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>テスト</title>
<script type='module'>
// テキストエリアに文字列を挿入する暫定的なイベントハンドラ
const outputToLog = (text) => {
const log = document.getElementById('log')
log.innerHTML += text + '\n'
}
// ツリーメニューの構造を表す暫定的な連想配列
const treeMenu = {}
treeMenu['menu1'] = (event) => outputToLog('menu1')
treeMenu['menu2'] = {}
treeMenu['menu2']['menu2-1'] = (event) => outputToLog('menu2-1')
treeMenu['menu2']['menu2-2'] = (event) => outputToLog('menu2-2')
treeMenu['menu2']['menu2-3'] = (event) => outputToLog('menu2-3')
treeMenu['menu3'] = (event) => outputToLog('menu3')
// menu.jsをインポートし、クラス名'menu'で
// ツリーメニューを展開するイベントハンドラを作成する関数を作成
import makeMakerOfOpenMenuHandler from 'menu.js'
const makeOpenMenuHandler = makeMakerOfOpenMenuHandler('menu')
// DOMの読み込み完了時、ツリーメニューを展開するイベントハンドラを作成し、
// id='menu'の'click'イベントに追加する
document.addEventListener('DOMContentLoaded', (event) => {
const openMenu = makeOpenMenuHandler(treeMenu)
const menu = document.getElementById('menu')
menu.addEventListener('click', openMenu)
})
</script>
</head>
<body>
<!-- メニューを展開する元となる要素 -->
<p id='menu'>≡</p>
<!-- ログを出力するテキストエリア -->
<textarea id='log'></textarea>
</body>
</html>
余談
JavaScriptは楽しい! え? こんなの〇〇っていうフレームワークを使えば簡単にできるって? いいのさ! 楽しければ!