入力フィールドでよく使いそうな jQuery 処理のメモ

HTML 入力フィールドでよく使いそうな jQuery 処理をまとめる。

/**
 * input-support.js
 */
/* for 全角数字入力→ 半角数字 */
var numberConvert = function(value){
   var str = new String(value);
   var han = str.replace(/[0-9]/g, function(s){ return String.fromCharCode(s.charCodeAt(0)-0xFEE0); });
   return han.replace(/^[\-|ー|―|-]/, function(s){ return '-'; }).replace(/[。.]/g, ".");
};
/* 全角英数字→半角英数字 */
var zenkakuTohan = function(txt){
   return txt.replace(/[A-Za-z0-9]/g, function(s){ return String.fromCharCode(s.charCodeAt(0)-0xFEE0); }).replace(/[。.]/g, ".");
};
/* 全角数字→半角数字 */
var zenkakuNumToHan = function(txt){
   return txt.replace(/[0-9]/g, function(s){ return String.fromCharCode(s.charCodeAt(0)-0xFEE0); });
};
/* ひらがな→カタカナ */
var hirakanaTokatakana = function(txt){
   return txt.replace(/[ぁ-ん]/g, function(s){ return String.fromCharCode(s.charCodeAt(0)+0x0060); });
}
/* カタカナ→ひらがな */
var hirakanaTokatakana = function(txt){
   return txt.replace(/[ァ-ン]/g, function(s){ return String.fromCharCode(s.charCodeAt(0)-0x0060); });
}
/* 日付入力チェック */
var isValidDate = function(s){
   if (!s.match(/^\d{4}\/(0{0,1}[1-9]|1[012])\/(0{0,1}[1-9]|[12][0-9]|3[01])$/)){
      return false;
   }
   var ary = s.split(/\//);
   var y = ary[0];
   var m = parseInt(ary[1], 10);
   var d = parseInt(ary[2], 10);
   if(m < 1 || m > 12 || d < 1 || d > 31) {
      return false;
   }
   var dt = new Date(y, m - 1, d, 0, 0, 0, 0);
   if(dt.getFullYear() != y || dt.getMonth() != m - 1 || dt.getDate() != d){
      return false;
   }
   return true;
};
/* 数値→3桁区切り : 逆は、.replace(/,/g, ''); を使う */
var digitFormat = function(str){
   var num = new String(str).replace(/,/g, "");
   while(num != (num = num.replace(/^(-?\d+)(\d{3})/, "$1,$2")));
   return num;
}
/**
 * 半角英数字入力:全角英数字入力は強制的に半角英数字入力
 * inputHankakuANK( selector );
 */
var inputHankakuANK = function(input){
   $(input).focus(function(eo){
      $(this).select();
   }).blur(function(eo){
      $(this).val(zenkakuTohan($(this).val()));
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(zenkakuTohan($(this).val()));
   });
};
/**
 * 半角英数字、記号のみしか打てない。
 * inputHankakuANKForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputHankakuANKForce = function(input, options){
   $(input).focus(function(eo){
      $(this).prop('type','tel');
   }).blur(function(eo){
      $(this).prop('type','text');
   }).bind("paste",function(){
      return false;
   }).keydown(function(eo){
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
      if (eo.keyCode==229){
         return false;
      }
   });
};
/**
 * 整数入力:
 * inputInteger( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputInteger = function(input, options){
   $(input).focus(function(eo){
      $(this).select();
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(numberConvert($(this).val()).replace(/[^\-0-9]/g, ''));
   }).keydown(function(eo){
      if ($(this).val()!=="" && eo.keyCode==189) return false;
      if ($(this).val()=="0"){ $(this).select(); }
      if ($(this).val()=="-"){
         if (eo.keyCode==48 || eo.keyCode==96) return false;
      }
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
   });
};
/**
 * 整数入力:IMEが全角だと反応させない
 * inputIntgerForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputIntegerForce = function(input, options){
   $(input).focus(function(eo){
      $(this).prop('type','tel');
      $(this).select();
   }).blur(function(eo){
      $(this).prop('type','text');
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val($(this).val().replace(/[^\-0-9]/g, ''));
   }).keydown(function(eo){
      if ($(this).val()!=="" && eo.keyCode==189) return false;
      if ($(this).val()=="0"){ $(this).select(); }
      if ($(this).val()=="-"){
         if (eo.keyCode==48 || eo.keyCode==96) return false;
      }
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
      if (eo.keyCode==229){
         return false;
      }
   });
};
/**
 * コード入力:マイナスは許さない
 * inputNumber( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputNumber = function(input, options){
   $(input).focus(function(eo){
      $(this).select();
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(numberConvert($(this).val()).replace(/[^0-9]/g, ''));
   }).keydown(function(eo){
      if (eo.keyCode==189) return false;
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
   });
};
/**
 * コード入力:IMEが全角だと反応させない
 * inputNumberForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputNumberForce = function(input, options){
   $(input).focus(function(eo){
      $(this).prop('type','tel');
      $(this).select();
   }).blur(function(eo){
      $(this).prop('type','text');
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val($(this).val().replace(/[^\0-9]/g, ''));
   }).keydown(function(eo){
      if (eo.keyCode==189) return false;
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
      if (eo.keyCode==229){
         return false;
      }
   });
};

/**
 * 金額入力 :
 * inputPriceForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputPrice = function(input, options){
   $(input).focus(function(eo){
      $(this).select();
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(digitFormat(numberConvert($(this).val()).replace(/[^\-0-9]/g, '')));
   }).keydown(function(eo){
      if ($(this).val()!=="" && eo.keyCode==189) return false;
      if ($(this).val()=="0"){ $(this).select(); }
      if ($(this).val()=="-"){
         if (eo.keyCode==48 || eo.keyCode==96) return false;
      }
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
   });
};
/**
 * 金額入力 : IMEが全角だと反応させない
 * inputPriceForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputPriceForce = function(input, options){
   $(input).focus(function(eo){
      $(this).prop('type','tel');
      $(this).select();
   }).blur(function(eo){
      $(this).prop('type','text');
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(digitFormat($(this).val().replace(/[^\-0-9]/g, '')));
   }).keydown(function(eo){
      if ($(this).val()!=="" && eo.keyCode==189) return false;
      if ($(this).val()=="0"){ $(this).select(); }
      if ($(this).val()=="-"){
         if (eo.keyCode==48 || eo.keyCode==96) return false;
      }
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
      if (eo.keyCode==229){
         return false;
      }
   });
};
/**
 * 電話番号入力:
 * inputPhone( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputPhone = function(input, options){
   $(input).focus(function(eo){
      $(this).select();
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val(numberConvert($(this).val()).replace(/[^\-0-9]/g, ''));
   }).keydown(function(eo){
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
   });
};
/**
 * 電話番号入力:IMEが全角だと反応させない
 * inputPhoneForce( selector , { numlock : function(){...} } );
 *   numlock: NumLock OFF(消灯) で入力できない時の関数
 */
var inputPhoneForce = function(input, options){
   $(input).focus(function(eo){
      $(this).prop('type','tel');
      $(this).select();
   }).blur(function(eo){
      $(this).prop('type','text');
   }).bind("paste",function(){
      return false;
   }).keyup(function(eo){
      $(this).val($(this).val().replace(/[^\-\0-9]/g, ''));
   }).keydown(function(eo){
      if ((eo.keyCode >= 33 && eo.keyCode <= 39)|| eo.keyCode==40 || eo.keyCode==45 || eo.keyCode==46 || eo.keyCode==12){
         if (event.getModifierState("NumLock")===false){
            if (options !== undefined && options['numlock'] !== undefined){
               options['numlock']();
            }
         }
      }
      if (eo.keyCode==229){
         return false;
      }
   });
};

/**
 * Tooltip 実行
 */
var callTooltip = function(input, message){
   $(input).prop("title", "");
   $(input).tooltip({
      content: message,
      show: { effect: "slideDown" },
      hide: { effect: "slideDown" },
   });
   $(input).tooltip("open");
   setTimeout("$('" + input + "').tooltip('close');", 2000);
};

java.util.regex.Matcher とリスト置換

テンプレートのような、ある文字列の特定の文字をパラメータ文字列リストに置き換える処理は簡単だと思ってたが
以外と難しい。java.util.regex.Matcher を回した時に、置換メソッド実行していけば良いと思ってたが、
Matcher に、replaceFirst か replaceAll しかないではないか。

お題:

String str = "123456?789?01234?5";
String[] parameters = { "A", "B", "'c'" };

とあった時、'?' の文字をこの parameters の文字列に順に置換して

123456A789B01234'c'5

としたいのである。

'?' でテンプレートにあたる文字列 str を split してなんてやり方は、なんか賢くない。。
Matcher を使うことにして考えて書いたら、なんか動物的な思考のコーディングだけど以下のようなった。

Matcher m = Pattern.compile("\\?").matcher(str);
List<Integer> ixlist = new ArrayList<>();
while(m.find()) ixlist.add(m.start());
Collections.reverse(ixlist);

AtomicReference<String> cache = new AtomicReference<String>(str);
AtomicInteger n = new AtomicInteger(parameters.length-1);
ixlist.stream().forEach(i->{
   cache.set(cache.get().substring(0, i) + parameters[n.get()] + cache.get().substring(i+1));
   n.set(n.get()-1);
});

String result = cache.get();

これで、求めてる結果を得ることができる。
Matcher の find() 後の start() の結果を逆順リストにして String#substring で文字列を作り直してる
→ なんか感情を丸出しのコードだ。

この題目を解く必要性を打ち明けると mybatis のログ、org.apache.ibatis.logging.jdbc.BaseJdbcLogger 
が出力する SQL
Preparing と Parameters から、動かせるSQL文をリメイクしたかったからだ。

textarea 横スクロール

HTML textarea の wrap 属性って気が付けば wrap="off" は、もう使用しないんですね。
横スクロールが自動で出るように wrap="off" を使うのではなく、

wrap="soft"

と指定する。

もしくはCSSで、

textarea{
    resize: none;

   white-space: pre;
   overflow-wrap: normal;
   overflow-x: auto;
}

でも良い。

右クリックコンテキストメニューの基本サンプル

jQuery で右クリックコンテキストメニューを表示するのに気に入って使うようになったもの
 
https://github.com/swisnl/jQuery-contextMenu

https://swisnl.github.io/jQuery-contextMenu/

以前、これを使用する時のアイコンを変える方法を書いた。→
コンテキストメニューのアイコンをカスタマイズ - Oboe吹きプログラマの黙示録

改めて、基本的な使い方として、テーブル(表)の 行挿入削除、コピー&Paste をサンプルとして書いておく。

jquery.contextMenu.css
jquery.ui.position.js
jquery.contextMenu.js

GitHub から入手して

<link rel="stylesheet" type="text/css"  href="jquery.contextMenu.css"/>
<script type="text/javascript" src="jquery.ui.position.js"></script>
<script type="text/javascript" src="jquery.contextMenu.js"></script>

さらに、文字フォントなどは調整することあるので、

/*--------- 右クリックコンテキストメニュー -------------------*/
.context-menu-list {
  min-width: 120px;
  max-width: 200px;
  padding: 4px 0;
  margin: 5px;
  font-size: 14px;
}
/* 非活性の文字色  */
.contextmenu-item-disabled{ color: #cccccc !important; }

/* 右クリックした行の選択色  */
.context-menu-active td{
	background-color: #ffffc0 !important;
}

table として、以下のようにあった場合

<table id="target-table">
   <thead>
     <tr><th>ID</th><th>Name</th><th>Date</th><th>price</th><th>code</th><th>select</th><th>memo</th></tr>
   </thead>
   <tbody>
     <tr>
      <td>10</td><td>apple</td><td>2017-12-11</td><td>200</td><td>104502</td><td>on</td><td></td>
     </tr>
   </tbody>
</table>

右クリックコンテキストメニューjQuery

var initList = function(){
   // 列数
   var columnLength = $('#target-table thead tr th').length;
   // コピー
   var copyTRhtml = null;
   // tbody contextmenu → 行挿入のみ
   $.contextMenu({
      selector: '#target-table tbody',
      build: function($trigger, e){
         return {
            callback: function(key, options){
               if (key=="add"){
                  $trigger.append("<tr></tr>");
                  for(var i=0;i < columnLength;i++) $trigger.children('tr:last').append("<td></td>");
               }
            },
            items: {
               "add":{ name:"1行挿入", icon:"add", },
            },
            events: {
               show: function(){
                  console.log("-- contextmenu tbody show ---" );
               },
               hide: function(){
                  console.log("-- contextmenu tbody hide ---" );
               }
            }
         };
      },
   });
   // tr contextmenu → 行挿入/削除/コピー/貼付
   $.contextMenu({
      selector: '#target-table tbody tr',
      build: function($trigger, e){
         // コンテキスト行選択色→ ON
         $trigger.addClass("seleted-context");
         return {
            callback: function(key, options){
               if (key=="add"){
                  // 追加
                  $trigger.before("<tr></tr>");
                  for(var i=0;i < columnLength;i++) $trigger.prev().append("<td></td>");
               }else if(key=="delete"){
                  // 削除
                  $trigger.remove();
               }else if(key=="copy"){
                  // コピー
                  copyTRhtml = "<tr>";
                  $trigger.children('td').each(function(ix, td){
                     copyTRhtml = copyTRhtml + "<td>" + $(td).html() + "</td>";
                  });
                  copyTRhtml = copyTRhtml + "</tr>";
               }else if(key=="paste"){
                  // 貼付け
                  if (copyTRhtml != null){
                     $trigger.before(copyTRhtml);
                     $trigger.remove();
                     copyTRhtml = null;
                  }
               }
            },
            items:{
               "add":    { name:"1行挿入", icon:"add",    visible: true, },
               "delete": { name:"削除",     icon:"delete", visible: true, },
               "copy":   { name:"コピー",   icon: "copy",  visible: true, },
               "paste":  { name:"貼付け",   icon: "paste", visible: truem\, className: copyTRhtml==null ? 'contextmenu-item-disabled' : '' },
            },
            events: {
               show: function(){
                  console.log("-- contextmenu show ---" );
               },
               hide: function(){
                  console.log("-- contextmenu hide ---" );
                  // コンテキスト行選択色→ OFF
                  $trigger.removeClass("seleted-context");
               }
            }
         };
      }
   });
};
$(function(){
   initList();
});

tbody と td に対するコンテキストメニューを書いている。これだけ書いておけば使い回せるだろう。


oboe2uran.hatenablog.com

リスト順のままのグルーピングカウント

リストをキーによってカウントする=グルーピングしてカウントするのに変な要求の題を突き付けられました。
例えば、以下のようなクラスのObjectのリスト、(内部にカウンタを持ちグルーピングカウントの結果を持つもの)とします。

public class Item{
   public int id;
   public String name;
   public int count;
   public Item(int id, String name){
      this.id = id;
      this.name = name;
   }
   @Override
   public String toString(){
      return "id=" + id + "  name=" + name + "  count="+count;
   }
}

これに対して、作成した List 以下のように作成したとして、、、

List<Item> list = new ArrayList<>();
list.add(new Item(1, "a"));
list.add(new Item(2, "a"));
list.add(new Item(3, "b"));
list.add(new Item(4, "b"));
list.add(new Item(5, "c"));
list.add(new Item(6, "a"));
list.add(new Item(7, "a"));

を グルーピングカウントして、Stream などで、

   list.stream().forEach(System.out::println);

の結果を、

id=1  name=a  count=4
id=2  name=a  count=0
id=3  name=b  count=2
id=4  name=b  count=0
id=5  name=c  count=1
id=6  name=a  count=0
id=7  name=a  count=0

ではなくて、、、リスト順にカウントして

id=1  name=a  count=2
id=2  name=a  count=0
id=3  name=b  count=2
id=4  name=b  count=0
id=5  name=c  count=1
id=6  name=a  count=2
id=7  name=a  count=0

のリストになるように、リストを作りなおしたいという変な結果を求める要求を突き付けられました。

困ったもので、悩んだあげく key と index と count を持つ次のようなクラスを用意して、、

public final class Meter implements Serializable{
   private String key;
   private int index;
   private int count;
   public Meter(){}
   // key, index, count 各々の setter, getter を用意
}

リストに対して、なんともクレイジーなコードを書くことになりました。

AtomicInteger index = new AtomicInteger(0);
Meter meter = new Meter();
list.stream().forEach(e->{
   int ix = index.getAndIncrement();
   if (meter.getCount()==0){
      meter.setKey(e.name);
      meter.setIndex(ix);
      meter.setCount(1);
   }else{
      if (meter.getKey().equals(e.name)){
         meter.setCount(meter.getCount()+1);
      }else{
         Item item = list.get(meter.getIndex());
         item.count = meter.getCount();
         list.set(meter.getIndex(), item);
         meter.setKey(e.name);
         meter.setIndex(ix);
         meter.setCount(1);
      }
   }
});
Item item = list.get(meter.getIndex());
item.count = meter.getCount();
list.set(meter.getIndex(), item);
list.stream().forEach(System.out::println);

の結果が、目的どおりにはなったけど、、こういう結果を求める仕様に釈然としません。

Arrays.asList が固定長のリストを返すことを忘れてはならない

訳あって String を確実に作れるカンマ区切りの文字列と単独の宣言済の String 数個 から
単独の宣言済の String を先頭に、String
を作ることに少し悩みました。

例えば、、

String str = "a,b,c";
String h1 = "1";
String h2 = "2";
String h3 = "3";

String[] ary = Arrays.stream(str.split(","))
.collect(()->Arrays.asList(h1, h2, h3), (t, u)->t.add(u), (t,r)->r.addAll(t))
.toArray(new String[]{});

と思いつきで考えたのですが、これは、Arrays.asList が、固定長のリストを返してしまうので
その後の add で UnsupportedOperationException が発生してしまいます。

しかたなく、終端操作の Supplier の中で Stream で Listを生成してやることにしました。

String[] ary = Arrays.stream(str.split(","))
.collect(()->Arrays.stream(new String[]{h1, h2, h3}).collect(Collectors.toList())
        , (t, u)->t.add(u)
        , (t,r)->r.addAll(t)
).toArray(new String[]{});