jQuery Treeview を使ってサーバ側のツリー構造データをブラウザで表示・検索するプロトを爆速で実装した話

#はじめに
まさに掲題の必要性に迫られて調べた時のメモを整理した記事。

もちろんツリー表示を自前で実装する気は1ミリもなく、使えそうなJavaScriptライブラリを色々と調べた結果、少々古いもののシンプルさと実績を考慮しjQuery Treeview に決定した。

さらにjQuery Treeviewの実装例を調べた所、HTML内の静的なデータをツリー表示させる例が多く、サーバからダイナミックにデータを取得して表示する例が少なかったが、[rails と jquery treeview でディレクトリツリーをらくらく実装する](https://blog.fusic.co.jp/archives/1829/) の記事を参考に、無事実現したいことをができたため、本記事でノウハウを共有したい。

# 環境
今回作成するプロトタイプの環境は以下の通りである。サーバ側はもちろん我らがPythonだ。

### クライアント側
- jQuery 1.2.6
- jQuery Treeview (https://github.com/jzaefferer/jquery-treeview)

### サーバ側
- Python 3系
- flask 2.0

# ライブラリのインストール手順
## jQueryのインストール
jQuery 1.2.6については以下のDownloadingのリンクを適当なフォルダに保存すればよい
https://blog.jquery.com/2008/05/24/jquery-1-2-6-released/

## jQuery Treeviewのインストール
jQueryViewは、以下のGitHubのファイルを一式ダウンロードして適当なフォルダに保存すればよい。
https://github.com/jzaefferer/jquery-treeview

# 今回作成するプロトタイプの仕様
今回TreeViewで作るプロトタイプを説明する。
表示対象のデータはツリー構造であれば何でも良かったのだが、今回はサーバ側のある特定のディレクトリ配下のファイルやディレクトリの構造を表示、検索するシステムとした。

図で表すとこんなイメージだ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329816/52aeeaa7-2f62-b2bd-910d-539c58b1cfae.png)

詳しい仕様は次の通りである。

- フォルダの場合、+もしくは-ボタンにより、ツリーを開いたり閉じたりすることができる。
- 初回表示時は、対象フォルダの配下の2階層目までのデータを取り込み、ツリーが展開された状態で表示する。3階層以降のデータは+がクリックされたタイミングでネットワークからダイナミックにデータを取得して表示するものとする。
- キーワードを入れて検索ボタンをクリックすると、そのキーワードに合致するノードを含むツリーのみ表示する。ただし、中間のディレクトリがマッチした場合は、その先のノードは表示しない。

やや複雑ではあるが、ツリーの表示のさせ方のバリエーションを検討するため、このような仕様とした。

# jQuery TreeView の使い方

jQuery TreeViewの使い方を解説する。

### クライアントの実装方法

ヘッダではこんな感じでcssやjsをインクルードしておく

```html
<link rel="stylesheet" href="./treeview/jquery.treeview.css">
<script src="./jquery-1.2.6.js"></script>
<script src="./treeview/jquery.treeview.js"></script>
<script src="./treeview/jquery.treeview.edit.js"></script>
<script src="./treeview/jquery.treeview.async.js"></script>
```

また、TreeViewを表示したい位置に以下のようにタグを入れる。idにはhtml上で一意な値を設定する。

```html
<ul id="treeview"></ul>
```

そして、scriptタグの中で以下のようにTreeViewの設定を行う。
urlには、この後実装するサーバのURLを指定する。

```javascript
$("#treeview").empty();
$("#treeview").unbind();
$("#treeview").treeview({
animated: 100,
url: "http://127.0.0.1:8889/
});
```

重要な点として、初期化時には**```root=source```**、ノードの+がクリックされた際には**```root=<id>```**というクエリがそれぞれURLの末尾に付加されてサーバに送られる。idとは後述するが、ノードを特定するための固有のidである。勘のいい人はお分かりかもしれないが、サーバ側は**rootパラメータの値に応じて適切なデータを返す**ように実装しておけばよい。

### サーバ側の実装方法
ではサーバ側の実装を解説しよう。
大きくはrootパラメータに応じて以下のようなデータを返す機能を実装する必要がある。

- root=sourceの場合は、初回に表示させたいツリーデータをjson形式で返すようにする。
- root=＜id＞の場合は、ノードid の配下に表示させたいツリーデータをjson形式で返すようにする。

では、json形式によるツリーデータの具体的書き方であるが、以下2つを抑えておくとよい。

- 各ノードはjsonオブジェクトで表す。
- jsonオブジェクトの"chidren"プロパティに、jsonオブジェクトの配列(子供が複数の場合もあるため）を持たせることで、親子関係を表現する。

jsonオブジェクトとしては以下のプロパティを設定しておけばよい。

| プロパティ |説明 |
|:-----------|:------------|
| id       | ノードを特定するid。クライアントがツリーのデータを動的に取得する場合に、このidをキーにするためサーバ側では、一意なidを生成して返す必要がある。今回のプロトでは各ディレクトリやファイルのパス(フルパス)とした。|
| text     | ツリーのノードに表示させる文字列を設定する。 |
| classes  | cssのスタイルを指定する。TreeViewにスタイルを指定しない場合は意味がないかも。今回は既存の事例に従いフォルダにはfolder, 末端ノードの場合fileを指定した。(効いてないかも) |
| hasChildren  | 子がいるかどうかをTrue/Falseで指定。これがTrueかどうかで展開用の+/-ボタンが表示されるかどうかが決まる。 |
| children   |子のjsonオブジェクトを配列で格納する。最初は子のidを指定するのかも？と悩んだが試行錯誤した結果この解に辿り着いた。 |
| expanded   |ノードを最初から展開させておきたい場合、Trueを指定する。|

ちなみに上のプロパティの説明はドキュメントには見当たらなかったため、**自分で試行錯誤しながらまとめたもの**である(笑)

#実装してみた

百聞は一見に如かず。実装してみよう。

### サーバ側

こんな感じ。ディレクトリの中を探索する処理や、keywordオプションが指定された時に別の処理をやってたりするので、複雑にみえるが雰囲気は伝わると思う。

```main.py
import os
import glob
from flask import Flask, request
import json

# Flaskオブジェクトの生成
app = Flask(__name__)

# CRLFのエラーが発生しないようヘッダを設定
@app.after_request
def after_request(response):
  response.headers.add('Access-Control-Allow-Origin', '*')
  response.headers.add('Access-Control-Allow-Headers', 'X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept')
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return response

@app.route("/",  methods=["GET"])
def get_dir_list():
    req = request.args
    root = req.get("root")
    keyword = req.get("keyword")

    # デフォルトの階層を設定
    if root == "source" or root is None:
        root = "c:/test/Music"

    array = [] 
    if keyword == "" or keyword is None:
        get_dir_list_sub(root, array, 1, 2)
    else:
        get_dir_list_sub(root, array, 1, None, keyword)
    text = json.dumps(array)
    return text


def get_dir_list_sub(path, array, currentDepth, viewDepth=None, keyword=None):
    paths = glob.glob(f"{path}/*")

    is_any = False
    for path in paths:
        name = os.path.split(path)[1]
        h = {}
        h["id"] = path
        h["text"] = name
        is_keyword_in_children = False
        if os.path.isdir(path):
            h["classes"] = "folder"
            if viewDepth is not None and (currentDepth +1 > viewDepth):
                # フォルダの配下が階層を超える場合、この時点までしか表示しないものとする。
                # ただし、子は存在するため、+で展開時に子階層を検索させるべくhasChildrenをTrueにしておく
                h["hasChildren"] = True
            else:
                # フォルダの配下を返す場合
                h["children"] = []
                is_keyword_in_children = get_dir_list_sub(path, h["children"], currentDepth + 1, viewDepth, keyword)
                # 最初から展開させない場合は、以下をFalseにする
                h["expanded"] = True 
                # ※注意 hasChildrenはここはTrueにしなくてよい。Trueにすると冗長な子階層が表示される。
        else:
            h["classes"] = "file"
            h["hasChildren"] = False

        if keyword is None:
            array.append(h)        
            is_any = True
        else:
            # キーワードが指定されている場合、自身または子がキーワードを含場合のみ格納
            if keyword in name or is_keyword_in_children:
                array.append(h)        
                is_any = True
    
    return is_any


if __name__ == "__main__" :
    app.run(debug=True, port=8889)

```

### クライアント側
続いてクライアント側の処理である。たったこれだけで素晴らしいツリービューの完成だ。
恐るべし、jQuery Treeview。

```treeview_search.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="./treeview/jquery.treeview.css">
<script src="./jquery-1.2.6.js"></script>
<script src="./treeview/jquery.treeview.js"></script>
<script src="./treeview/jquery.treeview.edit.js"></script>
<script src="./treeview/jquery.treeview.async.js"></script>

</head>
<body>
<h4>サーバと連動したツリービュー表示</h4>
<input id="keyword" type="text" />
<input type="button" onClick="javascript:updateTree()" value="検索"/>
<br/><br/>
<ul id="treeview"></ul>
</body>
<script>
  window.onload = function() {
    updateTree();
  };
  function updateTree(){
    var keyword = document.getElementById("keyword").value;
    $("#treeview").empty();
    $("#treeview").unbind();
    $("#treeview").treeview({
      animated: 100,
      url: "http://127.0.0.1:8889/?keyword=" + keyword 
    });
  }
 </script>
</html>  
```

# 使い方

以下のようにサーバを起動する。その後ブラウザでtreeview_search.htmlを直接表示すればよい。

```sh
python main.py
```

#おまけ　ノードにリンクをつける
ノードにリンクを張りたいときは、サーバ側でtextプロパティにaタグを埋め込んで返せばよい。
例えばこんな感じ。

```python
h["text"] = "<a href=\"javascript:alert('"+name +"')\">"+name+"</a>"
```

# おわりに
思った通りのことができそうで、ホッとしている。
新しいライブラリを使えばもっと素敵なことができるかもしれないが..

# 参考文献
- https://github.com/jzaefferer/jquery-treeview
- http://www.ne.jp/asahi/hatakeyama/design/csslayout/ajaxtest/index13.html
- [TreeViewコントロールを作成する[jquery.treeview.js]](https://www.petitmonte.com/javascript/treeview.html)
- [rails と jquery treeview でディレクトリツリーをらくらく実装する](https://blog.fusic.co.jp/archives/1829/)