jsTree 新規作成と名称変更を区別した処理

jsTree contextmenu プラグイン
dnd (ドラッグ&ドロップ)プラグインの処理で
イベントによる処理を書く場合、基本的には各イベント名で、
on の bind関数を書けばよいのだが、、
  .on( Eventname , function(ev, data){ }
https://www.jstree.com/api/#/?q=.jstree%20Event&f=cut.jstree

新規作成 create_node.jstree
名称変更 rename_node.jstree
削除 delete_node.jstree
切り取り cut.jstree
コピー(貼り付け実行) copy_node.jstree
貼り付け paste.jstree
移動 move_node.jstree

新規作成で新しい名称を入力して focus が外れた時に、
rename_node.jstree イベントが発生して
名称変更との区別が難しくなる。区別した処理を書くには
rename_node.jstree イベントの結合関数を書かずに、
contextmenu で定義する action で実行する edit 関数 で callback を書くしかない。
省略して、その部分だけ書くと、、

"contextmenu":{
"items":function($node){
   return {
      "createFolder":{
         "separator_before": false,
         "separator_after": false,
         "icon": "contextmenu-icon fa fa-plus",
         "label": "新規作成",
         "_disabled": false,
         "action": function(data){
            var inst = $.jstree.reference(data.reference),
            obj = inst.get_node(data.reference);
            inst.create_node(obj, { text:'New Item', 'icon':'jstree-notIcon' }
, "last", function(new_node){
               try{
                  inst.edit(new_node, new_node.text, function(data){
                     console.log("create  id = " + data.id  );
                     console.log("create  text = " + data.text );
                     console.log("parent  id = " + data.parent );
                  });
               }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, data.text, function(data){
               console.log("rename  id = " + data.id  );
              console.log("rename  new text = " + data.text );
               console.log("parent  id = " + data.parent );
            });
         }
      },
      "remove":{
         // 省略
      },
      "cut":{
         // 省略
      },
      "copy":{
         // 省略
      },
      "paste":{
         // 省略
      }
   };
},

ということになる。
また、貼り付けのイベント paste.jstree をハンドリングする場合、
「コピーから貼り付け」
「切り取りから貼り付け」
両方とも、paste.jstree イベントで動くので、
copy_node.jstree をハンドリングする関数を書いているのであれば、
   .on( "copy_node.jstree", function(ev, data){ }
以下のように、paste.jstree イベントハンドリングを書いた方が、
「切り取り」に限定することができる。

}).on('paste.jstree', function(e, data){
   if (data.mode =="move_node"){
      console.log("# paste 先 id   = "+  data.parent );
      console.log("# 対象     id   = "+  data.node[0].id );
      console.log("# 対象     text = "+  data.node[0].text );
      console.log("# paste 先 id   = "+  data.parent );
   }
}

jsTree 検索でHit だけでなく下の階層も表示する

jsTree  search プラグインで検索した結果、Hitしたノードの階層の下も
表示させるには、
以下の属性、default : false を true にする。

https://www.jstree.com/api/#/?f=$.jstree.defaults.search.show_only_matches_children

$.jstree.defaults.search.show_only_matches_children = true;

ModalWindow で処理中を表現する

先日、Wicket の ModalWindow の close ボタンを非表示にする - Oboe吹きプログラマの黙示録
を書きました。

↓ ↓ ↓  2018-12-6 に、更に改善 ↓ ↓ ↓ ↓
LazyModalPanel - Oboe吹きプログラマの黙示録

これを書いて思ったのですが、処理中画面操作させたくない時の為に ModalWindow を表示することを考えました。
CLOSE ボタンが無いモーダルウィンドウで、処理が終わったら自動的に閉じるものです。
注意が必要なのは、絶対、例外発生で閉じることができなくなってしまわないようにすることです。

処理中表現は、プログレスバーなどいろいろあるとは思いますが、進捗率が解らないことを想定して、
https://spin.js.org/
で表示しておくことにします。
f:id:posturan:20181124164735j:plain
表示中にユーザに中断させない。モーダルの後ろの画面コンテンツを触らせない。
という目的で、あえて CLOSEボタンが全くなく、右上端に CLOSE [×] もないようにします。
これを汎用的に使用できるようにします。

モーダルウィンドウのHTML :LazyModalPanel.html

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
   <form wicket:id="lazy_mocal_form" id="lazy_mocal_form" >
      <section class="lazy-modal-panel">
         <div>
            <ul>
               <li><div id="lazy_modal_progress"></div></li>
               <li><span wicket:id="message"></span></li>
               <li style="display:none"><button wicket:id="lazy_mocal_close" id="lazy_mocal_close" type="button">CLOSE</button></li>
            </ul>
         </div>
      </section>
   </form>
<script type="text/javascript">
$(function(){
   new Spinner().spin(document.getElementById('lazy_modal_progress'));
   setTimeout("$('#lazy_mocal_form').parent().parent().parent().parent().parent().prev().children('a').css('display','none');",100);
});
</script>
</wicket:panel>
</body>
</html>

モーダルウィンドウ表示の Java
内部で、
https://github.com/yipuran/yipuran-wicketcustom
にある、SerialThrowableConsumer :例外捕捉可能な シリアライズ可能な Consumer を使います。
これで、CLOSEボタンが無くても、永久ループにならない限りモーダルは必ず閉じます。

LazyModalPanel.java

import java.util.Optional;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.core.util.string.JavaScriptUtils;
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.resource.CssResourceReference;
import org.apache.wicket.request.resource.JavaScriptResourceReference;
import org.danekja.java.util.function.serializable.SerializableConsumer;
import org.yipuran.wicketcustom.function.SerialThrowableConsumer;
/**
 * LazyModalPanel
 */
public class LazyModalPanel extends Panel{

   public LazyModalPanel(String id, IModel<String> model, SerializableConsumer<AjaxRequestTarget> consumer, SerializableConsumer<Exception> oncatch){
      super(id, model);
      queue(new Form<Void>("lazy_mocal_form"));
      queue(new Label("message", Optional.ofNullable(model.getObject()).orElse("")));
      queue(new Button("lazy_mocal_close").add(AjaxEventBehavior.onEvent("click", SerialThrowableConsumer.of(t->{
         consumer.accept(t);
         ModalWindow.closeCurrent(t);
      },(t, x)->{
         oncatch.accept(x);
         ModalWindow.closeCurrent(t);
      }))));
   }
   @Override
   protected void onAfterRender(){
      super.onAfterRender();
      JavaScriptUtils.writeJavaScript(getResponse(), "setTimeout('sizefitMessageModal();$(\"#lazy_mocal_close\").trigger(\"click\");', 100);" );
   }
   @Override
   public void renderHead(IHeaderResponse response){
      super.renderHead(response);
      response.render(CssHeaderItem.forReference(new CssResourceReference(LazyModalPanel.class, "lazymodal.css")));
      response.render(JavaScriptHeaderItem.forReference(new JavaScriptResourceReference(LazyModalPanel.class, "spin.min.js")));
      response.render(JavaScriptHeaderItem.forReference(new JavaScriptResourceReference(LazyModalPanel.class, "lazymodal.js")));
   }
}

スタイルシート
lazymodal.css

@CHARSET "UTF-8";
/* lazymodal.css */
.lazy-modal-panel ul{
   margin: 10px 0;
   padding: 10px;
}
.lazy-modal-panel li{
   list-style-type: none;
   white-space: nowrap;
   display: flex;
   align-items: center;
   justify-content: space-around;
}
.lazy-modal-panel li span{
   margin: 40px 0 5px 0;
}
#lazy_modal_progress{
   position: absolute;
   margin-top: 10px;
}

描画時のサイズ調整 JavaScript
lazymodal.js

var sizefitMessageModal = function(){
   $('.w_content_container').css("height", $('.lazy-modal-panel ul').outerHeight(true) + "px" );
   $('.wicket-modal').css("width", $('.lazy-modal-panel ul').outerWidth(true) + 22 + "px" );
};

このモーダルウィンドウを呼出し表示の処理

final ModalWindow window = new ModalWindow("lazy_window").setResizable(true).setAutoSize(true);
queue(window);

queue(new Button("link").add(AjaxEventBehavior.onEvent("click", t->{
   window.setContent(new LazyModalPanel(window.getContentId(), Model.of("処理中..."), u->{

      // 時間がかかる処理

   }, x->{
      // 例外捕捉
   }));
   window.show(t);
})));

html2canvas+jsPDF で1ページに入りきらない時、

html2canvas+jsPDF で表示したHTML ページの中の任意の領域をPDFに作成する方法は、
以下のようにするが、
(入りきらない場合の対策をしていない)

$('#outpdf').click(function(){
   html2canvas(document.querySelector("#content")).then(function(canvas){
      var pdf = new jsPDF('p', 'pt', 'a4', false);
      var width = pdf.internal.pageSize.width * 0.90;
      var x = (pdf.internal.pageSize.width - width) / 2;
      pdf.addImage(canvas, 'JPEG', x, 30, width, 0);
      pdf.save('test.pdf');
   });
});

html2canvas の then で 受け取る canvas オブジェクトをJPEG変換出力する方法しかなく、
入りきらない場合はページ分割で、次のページが切れたところから描画するように、
html2canvas で部分切り出し(CROP)→ jsPDF という処理を繰り返さなくてはならない。

以下のように、何度も呼び出す html2canvas 部分切り出しをする処理の関数を用意しておく。

function screenshot(element, options){
   let cropper = document.createElement('canvas').getContext('2d');
   let finalWidth = options.width || window.innerWidth;
   let finalHeight = options.height || window.innerHeight;
   if (options.x){
      options.width = finalWidth + options.x;
   }
   if (options.y){
      options.height = finalHeight + options.y;
   }
   return html2canvas(element, options).then(function(c){
      cropper.canvas.width = finalWidth;
      cropper.canvas.height = finalHeight;
      cropper.drawImage(c, -(+options.x || 0), -(+options.y || 0));
      return cropper.canvas;
   });
};

この関数を目的のコンテンツの高さを計算して、実行する回数を算出して実行する。

$('#outpdf').click(function(){
   var hlen = 1820;
   var pdf = new jsPDF('p', 'pt', 'a4', false);
   var width = pdf.internal.pageSize.width * 0.90;
   var x = (pdf.internal.pageSize.width - width) / 2;
   var n = Math.ceil($('#content').height() / hlen);
   var offsetY = 0;
   var count = 1;
   for(var i=0;i < n;i++){
      screenshot(document.querySelector("#content"),{
         async:false, x:0, y:offsetY, height:hlen
      }).then(function(canvas){
         pdf.setPage(count);
         var width = pdf.internal.pageSize.width * 0.90;
         var x = (pdf.internal.pageSize.width - width) / 2;
         pdf.addImage(canvas, 'JPEG', x, 30, pdf.internal.pageSize.width, 0);
         if (count < n){
            pdf.addPage();
         }
         if (count==n){
            pdf.save('test.pdf');
         }
         count++;
      });
      offsetY = offsetY + 1820;
   }
});

jsPDF の setPage(n) で、書き出すページを指定する。
addImage したら、addPage() で次のページを付与する。
ページの最後=ループの最後で、pdf.save でPDF出力する

Wicket の ModalWindow の close ボタンを非表示にする

ModalWindow の 右端上の CLOSEボタン[×] を非表示にする。
もちろんCLOSEアクション()をする ModalWindow.closeCurrent(AjaxRequestTarget) 実行するものを
モーダル内でコンテンツ表示するようにしなければならないが、、
CSSで以下を書く。

.w_close{
	display: none;
}

Wicket が、8.2.0 になった!

Wicket 8.2.0 がリリースされた!

ただし、com.googlecode.wicket-jquery-ui は、8.1.0 のままだ。

wicket-guice も、最新の guice 4.2.2 に依存になっているので注意

guice 4.2.2 は、Java 11 対応のビルドのことで、diff が見当たらない。

jsTree ノード選択時の処理を整理する

jsTreeでノード選択時の処理、
jQuuery on メソッドで、select_node.jstree でイベントと結合

$('#tree').jstree({
   'plugins': [ 'contextmenu','dnd' ],
   'core':{
   data':{
         "url":"./tree.json",
         "dataType":"json"
      },
      'check_callback' : true,
   }
}).on('select_node.jstree', function(e, data){
   // 選択したノード情報
   var id = data.node.id;
   var icon = data.node.icon;
   var text = data.node.text;
   var parent = data.node.parent;
   var parents = data.node.parents;  // rootからの parent配列
   // parent  逆順にすれば Path になる。 */   
   var path =  data.node.parents.slice().reverse().join("/") + "/" + data.node.id;

   // 選択配下のノードツリーは、配列JSON で取得
   var selectedAry = $('#tree').jstree(true).get_json(data.node.id, {flat:true});
   $.each(selectedAry, function(i, e){
       console.log("# id = " + e.id + "  icon = " + e.icon + "  text = " + e.text + "  parent = " + e.parent);
   });
   var txt = JSON.stringify(selectAry)
   console.log( txt )
}).on('loaded.jstree', function(){
   $(this).jstree('open_all');
});

select_node.jstree の中で、input hidden にセットしたり、AJAX送信したり、
必要になることをすればよい。