ツリービューの情報を管理するのに、ノードの親子関係の処理を新たにコーディングするのは、どんな言語であれ
労力が必要です。
できれば、画面でツリーの描画操作の結果をそのまま管理するのが楽です。
せっかく JSON というオブジェクトで表現するのですから、このJSONの考えで管理したいです。
jsTree 使用のコード上で!JSONを出力させる方法は、、get_jsonを実行することです。
.on('loaded.jstree', function(){ var v = $(this).jstree(true).get_json('#', {flat:true}); console.log( JSON.stringify(v, null, " ") ); });
でもここで出力されるのは、、jSTree を呼ぶ時に書いたそのままのJSONではないです。
[ { "id": "1", "text": "Root", "icon": "jstree-folder", "li_attr": { "id": "1" }, "a_attr": { "href": "#", "id": "1_anchor" }, "state": { "loaded": true, "opened": true, "selected": false, "disabled": false }, "data": {}, "parent": "#" }, { "id":"2" :
この中で改めて再度jQueryでjsTree()を呼び出し、描画で必要な情報は、
"id", "text", "icon", "parent", と "state" の各boolean値です。
stateは最初表示し直すならもしかして要らないかもしれません。
そうすると、check_callback や、move_node.jstree のイベント捕捉で、
HTML-form の input type="hidden" フィールドにセットして
送信してあげれば、
「ツリーの編集結果」→ サーバに送信
ということができるわけです。
(例)
input type="hidden" フィールドにセットを関数にしておきます。
var setHiddenJson = function(){ var v = $('#tree').jstree(true).get_json('#', {flat:true}); $("#jsondata").val(JSON.stringify(v)); };
check_callback のリネーム、削除の捕捉では setTimeout でcallする必要があります
"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; } if (operation=="rename_node" || operation=="delete_node"){ setTimeout("setHiddenJson()", 100); } }
ドラッグ&ドロップ時はそのまま呼出します。
.on('move_node.jstree', function(e, data){ setHiddenJson(); })
受け取りサーバでの管理、Java前提ですが、、
受信テキストJSON → Google Gson で任意オブジェクトに変換します。
→ 以下、JstreeNode です。
public class JstreeNode implements Serializable{ public String id; public String text; public String icon; public String parent; public Object state; public boolean loaded; public boolean opened; public boolean selected; public boolean disabled; public JstreeNode(){ } // setter を用意します。getterメソッド は不要
肝になるのは、JSON → 任意オブジェクトのアダプタです。
デシリアライザが必要でシリアライザは不要なのですが、ついでなので書きます。
import java.lang.reflect.Type; import java.util.HashMap; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; /** * JstreeNodeAdapter */ public class JstreeNodeAdapter implements JsonDeserializer<JstreeNode> , JsonSerializer<JstreeNode>{ @Override public JstreeNode deserialize(JsonElement json, Type typeOfT , JsonDeserializationContext context) throws JsonParseException{ final JsonObject jo = json.getAsJsonObject(); JstreeNode rtn = new JstreeNode(); rtn.id = jo.get("id").getAsString(); rtn.text = jo.get("text").getAsString(); rtn.icon = Optional.ofNullable(jo.get("icon")).map(e->e.getAsString()).orElse(null); rtn.parent = Optional.ofNullable(jo.get("parent")).map(e->e.getAsString()).orElse(null); final JsonObject state = jo.getAsJsonObject("state"); if (state != null){ rtn.loaded = Optional.ofNullable(state.getAsJsonPrimitive("loaded")).map(e->e.getAsBoolean()).orElse(true); rtn.opened = Optional.ofNullable(state.getAsJsonPrimitive("opened")).map(e->e.getAsBoolean()).orElse(true); rtn.selected = Optional.ofNullable(state.getAsJsonPrimitive("selected")).map(e->e.getAsBoolean()).orElse(false); rtn.disabled = Optional.ofNullable(state.getAsJsonPrimitive("disabled")).map(e->e.getAsBoolean()).orElse(false); } return rtn; } @Override public JsonElement serialize(JstreeNode src, Type typeOfSrc , JsonSerializationContext context){ final JsonObject rtn = new JsonObject(); rtn.add("id", context.serialize(src.id)); rtn.add("text", context.serialize(src.text)); rtn.add("icon", context.serialize(src.icon)); rtn.add("parent", context.serialize(src.parent)); HashMap<String, Object> map = new HashMap<>(); map.put("loaded", src.loaded); map.put("opened", src.opened); map.put("selected", src.selected); map.put("disabled", src.disabled); rtn.add("state", context.serialize(map)); return rtn; } }
HTML-form 受信データをこのアダプタを使って、JstreeNodeリストを組み立てます。
Gson gson = new GsonBuilder().serializeNulls() .registerTypeAdapter(JstreeNode.class, new JstreeNodeAdapter()) .create(); List<JstreeNode> list = gson.fromJson(jsonHidden.getModelObject() , new TypeToken<List<JstreeNode>>(){}.getType());
TypeToken は、com.google.common.reflect.TypeToken です。
画面でツリー編集したものはこれで受け取り Javaオブジェクトから好きな形に変換
するなり、管理します。
問題は、JstreeNodeリストデータ→jsTree表示で、
このjsTree表示を実行するのは、JSON受信した時とは少し異なるJSONでなければ
なりません。
しかも、jsTree は、JSONのルールチェックを厳密に行います。
「少し異なるJSON」=子ノードは、"children" キーで書かなくてはならないです。
そこで、JstreeNodeリストデータ→jsTree 表示用、AJAX で表示するデータへの変換
という処理のクラスを用意します。
誰でも書くような、簡単な再帰ロジックです。
import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * JstreeNode のリストの InputStream から * jsTree jsTree AJAX に渡す JSONデータ を OutputStream に書き込む」 * * 例) * try(InputStream in = new FileInputStream(file); * ByteArrayOutputStream out = new ByteArrayOutputStream()){ * JstreeDataProvider p = JstreeDataProvider.of(in); * p.write(out); * }catch(IOException e){ * e.printStackTrace(); * } * */ public final class JstreeDataProvider{ private InputStream in; private Gson gson; public JstreeDataProvider(InputStream in){ this.in = in; gson = new GsonBuilder().serializeNulls() .registerTypeAdapter(JstreeNode.class, new JstreeNodeAdapter()) .create(); } public static JstreeDataProvider of(InputStream in){ return new JstreeDataProvider(in); } public void write(OutputStream out) throws IOException{ Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); List<JstreeNode> list = gson.fromJson(reader, new TypeToken<List<JstreeNode>>(){}.getType()); String result = list.stream().filter(e->e.parent.equals("#")) .map(root->{ StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("\"id\":\"" + root.id + "\",\"icon\":\"" + root.icon + "\",\"text\":\"" + root.text +"\""); sb.append( childParse(list, list.stream() .filter(e->e.parent.equals(root.id)).collect(Collectors.toList())) ); sb.append("}"); return sb.toString(); }).collect(Collectors.joining(",")); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); writer.write("["); writer.write(result); writer.write("]"); writer.flush(); } private String childParse(List<JstreeNode> orglist, List<JstreeNode> clist){ if (clist.size()==0) return ""; StringBuilder csb = new StringBuilder(); csb.append(",\"children\":["); csb.append( clist.stream().map(e->{ StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("\"id\":\"" + e.id + "\",\"icon\":\"" + e.icon + "\",\"text\":\"" + e.text +"\""); sb.append( childParse(orglist, orglist.stream() .filter(t->t.parent.equals(e.id)).collect(Collectors.toList())) ); sb.append("}"); return sb.toString(); }).collect(Collectors.joining(",")) ); csb.append("]"); return csb.toString(); } }
これで、
「画面でツリー編集操作」→「サーバ送信」→「JSONをJava Object に変換」→
→「DBなどに管理」→「再表示要求」→「Java Object からJSON」
→「jsTreeに送る」→「再描画」
のサイクルが作れます。わざわざ、JSONを別のデータ形式にするのは、
画面の描画以外の管理情報を付与して
jsTreeが抱える画面情報の表現と別の管理情報による管理が、現実は必要だからです。
つまり、jsTreeは、あくまでも描画における最低限の描画情報を持っているだけで、
「ツリー構造の情報管理」という、大きなものまでは、範疇にできないのです。