Handsontable で callback サンプル

Handsontable は、ちょっと勉強すれば、Excelのように簡単な表計算も、callback 関数で書ける。
最初、Handsonテーブルの説明ページを見た時、「こんなにイベントがあるの!」と驚いたけど。。。
まずは、beforeChange を例題に簡単に書いてみる前に認識しておくべき規則性が重要!

onBeforeChange function(changes, source){ } の引数 source と changes に渡されるものは、
セルが入力変更されると、
  source → 'edit' になる。
  changes → [ Array[4] ] 以下の配列になる。

  changes[0][0] // 変更行
  changes[0][1] // 列名
  changes[0][2] // 変更前の値
  changes[0][3] // 変更後の値


これは、以下のように変更入力した時に、onBeforeChange:の callBack関数で、コンソールログを
出力すると解る。

Handsontable で書いた以下の表があるとする。

f:id:posturan:20160313191729j:plain



callbackとして以下を付与して変更入力すると

onBeforeChange: function(changes, source){
  console.log($.toJSON(changes)); // ← Googlejquery.json-2.4.min.js を使って確認
  console.log(source);
}

f:id:posturan:20160313191723j:plain



このサンプル、”価格”と”本数”を変更入力すると自動計算して小計にセットするのに、
以下のように、コードを用意する。

サンプルなので、JSONデータが以下のように存在したとして、
var data = [
    { "price": "1100"  , "mt": "7"  ,  "sts": "8316"  },
    { "price": "200"   , "mt": "3"  ,  "sts": "648"   },
    { "price": "1200"  , "mt": "2"  ,  "sts": "2592"  }
];

このdata を以下のコードで組む

$('#example').handsontable({
   data: data,
   minSpareRows: 0,
   colHeaders: ["価格", "本数", "小計(税込)"],
   columns: [
               { data: "price" ,  type: 'numeric' , format: '0,0' },
               { data: "mt"    ,  type: 'numeric' },
               { data: "sts"   ,  type: 'numeric' , format: '0,0' , readOnly: true },
   ],
   contextMenu: false,
   onBeforeChange: function(changes, source){
      if (source==='edit'){
         if (changes[0][1]==='price'){
            data[changes[0][0]]['sts'] = changes[0][3] * data[changes[0][0]]['mt'] * 1.08;
         }else if(changes[0][1]==='mt'){
            data[changes[0][0]]['sts'] = data[changes[0][0]]['price'] * changes[0][3] * 1.08;
         }
      }
   },
});
HTMLは、、

<div id="example" class="handsontable"></div>


サンプルなので、データをソース上で書いたが、JSONデータをサーバから取得して、変更入力の後に、
再度、JSONデータでサーバにPOSTするアーキテクチャも魅力的になる。

Handsontable は、columns 定義を使うと良い。

先日、Handsontable を始めたばかりで、良く理解してなくて、
http://blog.zaq.ne.jp/oboe2uran/article/1002/
では、
データが以下のケースの場合、、、
var data = [
{"a":"1","b":"2"},
{"c":"3","c":"4"},
];

期待する結果を得られなかった。
key-valueJSON データは、key を列として定義するので、データも以下である必要がある。
var data = [
   { "a":1, "b":2 },
   { "a":3, "b":4 },
];

これで、以下のとおり列定義を指定しなくても、、

$('#example').handsontable({
   data: data,
   minSpareRows: 0,
   contextMenu: false,
});

f:id:posturan:20160313191621j:plain


となるのだが、これでは、あまり意味がない!
列の定義として、 columns というのを使うと、

var data = [
   { "a": 1200, "b":  21.343917  , "c": "2014/05/18" },
   { "a": 130,  "b": 4023.018636 , "c": "2014/06/02" },
];
というデータある時、、

$('#example').handsontable({
   data: data,
   minSpareRows: 0,
   contextMenu: false,
   columns: [
      { data: "a" ,  type: 'numeric' , format: '0,0' },
      { data: "b" ,  type: 'numeric' , format: '0,0.00' },
      { data: "c" ,  type: 'date'    , dateFormat: 'yy/mm/dd' },
   ],
});
これで、以下の表示ができる。

f:id:posturan:20160313191611j:plain



データの列の並びを変えて同じ結果が得られる。

var data = [
 {  "b":  21.343917  , "c": "2014/05/18" , "a": 1200  },
 {  "c": "2014/06/02", "a": 130          , "b": 4023.018636  },
];

これでも、同じ結果が得られるということは、Java Object List → JSON 結果を表示するのも、
そんなに壁が高くない。

GsonBuilder を使う。

GoogleJSON ライブラリ、GSON を使う時、

普通に使う、new演算子での生成、
  Gson gson = new Gson();
これをそのまま使うと、java.util.Date は、以下のようになってしまう。


System.out.println( gson.toJson(new Date()) );

の結果は、、
  "May 10, 2014 3:52:19 PM"

時刻を無視して日付だけを処理したい場合、これだと JavaScript で処理する時に面倒である。

出力されるJSONそのもを、yyyy/MM/dd の書式にしたければ、GsonBuilder を使う。


Gson gson = new GsonBuilder().setDateFormat("yyyy/MM/dd").create();

System.out.println( gson.toJson(new Date()) );

の結果は、、
  "2014/05/10"

と思いどおりになる。

Handsontable コンテキストメニューを日本語化

Handsontableコンテキストメニューの見出しを日本語にする場合は、以下のようにする。

contextMenu 属性の items は、Handsontable で約束された動作が、
キーとして以下のように定義されているので、このキーに対する
「name」属性が、コンテキストメニュー表示部分なので、
name属性を日本語で書き直す。


という作業をすれば良い。

Handsontable で約束されたキーは、以下のとおり。

row_above   1行挿入 above
row_below   1行挿入 below
hsep1     セパレータ
col_left     1列挿入 left
col_right    1列挿入 right
hsep2     セパレータ
remove_row    行削除
remove_col    列削除
hsep3     セパレータ
undo       アンドゥ
redo       リドゥ

以下のように、コンテキストメニューを日本語化する場合、

f:id:posturan:20160313191819j:plain



次のように contextMenu , items , name を指定する。


$(function(){
   var data = [
      ["", "Maserati", "Mazda", "Mercedes", "Mini", "Mitsubishi"],
      ["2009", 0, 2941, 4303, 354, 5814],
      ["2010", 5, 2905, 2867, 412, 5284],
      ["2011", 4, 2517, 4822, 552, 6127],
      ["2012", 2, 2422, 5399, 776, 4151],
   ];
   $('#example').handsontable({
      data: data,
      minSpareRows: 1,
      colHeaders: true,
      contextMenu: { items: { 'row_above': { name: '1行挿入、上に' },
                              'row_below': { name: '1行挿入、下に' },
                              'hsep1': "---------",
                              'col_left':  { name: '1列挿入、左に' },
                              'col_right': { name: '1列挿入、右に' },
                              "hsep2": "---------",
                              'remove_row': { name: '行削除' },
                              'remove_col': { name: '列削除' },
                              "hsep3": "---------",
                              'undo': { name: '戻る' },
                              'redo': { name: 'やり直す'   },
                     }
      }
   });
});

GPS緯度経度→Google API 住所文字列取得

AsyncTask を使ってGPS 位置情報を取得して、Google API サービスで住所文字列を取得する。

・AsyncTask で、LocationListener を実装する。
・LocationManager#requestLocationUpdates は、doInBackground で実行できないので、onPreExecute で実行する。
・HTTP Client で、GPS Locationで取得した緯度、経度をUriに埋め込みリクエストを投げる。
json で住所を受け取るGoogle API サービスの Uri は、
http://maps.googleapis.com/maps/api/geocode/json?latlng=xxxx.xxxxxx,yyyyy.yyyyy&sensor=true&language=ja

xxxx.xxxxxx,yyyyy.yyyyy = 緯度+','+経度

・結果 jsonformatted_address は、「国名,」が着いてしまうのでこれを取り除く


import java.io.ByteArrayOutputStream;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
/**
 * GPS location -> address
 */

public class LocationAddressTask extends AsyncTask<Void,Void,String> implements LocationListener{
   private Activity activity;
   private Location mlocation;
   private LocationManager locationManager;

   public LocationAddressTask(Activity activity){
      this.activity = activity;
   }
   @Override
   protected final void onPreExecute(){
      locationManager = (LocationManager)activity.getSystemService(Context.LOCATION_SERVICE);
      locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,this);

   }
   @Override
   protected final String doInBackground(Void...params){
      String addressString = null;
      while(mlocation==null){
         try{ Thread.sleep(100); }catch(Exception e){}
      }
      locationManager.removeUpdates(this);

      HttpClient httpclient = new DefaultHttpClient();
      String uri = "http://maps.googleapis.com/maps/api/geocode/json"
                  + "?latlng=" + Double.toString(mlocation.getLatitude())
                  + "," + Double.toString(mlocation.getLongitude())
                  + "&sensor=true&language=ja";
      HttpGet httpget = new HttpGet(uri.toString());
      try{
         HttpResponse response = httpclient.execute(httpget);
         if (response.getStatusLine().getStatusCode()==HttpStatus.SC_OK){
            ByteArrayOutputStream ost = new ByteArrayOutputStream();
            response.getEntity().writeTo(ost);
            String str = ost.toString();
            ost.close();
            addressString = parseAddress(str);
         }
      }catch(Exception e){
         Logger.w(e.getMessage(), e);
      }
      return addressString;
   }
   private String parseAddress(String jsonstr) throws JSONException{
      JSONObject json = new JSONObject(jsonstr);
      JSONArray array = json.getJSONArray("results");
      JSONArray address_components;
      String addressString = null;
      String formatted_address = null;
      for(int i=0;i < array.length();i++){
         JSONObject jsonObject = array.getJSONObject(i);
         address_components = jsonObject.getJSONArray("address_components");
         formatted_address = jsonObject.getString("formatted_address");
         if (!"".equals(formatted_address)){
            for(int n=0;n < address_components.length();n++){
               JSONArray types = address_components.getJSONObject(n).getJSONArray("types");
               if (types.length() > 1){
                  if ("country".equals(types.getString(0)) && "political".equals(types.getString(1))){
                     String country = address_components.getJSONObject(n).getString("long_name") + ", ";
                     addressString = formatted_address.replace(country, "").replaceAll("\\?", "-");
                     break;
                  }
               }
            }
            if (addressString != null) break;
         }
      }
      return addressString;
   }
   @Override
   protected final void onPostExecute(String result){
      // 取得した住所を使用する
   }
   @Override
   protected void onCancelled(){
      super.onCancelled();
      locationManager.removeUpdates(this);
   }

   @Override
   public void onLocationChanged(Location location){

      mlocation = location;
   }
   @Override
   public void onProviderDisabled(String provider){
   }
   @Override
   public void onProviderEnabled(String provider){
   }
   @Override
   public void onStatusChanged(String provider, int status, Bundle extras){
   }

}

Handsontable の Validate機能

結構よくできてる。。。

http://handsontable.com/demo/validation.html


でも、メールアドレスのチェックが誤ってる気がする。

以下の正規表現ではないだろうか。。。

^[_A-Za-z0-9-]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*((\.[A-Za-z]{2,}){1}$

Handsontable の datepicker スタイル調整

Handsontable datepicker jQuery ui ) は、デフォルトのスタイルだとちょっとかっこ悪い。

年と月のプルダウン表示が2段になったり、サイズも大きい。
Handsontable の CSSというより、jQuery ui ダウンロードしてそのまま使うとそうなってしまう。

(サンプル)jquery.ui.datepicker-ja.js を使ったとして、

$(function(){
   var data = [
      [ 1, "2014/03/07" ],
      [ 2, "2014/04/14" ],
      [ 3, "2014/05/25" ],
      [ 4, "2014/08/15" ],
   ];
   $('#example').handsontable({
      data: data,
      minSpareRows: 1,
      colHeaders: [ "", "予定日"],
      contextMenu: true,
      columns: [
                   {},
                   {
                      type: 'date',
                      dateFormat: 'yy/mm/dd'
                   },

                ],
   });
});


2列目最後をダブルクリックした時の表示は、

f:id:posturan:20160313191919j:plain



これでは、あんまりなので、まずは、jQuery uiをそのまま使ってしまったことの悪いところ修正、

以下のスタイルを記述→ width が、49% に固定されてたのを廃止

.ui-datepicker{
    font-size: 80%;
}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year {
    width: auto;
}


すると以下の表示になる。

f:id:posturan:20160313191910j:plain



ここで、「今日」のボタンを表示させたくない場合に悩んだ。。

Handsontable の jquery.handsontable.full.js は、datepicker の設定として、 showButtonPanel: true になっている。


5166行目あたりが、
    var defaultOptions = {
      dateFormat: "yy-mm-dd",
      showButtonPanel: true,
      changeMonth: true,
      changeYear: true,
      onSelect: function (dateStr) {
        that.setValue(dateStr);
        that.finishEditing(false);
      }
    };
    this.$datePicker.datepicker(defaultOptions);
となっている。


この showButtonPanel を false にすれば良いのだが、、
CSSスタイルシートで、
button.ui-datepicker-current { display: none; }

を指定するのが手っ取り早い。

f:id:posturan:20160313191902j:plain