티스토리 뷰

   라이브러리 사용해도 충분히 만들 수 있고 더 간편하지만..  기획과 설계가 쪽대본처럼 진행되는 내 경력사상 초유의 프로젝트에서.. 개발이 끝난다고 해도 어떤 요상한(-_-) 추가 요구사항이 들어올지 모르는 뭐같은 환경에서는 쌩으로 만드는 것이 추후 정신 건강에 좋을 수도 있다. 라이브러리는 갖다 쓰기는 편해도 입맛에 맞게 고치기 힘드니까 말이다.

   혹시라도 나처럼 쌩으로 만들어야 하는 사람들을 위해 위의 이미지 같이 구현되는 소스를 공유한다.

 

▽ page.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<script type="text/javascript" src="select.js"></script>
<script type="text/javascript" src="page.js"></script>
<style type="text/css">
    .multiSelect {
      width: 200px;
    }

    .innerSelectBox {
      position: relative;
    }

    .innerSelectBox select {
      width: 100%;
      font-weight: bold;
    }

    .overSelect {
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
    }

    .selectArea {
      display: none;
      border: 1px #dadada solid;
    }

    .selectArea label {
      display: block;
    }

    /* .selectArea label:hover {
      background-color: #FF84C3;
    } */

    .selectAreaHover {
      background-color: #FF84C3;
    }

    .txt-red {
      color: red;
    }
</style>

<h1>멀티플 셀렉트 체크박스</h1>

<!-- <div id="printArea"></div> -->

<div class="multiSelect">
    <div id="sbArea" class="innerSelectBox">
        <select>
            <option>선택하시오.</option>
        </select>
        <div class="overSelect" data-id="workingPlace"></div>
    </div>
    <div id="ckArea" class="selectArea" data-id="workingPlace">
      <!-- 데이터 꽂히는 곳 -->
      
    </div>
</div>
<br/>
<button id="saveSelect">저장</button>
<br/>
<br/>
<br/>
<br/>

<h2>자동검색완성 해보자.</h2>

<div id="autoSearchSelect" style="position: relative;">
    <input type="text" id="searchKeyword" class="txt-red" />
    <!-- <div id="autoSearchArea" class="selectArea" style="width: 200px; position: absolute; top: 100%; left: 0; right: 0; z-index:100; background: #FFFFFF; overflow: hidden; overflow-y: auto; height: 100px;"> -->
    <div id="autoSearchArea" class="selectArea" style="width: 200px; position: absolute; top: 100%; left: 0; right: 0; z-index:100; background: #FFFFFF; overflow: hidden;">
        <!-- 데이터 꽂히는 곳 -->
        <!-- <label>검색 단어가 이렇게 꽂힐 거임</label>
        <label>검색 단어2</label>
        <label>검색 단어3.</label> -->
    </div>
</div>

 

▽ page.js

document.addEventListener("DOMContentLoaded", function(event) {
	// 멀티플 셀렉트박스 영역을 클릭했을 때 동작
    Setting.showHideCkArea("sbArea", "ckArea");

	// 멀티플 셀렉트박스 데이터 뿌려주기
	PrintFunc.printMultiSelectList();

	// 저장버튼 클릭
	EventFunc.clickSaveSelect();

	// 자동 검색 완성
	SelectEventFunc.autoSearch("searchKeyword", "autoSearchArea", DataFunc.getDataList);
	SelectUserSetting.isRedText = true; // 검색어 빨간색 옵션을 true로 사용하려면, 검색 input에 기본 class="txt-red"를 추가해줘야 한다.
	SelectEventFunc.eraseIncompletion("searchKeyword", "autoSearchArea");
});

const DataFunc = {
	getDataList: function(searchInput){
		let searchingValue = searchInput.value;
		if(searchingValue.trim() != ""){ // 검색 값이 있을 때에만 조회
			let params = {
				"searchingKeyword": searchingValue
			}
			// 비동기로 DB 데이터 조회. ajax를 쓰든 fetch를 사용하든 알아서..
			apiFetchPost("/some/getDataList", params, function(resData) {
				// 받아온 데이터로 쿵짝쿵짝..
				// 검색결과 라벨 뿌림
				PrintFunc.printSearchLavel("autoSearchArea", resData.data, searchInput);

				// 라벨에 마우스 오버/아웃했을 때의 이벤트 걸어줌
				SelectEventFunc.hoverSelectLabelMouse("autoSearchArea");
			});
		}else{
			// 검색 칸이 비어있으면 라벨 div영역 숨김처리
			document.getElementById("autoSearchArea").style.display = "none";
		}
	}
}

const PrintFunc = {
	printMultiSelectList: function(){
		// 샘플 데이터
		let resultStr = '{"someData":[{"keyName1":"santa","keyName2":"새나클로zㅣ즈 커밍- 투타운-"},{"keyName1":"merryChristmas", "keyName2":"메리 크리스마스"}]}';
		const resultData = JSON.parse(resultStr);

		let htmlPrint = "";
		if(resultData.someData.length > 0){
			resultData.someData.forEach(dataOne => {
				htmlPrint += "<label for='" + dataOne.keyName1 + "'>"
									+ "<input type='checkbox' name='someSelect' id='" + dataOne.keyName1 + "' value='" + dataOne.keyName1 +  "' />"
											+ dataOne.keyName2
								+ "</label>";
			});
		}
		
		document.getElementById("ckArea").insertAdjacentHTML('beforeend', htmlPrint);
	}
	, printSearchLavel: function(searchAreaId, resData, searchInput){
		let autoSearchArea = document.getElementById(searchAreaId);
		autoSearchArea.innerHTML = "";
		
		let htmlPrint = "";
		if(resData.length > 0){
			resData.forEach(dataOne => {
				/**
				 * 1. data-value에 값을 넣어줘야 함
				 * 2. class명 명시하고 아래에 SelectEventFunc.clickAutoSearchingLavel 메서드에 클래스명 넘겨야 함
				 */
				htmlPrint += "<label class='autoSearchingLavel' data-value='" + dataOne + "'>" 
									+ SelectDataHandling.highlightMathedStr(dataOne, searchInput.value) // 검색 단어 중 결과와 일치하는 부분을 색깔로 칠해준다
								+ "</label>";
			});
			
			autoSearchArea.innerHTML = htmlPrint;
			autoSearchArea.style.display = "block"; // 라벨을 감싸고 있는 div영역 보이도록.
			
			// 검색 라벨 클릭 이벤트 걸어줌
			SelectEventFunc.clickAutoSearchingLavel(searchInput.id, searchAreaId, "autoSearchingLavel", SelectCallback.fillSelectedValue);
		}
	}
}

const EventFunc = {
	clickSaveSelect: function(){
		let saveSelect = document.getElementById("saveSelect");
		saveSelect.addEventListener("click", function(e){
			let selectedDataArr = SelectDataFunc.getSelectedDataArr("someSelect");
			alert(selectedDataArr);
		});
	}
	
}

자동검색완성의 DB조회하는 부분에서는 like 검색으로 최대 몇 개까지 잘라서 조회해왔다.

추후 스크롤바를 넣고 가장 밑의 검색 단어를 선택할 때마다 그 다음의 몇 개를 조회한다든지 하는 것을 구현해볼 수도 있겠다.

 

 

▽ select.js

let Setting = {
    expandedFlags: {} // 멀리플 셀렉트박스 펼침 여부 플래그
    , holdOnTimeout: {} // 자동검색완성 setTimeout id
    , completeFlags: {} // 자동검색완성 값선택 완성 여부 플래그
    , highlightTxt: "txt-red"
    , showHideCkArea: function(innerSelectBoxId, selectAreaId, dataId) {
        let innerSelectBox = document.getElementById(innerSelectBoxId);
        let selectedArea = document.getElementById(selectAreaId);

        innerSelectBox.addEventListener("click", function(e){
            if (!Setting.expandedFlags[selectAreaId]) {
                selectedArea.style.display = "block";
                Setting.expandedFlags[selectAreaId] = true;
            } else {
                selectedArea.style.display = "none";
                Setting.expandedFlags[selectAreaId] = false;
            }
        });
        
        document.body.addEventListener("click", event => {
            if(Setting.expandedFlags[selectAreaId]
                && event.target.dataset.id != dataId
            ){
                console.log("숨기시오.");
                selectedArea.style.display = "none";
                Setting.expandedFlags[selectAreaId] = false;
            }
        });

    }
}

let SelectUserSetting = {
    isRedText: false
}

const SelectDataFunc = {
    /**
     * 체크된 셀렉트박스 값을 배열로 반환
     * @param {string} inputName input tag name
     * @returns array
     */
    getSelectedDataArr: function (inputName) {
        let ckValArray = new Array();

        let ckEle = document.getElementsByName(inputName);
        for (let i = 0; i < ckEle.length; i++) {
            if (ckEle[i].checked) {
                ckValArray.push(ckEle[i].value);
            }
        }
        return ckValArray;   
    }
}

const SelectDataHandling = {
    /**
     * 검색 단어 매칭해서 일치하는 부분만 색깔 칠해서 반환
     * @param {stirng} str 검색된 전체 단어
     * @param {string} searchingValue 검색 단어
     * @returns 
     */
    highlightMathedStr: function(str, searchingValue){
        const regExp = new RegExp(searchingValue, "gi");
        let matchedStr = str.match(regExp);
        
        let highlightedStr = "<span style='color: #6BD089'>" + matchedStr + "</span>"
        return str.replace(regExp, highlightedStr);
    }
}

const SelectEventFunc = {
    /**
     * 자동 검색 완성
     * @param {string} searchInputId 검색 input id
     * @param {string} selectDivId 검색결과 div id
     * @param {method} getDataListMethod 검색어 입력 시 실행할 메서드
     */
    autoSearch: function(searchInputId, selectDivId, getDataListMethod){
        let searchInput = document.getElementById(searchInputId);
        
        searchInput.addEventListener("keydown", event => {
            if(event.code === "ArrowUp" || event.code === "ArrowDown" || event.code === "Enter"){
                // 화살표 위아래 혹은 엔터 눌렀을 때 실행
                let selectDiv = document.getElementById(selectDivId);
                let selectDivDisplay = selectDiv.style.display;
                let selectLabel = selectDiv.children;
                /**
                 * 한글/한자/히라가나,가타가나의 경우 갓 입력했을 때 keyCode가 229이거나 isComposing이 true를 반환
                 * 이 조건을 추가하지 않으면 가장 처음의 이벤트는 두 번 발생하는 참사가..
                 * 가능하면 change 이벤트를 사용하는 게 좋음.
                */ 
                if(selectDivDisplay === "block" && event.keyCode != 229 && !event.isComposing){
                    if(event.code == "ArrowUp"){
                        // 화살표 위↑ 누름
                        Setting.holdOnTimeout[searchInputId] = setTimeout(SelectEventFunc.arrowUp(selectLabel), 500);
                    }else if(event.code == "ArrowDown"){
                        // 화살표 아래↓ 누름
                        Setting.holdOnTimeout[searchInputId] = setTimeout(SelectEventFunc.arrowDown(selectLabel), 500);
                    }else if(event.code == "Enter"){
                        // 엔터┘ 누름
                        // 엔터 누르면 클릭해줌. 이미 클릭 이벤트가 걸려있기 때문에 이벤트에 걸려있는 메서드가 실행될 것임.
                        let selectedHover = document.getElementsByClassName("selectAreaHover")[0];
                        if(selectedHover) selectedHover.click();
                    }
                }
            }
        });

        // 검색 Input에 키를 누르고 뗐을 때의 이벤트
		searchInput.addEventListener("keyup", event => {
            if(event.code != "ArrowUp" && event.code != "ArrowDown" && event.code != "Enter"){
                getDataListMethod(searchInput); // 글자 입력했을 때 실행
                if(event.code != "ArrowLeft" && event.code != "ArrowRight"){
                    if(SelectUserSetting.isRedText){
                        searchInput.classList.add(Setting.highlightTxt); // 빨간색 글씨 On
                    }
                    Setting.completeFlags[searchInputId] = false; // 자동검색 미완성
                }
            }else{
                // 화살표 위아래 혹은 엔터 눌렀다 뗐을 때 실행
                SelectEventFunc.stopArrowKey(searchInputId);
            }
		});
    },
    stopArrowKey: function(searchInputId){
        clearTimeout(Setting.holdOnTimeout[searchInputId]);
        Setting.holdOnTimeout[searchInputId] = -1;
    },
    arrowUp: function(selectLabel){
        loop1: for (let i = 0; i < selectLabel.length; i++) {
            let labelClasses = selectLabel[i].classList;
            for (let j = 0; j < labelClasses.length; j++) {
                if (labelClasses[j] == "selectAreaHover") {
                    if (selectLabel[i - 1]) {
                        selectLabel[i].classList.remove("selectAreaHover");
                        selectLabel[i - 1].classList.add("selectAreaHover");
                    } else if (i == 0) {
                        // 첫번째 라벨일 때 제일 아래로 이동
                        selectLabel[i].classList.remove("selectAreaHover");
                        selectLabel[selectLabel.length - 1].classList.add("selectAreaHover");
                    }
                    break loop1;
                }
            }
        }
    },
    arrowDown: function(selectLabel){
        let isHovered = false;
        loop1:
        for(let i=0; i<selectLabel.length; i++){
            let labelClasses = selectLabel[i].classList;
            for(let j=0; j<labelClasses.length; j++){
                if(labelClasses[j] == "selectAreaHover"){
                    isHovered = true;
                    if(selectLabel[i+1]) {
                        selectLabel[i].classList.remove("selectAreaHover");
                        selectLabel[i+1].classList.add("selectAreaHover");
                    }else if(i+1 == selectLabel.length){
                        // 마지막 라벨일 때 제일 위로 이동
                        selectLabel[i].classList.remove("selectAreaHover");
                        selectLabel[0].classList.add("selectAreaHover");            
                    }
                    break loop1;
                }
            }
        }
        // 선택된 것이 아무것도 없다면 첫번째 선택
        if(isHovered == false){
            selectLabel[0].classList.add("selectAreaHover");
        }
    },
    hoverSelectLabelMouse: function(selectDivId){
        // 검색 셀렉트 div label에 마우스 오버/아웃했을 때의 이벤트
        let selectLabel = document.getElementById(selectDivId).children;
        for(let i=0; i<selectLabel.length; i++){
            selectLabel[i].addEventListener("mouseover", event => {
                selectLabel[i].classList.add("selectAreaHover");
            });
            selectLabel[i].addEventListener("mouseout", event => {
                selectLabel[i].classList.remove("selectAreaHover");
            });
        }
    },
    /**
     * 검색 결과 라벨 클릭 이벤트
     * @param {string} searchInputId 검색 input id
     * @param {string} searchAreaId 검색결과 div id
     * @param {string} lavelClassNm 라벨의 클래스명
     * @param {method} callbackMethod 검색 결과 라벨 클릭 시 실행될 메서드
     */
     clickAutoSearchingLavel: function(searchInputId, searchAreaId, lavelClassNm, callbackMethod){
		let searchingLaveles = document.getElementsByClassName(lavelClassNm);
		for (let i = 0; i < searchingLaveles.length; i++) {
			searchingLaveles[i].addEventListener("click", event => {
				let fillValue = searchingLaveles[i].dataset.value;
				callbackMethod(fillValue, searchInputId, searchAreaId);
			});
		}
	},
    /**
     * 검색 input에서 포커스 아웃 시
     * 자동검색완성이 미완일 경우, 즉 검색된 라벨 목록에서 클릭이나 엔터로 선택된 값이 아닐 경우,
     * 검색 input에 있는 텍스트는 지워준다.
     * @param {String} searchInputId 검색 input id
     * @param {String} searchAreaId 검색결과 div id
     */
    eraseIncompletion: function(searchInputId, searchAreaId){
        document.getElementById(searchInputId).addEventListener("focusout", event => {
            if(!Setting.completeFlags[searchInputId]){
                document.getElementById(searchInputId).value = "";
                document.getElementById(searchAreaId).style.display = "none";
            }
        });
    }

}

const SelectCallback = {
    /**
     * 선택한 값을 어떻게 할 것인지 여기서 처리
     * @param {String} fillValue 채워야 할 값. 즉 검색된 라벨 목록에서 클릭이나 엔터로 선택된 값
     * @param {String} objId 검색 input id
     * @param {String} searchAreaId 검색결과 div id
     */
    fillSelectedValue: function(fillValue, objId, searchAreaId){
        document.getElementById(objId).classList.remove(Setting.highlightTxt); // 빨간색 글씨 Off
        Setting.completeFlags[objId] = true; // 자동검색 완성

        document.getElementById(objId).value = fillValue;
        document.getElementById(searchAreaId).style.display = "none";
    }
}

 

 

   버그,오류는 없지만 완벽한 코드는 아니다. 기초 껍데기라고 보면 된다.

   이런 식으로 돌아가는구나 하고 참고하시길.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함