⭐️ 별점 기능 구현 ⭐️

jellykelly·2023년 8월 5일
0

UI Interaction

목록 보기
3/3
post-thumbnail
post-custom-banner

자바스크립트로 별점 기능을 구현해보았습니다..⭐️

🧙‍♂️ What to do

  1. 별점은 반 개 단위로 입력 가능할 것
  2. 마우스 hover시 active 될 것
  3. 별점을 선택하지 않고 영역을 벗어날 경우 이전 기선택된 별점을 유지할 것

⭐️ 디자인

별점은 5점을 만점 이지만, 0.5점 단위로 만들기 위해 input을 10개를 넣어줍니다.
못생긴 라디오버튼 등장 ㅠ

html

라디오 버튼 커스텀을 위해 label 태그를 사용합니다.
디자인 커스텀만 할때는 주로 가상선택자 ::before, ::after를 사용해서 background image만 삽입하는데, 오늘은 DOM제어를 해야하므로 아이콘이 들어갈 태그를 추가하고 label태그로 감싸겠습니다.

<div class="rating">
    <label class="rating__label rating__label--half" for="starhalf">
        <input type="radio" id="starhalf" class="rating__input" name="rating" value="">
        <span class="star-icon"></span>
    </label>
    <label class="rating__label rating__label--full" for="star1">
        <input type="radio" id="star1" class="rating__input" name="rating" value="">
        <span class="star-icon"></span>
    </label>
  	...
</div>

css

기본 라디오버튼을 별표 이미지로 변경할게요.
labelfor속성과 inputid가 같으면 label을 클릭했을 때에도 input이 선택되므로, 라디오버튼을 그냥 display: none 처리 해버리겠습니다.

아이콘의 이미지는 24*24로 준비했습니다.

.rating__input {
	display: none; /* 라디오버튼 hide */
}

.rating__label .star-icon {
	width: 24px;
	height: 24px;
	display: block;
	background-image: url("../images/ico-star-empty.svg");
	background-repeat: no-repeat;
}

별점 반 개를 구현해야 하므로 width를 반으로 쪼개버리겠습니다!!!

.rating__label {
	width: 12px; /* 원본 사이즈/2 */
	overflow: hidden;
	cursor: pointer;
}
.rating__label .star-icon {
	width: 12px; /* 원본 사이즈/2 */
	height: 24px;
	display: block;
	position: relative;
	left: 0;
	background-image: url("../images/ico-star-empty.svg");
	background-repeat: no-repeat;
}

background position을 조정하여 별 두개를 하나인것처럼 보이게 해줍니다

.rating__label--full .star-icon {
	background-position: right;
}
.rating__label--half .star-icon {
	background-position: left;
}

🧞‍♂️ JavaScript

const rateWrap = document.querySelectorAll('.rating'),
    label = document.querySelectorAll('.rating .rating__label'),
    input = document.querySelectorAll('.rating .rating__input'),
    labelLength = label.length,
    opacityHover = '0.5';

let stars = document.querySelectorAll('.rating .star-icon');

checkedRate();


rateWrap.forEach(wrap => {
    wrap.addEventListener('mouseenter', () => {
        stars = wrap.querySelectorAll('.star-icon');

        stars.forEach((starIcon, idx) => {
            starIcon.addEventListener('mouseenter', () => {
                if (wrap.classList.contains('readonly') == false) {
                    initStars(); // 기선택된 별점 무시하고 초기화
                    filledRate(idx, labelLength);  // hover target만큼 별점 active

                    // hover 시 active된 별점의 opacity 조정
                    for (let i = 0; i < stars.length; i++) {
                        if (stars[i].classList.contains('filled')) {
                            stars[i].style.opacity = opacityHover;
                        }
                    }
                }
            });

            starIcon.addEventListener('mouseleave', () => {
                if (wrap.classList.contains('readonly') == false) {
                    starIcon.style.opacity = '1';
                    checkedRate(); // 체크된 라디오 버튼 만큼 별점 active
                }
            });

            // rate wrap을 벗어날 때 active된 별점의 opacity = 1
            wrap.addEventListener('mouseleave', () => {
                if (wrap.classList.contains('readonly') == false) {
                    starIcon.style.opacity = '1';
                }
            });

            // readonnly 일 때 비활성화
            wrap.addEventListener('click', (e) => {
                if (wrap.classList.contains('readonly')) {
                    e.preventDefault();
                }
            });
        });
    });
});

// target보다 인덱스가 낮은 .star-icon에 .filled 추가 (별점 구현)
function filledRate(index, length) {
    if (index <= length) {
        for (let i = 0; i <= index; i++) {
            stars[i].classList.add('filled');
        }
    }
}

// 선택된 라디오버튼 이하 인덱스는 별점 active
function checkedRate() {
    let checkedRadio = document.querySelectorAll('.rating input[type="radio"]:checked');


    initStars();
    checkedRadio.forEach(radio => {
        let previousSiblings = prevAll(radio);

        for (let i = 0; i < previousSiblings.length; i++) {
            previousSiblings[i].querySelector('.star-icon').classList.add('filled');
        }

        radio.nextElementSibling.classList.add('filled');

        function prevAll() {
            let radioSiblings = [],
                prevSibling = radio.parentElement.previousElementSibling;

            while (prevSibling) {
                radioSiblings.push(prevSibling);
                prevSibling = prevSibling.previousElementSibling;
            }
            return radioSiblings;
        }
    });
}

// 별점 초기화 (0)
function initStars() {
    for (let i = 0; i < stars.length; i++) {
        stars[i].classList.remove('filled');
    }
}

🗝 결과

profile
Hawaiian pizza with extra pineapples please! 🥤
post-custom-banner

0개의 댓글