JavaScriptの連想配列をツリーメニューに変換し表示・展開する

# これはなに？
下記のような、イベントハンドラとなる関数が登録された連想配列を、ツリーメニューに変換し表示・展開できるようにします。

``` js
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で装飾して見栄えを整える必要があると思います。

``` html
<!-- '≡'をクリックするまでは折り畳まれている -->
<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>

```

## サンプル
<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="js,result" data-user="thanks-jp" data-slug-hash="aMwZYK" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="easy_tree_menu">
  <span>See the Pen <a href="https://codepen.io/thanks-jp/pen/aMwZYK/">
  easy_tree_menu</a> by THANKS-JP (<a href="https://codepen.io/thanks-jp">@thanks-jp</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 作り方
上記プログラムの作り方です。

## ツリーメニュー表示用のモジュールを作る
ツリーメニューの要件は以下のように定義しました。

- ≡をクリックすると、最上階層のメニューが表示される
- イベントハンドラが登録された項目をクリックすると、イベントハンドラが実行される
- サブメニューの連想配列が登録された項目をクリックすると、サブメニューが展開される
- 展開されたメニューの親項目をクリックすると、そのメニューは収納される

上記要件を実現するために、次のようなモジュールを作ります。

``` menu.js
"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'`部分に反映されるので、好きな名前を設定してください。

``` js
import makeMakerOfOpenMenuHandler from 'menu.js'
const makeOpenMenuHandler = makeMakerOfOpenMenuHandler('menu')
```

そして、`makeOpenMenuHandler()`で展開するツリーメニューの構造を表す連想配列を指定して、ツリーメニューを展開するためのイベントハンドラを作成します。

``` js
const openMenu = makeOpenMenuHandler(treeMenu)
```

そして、作成したイベントハンドラを、`<p id='menu'>`の`'click'`イベントに登録すると、`<p id='menu'>`をクリックしたときに、`treeMenu`で指定した構造を持つツリーメニューが展開されるようになります。

``` js
const menu = document.getElementById('menu')
menu.addEventListener('click', openMenu)
```

## テストページを作る
では、実際に上記モジュールを使用してテストページを作ってみましょう。`menu.js`は、ツリーメニューを実際に表示するためのモジュールです。

``` test.html
<!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は楽しい！　え？　こんなの〇〇っていうフレームワークを使えば簡単にできるって？　いいのさ！　楽しければ！