class CustomScrollbar {
constructor() {
this.scrollbar = null;
this.thumb = null;
this.isDragging = false;
this.startY = 0;
this.startScrollTop = 0;
this.rafId = null;
this.lastScrollTop = 0;
this.init();
}
init() {
this.createScrollbarElements();
this.bindEvents();
this.updateScrollbar();
}
createScrollbarElements() {
this.scrollbar = document.createElement('div');
this.scrollbar.className = 'custom-scrollbar';
this.thumb = document.createElement('div');
this.thumb.className = 'custom-scrollbar-thumb';
this.scrollbar.appendChild(this.thumb);
document.body.appendChild(this.scrollbar);
}
bindEvents() {
this.thumb.addEventListener('mousedown', this.onThumbMouseDown.bind(this), { passive: false });
window.addEventListener('scroll', this.onScroll.bind(this), { passive: true });
window.addEventListener('resize', this.onResize.bind(this), { passive: true });
this.thumb.addEventListener('touchstart', this.onThumbTouchStart.bind(this), { passive: false });
this.scrollbar.addEventListener('click', this.onTrackClick.bind(this));
}
onScroll() {
if (!this.rafId) {
this.rafId = requestAnimationFrame(() => {
this.updateScrollbar();
this.rafId = null;
});
}
}
onResize() {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.updateScrollbar();
}, 150);
}
updateScrollbar() {
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight;
if (scrollHeight <= clientHeight) {
this.scrollbar.style.opacity = '0';
this.scrollbar.style.pointerEvents = 'none';
return;
}
this.scrollbar.style.opacity = '1';
this.scrollbar.style.pointerEvents = 'auto';
const thumbHeight = Math.max(
(clientHeight / scrollHeight) * clientHeight,
30 );
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const maxScroll = scrollHeight - clientHeight;
const scrollPercentage = scrollTop / maxScroll;
const maxThumbTop = clientHeight - thumbHeight;
const thumbTop = scrollPercentage * maxThumbTop;
this.thumb.style.height = `${thumbHeight}px`;
this.thumb.style.transform = `translateY(${thumbTop}px)`;
this.showScrollbar();
}
showScrollbar() {
this.scrollbar.classList.add('visible');
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout(() => {
if (!this.isDragging) {
this.scrollbar.classList.remove('visible');
}
}, 1500);
}
onThumbMouseDown(e) {
e.preventDefault();
this.isDragging = true;
this.startY = e.clientY;
this.startScrollTop = window.pageYOffset || document.documentElement.scrollTop;
document.addEventListener('mousemove', this.onThumbMouseMove.bind(this));
document.addEventListener('mouseup', this.onThumbMouseUp.bind(this));
this.scrollbar.classList.add('dragging');
this.thumb.classList.add('active');
}
onThumbMouseMove(e) {
if (!this.isDragging) return;
const deltaY = e.clientY - this.startY;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
const thumbHeight = parseFloat(this.thumb.style.height);
const maxThumbTop = clientHeight - thumbHeight;
const scrollPercentage = deltaY / maxThumbTop;
const maxScroll = scrollHeight - clientHeight;
const newScrollTop = this.startScrollTop + (scrollPercentage * maxScroll);
window.scrollTo(0, Math.max(0, Math.min(newScrollTop, maxScroll)));
}
onThumbMouseUp() {
this.isDragging = false;
document.removeEventListener('mousemove', this.onThumbMouseMove.bind(this));
document.removeEventListener('mouseup', this.onThumbMouseUp.bind(this));
this.scrollbar.classList.remove('dragging');
this.thumb.classList.remove('active');
}
onThumbTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
this.isDragging = true;
this.startY = touch.clientY;
this.startScrollTop = window.pageYOffset || document.documentElement.scrollTop;
const onTouchMove = (e) => {
if (!this.isDragging) return;
const touch = e.touches[0];
const deltaY = touch.clientY - this.startY;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
const thumbHeight = parseFloat(this.thumb.style.height);
const maxThumbTop = clientHeight - thumbHeight;
const scrollPercentage = deltaY / maxThumbTop;
const maxScroll = scrollHeight - clientHeight;
const newScrollTop = this.startScrollTop + (scrollPercentage * maxScroll);
window.scrollTo(0, Math.max(0, Math.min(newScrollTop, maxScroll)));
};
const onTouchEnd = () => {
this.isDragging = false;
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
this.scrollbar.classList.remove('dragging');
this.thumb.classList.remove('active');
};
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd, { passive: true });
this.scrollbar.classList.add('dragging');
this.thumb.classList.add('active');
}
onTrackClick(e) {
if (e.target === this.thumb) return;
const rect = this.scrollbar.getBoundingClientRect();
const clickY = e.clientY - rect.top;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
const scrollPercentage = clickY / clientHeight;
const maxScroll = scrollHeight - clientHeight;
const targetScroll = scrollPercentage * maxScroll;
window.scrollTo({
top: targetScroll,
behavior: 'smooth'
});
}
destroy() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
}
clearTimeout(this.hideTimeout);
clearTimeout(this.resizeTimeout);
if (this.scrollbar && this.scrollbar.parentNode) {
this.scrollbar.parentNode.removeChild(this.scrollbar);
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.customScrollbar = new CustomScrollbar();
});
} else {
window.customScrollbar = new CustomScrollbar();
}