トップ画像イメージ

素のjsとcssでテキストボックスに自動補完機能を付与できるクラスを作成してみました

前回は素のjsでalert関数の代わりに使えるようなモーダルウィンドウを作成してみました。

今回も、ライブラリに頼らない素のjs力を鍛えるべく、任意のテキストボックスに自動補完機能を付与できる機能を作成してみました。

ちなみに元ネタはこちら(jquery ui)を参考にさせていただきました

機能一覧

  • 任意のテキストボックスに自動補完機能を付与する
    • テキストボックスにフォーカスを当てると、絞り込む前のリスト一覧を表示する
    • 文字列を入力すると、その文字列に部分一致するリストに絞り込まれる
    • 検索用のリストには、検索値を設定することができ、例えば「田中」を「たなか」で絞り込むことができるようになる。(「田」でも「た」でも絞り込みができる)
    • 表示されたリストは、上矢印キー、下矢印キーで移動可能。また、クリックの他、エンターキーでもリストから選択することができる
    • リストの最下部の要素で矢印キーを押下すると、テキストボックスにフォーカスが戻る。また、リストの最上部の要素で上矢印キーを押下すると、テキストボックスにフォーカスが戻る

イメージ

イメージ1

テキストボックスにフォーカスすると、候補の一覧が表示される オートコンプリートイメージ1

イメージ2

テキストボックスのなかで、下矢印キーや、上矢印キーで要素を選ぶことができる(クリックでも可)

オートコンプリートイメージ2

イメージ3

値の他、検索値を設定でき、漢字の読みなどで絞り込ませることができる

オートコンプリートイメージ3

イメージ4

リストの要素をクリック、もしくは、エンターキーで選択すると、テキストボックスに要素に設定した値が入る

オートコンプリートイメージ4

コード

以下、実装した時のコードです。1日で適当に作ったので(ありがちな言い訳😄)、冗長なコードがございますが、多めにご覧ください。

//js
class Autocomplete{
  //example
  //*must values = [ {value: "javascript",search_value:'js'},{value: 'ruby',search_value: 'rb'} ]
  //*must target_id = "target" (オートコンプリートを適用したいinput要素のid)
  constructor(values,targetId){
    this.values = values;
    this.targetId = targetId;
    this.target = document.querySelector(`#${this.targetId}`);
    //指定したテキストボックスにフォーカスインした時点で候補リスト表示
    this.target.addEventListener('focusin',(e) => {
      //list-itemからフォーカスされた場合は、リストの再表示を行わない
      if (e.relatedTarget){
        if (e.relatedTarget.classList.contains('autocomplete-list-item')){
          return;
        }
      }
      Autocomplete.listRemove();
      this.listOn();
    });
    //指定したテキストボックスからフォーカスが外れた場合はリストを削除する
    this.target.addEventListener('blur',(e) => {
      //fucus先がautocompleteのアイテム以外ならリストを削除
      const relatedTarget = e.relatedTarget;
      if (relatedTarget != null){
        if (relatedTarget.classList.contains('autocomplete-list-item')){
          return;
        }
      }else{
        Autocomplete.listRemove();
      }
    });
  }
  //クラスメソッド(リストをDOMからremove)
  static listRemove() {
    const lists = document.querySelectorAll('.autocomplete-list');
    if (!lists || lists.length == 0){
      return;
    }
    lists.forEach(function(list){
      list.parentNode.removeChild(list);
    });
  }
  //return Array[html obj]
  //現在表示されているリストのうち、display=none以外の要素を配列で返却(何もなければ[]が返る)
  getDispList = () => {
    let dispItems = [];
    let listItems = document.querySelectorAll('.autocomplete-list-item');
    //現在表示されているアイテムの取得
    listItems.forEach(function(item){
      if (item.style.display != "none"){
        dispItems.push(item);
      }
    });
    return dispItems;
  }
  //return html obj
  //現在フォーカスが当たっている要素の次の要素を返す(次の兄弟がなければnullを返す)
  nextElm = (node, selector) => {
    if (selector && document.querySelector(selector) !== node.nextElementSibling) {
      return null;
    }
    return node.nextElementSibling;
  }
  //return html obj
  //現在フォーカスが当たっている要素の前の要素を返す(前の兄弟がなければnullを返す)
  prevElm = (node, selector) => {
    if (selector && document.querySelector(selector) !== node.previousElementSibling) {
      return null;
    }
    return node.previousElementSibling;
  }
  //no return
  //指定した要素の下にリストを表示するメソッド(矢印キーなどのキー入力時のイベントも設定)
  listOn = () => {
    let self = this;
    //ターゲットの要素取得と高さ・横幅等の取得(int)
    const target = document.querySelector(`#${this.targetId}`);
    let height = target.offsetHeight;
    let width = target.offsetWidth;
    // 要素の位置座標を取得(int)
    let clientRect = target.getBoundingClientRect() ;
    let px = window.pageXOffset + clientRect.left;
    let py = window.pageYOffset + clientRect.top;
    //list表示用のdomを生成(str)
    let listDom = `<ul class="autocomplete-list" style="left: ${px}px; top: ${py + height}px; width: ${width}px; margin: 0px;">`
    if (this.values && this.values.length > 0 ){
      this.values.forEach(function(value,index){
        //検索値があればattrとして持たせる
        let search_value = value.search_value ? `search_value="${value.search_value}"` : "" ;
        listDom += `
          <li class="autocomplete-list-item" style="height="${height}px;" id="auto-id-${index}" val="${value.value}" tabindex="1" ${search_value}>
            <a class="list-btn">${value.value}</a>
          </li>
        `
      });
      listDom += "</ul>"
    }else{
      console.log('AutoCoplete no values error');
      return;
    }
    target.insertAdjacentHTML('afterend',listDom);
    //表示したリストアイテム(<li>)へのイベント設定
    let listItems = document.querySelectorAll('.autocomplete-list-item');
    //clickでテキストボックスに値を入れる
    listItems.forEach(function(item){
      item.addEventListener('click',function(){
        const val = item.getAttribute('val');
        target.value = val;
        Autocomplete.listRemove();
      },false);
      item.addEventListener('keydown',function(e){
        //下矢印キーで次の要素(次がなければテキストボックス)にフォーカス
        if (e.code == "ArrowDown"){
          let nextElm = self.nextElm(item);
          if (nextElm){
            if (nextElm.style.display == "none"){
              target.focus();
              target.value += '';
            }else{
              nextElm.focus();
            }
          }else{
            target.focus();
            target.value += '';
          }
        }
        //上矢印キーで前の要素(前がなければテキストボックス)にフォーカス
        if (e.code == "ArrowUp"){
          let prevElm = self.prevElm(item);
          if (prevElm){
            if (prevElm.style.display == "none"){
              target.focus();
              target.value += "";
            }else{
              prevElm.focus();
            }
          }else{
            target.focus();
            target.value += ''; //文字列の末尾にカーソルを合わせる
          }
        }
        //Enterキーでもクリックと同様のイベント
        if (e.code == "Enter"){
          const val = item.getAttribute('val');
          target.value = val;
          target.value += '';
          Autocomplete.listRemove();
        }
      });
    });
    //targetで入力する度にリストのアイテムから絞り込みを行う
    target.addEventListener('keyup',(e) => {
      if (e.code == "Enter" || e.code == "ArrowUp" || e.code == "ArrowDown"){
        return;
      }
      //検索文字列からの絞り込み
      const targetVal = target.value; //インプットボックスの文字列
      listItems.forEach(function(item){
        const itemVal = item.getAttribute('val'); //リストアイテムのval
        const itemSearchVal = item.getAttribute('search_value');
        if (itemVal.includes(targetVal) || itemSearchVal.includes(targetVal)){
          item.style.display = "list-item";
        }else{
          item.style.display = "none";
        }
      });
    });
    target.addEventListener('keydown',(e) => {
      //矢印キーでリストのアイテムへ移動
      let dispItems = this.getDispList();
      if (e.code == "ArrowDown"){
        //現在表示されているアイテムの最初のアイテムへfocusを合わせる
        dispItems[0].focus();
      }else if (e.code == "ArrowUp"){
        dispItems[dispItems.length - 1].focus();
      }
    })
  }
}

//利用例
window.onload = function(){
  //使用例
  let data = [
    {value: '飯塚',search_value:'いいづか'},
    {value: '豊本',search_value:'とよもと'},
    {value: '角田',search_value:'かくた'},
  ];
  const sample = new Autocomplete(values=data,targetId="autocomplete");
}

/* css */

/* autocompleteスタイル */

.autocomplete-list{
  position: absolute;
  padding: 0;
  z-index: 1;
  border: 1px solid #aaaaaa/*{borderColorContent}*/;
  background: #ffffff;
  color: #222222/*{fcContent}*/;
  font-family: Verdana,Arial,sans-serif/*{ffDefault}; */;
  font-size: 1.1em/*{fsDefault}*/;
  list-style: none;
  border-radius: 4px;
}

.autocomplete-list .autocomplete-list-item{
  margin: 0;
  padding: 0;
  zoom: 1;
  width: 100%;
}
.autocomplete-list .autocomplete-list-item:hover{
  background-color: #aecbeb;
}
.autocomplete-list .autocomplete-list-item:focus{
  background-color: #aecbeb;
}


<!-- サンプルhtml -->
<!DOCTYPE html>
<head>
    <meta charset="utf-8"/>
    <link rel='stylesheet' href='./base.css'>
    <script type="text/javascript" src="autocomplete.js"></script>
</head>

<body>
    <label>autocomplete</label>
    <input type="text" id="autocomplete" class="autocomplete">
</body>


まとめ

  • リストアイテムの矢印キーによる移動が地味に面倒だった
    • 多分li要素とaタグを使っているのもあまり良くない。(本家のを思考停止でパクってしまった)
  • このくらいの機能を作るだけであれば割と簡単に実装できた
  • 今回作ったクラスを継承させた別クラスを作れば、色々なパターンの自動補完機能が作れそう

ちなみに今回のものも、githubにあげさせていただきました。