INPUT要素の入力を補助する

日付や時間の入力を簡単にして、見やすくするためのライブラリ

input[type="datetime"]などでは、日本語表記の ****年 **月**日 などはサポートされていない。
なので作ることにした。
なので、input 要素の type 属性は text となる

基本的な構造

まず、例えとして年月日を3つの unit ととして分解する。
さらに unit は、先頭文字・数値部分・末尾文字に別れている。
数値部分は、内部的に桁数・詰め文字・数値の組み合わせで出力するものとして構成される。
詰め文字はその文字列の先頭1文字が適用され、それ以降の文字列は数値が未定義な場合に適用される
桁数がマイナスの場合は、右寄せとなる。0の場合は、詰め文字処理は行われない。


これらのユニットをまとめて管理するのが package である。
package は、表示用のユニット群・編集用のユニット群・省略できない unit 数・
inputの値をユニット数に応じて分解できる関数とを保持する。
表示用・編集用のテキストを得ることができる


input要素と package を管理するのが、InputAssist である。
InputAssist は、以下のイベントを利用する

    • focus: 編集用の文字列に切り替える
    • blur : 表示用の文字列に切り替える
    • dblclick: 未入力状態の時に初期値の値を入力する。([?]/[@]キーも有効)
    • keypress: カーソルキーの上下で数値を増減させる
    • mousewheel:マウスのホイールで数値の増減をさせる
    • DOMMouseScroll:(上に同じ)

InputAssist に渡す package は、object 型にして渡す。
構造は以下

{
   //要素から値を分解する関数
   getValue:
     function (target, offset = 0) {
       //target から値を得て何らかの方法で取り出したい値に分解し配列で返す関数
       //分解できない場合は null を返す
       //値が未定義で、offset値が 0 ならば初期値を配列で返す
       //以上の条件を満たすこと
     },
   
   //表示用 Unit
   view:
     [[桁数, 詰め文字, 先頭文字, 末尾文字], [...], ...],

   //編集用 Unit
   edit:
     [[桁数, 詰め文字, 先頭文字, 末尾文字], [...], ...],
   
   //省略できない unit数
   required: 1
}
DATETIME型

表示用

2100 年 01 月 02 日 03 時 04 分 56 秒 789 ms

編集用

2100-1-2 3:4:56.789

値は左側から採用され、省略することも可能(個別に省略できない)
編集用で省略した部分は、表示用でも省略される。
required で指定された数は、先頭から強制的に表示対象となる

DATE型

表示用

2100 年 01 月 02 日

編集用

2100-1-2
TIME型

表示用

03 時 04 分 56 秒 789 ms

編集用

3:4:56.789




何度も見直す。疲れた。説明するのも疲れた。

<!DOCTYPE html>
<html lang="ja">
<meta charset="UTF-8">
<style>
h4, p {
  margin: 0;

}
input[type="text"] {
  font-family: monospace;
  width: 50ex;
  font-size: 100%;
}
</style>
<form id="hoge">
  <ul>
    <li>
      <h4>DATE TIME</h4>
      <input type="text" name="DATETIME" value="1966-1-12 T 11:10:20"><br>
      <input type="text" name="DATETIME" value="">
    <li>
      <h4>DATE</h4>
      <input type="text" name="DATE" value=""><br>
      <input type="text" name="DATE" value="">
    <li>
      <h4>TIME</h4>
      <input type="text" name="TIME" value=""><br>
      <input type="text" name="TIME" value="">
    <li>
      <h4>Number</h4>
      <input type="text" name="NUMBER" value=""><br>
      <input type="text" name="NUMBER" value="">
    <li>
      <h4>整数</h4>
      <input type="text" name="INTEGER" value=""><br>
      <input type="text" name="INTEGER" value="">
    <li>
      <h4>地割番地</h4>
      <input type="text" name="TIWARI" value=""><br>
      <input type="text" name="TIWARI" value="">
    
  </ul>
  <input type="reset">
</form>
<script>

{
  /*
  ______________________________________________________________________

    input[type="text"]の要素の中にさらに擬似的に子(Unit)を作り、
    それらを Package に統合する。
    Unitは、ゼロパディングのような詰め文字を付加でき、前後にも文字を付加できる
    Package は、Unitの集合で、表示用と編集用との文字列の切り替(置換)えができる。
    InputAssist は、対象となる要素と Packageを結びつける。
    各種イベントに反応し、input要素の値に作用する
    
    onfocus      : 編集用の文字列に変換する
    onBlue       : 表示用の文字列に変換する(onChange イベントの発火も可)
    onDblClick   : 初期値の設定
    onMousewheel : 値の増減が可能
    onKeyPress   : [↑↓]キーで値の増減、[?]/[@]キーで初期値の入力が可能
  ______________________________________________________________________
  
  */

  //Input[type=text]の値を内部でさらに分割するための擬似子要素
  class Unit {
    constructor (digit = 0, paddingChar = ' ', firstChar = '', lastChar = '') {
      let [first = '', ...other] = paddingChar; //詰め文字(*1)を最初の1文字とそれ以外とに分離する

      this.digit            = digit; //数値が正なら左から負なら右側から指定数の文字を抜き出す
      this.firstChar        = firstChar; //詰めた文字列の最初に付加される文字列
      this.lastChar         = lastChar;//詰めた文字列の最後に付加される文字列
      this.paddingChar      = first; //実際に使われる詰め文字
      this.noneChar         = other.join ('') || '';//(*1)変数値が空だった場合の余白分として使われる
      // *1 詰め文字の取り出す桁(digit)の値が 0の場合にで空の場合に作用する
    }


    //擬似子要素の文字列を返す
    getApply (str = '', c = this.paddingChar) {
      let { digit, noneChar: none, firstChar, lastChar } = this;
      str = String (str);
      return [
        firstChar,
        (
          (digit)
          ? (0 < digit)
            ? (str + c.repeat (digit)).slice (0, digit) //左から
            : (c.repeat (-digit) + str).slice (digit)  //右から
          : (! str)
            ? none //空白
            : str
        ),
        lastChar
      ].join ('');
    }
  }
  
  //____________________________________

  class Package {
    constructor ({ getValue, view = [ ], edit = [ ], required = view.length } = { } , option) {
      this.getValue     = getValue;
      this.unit         = {
        view: view.map (a => new Unit (...a)),
        edit: edit.map (a => new Unit (...a))
      };
      this.required     = required;
      this.placeholder  = this.unit.view.slice (0, this.required)
        .map (unit => unit.getApply (unit.noneChar)).join ('');
      
      Object.keys (DEFAULT_OPTION_PACKAGE).forEach (key => this[key] = DEFAULT_OPTION_PACKAGE[key]);
      Object.keys (option).forEach (key => this[key] = option[key]);
    }


    //編集・表示用のテキストを生成する
    value (target, type = 'edit', offset = null) {
      let
        offset_ = (null === offset) ? null: offset * this.offsetStep,
        ary = this.getValue (target, offset_);
      
      if (ary) {
        let
          unit = this.unit[type],
          len = Math.max (ary.length, this.required),
          value = [ ];

        for (let i = 0; i < len; i += 1)
          value.push (unit[i].getApply (ary[i]));

        return value.join ('').replace (/\s*$/, '');//右側空白削除
      }
      else
        return null;
    }
  }

  const
    //初期値
    DEFAULT_OPTION_PACKAGE = {
      offsetStep: 1
    };
  
    
  this.Package = Package;//スコープの外で定義して使う?

  //____________________________________
  
  
  class InputAssist {
    constructor (target, pack, opt = { }) {
      if (2 > arguments.length)
        throw new Error ('引数が不足');
      
      let
        es = [...target],
        { placeholder } = pack,
        option = Object.assign ({ }, DEFAULT_OPTION_ASSIST, opt),
        isSetPH = option.placeholder;

      this.target = new Set (es);
      this.pack = pack;
      this.option = option;
      this.placeholder = placeholder;

      //イベントの装備
      for (let type of Object.keys (EVENT)) {
        let { target = document, useCapture = false} = EVENT[type];
        target.addEventListener (type, this, !! useCapture);
      }
      
      //プレースホルダーの設置
      if (isSetPH)
        (('auto' === isSetPH)
        ? es.filter (e => ! e.hasAttribute ('placeholder'))
        : es).forEach (e => e.placeholder = placeholder);

      //初期値を表示用に書き換える
      if (option.replaceDefaultValue)
        for (let e of es)
          this.convert (e, 'view');//
    }

    
    handleEvent (event) {
      let { type, target } = event;

      if (this.target.has (target))
        EVENT[type].handleEvent.call (this, event);
    }
    
        
    //要素の値を表示(編集)用に変換する offset == 0 -> defaultValue
    convert (e, type = 'edit', offset = null) {
      let
        state = ('view' === type || true === type) ? 'view': 'edit',
        value = this.pack.value (e, state, offset);

      if (null !== value) {
        if ('view' === type) {
          if (this.option.changeEventSupport) {
            dispatchChangeEvent (e);//表示用の書式に戻す前にイベント発火
          }
        }
        e.value = value;
      }
      return this;
    }
    

    //全ての要素の値を表示(編集)用に変換する
    convertAll (type = 'view') {
      this.target.forEach (e => this.convert (e, type));
    }
        
  }


  //____________________________________


  const
    //初期値
    DEFAULT_OPTION_ASSIST = {
      placeholder: 'auto', // true:強制書き換え, false: 何もしない, auto: 要素になかったら設定
      replaceDefaultValue: true, //立ち上げ時に初期値を表示用のテキストに書き換える
      changeEventSupport: true //値の成型前にonChangeイベントを発生させるか?
    },

    //InputAssistから呼ばれるイベントハンドラ
    EVENT = {
      focus: {
        useCapture  : true,
        handleEvent : function ({ target }) { this.convert (target) }
      },
      
      
      blur: {
        useCapture  : true,
        handleEvent : function ({ target }) { this.convert (target, 'view'); }
      },
      
      
      dblclick: {
        handleEvent : function ({ target }) { this.convert (target, 'edit', 0) }
      },
        
      
      keypress: {
        handleEvent : function
          (event) {
            let
              { key, target } = event,
              offset = (key === 'ArrowUp') - (key === 'ArrowDown');

            if (/\?|@/.test (key)) {
              if (! target.value) {
                this.convert (target, 'edit', 0);
                event.preventDefault ();
              }
            }
            else if (offset)
              this.convert (target, 'edit', offset);
          }
      },
      

      DOMMouseScroll: {//Firefox
        handleEvent : function
          (event) {
            let { target } = event;
            if (! isActive (target)) return; //has ?
            this.convert (target, 'edit', Math.sign (- event.detail));
            event.preventDefault ();
          }
      },
      

      mousewheel: {//Chrome
        handleEvent : function
          (event) {
            let { target } = event;
            if (! isActive (target)) return;
            this.convert (target, 'edit', Math.sign (event.wheelDelta));
            event.preventDefault ();
          }
      }
      
    }
    ,
    

    ASSIST_TYPE = {

      'DATETIME': {
        getValue: function (target, offset = 0) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_DATETIME, value);
            if (rst) {
              let
                YMD = rst.map (a => parseInt (a, 10)),
                len = YMD.length,
                [Y = 1, M = 0, D = 1, H = 0, m = 0, s = 0, ms = 0] = YMD,
                dt = new Date (Y, M - 1, D, H, m, s, ms);

              [Y = 1, M = 0, D = 1, H = 0, m = 0, s = 0, ms = 0] = InputAssist.GET_YMDHms (dt);
              dt = new Date (Y, M - 1, D, H + offset, m, s, ms);
              rst = InputAssist.GET_YMDHms (dt).slice (0, len);
            }
          }
          else
            if (0 === offset) {
              let [Y, M, D, H, m, s, ms] = InputAssist.GET_YMDHms ();
              rst = [Y, M, D, 12, 0]; // defaultValue
            }

          return rst;
        }
        ,
        view: [//new Package によって置き換えられる
          //桁, *1詰文字, 先頭文字, 末尾文字  *1:2文字目以降は未定義時に使われる
          [ 4, '0    ', ' ', ' 年'],
          [-2, '0  ',   ' ', ' 月'],
          [-2, '0  ',   ' ', ' 日'],
          [-2, '0  ',   ' ', ' 時'],
          [-2, '0  ',   ' ', ' 分'],
          [-2, '0  ',   ' ', ' 秒'],
          [ 3, '0  ',   ' ', '  ms']
        ]
        ,
        edit: [
          [ 0, '', ''],
          [ 0, '', '-'],
          [ 0, '', '-'],
          [ 0, '', ' '],
          [ 0, '', ':'],
          [ 0, '', ':'],
          [ 3, '0   ', '.']
        ]
        ,
        required: 5 //default は、viewUnit.length
      },

      
      'DATE': {
        getValue: function (target, offset = null) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_DATE, value);
            if (rst) {
              let
                YMD = rst.map (a => parseInt (a, 10)),
                len = YMD.length,
                [Y, M, D] = YMD,
                dt = new Date (Y, M - 1, D);

              [Y, M, D] = InputAssist.GET_YMD (dt);
              dt = new Date (Y, M - 1, D + offset);
              rst = InputAssist.GET_YMD (dt).slice (0, len);
            }
          }
          else
            if (0 === offset) {
              let [Y, M, D] = InputAssist.GET_YMD ();
              rst = [Y, M, D];
            }

          return rst;
        }
        ,
        view: [
          [ 4, '0    ', ' ', ' 年'],
          [-2, '0  ',   ' ', ' 月'],
          [-2, '0  ',   ' ', ' 日']
        ]
        ,
        edit: [
          [ 0, '', '',  ''],
          [ 0, '', '-', ''],
          [ 0, '', '-', '']
        ]
        ,
        required: 3 //default は、viewUnit.length
      }
      ,

      'TIME': {
        getValue: function (target, offset = 0) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_TIME, value);
            if (rst) {
              let
                hms = rst.map (a => parseInt (a, 10)),
                len = hms.length,
                dt = new Date (1, 0, 1, ...hms);

              hms = InputAssist.GET_Hms (dt);
              hms[1] += offset; //増減は分が対象
              dt = new Date (1, 0, 1, ...hms);
              rst = InputAssist.GET_Hms (dt).slice (0, len);
            }
          }
          else
            if (0 === offset) {
              let [H, m, s] = InputAssist.GET_Hms ();
              rst = [H, m];//初期値は秒を利用しない
            }

          return rst;
        },
        view: [
          [-2, '0  ', ' ', ' 時'],
          [-2, '0  ', ' ', ' 分'],
          [-2, '0  ', ' ', ' 秒'],
          [ 3, '0  ', ' ', '']
        ]
        ,
        edit: [
          [ 0, '', '',  ''],
          [ 0, '', ':', ''],
          [ 0, '', ':', ''],
          [ 3, '0   ', '.', '']
        ]
        ,
        required: 2,
        step: 60
      }
      ,
      
      'NUMBER': {
        getValue: function (target, offset = 0) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_NUMBER, value);
            if (rst) {
              rst[0] = (parseInt (rst[0], 10) || 0) + offset;
              //ary[1] は小数点以降は文字列として扱う
            }
          }
          else
            if (0 === offset)
              return [0];//初期値
          
          return rst;
        },
        view: [[ 0, ' '], [ 0, '     ', '.']],
        edit: [[ 0, ' '], [ 0, '     ', '.']],
        required: 1
      },


      'INTEGER': {
        getValue: function (target, offset = 0) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_INTEGER, value);
            if (rst)
              rst[0] = (parseInt (rst[0], 10) || 0) + offset;
          }
          else
            if (0 === offset)
              return [0];//初期値
          
          return rst;
        }
        ,
        view: [[0, ' '+'     ']],
        edit: [[0, ' '+'     ']],
        required: 1
      }
      ,

      'TIWARI': {
        getValue: function (target, offset = 0) {
          let { value } = target, rst = null;

          if (value) {
            rst = InputAssist.REG_PICKUP (InputAssist.REG_IS_TIWARI, value);
            if (rst)
              rst = rst.map (a => parseInt (a, 10));
          }
          else
            if (0 === offset)
              return null;

          return rst;
        },
        view: [
          [0, '     ', '第 ', ' 地割'],
          [0, '     ', ' ',   ' 番地'],
          [0, '     ', ' ',   ' 号']
        ]
        ,
        edit: [
          [0, ' '],
          [0, ' ', '-'],
          [0, ' ', '-']
        ]
        ,
        required: 2
      }
    },

    //イベント発火
    dispatchChangeEvent = element => {
      let
        doc = element.ownerDocument,
        event = doc.createEvent ('HTMLEvents');
      event.initEvent ('change', true, true );
      element.dispatchEvent (event);
    }
    ,
    //フォーカスが当たっている要素か
    isActive = e =>
      e === e.ownerDocument.activeElement
    ,
    //年月日時刻を配列で返す * M += 1
    getYMDHms = (dt = new Date) =>
      [ dt.getFullYear (), dt.getMonth () + 1, dt.getDate (),
        dt.getHours (), dt.getMinutes (), dt.getSeconds (), dt.getMilliseconds ()]
    ,
    //年月日を配列で返す * M += 1
    getYMD = (dt = new Date) =>
      [ dt.getFullYear (), dt.getMonth () + 1, dt.getDate () ]
    ,
    //時刻を配列で返す * M += 1
    getHms = (dt = new Date) =>
      [ dt.getHours (), dt.getMinutes (), dt.getSeconds (), dt.getMilliseconds ()]
    ,
    //配列の値で未定義を排除する(本当は後半から有効なところまで)
    pickup = (reg, value) => {
      let ary = reg.exec (value);
      return (ary)
        ? ary.slice (1).filter (a => 'undefined' !== typeof a)
        : null;
    },
    //
    reg_is_datetime = /^\s*(?!\d{4}\D*(?:(?:0?(?:2|4|6|9)|11)\D*31|0?2\D*30)\D*)(?!(?:(?:[02468][1235679]|[13579][01345789])00|\d{2}(?:\s*[02468][1235679]|[13579][01345789]))\D*0?2\D29\D*)([0-9]{4})\s*[年\-\/]\s*(0?[1-9]|1[0-2])\s*[月\/\-]\s*(0?[1-9]|[12][0-9]|3[01])\s*日?\s*[ Tt]\s*([01]?[0-9]|2[0-4])(?:\s*[:時]\s*(?:([0-5]?[0-9])(?:\s*[:分]\s*(?:([0-5]?[0-9])(?:\s*[秒\.:]\s*(?:(\d{1,3})\d*\s*(?:ms)?)?)?)?)?)?)?$/,
    reg_is_date     = /^\s*(?!\d{4}\D*(?:(?:0?(?:2|4|6|9)|11)\D*31|0?2\D*30)\D*)(?!(?:(?:[02468][1235679]|[13579][01345789])00|\d{2}(?:\s*[02468][1235679]|[13579][01345789]))\D*0?2\D29\D*)([0-9]{4})\s*[年\-\/]\s*(0?[1-9]|1[0-2])\s*[月\/\-]\s*(0?[1-9]|[12][0-9]|3[01])\s*日?\s*$/,
    reg_is_time     = /^\s*(?:([01]?[0-9]|2[0-4])(?:\s*[:時]\s*(?:([0-5]?[0-9])(?:\s*[:分]\s*(?:([0-5]?[0-9])(?:\s*[秒\.:]\s*(?:(\d{1,3})\d*)?)?)?)?)?)?)\s*$/,
    reg_is_number   = /^\s*(?:(\-?[0-9]+)(?:\.([0-9]*)?)?)\s*$/,
    reg_is_integer  = /^\s*(\-?[1-9]?[0-9]+)\s*$/,
    reg_is_tiwari   = /^\s*(?:第?\s*(\d+))(?:\s*(?:地割|丁目|\-)(?:\s*(\d+)?(?:\s*(?:番地|\-)(?:\s*(\d+)?(?:\s*号?)?)?)?)?)?\s*$/,
    
    ProduceBy = 'babui_babu_baboo';
  
 
  //____________________________________


  function create (elements, type, option = { }) {
    if (2 > arguments.length)
      throw new Error ();

    let
      pack = new Package (ASSIST_TYPE[type], option);
      obj = new InputAssist (elements, pack, option);

    return obj;
  }

  //____________________________________
  
  InputAssist.GET_YMDHms = getYMDHms;
  InputAssist.GET_YMD    = getYMD;
  InputAssist.GET_Hms    = getHms;
  InputAssist.REG_PICKUP = pickup;
  
  InputAssist.REG_IS_DATETIME = reg_is_datetime;
  InputAssist.REG_IS_DATE     = reg_is_date;
  InputAssist.REG_IS_TIME     = reg_is_time;
  InputAssist.REG_IS_NUMBER   = reg_is_number;
  InputAssist.REG_IS_INTEGER  = reg_is_integer;
  InputAssist.REG_IS_TIWARI   = reg_is_tiwari;
  
  InputAssist.create = create;
//____________________________________
  
  this.InputAssist = InputAssist;
  
}




let form = document.forms[0];
let DATETIME = document.querySelectorAll ('form input[name="DATETIME"]');
let DATE = document.querySelectorAll ('form input[name="DATE"]');
let TIME = document.querySelectorAll ('form input[name="TIME"]');
let NUMBER = document.querySelectorAll ('form input[name="NUMBER"]');
let INTEGER = document.querySelectorAll ('form input[name="INTEGER"]');
let TIWARI = document.querySelectorAll ('form input[name="TIWARI"]');
InputAssist.create (DATETIME, 'DATETIME');

InputAssist.create  (DATE, 'DATE');
InputAssist.create  (TIME, 'TIME', { offsetStep: 60 });
InputAssist.create  (NUMBER, 'NUMBER');
InputAssist.create  (INTEGER, 'INTEGER');
InputAssist.create  (TIWARI, 'TIWARI');


form.addEventListener ('change', function (event) {
  let e = event.target;
  console.log (e.name, e.value);
}, false);


</script>