LoginSignup
1
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-09

これはなに?

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

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.

作り方

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

ツリーメニュー表示用のモジュールを作る

ツリーメニューの要件は以下のように定義しました。

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

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

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

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は、ツリーメニューを実際に表示するためのモジュールです。

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1