내가 만들고자 하는 바텀시트는
1. pc에서는 모달형태로 나타나고 mobile에서는 아래에서 위로 올라오는 바텀시트 형태로 나타나야 한다.
2. 외부에서 열고 닫을 수 있어야 한다.
3. full(전체가 채워지도록)로 당겨질 수 있어야 한다.
4. 제목과 상단에 닫기 버튼의 여부를 attribute 형태로 전달받는다.
이런 기능을 가지고 있어야 한다.
+ 미리 결과를 보여주자면...!
순수한 자바스크립트로 바텀시트를 만들어봤다
바텀시트를 여러 곳에서 간편하게 쓰기 위해(함수로도 만들어보고 클래스로 해보려다가...)
Web components로 만들게 되었다 ㅎㅎ
리액트나 뷰 등을 사용하지 않고 순수 자바스크립트로 컴포넌트를 만들 수 있는 방법이 있는데
바로 Web components를 사용하면 된다!!
Web Components
- 이름 규칙에 -가 꼭 포함되어야 한다. (html 파서가 customElement가 될지도 모르는 아이들과 구분하기 위해서)
- HTMLElement를 상속받아 클래스를 만든다.
- react, vue처럼 custom element도 생명주기를 가지고 있다.
customElement 생명주기
connectedCallback()
DOM에 추가되었을 때 실행된다. (화면을 그려주기)
disconnectedCallback()
DOM에서 제거되었을 때 실행된다. (불필요한 event 핸들러 정리하기)
adoptedCallback()
요소가 새로운 document로 이동되었을 때 실행된다.
attributeChangedCallback()
특정 속성의 추가, 삭제, 변경을 감지해서 실행된다.
static get observedAttributes() {
// 모니터링 할 속성 이름
return ['locale'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
// 속성이 추가, 제거, 변경 시
this[attrName] = newVal;
}
Custom Element 관련 참고자료
https://meetup.nhncloud.com/posts/115
<bottom-sheet>
1. mobile과 pc 구분하기 위해서 isMobile이라는 변수를 생성
const isMobile = (() => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
})();
2. custom element로 bottom-sheet 만들기
class BottomSheet extends HTMLElement {
constructor() {
super();
this.setAttribute("aria-hidden", true);
this.contentId = this.getAttribute("id");
this.isClose = this.getAttribute("isClose")
? this.getAttribute("isClose")
: true;
this.defaultVh = 0; // 기본(처음) 생성된 높이
this.beforeVh = 0; // 이전의 높이(drag하기 전에)
this.sheetHeight; // 현재 bottomsheet의 높이(vh)
this.sheetWrapper; // .sheet__wrapper
}
connectedCallback() {
this.renderBottomSheet(); // DOM에 추가되면 bottomsheet를 생성한다.
}
disconnectedCallback() {
// DOM에서 제거
}
renderBottomSheet() { // bottomsheet html 생성
this.id = `${this.contentId}`;
this.className = "customBottomsheet";
if (!isMobile) { // pc에서는 modal로 만들기
this.classList.add("__modal");
}
const overlayDiv = document.createElement("div");
overlayDiv.className = "overlay";
const sheetWrapperDiv = document.createElement("div");
sheetWrapperDiv.className = "sheet__wrapper";
const headerDiv = document.createElement("header");
headerDiv.className = "controls";
headerDiv.innerHTML = `
<div class="draggable__area">
<div class="draggable__thumb"></div>
</div>
<div class="title__wrapper">
<span class="title">${this.getAttribute("title")}</span>
${
this.isClose
? `<button type="button" class="close__btn">✕</button>`
: ``
}
</div>
`;
const contentDiv = this.querySelector(`#${this.contentId} > main`);
contentDiv.className = `${contentDiv.className} content`;
sheetWrapperDiv.appendChild(headerDiv);
sheetWrapperDiv.appendChild(contentDiv);
this.appendChild(overlayDiv);
this.appendChild(sheetWrapperDiv);
this.defaultVh =
Number((sheetWrapperDiv.offsetHeight / window.innerHeight) * 100) < 65
? Number((sheetWrapperDiv.offsetHeight / window.innerHeight) * 100)
: 65;
this.beforeVh = this.defaultVh;
this.sheetWrapper = this.querySelector(".sheet__wrapper");
if (this.isClose) {
this.querySelector(".close__btn").addEventListener("click", () => {
this.closeSheet();
});
}
this.querySelector(".overlay").addEventListener("click", () => {
this.closeSheet();
});
if (isMobile) {
const draggableArea = this.querySelector(".draggable__area");
const touchPosition = (event) => {
return event.touches ? event.touches[0] : event;
};
let dragPosition;
const onTouchStart = (event) => {
dragPosition = touchPosition(event).pageY;
sheetWrapperDiv.classList.add("not-selectable");
draggableArea.style.cursor = document.body.style.cursor = "grabbing";
};
const onTouchMove = (event) => {
if (dragPosition === undefined) return;
const y = touchPosition(event).pageY;
const deltaY = dragPosition - y;
const deltaHeight = (deltaY / window.innerHeight) * 100;
this.setSheetHeight(this.sheetHeight + deltaHeight);
dragPosition = y;
};
const onTouchEnd = () => {
dragPosition = undefined;
sheetWrapperDiv.classList.remove("not-selectable");
draggableArea.style.cursor = document.body.style.cursor = "";
if (this.beforeVh - 5 > this.sheetHeight) {
this.setIsSheetShown(false);
} else if (this.sheetHeight < this.defaultVh - 10) {
this.setIsSheetShown(false);
} else if (this.sheetHeight > this.defaultVh + 10) {
this.setSheetHeight(100);
} else {
this.setSheetHeight(this.defaultVh);
}
this.beforeVh = this.sheetHeight;
};
draggableArea.addEventListener("touchstart", onTouchStart);
this.addEventListener("touchmove", onTouchMove);
this.addEventListener("touchend", onTouchEnd);
}
}
setSheetHeight(value) { // bottomsheet 높이 셋팅
if (!isMobile) {
this.sheetWrapper.classList.add("fullscreen");
return;
}
this.sheetHeight = Math.max(0, Math.min(100, value));
this.sheetWrapper.style.height = `${this.sheetHeight}vh`;
if (this.sheetHeight === 100) {
this.sheetWrapper.classList.add("fullscreen");
} else {
this.sheetWrapper.classList.remove("fullscreen");
}
}
setIsSheetShown(value) { // hidden인지 아닌지 셋팅
this.setAttribute("aria-hidden", String(!value));
if (!value) {
document.body.classList.remove("no-scroll");
} else {
document.body.classList.add("no-scroll");
}
}
openSheet() {
this.beforeVh = 0;
this.setSheetHeight(this.defaultVh);
this.setIsSheetShown(true);
}
closeSheet() {
this.setIsSheetShown(false);
this.setSheetHeight(this.defaultVh);
}
fullSheet() {
this.beforeVh = 100;
this.setSheetHeight(100);
}
}
document.addEventListener("DOMContentLoaded", function () {
customElements.define("bottom-sheet", BottomSheet); // 태그와 클래스를 연결
});
*** <bottom-sheet> 안에 main으로 감싸줘야 bottomsheet 내용으로 들어간다 ***
<bottom-sheet id="testID" title="Title">
<main>
<div class="btn__wrapper">
<button onclick="document.getElementById('testID').closeSheet()">
close
</button>
<button onclick="document.getElementById('testID').fullSheet()">
full
</button>
</div>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Impedit
explicabo vero quas eligendi, eos cupiditate sint aliquam a omnis
commodi quos in libero veniam. Quidem, non a quibusdam consequuntur
mollitia officia numquam sit quos dolorum quaerat reprehenderit
laboriosam perspiciatis consequatur odit error dolore recusandae iste
id quam magnam ut! Sint nulla minus excepturi libero officiis,
deleniti, delectus obcaecati saepe natus rerum nesciunt! Nemo quo id
ipsum fugiat voluptas ducimus incidunt nulla sed, voluptates modi
exercitationem quod obcaecati corporis perspiciatis dolorum ullam
provident sint iusto consequatur totam dolorem. Amet, praesentium
accusamus dolore hic ad iusto nostrum exercitationem velit ex optio
nihil obcaecati provident enim nobis molestiae cupiditate possimus
error itaque facilis eligendi placeat eos quam! Consequatur eveniet
corporis nam accusantium nemo harum non explicabo accusamus.
Voluptatem laborum dolores magni voluptatibus doloribus sunt? Id quae
vero, nemo nam dolore ab amet distinctio molestias excepturi
blanditiis quia eos et eligendi magnam voluptas iusto.
</p>
</main>
</bottom-sheet>
<button onclick="document.getElementById('testID').openSheet()">
open bottomsheet
</button>
bottom sheet 관련 참고자료
https://github.com/ivteplo/bottom-sheet
+ 추후 조금 더 개선하여 라이브러리로 만들어봤습니다!
'IT > JavaScript' 카테고리의 다른 글
모바일 웹에서 app으로 이동(딥링크) (0) | 2023.10.26 |
---|---|
axios interceptors로 refreshToken을 이용해서 accessToken 재발급 (0) | 2023.10.17 |
자바스크립트에서 뒤로가기 감지하기 (0) | 2022.06.07 |
slice와 splice의 차이점 (0) | 2022.04.05 |
padEnd() (0) | 2022.03.30 |