JavaScript エンターキーで移動する。その3

隠された要素は飛ばす。
[CTRL]+[Enter]では、 checked にする
テキストエリア内での処理を追加
IMEの未変換文字がある状態での Enter キーの対応
ルートの設定が個別にできること

<!DOCTYPE html>
<title></title>
<meta charset="utf-8">
<style>
</style>


<body>
<form id="F">
  <p>
    <input name="a" value="a">
    <input name="b" value="b" type="hidden">
    <input name="c" value="c">
    <span style="display:none;">
      <input name="d" value="d" id="d">

    </span>
  </p>
  <textarea name="j" cols="40" rows="10">fcvnruewihvnbre
vfreuoih\vreiojnreoi
vreiojnreoivreiojnreoi
  </textarea>
  <p>
    <input name="e" value="e">
    <input name="f" value="f" id="f">
  </p>
  <input type="reset">
  <p>
    <input type="radio" name="g" value="g0" checked>
    <input type="radio" name="g" value="g1">
    <input type="radio" name="g" value="g2">
  </p>

  <p>
    <input type="checkbox" name="h" value="h0" checked>
    <input type="checkbox" name="h" value="h1">
    <input type="checkbox" name="h" value="h2">
  </p>
  <p>
    <select name="i">
      <option value="i0">i0</option>
      <option value="i1">i1</option>
      <option value="i2">i2</option>
    </select>
  </p>
</form>


<script>

class ExEnter {

  constructor (root = document.documentElement, option = { }) {
    this.root = root;
    this.walker = document.createTreeWalker (root, NodeFilter.SHOW_ELEMENT, this.filter, true);
    this.option = Object.assign ({ }, this.constructor.defaultOption, option);

    //IME日本語入力中で未変換文字があり Enterが押されたことを感知するには
    //keypress イベントがが実行されないことを利用する
    this.imeFlag = null; //ime
  }


  //ルートの中の最初の要素を返す
  get firstElement () {
    this.walker.currentNode = this.root;
    return this.walker.firstChild ();
  }


  //ルートの最後の要素を返す
  get lastElement () {
    this.walker.currentNode = this.root;
    return this.walker.lastChild ();
  }


  //要素を移動する
  move (target, direction = false) {
    const
      walker = this.walker,
      isLoop = this.option.loop;

    this.walker.currentNode = target;
    let e = (direction)
    ? walker.previousNode () || (isLoop ? this.lastElement: null)
    : walker.nextNode ()     || (isLoop ? this.firstElement: null);
    e && e.focus ();
  }


  filter (node) {
    if (/^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test (node.tagName))
      if (! node.disabled && ! node.readOnly) {
        if (! isHide (node)) 
          return NodeFilter.FILTER_ACCEPT;
      }
    return NodeFilter.FILTER_SKIP;
    //__
    function isHide (node) {
      for (let e = node, s; e; e = e.parentNode) {
        if (e === document.body) break;
        s = getComputedStyle (e, null);
        if ('none' === s.display
          || 'hidden' === s.visibility
          || !parseFloat (s.opacity || '') 
          || !parseInt (s.height || '', 10)
          || !parseInt (s.width || '', 10)
        ) return true;
      }
      return false;
    }
  }


  //_____

  //テキストエリアのキャレットの位置で分割(改行)する
  textareaSpliter (e) {
    let
      v = e.value,
      a = v.substring (0, e.selectionStart);

    e.value = a + '\n' + v.substr (e.selectionEnd);
    e.selectionStart = e.selectionEnd = a.length + 1;
  }

  
  //[CTRL]が押されていれば、適用させる
  apply (e, sw, prev) {
    let r = false; //要素に対して操作を行ったか
    switch (e.type) {
      case 'radio': case 'checkbox' :
        if (sw) {
          e.checked = !e.checked;
          r = this.option.stay;
        }
        break;
      case 'button' : case 'submit' : case 'reset' :
        if (sw) {
          let event = document.createEvent ('MouseEvents');
          event.initEvent("click", false, true);
          e.dispatchEvent (event);
          r = this.option.stay;
        }
        break;
      case 'textarea' :
        if ((sw ^ this.option.textarea_enter) && !prev) {
          this.textareaSpliter (e);
          r =true;
        }
        break;
    }
    return r;
  }


  //_____

  handleEvent (event) {
    this[event.type+ 'Handler'].call (this, event, event.target);
  }

  keyupHandler (event, e) {
    let { code, shiftKey, ctrlKey } = event;
    if ('Enter' !== code) return;
    if (! this.imeFlag) //IMEの動作で未変換中の文字があると判断してスルー
      return event.preventDefault ();
    this.imeFlag = null;
    if (! this.apply (e, ctrlKey, shiftKey))
      this.move (e, shiftKey);
  }

  keypressHandler (event, e) {
    if (/^(submit|reset|button|textarea)$/.test (e.type))
      event.preventDefault ();
    this.imeFlag = true;//未変換文字がある場合の対処として
  }

  //________

  static defaultOption = {
    stay: false, //true: radio,checkbox,submit,button を作用させた後に自動的に移動する
    textarea_enter: false, //true:テキストエリア内では改行する ,false:[Ctrl]+[Enter]で改行
    loop: true, //要素を巡回する
  };


  static create (root = document.documentElement, option = { }) {
    let obj = new this (root, option);
    root.addEventListener ('keyup', obj, false);
    root.addEventListener ('keypress', obj, false);
    return obj;
  }
}

ExEnter.create ();


</script>