tab-scroller
Version:
A JavaScript library for creating scrollable tabs with drag-and-drop functionality
314 lines (258 loc) • 9.72 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TabScroller = {}));
})(this, (function (exports) { 'use strict';
//----------------------------
// 標籤水平移動
//----------------------------
class tabScroller {
constructor(containerSelector = '', options = {}) {
if (!containerSelector || typeof containerSelector !== 'string') {
throw new Error('tabScroller: containerSelector must be a non-empty string');
}
this.containerSelector = containerSelector;
this.options = {
spaceBetween: 0,
clickToActive: false,
clickToScroll: false,
breakpoints: {},
...options
};
this.wrap = document.querySelector(this.containerSelector);
if (!this.wrap) {
throw new Error(`tabScroller: Element with selector "${containerSelector}" not found`);
}
this.item = this.wrap.querySelectorAll('.scroller-item');
if (this.item.length === 0) {
console.warn(`tabScroller: No elements with class "scroller-item" found in "${containerSelector}"`);
}
// 綁定方法到實例以確保正確的 this 上下文
this.handleResize = this.handleResize.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.handleItemSlideClick = this.handleItemSlideClick.bind(this);
this.init();
}
init() {
if (!this.wrap || this.item.length === 0) {
console.warn('tabScroller: Cannot initialize - missing container or items');
return;
}
this.applyOptions();
this.setScroller();
this.setSpaceBetween();
this.setInitActive();
this.setActive();
this.setSlide();
// 添加 resize 事件監聽器,使用防抖處理
window.addEventListener('resize', this.handleResize);
}
handleResize() {
if (this.resizeTimer) {
clearTimeout(this.resizeTimer);
}
this.resizeTimer = setTimeout(() => {
this.applyOptions();
}, 250);
}
destroy() {
// 清理事件監聽器
window.removeEventListener('resize', this.handleResize);
if (this.wrap) {
this.wrap.removeEventListener('mousemove', this.move);
this.wrap.removeEventListener('mousedown', this.startDragging);
this.wrap.removeEventListener('mouseup', this.stopDragging);
this.wrap.removeEventListener('mouseleave', this.stopDragging);
}
if (this.item) {
this.item.forEach(item => {
item.removeEventListener('click', this.handleItemClick);
item.removeEventListener('click', this.handleItemSlideClick);
});
}
if (this.resizeTimer) {
clearTimeout(this.resizeTimer);
}
}
applyOptions() {
const width = window.innerWidth;
let activeOptions = {...this.options};
if (this.options.breakpoints && typeof this.options.breakpoints === 'object') {
const breakpoints = Object.keys(this.options.breakpoints)
.map(key => parseInt(key))
.filter(key => !isNaN(key))
.sort((a, b) => a - b);
for (let i = 0; i < breakpoints.length; i++) {
if (width > breakpoints[i]) {
const breakpointOptions = this.options.breakpoints[breakpoints[i]];
if (breakpointOptions && typeof breakpointOptions === 'object') {
activeOptions = {...activeOptions, ...breakpointOptions};
}
}
}
}
this.spaceBetween = typeof activeOptions.spaceBetween === 'number' ? activeOptions.spaceBetween : 0;
this.clickToActive = Boolean(activeOptions.clickToActive);
this.clickToScroll = Boolean(activeOptions.clickToScroll);
// 重新設定間距和活動項目
this.setSpaceBetween();
this.setActive();
this.setSlide();
}
setScroller() {
if (!this.wrap) return;
let mouseDown = false;
let startX, scrollLeft;
this.startDragging = (e) => {
mouseDown = true;
startX = e.pageX - this.wrap.offsetLeft;
scrollLeft = this.wrap.scrollLeft;
this.wrap.style.cursor = 'grabbing';
}
this.stopDragging = () => {
mouseDown = false;
this.wrap.style.cursor = 'grab';
if (this.item && this.item.length > 0) {
this.item.forEach((item) => {
item.style.pointerEvents = 'auto';
});
}
}
this.move = (e) => {
if (!mouseDown) return;
e.preventDefault();
const x = e.pageX - this.wrap.offsetLeft;
const scroll = x - startX;
this.wrap.scrollLeft = scrollLeft - scroll;
if (this.item && this.item.length > 0) {
this.item.forEach((item) => {
item.style.pointerEvents = 'none';
});
}
}
// 清理舊的事件監聽器
this.wrap.removeEventListener('mousemove', this.move);
this.wrap.removeEventListener('mousedown', this.startDragging);
this.wrap.removeEventListener('mouseup', this.stopDragging);
this.wrap.removeEventListener('mouseleave', this.stopDragging);
// 添加新的事件監聽器
this.wrap.addEventListener('mousemove', this.move);
this.wrap.addEventListener('mousedown', this.startDragging);
this.wrap.addEventListener('mouseup', this.stopDragging);
this.wrap.addEventListener('mouseleave', this.stopDragging);
// 設定初始游標樣式
this.wrap.style.cursor = 'grab';
}
setSpaceBetween() {
if (!this.item || this.item.length === 0) return;
this.item.forEach((item) => {
item.style.marginRight = this.spaceBetween + 'px';
});
const lastItem = this.wrap.querySelector('.scroller-item:last-of-type');
if (lastItem) {
lastItem.style.marginRight = '0px';
}
}
setInitActive() {
if (!this.item || this.item.length === 0) return;
const itemActiveArray = [];
let prevItemTotalWidth = 0;
let totalSpace = 0;
this.item.forEach((item) => {
itemActiveArray.push(item.classList.contains('scroller-item-active'));
});
const initActiveItemIndex = itemActiveArray.indexOf(true);
if (initActiveItemIndex >= 0) {
for (let i = 0; i < initActiveItemIndex; i++) {
const rect = this.item[i].getBoundingClientRect();
prevItemTotalWidth += rect.width;
}
totalSpace = this.spaceBetween * initActiveItemIndex;
this.wrap.scroll({
left: initActiveItemIndex === 0 ? 0 : prevItemTotalWidth + totalSpace,
behavior: 'smooth'
});
}
}
setActive() {
if (!this.item || this.item.length === 0) return;
// 移除現有的 click 事件監聽器
this.item.forEach(item => {
item.removeEventListener('click', this.handleItemClick);
});
if (this.clickToActive) {
this.item.forEach(item => {
item.addEventListener('click', this.handleItemClick);
});
}
}
handleItemClick(e) {
if (!this.item || this.item.length === 0) return;
this.item.forEach(item => {
item.classList.remove('scroller-item-active');
});
e.currentTarget.classList.add('scroller-item-active');
// 觸發自定義事件
const event = new CustomEvent('tabScrollerActiveChange', {
detail: {
activeItem: e.currentTarget,
activeIndex: Array.from(this.item).indexOf(e.currentTarget)
}
});
this.wrap.dispatchEvent(event);
}
setSlide() {
if (!this.item || this.item.length === 0) return;
// 移除現有的 click 事件監聽器
this.item.forEach(item => {
item.removeEventListener('click', this.handleItemSlideClick);
// 啟用 transition
const childElements = item.querySelectorAll('*');
childElements.forEach((tag) => {
tag.classList.remove('transition-none');
});
});
if (this.clickToScroll) {
this.item.forEach((item) => {
// 停用 transition
const childElements = item.querySelectorAll('*');
childElements.forEach((tag) => {
tag.classList.add('transition-none');
});
});
this.item.forEach(item => {
item.addEventListener('click', this.handleItemSlideClick);
});
}
}
handleItemSlideClick(e) {
if (!this.item || this.item.length === 0) return;
const item = e.currentTarget;
const itemIndex = Array.from(this.item).indexOf(item);
let prevItemTotalWidth = 0;
for (let i = 0; i < itemIndex; i++) {
const rect = this.item[i].getBoundingClientRect();
prevItemTotalWidth += rect.width;
}
const totalSpace = this.spaceBetween * itemIndex;
this.wrap.scroll({
left: prevItemTotalWidth + totalSpace,
behavior: 'smooth'
});
// 觸發自定義事件
const event = new CustomEvent('tabScrollerSlideChange', {
detail: {
targetItem: item,
targetIndex: itemIndex,
scrollLeft: prevItemTotalWidth + totalSpace
}
});
this.wrap.dispatchEvent(event);
}
}
// 將 tabScroller 設為全域變數
if (typeof window !== 'undefined') {
window.tabScroller = tabScroller;
}
exports.tabScroller = tabScroller;
}));