編集可能 jsTree の編集結果を Pyhon で、JSON 保存

「jsTree 描画→Treeの編集操作→ 編集結果を次回表示の為に保存」
(ここでいうTreeの編集操作は、ツリーアイテムを移動したり新規作成・削除・名称変更をツリー図上で実行することです)
通常は、ブラウザで表示した jsTree → JavaScriptJSON 変換、→ サーバへ送信 → サーバ側の処理で、
jsTree 用の JSON に変換して次回の表示のデータにする。
ということをするのが一般的だと思います。
しかし、この為にサーバ側で処理を書くのがめんどくさい時など、
手元の開発用に、jsTree 描画→Treeの編集操作→ 編集結果を次回表示の為に保存、ということをするために、
Python の助けを借ります。
Python eel で、chrome 起動 → jsTree の HTML を表示、→ ツリーの編集操作
JavaScript で、ツリー図データを JSON にして、Python に渡す、
PythonJSONを解析して、jsTree がツリー表示できる形式の JSONに変換する
Python でローカルPCに保存する。→ 次回起動再読み込む JSON として保存する

f:id:posturan:20181117145940j:plain

HTMLソース
JavaScript  src="/eel.js" を指定するのを忘れないようにします。
↓ font-awesome は、右クリックコンテキストメニューのアイコンで利用します。ここでは CDNサイトを指定してます。
↓ BootStrap も CDNサイト利用です。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>develop.html</title>
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.5.0/css/all.min.css" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"/>
<link href="jstree/themes/default/style.min.css" rel="stylesheet" type="text/css"/>
<link href="css/develop.css" rel="stylesheet" type="text/css"/>
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<script src="jstree/jstree-3.3.6.min.js" type="text/javascript"></script>
<script src="js/develop-tree.js" type="text/javascript"></script>
<script type="text/javascript" src="/eel.js"></script>
<script src="js/develop.js" type="text/javascript"></script>
</head>
<body>
<div class="container-fluid">
<div class="d-flex justify-content-center edit-action">
   <button id="create" type="button" class="btn btn-outline-primary">SAVE</button>
</div>
<div>
   <ul class="edit-container">
      <li class="edit-border">
         <div class="tree-container">
            <div id="tree" class="tree-div"></div>
         </div>
      </li>
   </ul>
</div>
</div>
</body>
</html>

CSS ソース

@charset "UTF-8";
/**
 * develop.css
 */
ul{ margin: 0; padding: 0; }
li{ list-style-type: none; }

ul.edit-container::after{ content: ""; display: block; clear: both; }
li.edit-border{
   padding: 2px 10px;
   float: left;
}
li.edit-border:nth-child(1){
   width: 100%;
   height: 400px;
   overflow: auto;
}
li.edit-border:nth-child(2){
   width: 60%;
}
.edit-action{ margin: 20px 0px; }
/* ツリー図エリア背景色 */
.tree-div{
   background-color: #ffffff;
}
.edit-border{
   border: 1px solid #a0c0b0;
}
.tree-container{
   max-height: 360px;
   overflow-y: auto;
}
/*------------- for jsTee ---------------*/
/* jsTreeアイコン画像無し */
.jstree-default-large .jstree-notIcon {
   display: none;
}
.contextmenu-icon{
   color: #0085c9;
}

jsTree を描画する JS ソース

$(function(){
    $.jstree.defaults.core.themes.variant = "large";
    $.jstree.defaults.core.themes.responsive = true;

    $('#tree').jstree({
        'plugins': [ 'contextmenu','dnd' ],
        'core':{
            'data':{
                "url":"./tree.json",
                "dataType":"json"
            },
            "check_callback" : function(operation, node, node_parent, node_position, more){
                    if (operation=="move_node"){
                        if (node_parent.icon != "jstree-folder" && node_parent.id != "#") return false;
                    }
            },
        },
        "contextmenu":{
            "items":function($node){
                return {
                    "createFolder":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-folder-plus",
                        "label": "新規フォルダ作成",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).icon != "jstree-folder";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.create_node(obj, { text:'New Folder', 'icon':'jstree-folder' }, "last", function(new_node){
                                try{
                                    inst.edit(new_node);
                                }catch(ex){
                                    setTimeout(function(){ inst.edit(new_node); },0);
                                }
                            });
                        }
                    },
                    "createFile":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon far fa-file",
                        "label": "新規ファイル作成",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).icon != "jstree-folder";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.create_node(obj, { text:'New File', 'icon':'jstree-file' }, "last", function(new_node){
                                try{
                                    inst.edit(new_node);
                                }catch(ex){
                                    setTimeout(function(){ inst.edit(new_node); },0);
                                }
                            });
                        }
                    },
                    "rename":{
                        "separator_before": true,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-edit",
                        "label": "名称の変更",
                        "_disabled": false,
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.edit(obj);
                        }
                    },
                    "remove":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-trash-alt",
                        "label": "削除",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.delete_node(inst.get_selected());
                            }else{
                                inst.delete_node(obj);
                            }
                        }
                    },
                    "cut":{
                        "separator_before": true,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-cut",
                        "label": "切り取り",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.cut(inst.get_top_selected());
                            }else{
                                inst.cut(obj);
                            }
                        }
                    },
                    "copy":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-copy",
                        "label": "コピー",
                        "_disabled": function(data){
                            return $.jstree.reference(data.reference).get_node(data.reference).parent == "#";
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            if (inst.is_selected(obj)){
                                inst.copy(inst.get_top_selected());
                            }else{
                                inst.copy(obj);
                            }
                        }
                    },
                    "paste":{
                        "separator_before": false,
                        "separator_after": false,
                        "icon": "contextmenu-icon fa fa-paste",
                        "label": "貼り付け",
                        "_disabled": function(data){
                            return !$.jstree.reference(data.reference).can_paste();
                        },
                        "action": function(data){
                            var inst = $.jstree.reference(data.reference), obj = inst.get_node(data.reference);
                            inst.paste(obj);
                        }
                    }
                };
            }
        }
    }).on('loaded.jstree', function(){
        $(this).jstree('open_all');
    });
});

ツリー表示の JSON データです。 tree.json の内容

[
  {
    "id": "1",
    "icon": "jstree-folder",
    "text": "ルート",
    "children": [
      {
        "id": "11",
        "icon": "jstree-file",
        "text": "A",
        "children": []
      }
    ]
  }
]

注目のPython コードです。

# -*- coding: utf-8 -*-
import eel
import json
import codecs

@eel.expose
def writeFile(str):
    # JavaScript から受け取ったテキスト→JSON parse → jsTree用 JSON作成
    trees = []
    for node in json.loads(str):
        if node['parent'] == '#':
            trees.append(makeNode(node))
        else:
            setLeaf(node, trees)
    print(json.dumps(trees, ensure_ascii=False, indent=2))
    # ファイル保存
    with codecs.open('WebContent/tree.json', 'w+', 'utf-8') as fp:
        json.dump(trees, fp, ensure_ascii=False, indent=2)

def makeNode(node):
    return dict(id=node['id'], icon=node['icon'], text=node['text'], children=[])

def setLeaf(node, trees):
    for n in trees:
        if (n['id']==node['parent']):
            n['children'].append(makeNode(node))
            break
        else:
            setLeaf(node, n['children'])

if __name__ == '__main__':
    web_app_options = {
        'mode': "chrome-app",
        'port': 8000,
        'chromeFlags': ["--browser-startup-dialog"]
    }
    eel.init("WebContent")
    eel.start("develop.html", size=(800, 600), options=web_app_options)

「SAVE」ボタンをクリックした時に Python eel でエクスポーズしたメソッドを呼び出す
JavaScript です。「eel. + (Python で @expose したメソッド名)」で記述します。

/**
 * develop.js
 */
$(function(){
    $('#create').click(function(){
        var v = $('#tree').jstree(true).get_json('#', {flat:true});
        eel.writeFile(JSON.stringify(v))
    });
});

これらのコードの配置は、WebContent フォルダの下に HTML と tree.json
そして、Javascriptcss ファイルを直下または相対で配置します。
Python コードの eel initメソッドが、"WebContent" としているので、Python コードは、
WebContent フォルダと同じ階層に置きます。