ツリービューの情報を管理するのに、ノードの親子関係の処理を新たにコーディングするのは、どんな言語であれ
労力が必要です。
できれば、画面でツリーの描画操作の結果をそのまま管理するのが楽です。
せっかく 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(){
}
肝になるのは、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は、あくまでも描画における最低限の描画情報を持っているだけで、
「ツリー構造の情報管理」という、大きなものまでは、範疇にできないのです。