자바스크립트로 별점 기능을 구현해보았습니다..⭐️
- 별점은 반 개 단위로 입력 가능할 것
- 마우스 hover시 active 될 것
- 별점을 선택하지 않고 영역을 벗어날 경우 이전 기선택된 별점을 유지할 것
별점은 5점을 만점 이지만, 0.5점 단위로 만들기 위해 input을 10개를 넣어줍니다.
못생긴 라디오버튼 등장 ㅠ
라디오 버튼 커스텀을 위해 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>
기본 라디오버튼을 별표 이미지로 변경할게요.
label
의 for
속성과 input
의 id
가 같으면 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;
}
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');
}
}