検索機能

Javascript

こんにちはkazutoです。今回は、JavaScriptのみで、検索機能を実装していきます。

事前準備

まずは事前準備をします。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="search.css">
  <script src="search.js"defer></script>
</head>
<body>
  <h1>Search</h1>
  <main>
    <div class="inputBox">
      <form id="SearchEventForm" >
        <label><input type="text"></label>
      </form>
    </div>
    <div class="searchValue">
    </div>
  </main>
<div class="modalwindow">
</div>
</body>
</html>
body{
  background-color: #222222;
  color: white;
  font-family: cursive;
}
main{
  width: 800px;
  height: 500px;
  margin: auto;
  text-align: center;
}
h1{
  text-align: center;
  font-size: 60px;
}
.inputBox{
  width: 100%;
  height: 50px;
  margin: 0 auto ;
}

.SearchEventForm{
  width: 100px;
  height: 500px;
  margin: auto;
  background-color: darkcyan;
}

input[type="text"]{
  width: 250px;
  border-radius: 30px;
  background-color: #111111;
  border: 10px solid black;
  color: white;
  font-size: 20px;
  padding:2px 10px;
  font-family: cursive;
}

button[type="submit"]{
  height: 40px;
  width: 40px;
  border-radius: 30px;
  background-color: #111111;
  border: 10px solid black;
  color: white;
}


#typed{
  color: aqua;
}

#text{
  margin-top: 20px;
  font-size: 40px;
  z-index: 10;
}
.transparent{
  opacity: 0.4;
}

.modalwindow{
  position: fixed;
  background-color:black;
  width: 600px;
  height: 400px;
  top: 50px;
  left: 400px;
  right: 400px;
  display: none;
  color: white;
  text-align: center;
  font-size: 40px;
  overflow:scroll;
  padding: 40px;
  padding-top:0 ;
  border-radius: 40px;
}

.block{
  display: block;
}

※必ずコピペをしてください。正常に動作しない恐れがあります。

今回の「検索機能」は実装する機能が多いので、こちらのトピックで、機能の簡単な解説をします。

  • 検索機能
  • サジェスト機能
  • 送信機能

検索機能(前方一致検索)

今回の検索機能は前方一致検索という検索手法を用いて実装しました。前方一致検索とは語句の先頭が一致する文字列を調べるための検索方法です。

  • 検索ヒット数が複数ある場合
  • 検索ヒット数が1つの場合
  • 検索ヒット数が0の場合

上記の条件によって挙動を分けています。なお実装に使用するイベントはkeyupイベントです。

サジェスト機能

前方一致検索の語句の先頭が一致する文字列を調べる特性を応用し、サジェスト機能も実装しました。サジェスト機能とは、入力途中に予測を行い、入力文字列の下に候補として表示する機能です。要するに、補完機能みたいなものです。

今回は、サジェスト結果をクリックすると、その文字列が入力フォームに置換されるという仕様に実装しました

この機能を応用する事でインクリメンタルサーチという本格的な検索機能を実装する事が可能になります。

なお実装に使用するイベントは、

  • mouseoverイベント
  • mouseoutイベント
  • clickイベント

になります。

送信機能

簡易的な送信フォームをDOM操作してみよう」で用いた、フォームのDOM操作方法で、最終的に該当する検索結果を表示します。

  • 検索ヒット数が複数ある場合
  • 検索ヒット数が1つの場合
  • 検索ヒット数が0の場合

上記の条件によって挙動を分けております。なお実装に使用するイベントはsubmitイベントになります

以上で各種機能の解説を終わります。

検索機能

それでは本各的に実装していきましょう。まずは検索機能を実装していきます。

  • ノードの取得
  • イベント登録
  • イベント発火後の処理を実装

ノードの取得

まずは、実装に必要なノードを取得しましょう。

const searchsTarget =
["HTML","CSS","JavaScript","Ruby","Ruby on Rails",
"PHP","GO","React","Apple","Google,"Vue","game",
"Hello","App","鬼滅の刃","ポケモン","サッカー","野球","Java",
"Ruby Gem","ruby Class","PHP if","Apple store","Japan","English","Math","AWS"]
const searchFiled   = document.querySelector("input[type='text']")

const  elment = ".searchValue"
const  beforeHTML =  `<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`
const  noSearchResult =`<p id = "text">検索結果はありません<p>`

let untyped = ``
let typed   = ``
let back    = ``
let html ;
let afterHTML;
let reg
const searchsTarget =
["HTML","CSS","JavaScript","Ruby","Ruby on Rails",
"PHP","GO","React","Apple","Google,"Vue","game",
"Hello","App","鬼滅の刃","ポケモン","サッカー","野球","Java",
"Ruby Gem","ruby Class","PHP if","Apple store","Japan","English","Math","AWS"]
const searchFiled   = document.querySelector("input[type='text']")

定数searchsTarget は、検索する対象の配列です。定数searchFiledは、テキストフィールド を取得しています。定数searchFiledに対してkeyupイベントをイベント登録する事で前方一致検索を実現していきます。

const  elment = ".searchValue"
const  beforeHTML =  `<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`
const  noSearchResult =`<p id = "text">検索結果はありません<p>`

上記の定数は、DOMにHTML要素を挿入する際に、使用しています。定数化した意図として、何回も同じHTML要素を記述するため です。 なので使い回せる様、定数化しました。

let untyped = ``
let typed   = ``

上記は、定数searchFiledで入力した文字列が検索対象配列の文字列と一致した際に、「searchFiledに入力された文字列の色を変える」という動作を再現するために必要な変数です。

具体的には

  • untyped
    →入力がされていない
  • typed
    →入力済

という感じに変数を使用していきます。抽象的で分かりづらいと思いますが、実装していけば理解できてくると思いますので安心してください。

では、トピックの最後に定数searchFiledが正しく取得できいるかデバックしていきましょう。

console.log(searchFiled)

正しくノードが取得できているのが確認できたら、次のステップに進みましょう。

イベント登録

ノードを正しく取得できた所で、イベントを登録していきましょう。今回は動的に検索候補を取得していきたいので、keyupイベントを用いて実装していきます。なので、定数searchFiledに対してkeyupイベントを登録していきましょう。

searchFiled.addEventListener("keyup",function(e){
})

実際にイベントが登録できたかデバックしてみましょう。

searchFiled.addEventListener("keyup",function(e){
  console.log("test")
})

無事testという文字列が出力されているのでイベントが登録できました。なので次のステップに進みましょう。

イベント発火後の処理を実装

それではイベント発火後の処理を実装していきます。

function multipleSearches(vl,targets) {
  index = 0
  while (index<targets.length) {
      bulidHTML(elment,beforeHTML,"insertAdjacentHTML")
      typed = vl
      untyped = targets[index].replace(typed,"")
      afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
      bulidHTML(elment,afterHTML,"insertAdjacentHTML")
      index++
    }
}
function bulidHTML(elment,html,type="innerHTML") {
  switch (type) {
    case "innerHTML":
      document.querySelector(elment).innerHTML = html
      break;
    case "insertAdjacentHTML":
      document.querySelector(elment).insertAdjacentHTML(`beforeend`,html)
      break;
  }
}

function insertTypingResult() {
  bulidHTML(elment,beforeHTML)
  bulidHTML("#typed",typed)
  bulidHTML("#untyped",untyped)
}

function shapingArray( searchsTarget,reg){
return searchsTarget.map((target)=>{
    if(target.match(reg)){
      return  target
    }
  }).filter(target=>target)
}

function checkTarget(target,reg,vl,elment){
  if(target.match(reg)){
    bulidHTML(elment,`<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`)
    typed = vl
    untyped = target.replace(typed,"")
    bulidHTML(elment,`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`)
  }
}

searchFiled.addEventListener("keyup",function(e){
  if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"){ return}
  if (e.keyCode==187||e.keyCode==222) {
   return
  }
  reg = new RegExp("^"+this.value)
  let targets = shapingArray(searchsTarget,reg)
  if(targets.length==0){ return bulidHTML(elment,noSearchResult) }

  targets.forEach((target)=>{
    if(this.value===``){return bulidHTML(elment,``)}
    if(document.querySelectorAll("#text").length>0){
            document.querySelectorAll("#text").forEach((text)=>{
              document.querySelector("#text").remove()
            })
      }
          if (targets.length>=2) {
            multipleSearches(this.value,targets)
            html= document.querySelectorAll("#text")
            return
          }
    i=this.value.length
    switch (i) {
      case 3:
        checkTarget(target,reg,this.value,elment)
        return
      break;
      case 2:
        if (e.code=="Backspace") {
          checkTarget(target,reg,this.value,elment)
          return
        }
      break;
      case 1:
        if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          return
        }
          afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          if (this.value!==untyped.substring(0,1)){return}
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          return
        default:
        if (e.code=="Backspace") {
            checkTarget(target,reg,this.value,elment)
            return
          }
    }
       if (html.length>=2) {
          bulidHTML(elment,beforeHTML)
          typed = this.value
          untyped = target.replace(typed,"")
          afterHTML =`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
          bulidHTML(elment,afterHTML)
          return
        }else{
          document.querySelector(".searchValue").appendChild(html)
          typed = this.value
          untyped = untyped.substring(1)
          insertTypingResult()
        }
    })
})

少し量が多いので抜粋して解説していきます。

 if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"){ return}
  if (e.keyCode==187||e.keyCode==222) {
   return
  }

こちらのソースコードは、対象のキーがタイピングをしても無視してねという意味です。returnメソッドを使うと、以降の処理を強制終了をする事が可能です。

function shapingArray( searchsTarget,reg){
return searchsTarget.map((target)=>{
    if(target.match(reg)){
      return  target
    }
  }).filter(target=>target)
}
  reg = new RegExp("^"+this.value)
  let targets = shapingArray(searchsTarget,reg)

こちらのソースコードは,まず初めにRegExpオブジェクトを生成をし”^”と定数searchFiledを引数に渡す事で、前方一致検索を実現しています。RegExp オブジェクトとは、パターンでテキストを検索するために使用するオブジェクトです。

その後、関数shapingArrayを用いて、マッチした要素のみを抽出した配列targetsを定義をしています。

function shapingArray( searchsTarget,reg){
return searchsTarget.map((target)=>{
    if(target.match(reg)){
      return  target
    }
  }).filter(target=>target)
}

関数shapingArrayの処理内容を確認しておきましょう。引数と渡された、定数searchsTargetに対してmapメソッドを用いて、新しく配列を作り直しています。mapメソッドで新しく配列を作成した場合は、返り値をreturnメソッドを用いて明示的に示さないといけません。returnメソッドの記述を忘れると配列を生成する事ができないので注意してください。

 if(target.match(reg)){
      return  target
    }

mapメソッドで取り出した、配列searchsTargetの各要素に対してmatchメソッドを用いて、文字列が一致するか評価をしています。matchメソッドは、正規表現に対する文字列のマッチングを確認するメソッドです。

}).filter(target=>target)
}

こちらのソースコードは、メソッドチェーンを組んで、filterメソッドを用いて、配列を整形をしています。filterメソッドとは、ある条件を満たした要素のみを新しい配列として生成するメソッドです

実は、mapメソッドのみで「綺麗な配列」を返せません。実際に

  • fliterメソッドを用いない場合
  • fliterメソッドを用いた場合

のtargetsの中身をみてみましょう。

[fliterメソッドを用いない場合]

[fliterメソッドを用いた場合]

mapメソッドのみだとundefinedという表示されていて、マッチした要素以外にも無駄な要素を含んでしまいます。一方、fliterメソッドを用いてメソッド チェーンを組んだ場合だと、マッチした要素のみを抽出した配列を生成する事ができました。

上記の理由からメソッドチェーンを組んでfliterメソッドを用いています。メソッドチェーンとは、あるオブジェクトに対して、メソッドとメソッドをつなげる事を指します

ちなみに以下の様に記述してもOKです。

function shapingArray( searchsTarget,reg){
return searchsTarget.filter((target)=>{
    if(target.match(reg)){
      return  target
    }
  })
}

 ※今回は、メソッドチェーンについて解説したかったためmapメソッドを用いて実装しました。

if(targets.length==0){ return bulidHTML(elment,noSearchResult) }

配列targetsが空の配列だった場合、「検索結果ありません」と関数bulidHTMLを用いて画面に表示します。

 targets.forEach((target)=>{
  //処理
 });

配列targetsが空の配列ではない場合、forEachメソッドを用いて要素をばらしていきます。

 if(this.value===``){return bulidHTML(elment,``)}

定数searchFiledのvalueが空の文字列だった場合、関数bulidHTMLを用いて、HTML要素を初期化をしています。この作業をする事により、Backspeceで文字列を消した際の挙動を調整をしています。

if(document.querySelectorAll("#text").length>0){
            document.querySelectorAll("#text").forEach((text)=>{
              document.querySelector("#text").remove()
            })
      }

if文を用いてtextというid属性が1つでも存在していた場合、removeメソッドを用いて対象のHTML要素を削除しています。この作業を行う事で、「イベントが発火する事にHTML要素が増える」という謎の挙動を整えています。

続いて、今回の実装の肝の部分について解説していきます。「検索機能」では、

  • 複数検索
  • 単数検索

という2つの検索挙動を取ります。ポイントとして抑えて欲しいのは、検索手法が前方一致検索という点です。前方一致検索の特性上、文字列が入力されていけばいくほど、検索精度は高まってきます。この、前方一致検索の特性から考えられる挙動として,以下の画像の様な事が事象として起きうる可能性があります。

画像を見て頂くと先程、解説した通り文字列が入力されていけばいくほど、検索精度は高まっている事が画像から読み取れると思います。この作業を抽象化すると、「複数検索から単数検索に切り替わり」となります。なので、複数検索から単数検索に切り替える所が今回の実装の肝になります。

まずは、複数検索から確認をしていきます。

function multipleSearches(vl,targets) {
  index = 0
  while (index<targets.length) {
      bulidHTML(elment,beforeHTML,"insertAdjacentHTML")
      typed = vl
      untyped = targets[index].replace(typed,"")
      afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
      bulidHTML(elment,afterHTML,"insertAdjacentHTML")
      index++
    }
} 
if (targets.length>=2) {
            multipleSearches(this.value,targets)
            html= document.querySelectorAll("#text")
            return
       }

こちらのソースコードは、配列targetsがの長さが2以上の場合、処理が読み込まれます。要するに複数、検索がヒットしたシチュエーションです。

if文のブロック内では、関数multipleSearchesを用いて繰り返し、配列targetsに対して繰り返し処理を行っています。

function multipleSearches(vl,targets) {
  index = 0
  while (index<targets.length) {
      bulidHTML(elment,beforeHTML,"insertAdjacentHTML")
      typed = vl
      untyped = targets[index].replace(typed,"")
      afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
      bulidHTML(elment,afterHTML,"insertAdjacentHTML")
      index++
    }
} 
 multipleSearches(this.value,targets)

まずは、関数multipleSearchesの処理内容を確認していきましょう。

index = 0
  while (index<targets.length) {
      //処理
      index++
    }

まずは、繰り返し処理の仕組みについて確認していきましょう。while文ブロック外にindexを初期化し、ブロック内でindexの値を更新しています。繰り返し処理の条件を確認をしてみると、配列targetの長さがindexより大きい限り処理を続けるという条件提示をしています。要するに、indexの数字が配列targetsの長さより大きくなるまで繰り返すという事です。

 bulidHTML(elment,beforeHTML,"insertAdjacentHTML")

まずは、関数bulidHTMLを用いて、HTML要素を初期化しています。今回、関数bulidHTML要素の挙動を少し変えています。一度、関数bulidHTMLの処理内容を確認をしてみましょう。

function bulidHTML(elment,html,type="innerHTML") {
  switch (type) {
    case "innerHTML":
      document.querySelector(elment).innerHTML = html
      break;
    case "insertAdjacentHTML":
      document.querySelector(elment).insertAdjacentHTML(`beforeend`,html)
      break;
  }
}

上記が関数bulidHTML要素の中身になります。

switch (type) {
    case "innerHTML":
      document.querySelector(elment).innerHTML = html
      break;
    case "insertAdjacentHTML":
      document.querySelector(elment).insertAdjacentHTML(`beforeend`,html)
      break;
  }

swith文を用いて条件分岐をしています。

  • typeの中身が文字列のinnerHTMLの場合
    →innerHTMLメソッドを用いてHTML要素を挿入
  • typeの中身が文字列のinsertAdjacentHTMLの場合
    →insertAdjacentHTMLメソッドを用いてHTML要素を挿入

という挙動になります。

複数検索ではinsertAdjacentHTMLメソッドを用いてHTML要素を挿入していますが、その意図は、innerHTMLメソッドの性質になります。innerHTMLメソッドで挿入した場合で且つ、同じDOMツリー内にすでにHTML要素が存在していた場合は、その要素に対して上書きする形で挿入します

複数検索では、繰り返し処理が行われてHTML要素を挿入をしています。なのでinnerHTMLメソッドを用いてHTML要素を挿入した場合、複数マッチしているのにも関わらず、画面に表示されているのは、一番最後に挿入されHTML要素になってしまいバグが起こってしまいます。

この様な理由から、複数検索では、insertAdjacentHTMLメソッドを用いています。

 typed = vl
 untyped = targets[index].replace(typed,"")

こちらのソースコードは、変数typedには 定数searchFiledのvalue値を格納し、変数 untypedには,対象の要素からreplaceメソッド用いてtypedに格納されている文字列の重複を取り除いた文字列を格納しています。

この作業を行う事で、定数searchFiledに入力した文字が、ヒットした場合に、色が変わっていくという動作を実現をしています。

replaceメソッドは、第一引数に渡したpatternがマッチした場合に文字列の一部またはすべてを書き換えるメソッドです。今回は、第二引数を空の文字列にする事で

  • 変数typed
  • 変数untyped

の重複を取り除いています。

afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
bulidHTML(elment,afterHTML,"insertAdjacentHTML")

先程の

  • 変数typed
  • 変数untyped

を埋め込んだHTML要素を関数bulidHTMLを用いてHTML要素を挿入をしています。直感的にHTML要素を生成したい場合はテンプレートリテラル記法を用いると、とても簡単に記述できます。

index++

while文のブロックの最後でindexに対して+1して上げています。この記述を忘れると無限ループが起こってしまいますので必ず記述してください

以上が関数multipleSearchesの挙動の解説でした。

最終的に

html= document.querySelectorAll("#text")

 if (html.length>=2) {
          bulidHTML(elment,beforeHTML)
          typed = this.value
          untyped = target.replace(typed,"")
          afterHTML =`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
          bulidHTML(elment,afterHTML)
          conditionEvaluation()
          return
        }else{
          document.querySelector(".searchValue").appendChild(html)
          typed = this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          conditionEvaluation()
        }

変数htmlにquerySelectorAllメソッドを用いて,id名がtextの全てのHTML要素をNodeListとして格納しています。変数htmlの長さによって、単数検索か複数検索か条件分岐を行います。

条件分岐を行う意図として、複数検索から単数検索に切り替わる際に挙動を少し変えて上げないとエラーが出てしまうからです。なので「htmlの長さが2以上だった場合は、複数検索ですよ」とコンピューター側に解釈させるために、変数html要素を定義しました。

if文のブロック内の処理については、先程、解説した内容にかぶるので省略させて頂きます。以上で、複数検索の解説は、終わりです。続いて、単数検索について解説をしていきます。

 function checkTarget(target,reg,vl,elment){
  if(target.match(reg)){
    bulidHTML(elment,`<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`)
    typed = vl
    untyped = target.replace(typed,"")
    bulidHTML(elment,`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`)
  }
}

i=this.value.length
    switch (i) {
      case 3:
        checkTarget(target,reg,this.value,elment)
        return
      break;
      case 2:
        if (e.code=="Backspace") {
          checkTarget(target,reg,this.value,elment)
          return
        }
      break;
      case 1:
        if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          return
        }
          afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          return
        default:
        if (e.code=="Backspace") {
            checkTarget(target,reg,this.value,elment)
            return
          }
    }

単数検索の場合は、switch文を用いて条件分岐を行いました。

i = this.value.length

まずは、定数searchFiledのvalue値の長さをiという変数に代入をします。その後、以下の様に条件分岐をしていきます。

  • iの値が3だった場合
  • iの値が2だった場合
  • iの値が1だった場合

となります。

swich文はif文よりもシンプルに条件分岐を記述できるのがメリットになります。

case 3:
        checkTarget(target,reg,this.value,elment)
        return

iの値が3だった場合は、関数checkTargetを呼び出しreturnメソッドで処理を中断して終わります。

 case 2:
        if (e.code=="Backspace") {
          checkTarget(target,reg,this.value,elment)
          return
        }

iの値が2だった場合、Backspaceキー押したつまり文字を消し際に関数checkTargetを呼び出します。

 case 1:
        if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          return
        }
          afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          return

iの値が1だった場合は仕様の関係上、少し挙動を変えております。

 if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          return
        }

文字を消して、iの値が1だった場合は、関数checkTargetを呼び出して終わりです。

afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          return

シンプルにiの値が1だった場合は、if文が無視され、こちらのソースコードが読み込まれます。

afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
bulidHTML(elment,afterHTML)
untyped= document.querySelector("#untyped").innerHTML

テンプレートリテラル記法を用いてHTML要素を生成し、関数bulidHTMLでHTML要素を挿入します。その後,querySelectorメソッドを用いて対象のノードを取得して、innerHTMLメソッドを用いて、値を取得しています。

typed= this.value
untyped = untyped.substring(1)

変数typedには、定数searchFiledのvalu値を格納し、変数untypedには、substringメソッドを用いて、元ある文字列の長さから、インデックス番号1から〜最後までを取り出して、untypedに格納。

上記の作業を行う事で、typedとuntyped間にリレーションを組む事が可能になり、結果的に「入力された文字の色が変わる」という動作を再現をしています。

insertTypingResult()
html =  document.querySelector("#text")
return

関数insertTypingResultを用いてHTML要素を挿入をし、変数htmlには、querySelectorメソッドを用いて、対象のノードを取得しています。

if (html.length>=2) {
          bulidHTML(elment,beforeHTML)
          typed = this.value
          untyped = target.replace(typed,"")
          afterHTML =`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
          bulidHTML(elment,afterHTML)
          conditionEvaluation()
          return
        }else{
          document.querySelector(".searchValue").appendChild(html)
          typed = this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          conditionEvaluation()
        }

htmlにノードを格納する意図は、先程の解説にもあった通り、変数htmlの長さによって条件分岐をするためです。この作業を行う事で複数検索→単数検索の切り替えを実現しています。

以上で検索機能についての実装は以上です。続いてサジェスト機能を実装していきます。

サジェスト機能

必要な変数・ノードについては、以前のトピックで定義済みなのでイベント登録から進めていきます。

使用するイベントは、

  • mouseoverイベント
  • mouseoutイベント
  • clickイベント

となります。

イメージは,mouseoverイベントの中で

  • mouseoutイベント
  • clickイベント

を登録する感じです。

関数を定義しよう

今回は、色々と条件分岐等で挙動をかなり分けてしまっています。なので、サジェスト機能には、機動力が必要です。この様な観点から、サジェスト機能を実装するのに必要なイベントは、関数化して使い回しができる様にします。

まずは、関数間のロジックを把握しておきましょう。

  • 関数conditionEvaluation
    →イベントが登録できるかを評価
  • 関数suggest
    →各要素にmouseoverイベントを登録
  • 関数suggestCallBack
    →サジェスト機能本体

と上記の様な関数を定義します。下記のソースコードをご覧ください。

function conditionEvaluation(){
  if(document.querySelector("#text")){suggest(document.querySelectorAll("#text"))}
}

function suggest(elments){
    elments.forEach(text=>{ text.addEventListener("mouseover",suggestCallBack) })
}

function suggestCallBack(e){
  this.classList.add("transparent")
  this.addEventListener("click",function(){
    searchFiled.value = this.textContent
    typed = searchFiled.value
    untyped = ``
    insertTypingResult()
    if (html.length>=2) {
     reg =new RegExp("^"+searchFiled.value)
     targets=shapingArray(searchsTarget,reg)
      if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
      return
      }
    }
  })
  this.addEventListener("mouseout",function(){
    this.classList.remove("transparent")
  })
}

少し処理が複雑なので関数ごとに分けて解説していきます。

function conditionEvaluation(){
  if(document.querySelector("#text")){suggest(document.querySelectorAll("#text"))}
}

関数conditionEvaluationは、イベントが登録できるかを状態か評価する関数です。サジェストする対象が存在していない場合にイベント登録してしまうと、エラーになります。なのでquerySelectorメソッドを用いて対象のノードが存在しているか確認をし、存在していれば、querySelectorAllメソッドを用いて条件に当てはまるノードを全て取得します。

suggest(document.querySelectorAll("#text"))

条件が一致すれば、関数suggestを呼び出します。

function suggest(elments){
  elments.forEach(text=>{ text.addEventListener("mouseover",suggestCallBack) })
}

関数suggestは、とてもシンプルです。引数に貰った、elmentsをforEachメソッドを用いてNodeListから取り出し、各要素にmouseoverイベントを登録しています。

text.addEventListener("mouseover",suggestCallBack) 

関数suggestCallBackをコールバック関数として受け取ります。

function suggestCallBack(e){
  this.classList.add("transparent")
  this.addEventListener("click",function(){
    searchFiled.value = this.textContent
    typed = searchFiled.value
    untyped = ``
    insertTypingResult()
    if (html.length>=2) {
     reg =new RegExp("^"+searchFiled.value)
     targets=shapingArray(searchsTarget,reg)
      if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
      return
      }
    }
  })
  this.addEventListener("mouseout",function(){
    this.classList.remove("transparent")
  })
}

関数suggestCallBackはサジェスト機能本体です。

 this.classList.add("transparent")

text(this)に対してtransparentクラスを追加しています。

this.addEventListener("click",function(){
    searchFiled.value = this.textContent
    typed = searchFiled.value
    untyped = ``
    insertTypingResult()
    if (html.length>=2) {
     reg =new RegExp("^"+searchFiled.value)
     targets=shapingArray(searchsTarget,reg)
      if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
      return
      }
    }
  })

text(this)に対してclickイベントを登録しています。詳しく解説していきます。

  • 単数検索
  • 複数検索

の場合で挙動が少し違うので分けて解説していきます。 

[単数検索の場合]

 searchFiled.value = this.textContent
    typed = searchFiled.value
    untyped = ``
    insertTypingResult()

単数検索の場合は、直感的で理解しやすいと思います。

まず、定数searchFiledのvalue値にtextContentメソッドを用いてtext(this)にある文字列を取得します。その後、変数typedに定数searchFiledのvalue値を格納し、変数untypedの中身を空にし、最終的に関数insertTypingResultを用いてHTML要素を描写してサジェスト完了になります。

[複数検索の場合]

 if (html.length>=2) {
     reg =new RegExp("^"+searchFiled.value)
     targets=shapingArray(searchsTarget,reg)
      if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
      return
      }
    }

複数検索の場合、少しややこしいです。

if (html.length>=2) {
    }

まず、複数検索である事を評価します。複数検索である事を証明するには、 変数htmlの長さが2以上である事を証明をすれば良いだけです。よって上記の様に条件を提示ます。

reg =new RegExp("^"+searchFiled.value)
targets=shapingArray(searchsTarget,reg)

ここでもう一度、targetsの配列を作り直してあげます。この作業を行う事により関数内でリアルタイムの検索マッチ数を取得できます。

上記の画像を見てください。Rubyという文字列をサジェストしていますが、複数、マッチしていますね?
通常、サジェストしたら単数検索に切り替わるのですが、例外として、サジェストした文字列を含む検索対象が複数あった場合は、条件に当てはまる検索対象を全て表示し、一致している部分の色を変えるという動作を取ります。

言葉だけで解説されてもわからないと思うので、下記のソースコードを確認をしてみましょう。

if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
        return
      }
if (targets.length>=2) {
}

「サジェストした結果、検索結果が複数ある場合」と条件分岐をしたいため、上記のソースコード通り、条件分岐をしました。

  bulidHTML(elment,beforeHTML)

まずは、対象のHTML要素を全て初期化します。この作業を忘れるとマッチするたびにHTML要素が増えてしまうので、必ず忘れないで記述してください。

function multipleSearches(vl,targets) {
  index = 0
  while (index<targets.length) {
      bulidHTML(elment,beforeHTML,"insertAdjacentHTML")
      typed = vl
      untyped = targets[index].replace(typed,"")
      afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
      bulidHTML(elment,afterHTML,"insertAdjacentHTML")
      index++
    }
}
 multipleSearches(searchFiled.value,targets)

複数、検索結果があるという事なので複数検索様に処理を実装しなければ挙動がおかしくなってしまいますので、「複数検索様の関数multipleSearches」を用いてます。

関数の内容については、先程の解説をしたので、省略させていただきます。

 conditionEvaluation()
 html= document.querySelectorAll("#text")
 return

一度、初期化してしまっているので再度、関数conditionEvaluationを呼び出します。その後、次の処理に影響する変数htmlの値にquerySelectorAllメソッドを用いて、対象のノードをNodeListとして取得し、格納します。最終的にreturnメソッドを用いて処理を強制終了します。

以上が、サジェス機能のロジックでした。

次のトピックで、サジェスト機能を検索機能の中に埋め込んでいきましょう。

サジェスト機能と検索機能を連結させよう

作成した、サジェスト機能を検索機能に追加していきましょう。

関数conditionEvaluationを下記の様に追加してください。

searchFiled.addEventListener("keyup",function(e){
  if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"){ return}
  if (e.keyCode==187||e.keyCode==222) {
   return
  }
   reg = new RegExp("^"+this.value)
  let targets = shapingArray(searchsTarget,reg)
  console.log(targets);
  if(targets.length==0){ return bulidHTML(elment,noSearchResult) }

  targets.forEach((target)=>{
    if(this.value===``){return bulidHTML(elment,``)}
    if(document.querySelectorAll("#text").length>0){
            document.querySelectorAll("#text").forEach((text)=>{
              document.querySelector("#text").remove()
            })
      }
         if (targets.length>=2) {
            multipleSearches(this.value,targets)
            // ここに追加
            conditionEvaluation()
            html= document.querySelectorAll("#text")
            return
          }
    i=this.value.length
    switch (i) {
      case 3:
        checkTarget(target,reg,this.value,elment)
        // ここに追加
        conditionEvaluation()
        return
      break;
      case 2:
        if (e.code=="Backspace") {
          checkTarget(target,reg,this.value,elment)
          // ここに追加
          conditionEvaluation()
          return
        }
      break;
      case 1:
        if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          // ここに追加
          conditionEvaluation()
          return
        }
          afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          // if (this.value!==untyped.substring(0,1)){return}
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          // ここに追加
          conditionEvaluation()
          return
        default:
        if (e.code=="Backspace") {
            checkTarget(target,reg,this.value,elment)
            // ここに追加
            conditionEvaluation()
            return
          }
    }
       if (html.length>=2) {
          bulidHTML(elment,beforeHTML)
          typed = this.value
          untyped = target.replace(typed,"")
          afterHTML =`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
          bulidHTML(elment,afterHTML)
          // ここに追加
          conditionEvaluation()
          return
        }else{
          document.querySelector(".searchValue").appendChild(html)
          typed = this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          // ここに追加
          conditionEvaluation()
        }
    })
})

イメージは関数conditionEvaluationを用いてサジェスト機能と検索機能を紐づけている感じです。サジェストできる状態であれば、処理が続き、そうでなければ無視されます。

以上でサジェスト機能の実装を終えます。最後に送信機能を実装していきましょう。

送信機能

最後に検索結果をモーダルウィンドウに表示させる送信機能を実装していきます。サジェスト機能に関しては検索機能内に埋め込む形で実装しましたが、送信機能は独立させて実装していきます。なお使用するイベントは、submitイベントです

  • ノードの取得
  • イベント登録
  • イベント発火後の処理を実装

ノードの取得

まずは必要なノードを取得していきましょう。

const form  = document.querySelector("#SearchEventForm")
const modalwindow   = document.querySelector(".modalwindow")

定数formは送信データを取得するために必要です。
定数modalwindowは、検索結果を表示する際に使用します。

次に正しくノードを取得できた確認をしてみましょう。

forLogs = (elements)=>{
  elements.forEach(element=>console.log(element))
}
forLogs([form,modalwindow])

無事、ノードを取得できているのが確認できたら次のステップに進みましょう。

イベント登録

前回のトピックでノードを取得できたので、こちらのトピックでは、イベントを登録していきましょう。

form.addEventListener("submit",function (e) {})

次にイベントが登録できたのか確認をしてみましょう。

form.addEventListener("submit",function (e) {
e.preventDefault()
console.log("test")
})

submitイベントでデバックする際にはpreventDefaultを用いてsubmitのデフォルトアクションを変更します。

無事testという文字列が表示されているのが確認できると思いますので、次のステップに進みましょう。

イベント発火後の処理を実装

では最後にイベント発火後の処理を実装を実装していきましょう。下記のソースコードをご覧ください。

form.addEventListener("submit",function (e) {
  e.preventDefault()
  reg = new RegExp("^"+searchFiled.value)
  targets = shapingArray(searchsTarget,reg)
  if (searchFiled.value ==``) { return}
   modalwindow.classList.add("block")
  if (targets.length>=1) {
    bulidHTML(".modalwindow",`<p id ="SearchResult">検索ヒット数${targets.length}<p>`,"insertAdjacentHTML")
    targets.forEach(target=>{
      bulidHTML(".modalwindow",`<p id ="SearchResult">${target}<p>`,"insertAdjacentHTML")
    })
  }else{
    bulidHTML(".modalwindow",noSearchResult)
  }
})
 reg = new RegExp("^"+searchFiled.value)
 targets = shapingArray(searchsTarget,reg)

まずは、submitイベントのブロックの中にも、マッチしている文字列を検索するためにRegExpのインスタンスを生成しましょう。その後、関数shapingArrayを用いて、マッチした文字列のみで抽出された定数targetsを生成しましょう。

if (searchFiled.value ==``) { return}

入力フォームに何も入力されていない場合は、returnメソッドを用いて処理を中断しています。何も入力されていない場合は、全ての要素がtargetsに含まれている状態になり、結果的に配列の全ての要素がモーダルウィンドウに表示されてしまいます。個人的には「入力フォームに何も入力されていない場合は、何も起こらない」という挙動の方が好みなので上記の様に条件分岐をした次第です。

modalwindow.classList.add("block")

定数modalwindowにblockクラスを追加しています。

 if (targets.length>=1) {
  }else{
  }

こちらのソースコードは、targetsの長さが1以上の場合、要するに検索結果があった場合にif文内の処理が読み込まれます。

bulidHTML(".modalwindow",`<p id ="SearchResult">検索ヒット数${targets.length}<p>`,"insertAdjacentHTML")
targets.forEach(target=>{
bulidHTML(".modalwindow",`<p id ="SearchResult">${target}<p>`,"insertAdjacentHTML")
})
  • ヒットした検索数
  • 検索結果

をmodalwindowにHTML要素として挿入をしています。細かい処理については、省かせてもらいます。

}else{
    bulidHTML(".modalwindow",noSearchResult)
}

それ以外、マッチする文字列がない場合は、「検索結果はありません」と表示して終わりです。

以上で、「検索機能」の全ての実装が終了しました。完成したソースコードは、下記に貼り付けておきます。

const form          = document.querySelector("#SearchEventForm")
const searchsTarget = ["HTML","CSS","JavaScript","Ruby","Ruby on Rails","PHP","GO","React","Apple","Google","Vue","game","Hello","App","鬼滅の刃","ポケモン","サッカー","野球","Java","Ruby Gem","ruby Class","PHP if","Apple store","Japan","English","Math","AWS"]
const searchFiled   = document.querySelector("input[type='text']")
const main          = document.querySelector("main")
const modalwindow   = document.querySelector(".modalwindow")

const  elment = ".searchValue"
const  beforeHTML =  `<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`
const  noSearchResult =`<p id = "text">検索結果はありません<p>`

let untyped = ``
let typed   = ``
let back    = ``
let html ;
let afterHTML;
let reg



function multipleSearches(vl,targets) {
  index = 0
  while (index<targets.length) {
      bulidHTML(elment,beforeHTML,"insertAdjacentHTML")
      typed = vl
      untyped = targets[index].replace(typed,"")
      afterHTML  =  `<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
      bulidHTML(elment,afterHTML,"insertAdjacentHTML")
      index++
    }
}

function bulidHTML(elment,html,type="innerHTML") {
  switch (type) {
    case "innerHTML":
      document.querySelector(elment).innerHTML = html
      break;
    case "insertAdjacentHTML":
      document.querySelector(elment).insertAdjacentHTML(`beforeend`,html)
      break;
  }
}

function insertTypingResult() {
  bulidHTML(elment,beforeHTML)
  bulidHTML("#typed",typed)
  bulidHTML("#untyped",untyped)
}

function shapingArray( searchsTarget,reg){
return searchsTarget.map((target)=>{
    if(target.match(reg)){
      return  target
    }
  }).filter(target=>target)}

function checkTarget(target,reg,vl,elment){
  if(target.match(reg)){
    bulidHTML(elment,`<p id ="text"><span id="typed"></span><span id="untyped"></span></p>`)
    typed = vl
    untyped = target.replace(typed,"")
    bulidHTML(elment,`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`)
  }
}

function conditionEvaluation(){
  if(document.querySelector("#text")){suggest(document.querySelectorAll("#text"))}
}

function suggest(elments){
  elments.forEach(text=>{ text.addEventListener("mouseover",suggestCallBack) })
}

function suggestCallBack(e){
  this.classList.add("transparent")
  this.addEventListener("click",function(){
    searchFiled.value = this.textContent
    typed = searchFiled.value
    untyped = ``
    insertTypingResult()
    if (html.length>=2) {
     reg =new RegExp("^"+searchFiled.value)
     targets=shapingArray(searchsTarget,reg)
      if (targets.length>=2) {
        bulidHTML(elment,beforeHTML)
        multipleSearches(searchFiled.value,targets)
        conditionEvaluation()
        html= document.querySelectorAll("#text")
      return
      }
    }
  })
  this.addEventListener("mouseout",function(){
    this.classList.remove("transparent")
  })
}


searchFiled.addEventListener("keyup",function(e){
  if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"){ return}
  if (e.keyCode==187||e.keyCode==222) {
   return
  }
   reg = new RegExp("^"+this.value)
  let targets = shapingArray(searchsTarget,reg)
  if(targets.length==0){ return bulidHTML(elment,noSearchResult) }

  targets.forEach((target)=>{
    if(this.value===``){return bulidHTML(elment,``)}
    if(document.querySelectorAll("#text").length>0){
            document.querySelectorAll("#text").forEach((text)=>{
              document.querySelector("#text").remove()
            })
      }
         if (targets.length>=2) {
            multipleSearches(this.value,targets)
            conditionEvaluation()
            html= document.querySelectorAll("#text")
            return
          }
    i=this.value.length
    switch (i) {
      case 3:
        checkTarget(target,reg,this.value,elment)
        conditionEvaluation()
        return
      break;
      case 2:
        if (e.code=="Backspace") {
          checkTarget(target,reg,this.value,elment)
          conditionEvaluation()
          return
        }
      break;
      case 1:
        if (e.code=="Backspace") {
          if(e.key =="Shift"||e.key=="ArrowLeft"||e.key=="ArrowRight"||e.key=="+"||e.key=="*"){ return}
          checkTarget(target,reg,this.value,elment)
          conditionEvaluation()
          return
        }
          afterHTML =`<p id ="text"><span id="typed"></span><span id="untyped">${target}</span></p>`
          bulidHTML(elment,afterHTML)
          untyped= document.querySelector("#untyped").innerHTML
          // if (this.value!==untyped.substring(0,1)){return}
          typed= this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          html =  document.querySelector("#text")
          conditionEvaluation()
          return
        default:
        if (e.code=="Backspace") {
            checkTarget(target,reg,this.value,elment)
            conditionEvaluation()
            return
          }
    }
       if (html.length>=2) {
          bulidHTML(elment,beforeHTML)
          typed = this.value
          untyped = target.replace(typed,"")
          afterHTML =`<p id ="text"><span id="typed">${typed}</span><span id="untyped">${untyped}</span></p>`
          bulidHTML(elment,afterHTML)
          conditionEvaluation()
          return
        }else{
          document.querySelector(".searchValue").appendChild(html)
          typed = this.value
          untyped = untyped.substring(1)
          insertTypingResult()
          conditionEvaluation()
        }
    })
})


form.addEventListener("submit",function (e) {
  e.preventDefault()
  reg = new RegExp("^"+searchFiled.value)
  targets = shapingArray(searchsTarget,reg)
  if (searchFiled.value ==``) { return}
   modalwindow.classList.add("block")
  if (targets.length>=1) {
    bulidHTML(".modalwindow",`<p id ="SearchResult">検索ヒット数${targets.length}<p>`,"insertAdjacentHTML")
    targets.forEach(target=>{
      bulidHTML(".modalwindow",`<p id ="SearchResult">${target}<p>`,"insertAdjacentHTML")
    })
  }else{
    bulidHTML(".modalwindow",noSearchResult)
  }
})

まとめ:検索機能

今回は、検索機能を作成していきました。かなり濃い内容だと思います。具体的には、3記事分くらいですね。最後まで、実装する事ができた方は、かなり素のJavaScriptでコーディングできるレベルになっていると思います。

なので素のJavaScriptで制作物を作って、世界に公開する方向で学習を進めていくのもありだと思います。

  • ブロック崩しゲーム
  • タイピングゲーム
  • 簡易的なチャットボット

など

以上、kazutoでした。

関連記事