요구사항

키보드와 마우스를 이용해서 추천 검색어를 선택할 수 있도록 합니다.

  1. esc를 누르면 추천 검색어 창이 닫여야 합니다.
  2. 키보드의 위, 아래 키를 누르면 추천 검색어 하이라이트가 옮겨지고 엔터를 누르면 하이라이트가 위치한 검색어가 입력창에 반영되어야 합니다.
  3. 마우스로 다른 곳을 클릭하여 input이 focus를 잃어버리는 경우 추천 검색어 창이 닫여야 합니다.
  4. 마우스로 추천 검색어를 누르면 커서가 위치한 검색어가 입력창에 반영되어야 합니다.

 

기본 HTML은 다음과 같습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>검색하기</title>
    <style>
        header {
            display: flex;
            width: 600px;
            flex-direction: column;
            justify-content: center;
        }

        .keywords {
            z-index: 1;
            background-color: white;
            display: none;
            position: absolute;
            top: 110px;
            width: 600px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        .keywords li:hover,
        .keywords .selected {
            cursor: pointer;
            background-color: lightskyblue;
            color: white;
        }

        .keywords .active {
            background-color: lightskyblue;
            color: white;
        }

        .keywords ul {
            padding: 0;
            margin: 0;
        }

        .keywords li {
            list-style: none;
            padding-left: 10px;
        }
        
        table {
            margin-top: 10px;
        }
    </style>
</head>

<body>
    <header>
        <h1>검색하기</h1>
        <input class="keyword" autocomplete="off" />
    </header>
    <div class="keywords">

    </div>
    <div class="search-results">
        <table border=1>
            <tr>
                <th width="120px" height="50px">가</th>
                <th width="120px" height="50px">나</th>
                <th width="120px" height="50px">다</th>
                <th width="120px" height="50px">라</th>
                <th width="120px" height="50px">마</th>
            </tr>
        </table>
    </div>
    
    <script>
        // .......... //  
    </script>
</body></html>

 

script의 전체 내용은 다음과 같습니다.
const getData = async () => {
    const response = await fetch("https://reqres.in/api/users?page=1")
    return response.json()
}

const keyword = document.querySelector(".keyword")
const keywords = document.querySelector(".keywords")

function closeKeywords() {
    keywords.style.display = "none"
    keywords.innerHTML = ""
}

keyword.addEventListener("keyup", async (e) => { 
    
    const selectedKeyword = keywords.querySelector("li.selected")
    
    // li.selected 가 없는 경우에만 api에서 데이터를 가져옴
    if(keyword.value.length > 2 && !selectedKeyword) {
        console.log("=== API 호출 ===")
        const list = (await getData()).data
        
        keywords.innerHTML = ""
        
        const $ul = document.createElement("ul")
        for(let person of list) {
            const $li = document.createElement("li")
            $li.textContent = `${person.first_name} ${person.last_name}`
            $ul.append($li)
        }
        keywords.append($ul)
        
        keywords.style.display = "block" 
    } 
    if(keyword.value.length === 0) {
        keywords.innerHTML = ""
    }
    
    // 요구사항 1 - esc를 누르면 추천 검색어 창이 닫여야 합니다.
    if(e.key === "Escape") {
        closeKeywords()
    }
    
    // console.log(e.key)
    
    // 요구사항 2 - 키보드의 위, 아래 키를 누르면 추천 검색어 하이라이트가 옮겨지고 엔터를 누르면 하이라이트가 위치한 검색어가 입력창에 반영되어야 합니다.
    
    const keywordsList = keywords.querySelectorAll("li")
    
    if((e.key === "ArrowUp" || e.key === "ArrowDown") && keywords.style.display === "block") {
        let target
        const initIndex = e.key === "ArrowUp" ? keywordsList.length - 1 : 0
        const adjacentSibling = selectedKeyword && (e.key === "ArrowUp" ? selectedKeyword.previousElementSibling : selectedKeyword.nextElementSibling)
        
        if(adjacentSibling) {
            target = adjacentSibling
        } else {
            target = keywordsList.item(initIndex)
        } 
        
        selectedKeyword && selectedKeyword.classList.remove("selected")
        target.classList.add("selected")
        keyword.value = target.textContent
    }
})

document.addEventListener("click", e => {
    // 요구사항 3 - 마우스로 다른 곳을 클릭하여 input이 focus를 잃어버리는 경우 추천 검색어 창이 닫여야 합니다.
    const closestKeywords = e.target.closest(".keywords") // 부모 요소 중에 keywords 클래스를 가진 부모가 있는지 확인
    if(!closestKeywords && keywords.style.display === "block") {
        closeKeywords()
    }
})

keywords.addEventListener("click", e => {
    // 요구사항 4 - 마우스로 추천 검색어를 누르면 커서가 위치한 검색어가 입력창에 반영되어야 합니다.
    keyword.value = e.target.textContent
})

 

요구사항 1 – esc를 누르면 추천 검색어 창이 닫여야 합니다.

추천어 검색창이 닫힌다는 것은

  • 검색창이 보여지지 않음
  • 검색창의 내용이 초기화됨

을 의미합니다. 닫힘 기능이 있는 함수를 먼저 만듭니다.

function closeKeywords() {
    keywords.style.display = "none"
    keywords.innerHTML = ""
}

그 다음 검색어 입력창에 keyup 이벤트를 부여하고, 해당 키가 ESC인 경우 추천어 검색창을 닫도록 합니다.

// 요구사항 1 - esc를 누르면 추천 검색어 창이 닫여야 합니다.
if(e.key === "Escape") {
    closeKeywords()
}

 

요구사항 2 – 키보드의 위, 아래 키를 누르면 추천 검색어 하이라이트가 옮겨지고 엔터를 누르면 하이라이트가 위치한 검색어가 입력창에 반영되어야 합니다.

키보드 위 키가 눌린 경우

  • 현재 선택된 추천 검색어가 없는 경우 또는 선택된 추천 검색어가 첫번째(맨 위)에 위치한 경우 목록의 맨 마지막이 선택되도록 합니다.
  • 그 외의 경우 현재 선택된 검색어의 바로 위(자매 요소)가 선택되도록 합니다.

키보드 아래 키가 눌린 경우

  • 현재 선택된 추천 검색어가 없는 경우 또는 선택된 추천 검색어가 마지막에 위치한 경우 목록의 첫번째가 선택되도록 합니다.
  • 그 외의 경우 현재 선택된 검색어의 바로 아래(자매 요소)가 선택되도록 합니다.

검색어 목록(<li>)에 selected 클래스 유무 여부에 따라 선택 여부가 결정됩니다.

// 요구사항 2 - 키보드의 위, 아래 키를 누르면 추천 검색어 하이라이트가 옮겨지고 엔터를 누르면 하이라이트가 위치한 검색어가 입력창에 반영되어야 합니다.

const keywordsList = keywords.querySelectorAll("li")

if((e.key === "ArrowUp" || e.key === "ArrowDown") && keywords.style.display === "block") {
    let target
    const initIndex = e.key === "ArrowUp" ? keywordsList.length - 1 : 0
    const adjacentSibling = selectedKeyword && (e.key === "ArrowUp" ? selectedKeyword.previousElementSibling : selectedKeyword.nextElementSibling)
    
    if(adjacentSibling) {
        target = adjacentSibling
    } else {
        target = keywordsList.item(initIndex)
    } 
    
    selectedKeyword && selectedKeyword.classList.remove("selected")
    target.classList.add("selected")
    keyword.value = target.textContent
}

 

요구사항 3 – 마우스로 다른 곳을 클릭하여 input이 focus를 잃어버리는 경우 추천 검색어 창이 닫여야 합니다.

document에 마우스 이벤트를 부여해서 추천 검색어 창이 켜진 상태이고, 클릭된 부분이 추천 검색어 창이 아닌 경우 추천 검색어 창을 닫히게 합니다.

document.addEventListener("click", e => {
    // 요구사항 3 - 마우스로 다른 곳을 클릭하여 input이 focus를 잃어버리는 경우 추천 검색어 창이 닫여야 합니다.
    const closestKeywords = e.target.closest(".keywords") // 부모 요소 중에 keywords 클래스를 가진 부모가 있는지 확인
    if(!closestKeywords && keywords.style.display === "block") {
        closeKeywords()
    }
})

 

요구사항 4 – 마우스로 추천 검색어를 누르면 커서가 위치한 검색어가 입력창에 반영되어야 합니다.

키워드 목록 창에 마우스 클릭 이벤트를 부여하고, 목록을 클릭하면 클릭된 목록의 textContent를 입력창에 반영하도록 합니다.

keywords.addEventListener("click", e => {
    // 요구사항 4 - 마우스로 추천 검색어를 누르면 커서가 위치한 검색어가 입력창에 반영되어야 합니다.
    keyword.value = e.target.textContent
})

 


문의 | 코멘트 또는 yoonbumtae@gmail.com  donaricano-btn

카테고리: WEB: Frontend

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다