配列を回転する

条件

  • 参照元の配列を破壊しないこと。
  • 1行で済ませること。
  • 身勝手ではあるがなるべく reverse は使わないこと。
  • なるべく速度を優先すること。(調べていないけど)
const ary = [
  [1,2,3],
  [4,5,6],
  [7,8,9],
  [10,11,12]
];

const tb = a=> a.map (b=>b.join (', ')).join ('\n');


//転置(XとYを交換)
const transpose=a=>a[0].map((_,i)=>a.map(b=>b[i]));

//回転(時計回り)
//const rotate=Right a=> a[0].map((_,i)=>a.map(b=>b[i]).reverse());
const rotateRight = a=> a[0].map((_,i)=>a.reduceRight((b,c)=>[...b,c[i]],[]));

//回転(180度)
//const inversion =a=> [...a].reverse().map(b=>[...b].reverse());
//const inversion =a=> a.slice().reverse().map(b=>b.slice().reverse());
//const inversion =a=> a.reduceRight((b,c)=>[...b,[...c].reverse()],[]);
const inversion = a=>a.reduceRight((b,c)=>[...b,c.reduceRight((d,e)=>[...d,e],[])],[]);

//回転(反時計回り)
const rotateLeft = a=> a[0].reduceRight((b,_,i)=>[...b,a.map(c=>c[i])],[]);



//_____________
let A = ary;
console.log (tb (A));
console.log (tb (rotateRight (rotateRight (A))));
console.log (tb (inversion (A)));
console.log (tb (rotateLeft (rotateLeft (A))));

/*
[
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
  [10, 11, 12],
]

[
  [12, 11, 10],
  [9, 8, 7],
  [6, 5, 4],
  [3, 2, 1],
]

*/



連続したイベントをキャンセルし、一定時間が過ぎたらイベントを発火させる

寺尾で質問をした。

イベントを遅らせて発火させ、イベントオブジェクトから target 要素を取得したい
もうちょっと自身で精査すればよかったのに・・・自己解決した。
これで文字を入力するたびに処理しなくてもよくなった。

<!DOCTYPE html>
<meta charset="UTF-8">
<title>イベントを遅らせられた</title>

<body>
<input type="text" name="inp"><br />
<input type="text" name="inp"><br />


<script>

/*****************************
  連続するイベントをキャンセルし、一定時間が経過したらイベントを発生させる

  const obj = EventDelay.create (eventType [, target[, delayTime[, useCapture]]]);

******************************/


class EventDelay {

  constructor (type, target, delayTime) {
    this.type      = type;
    this.target    = target;
    this.delayTime = delayTime;

    this.timerId   = null;
    this.event     = null;
    this.disabled  = false;
  }


  dispatch () {
    this.timerOff ();
//× this.target.dispatchEvent (this.event);
    
    if (! this.disabled)
      this.event.target.dispatchEvent (this.event);//イベントのターゲットが肝だった
  }


  timerOff () {
    if (this.timerId)
      clearTimeout (this.timerId);

    this.timerId = null;
  }


  timerOn () {
    if (this.disabled) return;

    let cbFunc = this.dispatch.bind (this);
    this.timerId = setTimeout (cbFunc, this.delayTime);
  }


  handleEvent (event) {
    this.timerOff ();

    if (this.disabled) return;

    if (event.isTrusted) {
      this.event = event;
      event.stopImmediatePropagation ();
      this.timerOn ();
    }
  }


  static create (type, target = document, delayTime = 333, useCapture = false) {
    if (1 > arguments.length || ! type)
      throw new Error ('イベントタイプが不正です');
    
    let obj = new this (type, target, delayTime);
    target.addEventListener (type, obj, useCapture);
    target = null;

    return obj;
  }

}

//-----------------------------------------------

//document に対する inputイベントを 333m/s 遅らせる
EventDelay.create ('input');

//-----------------------------------------------

document.addEventListener ('input', event=> {
  let e = event.target;
  console.log (event, e.value);// value値を取得
}, false);

</script> 

ちょっとした時間があり、全面的に見直した。

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

<script>

  class DelayTask {

    #id = null; //setTimeout関数の戻り値に使用
 
    constructor (callBackFunc, delayTime) {
      this.callBackFunc = callBackFunc; // func.bind (this)
      this.delayTime = delayTime;
    }

    execute (...args) {
      this.timer = null;
      this.callBackFunc (...args);
    }


    start (...args) {
      let cbFunc = this.execute.bind (this, ...args);
      this.timer = setTimeout (cbFunc, this.delayTime);
      return this;
    }

    stop () {
      this.timer = null;
      return this;
    }

    set timer (id) {
      if (this.#id)
        clearTimeout (this.#id);
      this.#id = id;
    }

    get timer () {
      return this.#id;
    }

    //__________________

    static create (cbFunc, delayTime = 500) {
      if ('function' != typeof cbFunc)
        throw new Error ('関数ではありません');
      return new this (cbFunc, delayTime);
    }
  }

  //_____________________________________________________

  //遅延させてハンドラを呼び出す
  class DelayHandler extends DelayTask {

    constructor (handler, delayTime) {
      super (handler, delayTime);
    }

    handleEvent (event) {
      this.stop ().start (event);
    }

    static create (handler, delayTime = 300) {
      let cbFunc;

      //引数のチェック
      switch (Object.prototype.toString.call (handler)) {
      case '[object Function]' :
        cbFunc = handler;
        break;

      case '[object Object]' :
        if ('handleEvent' in handler) {
          if ('function' === typeof handler.handleEvent)
            cbFunc = handler.handleEvent.bind (handler);
        } //else Error !! 

      default:
        throw new Error ('Not handleEvent');
        break;
      }

      return new this (cbFunc, delayTime);
    }
  }

//_____________________________________________________

function handler (event) {
  console.log(event);
  alert ('クリックされた');
}

document.addEventListener ('click', DelayHandler.create (handler, 1000), false);


</script>

WEB画面に枯葉が舞う

それにしても短い休日だった。
母さん、三途の川を渡ったころだろうか。


https://oshiete.goo.ne.jp/qa/12665779.html
枯葉よ~

<!DOCTYPE html>
<meta charset="UTF-8">
<title>Test</title>

<body>
<img class="kareha" src="data:image/gif;base64,R0lGODlhLQAqAKIAAP7jRvTSMvfZO/veQuS8H+zFJ/DMLv///yH5BAEAAAcALAAAAAAtACoAAAP4eLrc/tCRGau1k97NTyZdWBWZaDaGQZ6sEqRFew7CK89CfYsAQO88HzDUEw43RYAEdGQkH59CbDioDqAkgrSQMtwCOQEks+0GzmBR6lzRltfosKB6cXvbbi4cnLMOng0fG3lmZ2F+SU8fTHVvBnF9VYlJgh2OkHOSk0lXIpeGkX+bgCGffJmioz0mpoeaqkqeUoWnqLCxpbO0rq+bJoR7coiTnSLAdwrCfsvFIW4PoLzLYkdotXLYR3uY2GxAXdvW4gFAjrTiXTt5uuDtXVMtZFvm4PM3E1nr8/tSjPGL8vZVAgKw4KImBhM2UZAQ4MIFDUs8DGSQRQIAOw==">
<img class="kareha" src="data:image/gif;base64,R0lGODlhMAAnAKIAAOlcNOM+E8cqB9RxW96dkfDUz8hPPP///yH5BAEAAAcALAAAAAAwACcAAAP/eLrc/qS8Sas9Q97NndFdeBGfaFbDcK5OYahsfBBCKZ/FEMC3qQeGnskVAOJixGLQRMisfkUBKJQiDA3F6DREinCzAOCWa3iNH8lAWGBlkcqGNoWwVgcEPNzgdR/IHQNrAGt/JySDa0sNBWF3dmwyOQYAgQI1W3RgSj0kRY19DJkCdkYhZzmNg1GKqqONUhUFsgRvhQpNqa1Lra15BbQpNcI1Xmgpqqp3Cq2PGr9YltGWcSOBuWsYmjUKOpaOd5Z5Fri5ygSOjTNR3+CQXIFgYX7fAgpq0VmjeIbMqt1hAM5lGRhNXAdUn/oxIxjtzMFf11wN9NTOBgtZED2lGkUQYJwtEb+OWaM4EdwLJxfJIUJU0pOfG88mrZwJMB8QlHoM1AAgc6UnOjQYGuTQxA+wPWV2KJEA4MArjtuGbJnlJwfKpgd0SrP0UciCAApCbnXn1QHWBVQHcC37AOwcRRUSAAA7">

<script>

const {sin, cos, random, PI} = Math, DEG = PI / 180;

//_________

class P2 {
  constructor (x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

//_________

class Status {
  constructor (...args) {
    this.reset (...args);
  }

  reset (fall = 0, swing = 0, spin = 0) {
    this.fall = fall;
    this.swing = swing;
    this.spin = spin;
    return this;
  }

  randomReset (max, min) {
    for (let name of Object.keys (max))
      this[name] = rand (max[name], min[name]);
    return this;
  }
}

//_________

class Kareha {

  constructor (terms, target, position, character, spin, life) {
    this.terms = terms;
    this.target = target;
    this.position = position;
    this.character = character;
    this.life = life;
    this.spin = spin;
  }


  reset () {
  let t = this.terms;
    this.position = new P2 (rand (t.endArea.x, t.beginArea.x), 0);
    this.character.randomReset (t.maxStatus, t.minStatus);
    this.life = this.spin = 0;
   
    return this.disp ();
  }


  move () {
    if (null === this.life)
      this.reset ();

    let {x, y} = this.position;
    if (this.terms.endArea.y < y)
      return this.reset ();

    this.life++;
    this.spin += this.character.spin;
    y += this.character.fall + cos (this.spin * DEG);
    x += sin (this.spin * DEG) * this.character.swing;

    this.position.x = x;
    this.position.y = y;

    return this.disp ();
  }


  disp () {
    let
      style = this.target.style,
      {x, y} = this.position;
    
    if (null === this.life)
      style.opacity = 0 + '';
    else {
      let n = this.spin |0;
      style.opacity = 1 + '';
      style.left = (x |0)+ 'px';
      style.top = (y |0)+ 'px';
      style.transform = `rotate(${this.spin}deg)`;
    }
    return this;
  }


  static create (terms, target, position = new P2, character = new Status, spin = 0, life = null) {
    let obj = new this (terms, target, position, character, spin, life);
    target.style.position = 'absolute';
    return obj.reset ();
  }
 
}


function rand (max = 100, min = 0) {
  let sa = max - min;
  return random () * sa + min;
}


//_________


{
  let img = document.querySelectorAll ('img.kareha');
  
  for (let i = 0; i < 20; i++) {
    img[0].parentNode.appendChild (img[0].cloneNode (false));
    img[1].parentNode.appendChild (img[1].cloneNode (false));
    
  }
}


const
  TERMS = {
    minStatus: new Status (1, .5, .1),
    maxStatus: new Status (3, 3, 2),
    beginArea: new P2 (0, 0),
    endArea: new P2 (640, 800)
  },

  KAREHA = [...document.querySelectorAll ('img.kareha')].map (e=> Kareha.create (TERMS, e));


(function loop () {
  KAREHA.forEach (obj=> obj.move ());
  window.requestAnimationFrame (loop);
}) ();

</script>


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

teratail で回答したものより更に加筆
https://teratail.com/questions/325107

  • radio の checked は1つだけなので、最後に移動する。
  • textarea 内の改行はオートインデントが可能。
  • [Shift]+[Enter]は逆順に、[Ctrl]+[Enter]で checked の反転と textarea 内での改行
  • datalist 内の summary に対応
  • アンカータグの移動に対応
<!DOCTYPE html><title></title><meta charset="utf-8">
<style>
  a:focus, datalist:focus, input:focus {
    background: rgba(255,0,0,.2);
  }
</style>

<body>
<nav>
  <p>最初に[Tab]でフォーカスを移動した場合テストできる<br>
  <a href="#C">ABC</a>
  <a href="#C">DEF</a>
  <a href="#A">GHI</a><br>
</nav>

<form>
  <p>フォーム内のアンカータグを移動する<br>
    <a href="#C">ABC</a>
    <a href="#C">DEF</a>
    <a href="#A">GHI</a>

  <p>通常の INPUT要素<br>
    1.<input name="d0" value="">
    2.<input name="d1" value="">
  
  <p>表示されていない要素<br>
    1.<input type="hidden" name="d2">
    2.<input type="hidden" name="d2" style="display:none;">
    3.<input type="hidden" name="d2" style="visibility:hidden;">
    4.<input type="hidden" name="d2" style="opacity: 0;">
  
  <p>INPUT[type=radio]要素の場合<br>
    <label>1.<input type="radio" name="d3" value="1">abc</label>
    <label>2.<input type="radio" name="d3" value="2">def</label>
    <label>3.<input type="radio" name="d3" value="3">ghi</label>
  
  <p>INPUT[type=checkbox]要素の場合<br>
    <label><input type="checkbox" name="d4" value="1">abc</label>
    <label><input type="checkbox" name="d4" value="2">def</label>
    <label><input type="checkbox" name="d4" value="3">ghi</label><br>

  <p>SELECT要素<br>
    <select name="d5"><option value="">-- <option value="1">abc <option value="2">def</select>

  <p>TEXTAREA要素<br>
    <textarea name="d6" cols="80" rows="5">
  改行すると、その行の最初のインデントが継続されます
    改行せずに移動する場合は[Ctrl]+[Enter]、設定で逆の動作をします
      改行すると、その行の最初のインデントが継続されます
    </textarea>

  <p>DATALIST要素にSUMMARYがある場合<br>
    <details>
      <summary>Summary OPEN1</summary>
      <ol><li>abc <li>def <li>ghi</ol>
    </details>
    <details open>
      <summary>Summary OPEN2</summary>
      <ol><li>abc <li>def <li>ghi</ol>
    </details>
  
  <p>INPUT[type=button] / BUTTON 要素の場合<br>
    <input type="button" value="フォームの移動を無効にする" onclick="F.disabled=true">
    <input type="button" value="フォームの移動を有効にする" onclick="F.disabled=false">
  </p>
      
</form>
<script>

/*
  [Enter] 要素を次に移動する
  [Shift]+[Enter] 前に移動する
  [Ctrl]+[Enter]: 作用する
*/

class ExEnter {

  constructor (root = document.documentElement, option = { }) {
    const
      filter = this.filter.bind (this),
      Nfilter = NodeFilter.SHOW_ELEMENT;

    this.root = root;
    this.walker = root.ownerDocument.createTreeWalker (root, Nfilter, filter, true);
    this.option = Object.assign ({ }, this.constructor.defaultOption, option);

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


  //ルートの中の最初の要素を返す
  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) {
    if (this.disabled)
      return;

    const
      walker = this.walker,
      isLoop = this.option.loop;

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

    if (e) e.focus ();
  }


  //TreeWalker用のフィルター関数(要.bind(this))
  filter (node) {
    const
      accept = NodeFilter.FILTER_ACCEPT,
      skip = NodeFilter.FILTER_SKIP;
    
    switch (node.nodeName) {
    case 'INPUT' : case 'TEXTAREA' : case 'SELECT' : case 'BUTTON' :
      if (node.disabled) break;
      if (node.readOnly) break;
      if (this.option.tabIndex && ('-1' === node.getAttribute ('tabIndex'))) break;
      if (isHide (node)) break;
      return accept;

    case 'A' : //href = '#...' で始まるもののみ対象とする
      if (this.option.anchor) {
        let href = node.getAttribute ('href');
        if (href)
          if (! href.indexOf ('#'))
            return accept;
      }
      break;

    case 'SUMMARY' :
      return accept;
    }
    return skip;

    //__
    //祖先の要素が隠された状態にあるか?
    function isHide (node) {
      const chks = ['display', 'visibility', 'opacity', 'height', 'width'];

      for (let e = node; e !== document.body; e = e.parentNode) {
        let cs = getComputedStyle (e, null);
        let [d, v, o, h, w] = chks.map (p=> cs.getPropertyValue (p));
        if (
          'none' === d || 'hidden' === v || 0 == parseFloat (o) ||
          ! ('auto' === h || 0 < parseInt (h, 10)) ||
          ! ('auto' === w || 0 < parseInt (w, 10))
        ) return true;
      }
      return false;
    }
  }


  //_____

  //[CTRL]が押されていれば、適用させる
  apply (e, sw, prev) {
    let stay = false, tag = e.nodeName;

    if ('TEXTAREA' === tag && !prev) {
      if (sw ^ this.option.textarea) {
        this.constructor.insertCRLF (e, this.option.autoIndent);
        stay = true;
      }
    }
    else if (sw) {
      stay = this.option.stay;
      switch (tag) {
      case 'SUMMARY' :
        let parent = e.parentNode;
        parent.hasAttribute ('open')
          ? parent.removeAttribute ('open')
          : parent.setAttribute ('open', '');
        break;

      case 'A' :
        fireEvent (e);
        break;
      
      case 'INPUT' :
        switch (e.type) {
        case 'radio':
          if (e.checked = !e.checked) {//radio の checkedは選択肢が1つなので群の最後に移動
            let radios = this.root.querySelectorAll (`input[type="radio"][name="${e.name}"]`);
            this.move (radios[radios.length-1]);
          }
          break;
        case 'checkbox' :
          e.checked = !e.checked;
          break;

        case 'button' : case 'submit' : case 'reset' :
          fireEvent (e);
          break;
        }
        break;
      }
    }
    //値が有効か?
    if (this.option.invalidStop && e.checkValidity)
      stay = ! e.checkValidity ();

    return stay;

    //__

    function fireEvent (e) {
      let event = document.createEvent ('MouseEvents');
      event.initEvent ('click', false, true);
      e.dispatchEvent (event);
      return event;
    }
  }


  //_____

  //イベントハンドラ(各typeによって分岐)
  handleEvent (event) {
    this[event.type + 'Handler'].call (this, event);
  }

  //キーアップハンドラ
  keyupHandler (event) {
    let { code, shiftKey, ctrlKey, target: e } = 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) {
    let { target: { nodeName: n, type: t }, code } = event;
    if ('Enter' == code)
      if (/^(submit|reset|button|textarea)$/.test (t) || 'SUMMARY' === n || 'A' == n)
        event.preventDefault ();
    this.imeFlag = 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;
  }

  //textarea要素に改行を挿入する
  static insertCRLF (t, autoIndent = false, start = t.selectionStart || 0) {
    let
      str = '\n',
      value = t.value,
      first = value.slice (0, start),
      last = value.slice (start).replace (/^([\t\u3000\u0020]+)/, '');

    if (autoIndent) {
      let spc = /(?:\n|^)([\t\u3000\u0020]+).*$/.exec (first);
      if (spc) str += spc[1];
    }

    t.value = first + str + last;
    t.selectionStart = t.selectionEnd = start + str.length;
  }



  //初期設定オプション値
  static defaultOption = {
    stay: true, //true: radio,checkbox,submit,button,summary を作用させた後も留まる
    loop: true, //要素を巡回する
    tabIndex: true, //tabIndex="-1" を有効にする
    anchor: true, //アンカータグを有効にする(但し href属性の値が "#"から始まる場合)
    textarea: true, //true:テキストエリア内では改行する, false:次に移動する(改行は、[Ctrl]+[Enter])
    autoIndent: true, //textarea要素内でのオートインデントを行う
    invalidStop: false,//要素の値が不正な場合でも移動する
  };
}

const
  nav = ExEnter.create (document.querySelector ('nav')),
  F = ExEnter.create (document.querySelector ('form'));

</script>

修正


class ExEnter {

  #option = null;
  #walker = null;//フォーカス要素の移動は treeWalker を利用する
  #imeFlag = null;//IME日本語入力中で未変換文字があり Enterが押されたことを感知するにはkeypress イベントがが実行されないことを利用する

  //_________________________

  //option 値を上書き
  #setOption = function (argsObj = { }) {
    /* this.#option = */ Object.assign (this.#option, argsObj);
  }
  //option値を返す
  #getOption = function (argsObj = null) {
    return this.#isObject (argsObj)
      ? Objects.keys.reduce ((o, k)=> ((o[k]= this.#option[k]), o), argsObj)
      : Object.assign ({}, this.#option);
  }

  //引数がオブジェクト型か?
  #isObject (arg) {
    return arg !== null && 'object' === typeof arg && 'Object' === arg.constructor.name;
  };


  //TreeWalker用のフィルター関数(要.bind(this))
  #filter = function (node) {
    const
      accept = NodeFilter.FILTER_ACCEPT,
      skip = NodeFilter.FILTER_SKIP;
    
    switch (node.nodeName) {
    case 'INPUT' : case 'TEXTAREA' : case 'SELECT' : case 'BUTTON' :
      if (node.disabled || node.readOnly) break;
      if (this.#option.tabIndex && ('-1' === node.getAttribute ('tabIndex'))) break;
      if (this.#isHide (node)) break;
      return accept;

    case 'A' : //href = '#...' で始まるもののみ対象とする
      if (this.#option.anchor) {
        let href = node.getAttribute ('href');
        if (href)
          if (0 === href.indexOf ('#'))
            return accept;
      }
      break;

    case 'SUMMARY' :
      return accept;
    }
    return skip;
  };


  //祖先の要素が隠された状態にあるか?
  #isHide = function (node) {
    const chks = ['display', 'visibility', 'opacity', 'height', 'width'];

    for (let e = node; e !== document.body; e = e.parentNode) {
      let
        cs = getComputedStyle (e, null),
        [d, v, o, h, w] = chks.map (p=> cs.getPropertyValue (p));//それぞれchksの値

      if (
        'none' === d || 'hidden' === v || 0 == parseFloat (o) ||
        ! ('auto' === h || 0 < parseInt (h, 10)) ||
        ! ('auto' === w || 0 < parseInt (w, 10))
      ) return true;
    }
    return false;
  }


  //textarea要素に改行を挿入する
  #insertCRLF = function (t, autoIndent = false, start = t.selectionStart || 0) {
    let
      str = '\n',
      value = t.value,
      first = value.slice (0, start),
      last = value.slice (start).replace (/^([\t\u3000\u0020]+)/, '');

    if (autoIndent) {
      let spc = /(?:\n|^)([\t\u3000\u0020]+).*$/.exec (first);
      if (spc) str += spc[1];
    }

    t.value = first + str + last;
    t.selectionStart = t.selectionEnd = start + str.length;
  }


  //対象の要素にクリックイベントを発火させる
  #fireEvent = function (target) {
    if (! target)
      throw new Error ('引数が無効!!');

    let event = target.ownerDocument.createEvent ('MouseEvents');
    event.initEvent ('click', false, true);
    target.dispatchEvent (event);
    return event;
  }

  
  //記載されたイベントは定義時に登録される
  #event = {
    'keydown':
      function (event) {
        if ('Enter' === event.code) {
          let { target: { nodeName, type } } = event;
          if (/^(submit|reset|button|textarea|file|color)$/.test (type) || /^(SUMMARY|A)$/.test (nodeName))
            event.preventDefault ();
          this.#imeFlag = event.isComposing;//IME変換中にEnterキーで確定、keyupでスルーするため
        } else
          this.#imeFlag = false;
      },

    'keyup':
      function (event) {
        if ('Enter' === event.code) {
          //IMEの動作で未変換中の文字があると判断してスルー
          if (this.#imeFlag)
            return event.preventDefault ();

          let { shiftKey, ctrlKey, target } = event;
          if (! this.#execute (target, ctrlKey, shiftKey))
            this.move (shiftKey, target);
          this.#imeFlag = false;
        }
      }
  };


  #execute = function (e, sw, prev) {
    let stay = false, tag = e.tagName;

    if ('TEXTAREA' === tag && !prev) {
      if (sw ^ this.#option.textarea) {
        this.#insertCRLF (e, this.#option.autoIndent);
        stay = true;
      }
    }
    else if (sw) {
      stay = this.#option.stay;
      switch (tag) {
      case 'SUMMARY' :
        let parent = e.parentNode;
        parent.hasAttribute ('open')
          ? parent.removeAttribute ('open')
          : parent.setAttribute ('open', '');
        break;

      case 'A' :
        this.#fireEvent (e);
        break;
      
      case 'INPUT' :
        console.log(e.type);
        switch (e.type) {
        case 'radio':
          if (e.checked = !e.checked) {//radio の checkedは選択肢が1つなので群の最後に移動
            let radios = this.root.querySelectorAll (`input[type="radio"][name="${e.name}"]`);
            this.move (false, radios[radios.length-1]);
          }
          break;
        case 'checkbox' :
          e.checked = !e.checked;
          break;
          
        case 'file' : case 'color' :
        case 'button' : case 'submit' : case 'reset' :
          this.#fireEvent (e);
          break;
        }
        break;
      }
    }
    //値が有効か?
    if (this.#option.invalidStop && e.checkValidity)
      stay = ! e.checkValidity ();

    return stay;
  }; 


  //_________________________

  constructor (root = document.documentElement, option = { }) {
    this.root = root;
    this.disabled = false; //true:機能停止

    this.#option = Object.assign ({ }, this.constructor.defaultOption, option);
    this.#walker = root.ownerDocument.createTreeWalker (
      root,
      NodeFilter.SHOW_ELEMENT, //要素ノードのみ
      this.#filter.bind (this)
    );

    //イベントを登録
    Object.keys (this.#event).forEach (eType=>
      root.addEventListener (eType, this, false)
    );
  }


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


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


  //要素を移動する direction= true:戻る /false:次へ
  move (direction = false, target = this.root.ownerDocument.activeElement) {
    if (this.disabled)
      return;

    let
      walker = this.#walker,
      isLoop = this.#option.loop;

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

    if (e) e.focus ();
  }


  //イベントハンドラ(各typeによって分岐)
  handleEvent (event) {
    let handler = this.#event[event.type];
    if (handler)
      handler.call (this, event);
  }

  //________

  //インスタンスの作成
  static create (root = document.documentElement, option = { }) {
    let obj = new this (root, option);
    return obj;
  }

  //初期設定オプション値
  static defaultOption = {
    stay: true, //true: radio,checkbox,submit,button,summary を作用させた後も留まる
    loop: true, //要素を巡回する
    tabIndex: true, //tabIndex="-1" を有効にする
    anchor: true, //アンカータグを有効にする(但し href属性の値が "#"から始まる場合)
    textarea: true, //true:テキストエリア内では改行する, false:次に移動する(改行は、[Ctrl]+[Enter])
    autoIndent: true, //textarea要素内でのオートインデントを行う
    invalidStop: false,//要素の値が不正な場合でも移動する
  };
}
 
const enter = ExEnter.create (TEST);