class BottomSheet extends HTMLElement { defaultVh: number; beforeVh: number; sheetHeight: number; readonly mobileVh: number; constructor() { super(); this.defaultVh = 0; // default height of .sheet__wrapper (vh) this.beforeVh = 0; // height of .sheet__wrapper before change (vh) this.sheetHeight = 0; // height of .sheet__wrapper (vh) this.mobileVh = window.innerHeight * 0.01; // 1vh } connectedCallback() { this.setAttribute('aria-hidden', 'true'); this.renderBottomSheet(); } renderBottomSheet() { const id = this.getAttribute('id'); this.className = 'customBottomsheet'; if (!isMobile) { 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 = `
${ this.getAttribute('title') ? `
${this.getAttribute('title')}
` : `` } `; const contentDiv = this.querySelector(`#${id} > main`)! as HTMLElement; contentDiv.className = `${contentDiv.className} content`; sheetWrapperDiv.appendChild(headerDiv); sheetWrapperDiv.appendChild(contentDiv); this.appendChild(overlayDiv); this.appendChild(sheetWrapperDiv); (this.querySelector('.overlay')! as HTMLDivElement).addEventListener( 'click', () => { this.closeSheet(); } ); if (isMobile) { const draggableAreaDiv = this.querySelector( '.draggable-area' )! as HTMLDivElement; let dragPosition = 0; const onTouchStart = (event: TouchEvent) => { dragPosition = event.touches[0].clientY; sheetWrapperDiv.classList.add('not-selectable'); }; const onTouchMove = (event: TouchEvent) => { if (dragPosition === 0) return; const y = event.touches[0].clientY; const deltaY = dragPosition - y; const deltaHeight = (deltaY / window.innerHeight) * 100; this.setSheetHeight(this.sheetHeight + deltaHeight); dragPosition = y; }; const onTouchEnd = () => { dragPosition = 0; sheetWrapperDiv.classList.remove('not-selectable'); if (this.sheetHeight < this.beforeVh - 5) { this.closeSheet(); } else if (this.sheetHeight > this.defaultVh + 10) { this.setSheetHeight(100); } else { this.setSheetHeight(this.defaultVh); } this.beforeVh = this.sheetHeight; }; draggableAreaDiv.addEventListener('touchstart', onTouchStart, { passive: true, }); this.addEventListener('touchmove', onTouchMove, { passive: true }); this.addEventListener('touchend', onTouchEnd, { passive: true }); } } setSheetHeight(heightVh: number) { const sheetWrapper = this.querySelector( '.sheet__wrapper' )! as HTMLDivElement; if (!isMobile) return; this.sheetHeight = Math.max(0, Math.min(100, heightVh)); sheetWrapper.style.height = `${this.sheetHeight * this.mobileVh}px`; if (this.sheetHeight === 100) { sheetWrapper.classList.add('fullscreen'); } else { sheetWrapper.classList.remove('fullscreen'); } } setIsSheetShown(isShown: boolean) { this.setAttribute('aria-hidden', String(!isShown)); if (isShown) { document.body.classList.add('no-scroll'); } else { const shownBottomSheet = Array.from( document.querySelectorAll('bottom-sheet') ).find((el) => el.ariaHidden === 'false'); if (!shownBottomSheet) { document.body.classList.remove('no-scroll'); } } } openSheet() { if (this.defaultVh === 0) { const sheetWrapperDiv = this.querySelector( '.sheet__wrapper' )! as HTMLDivElement; if (this.getAttribute('vh')) { this.defaultVh = Number(this.getAttribute('vh')); } else { this.defaultVh = Number( (sheetWrapperDiv.offsetHeight / window.innerHeight) * 100 ); } } this.beforeVh = this.defaultVh; this.setSheetHeight(this.defaultVh); this.setIsSheetShown(true); } openFullSheet() { this.beforeVh = 100; this.setSheetHeight(100); this.setIsSheetShown(true); } closeSheet() { this.setSheetHeight(0); this.setIsSheetShown(false); } fullSheet() { this.beforeVh = 100; this.setSheetHeight(100); } } const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); customElements.define('bottom-sheet', BottomSheet);