universal-slider.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. // ==========================================================================
  2. // UNIVERSAL SLIDER CLASS
  3. // ==========================================================================
  4. class UniversalSlider {
  5. constructor(selector, options = {}) {
  6. this.config = {
  7. slidesPerView: {
  8. desktop: 3,
  9. tablet: 2,
  10. mobile: 1
  11. },
  12. slideBy: 1, // How many slides to move at once
  13. breakpoints: {
  14. mobile: 576,
  15. tablet: 992
  16. },
  17. autoplay: true,
  18. autoplayDelay: 4000,
  19. pauseOnHover: true,
  20. pagination: true,
  21. arrows: false,
  22. transitionDuration: 500,
  23. touchEnabled: true,
  24. swipeThreshold: 50,
  25. loop: true,
  26. classes: {
  27. container: 'slider-container',
  28. slide: 'slider-slide',
  29. pagination: 'slider-pagination',
  30. paginationDot: 'pagination-dot',
  31. paginationActive: 'active',
  32. arrow: 'slider-arrow',
  33. arrowPrev: 'slider-arrow-prev',
  34. arrowNext: 'slider-arrow-next'
  35. },
  36. onInit: null,
  37. onSlideChange: null,
  38. onResize: null,
  39. ...options
  40. };
  41. this.slider = document.querySelector(selector);
  42. if (!this.slider) {
  43. console.warn(`Slider not found: ${selector}`);
  44. return;
  45. }
  46. this.container = null;
  47. this.slides = [];
  48. this.pagination = null;
  49. this.arrows = {};
  50. this.currentIndex = 0;
  51. this.slidesPerView = this.getSlidesPerView();
  52. this.totalSlides = 0;
  53. this.maxIndex = 0;
  54. this.autoplayInterval = null;
  55. this.isTransitioning = false;
  56. this.init();
  57. }
  58. init() {
  59. this.setupSlider();
  60. this.calculateDimensions();
  61. if (this.config.pagination) this.createPagination();
  62. if (this.config.arrows) this.createArrows();
  63. this.bindEvents();
  64. this.updateSlider();
  65. if (this.config.autoplay) this.startAutoplay();
  66. if (this.config.onInit) this.config.onInit(this);
  67. }
  68. setupSlider() {
  69. this.container = this.slider.querySelector(`.${this.config.classes.container}`);
  70. if (!this.container) {
  71. this.container = document.createElement('div');
  72. this.container.className = this.config.classes.container;
  73. const children = Array.from(this.slider.children);
  74. children.forEach(child => {
  75. if (!child.classList.contains(this.config.classes.pagination) &&
  76. !child.classList.contains(this.config.classes.arrow)) {
  77. child.classList.add(this.config.classes.slide);
  78. this.container.appendChild(child);
  79. }
  80. });
  81. this.slider.appendChild(this.container);
  82. }
  83. this.slides = Array.from(this.container.children);
  84. this.totalSlides = this.slides.length;
  85. this.applyBaseStyles();
  86. }
  87. applyBaseStyles() {
  88. this.slider.style.position = 'relative';
  89. this.slider.style.overflow = 'hidden';
  90. this.container.style.display = 'flex';
  91. this.container.style.transition = `transform ${this.config.transitionDuration}ms ease-in-out`;
  92. this.container.style.width = '100%';
  93. this.slides.forEach(slide => {
  94. slide.style.flexShrink = '0';
  95. slide.style.userSelect = 'none';
  96. });
  97. }
  98. getSlidesPerView() {
  99. const width = window.innerWidth;
  100. if (width <= this.config.breakpoints.mobile) {
  101. return this.config.slidesPerView.mobile;
  102. } else if (width <= this.config.breakpoints.tablet) {
  103. return this.config.slidesPerView.tablet;
  104. } else {
  105. return this.config.slidesPerView.desktop;
  106. }
  107. }
  108. calculateDimensions() {
  109. this.slidesPerView = this.getSlidesPerView();
  110. // Calculate max index based on slideBy setting
  111. if (this.config.slideBy === 1) {
  112. // Move one slide at a time
  113. this.maxIndex = Math.max(0, this.totalSlides - this.slidesPerView);
  114. } else {
  115. // Move by groups (original behavior)
  116. this.maxIndex = Math.max(0, Math.ceil(this.totalSlides / this.slidesPerView) - 1);
  117. }
  118. }
  119. createPagination() {
  120. const existingPagination = this.slider.querySelector(`.${this.config.classes.pagination}`);
  121. if (existingPagination) existingPagination.remove();
  122. this.pagination = document.createElement('div');
  123. this.pagination.className = this.config.classes.pagination;
  124. // Create pagination based on slideBy setting
  125. let totalPages;
  126. if (this.config.slideBy === 1) {
  127. totalPages = this.maxIndex + 1;
  128. } else {
  129. totalPages = Math.ceil(this.totalSlides / this.slidesPerView);
  130. }
  131. for (let i = 0; i < totalPages; i++) {
  132. const dot = document.createElement('button');
  133. dot.className = this.config.classes.paginationDot;
  134. dot.setAttribute('data-index', i);
  135. dot.setAttribute('aria-label', `Go to slide ${i + 1}`);
  136. if (i === 0) dot.classList.add(this.config.classes.paginationActive);
  137. this.pagination.appendChild(dot);
  138. }
  139. this.slider.appendChild(this.pagination);
  140. }
  141. createArrows() {
  142. this.arrows.prev = document.createElement('button');
  143. this.arrows.prev.className = `${this.config.classes.arrow} ${this.config.classes.arrowPrev}`;
  144. this.arrows.prev.innerHTML = '‹';
  145. this.arrows.prev.setAttribute('aria-label', 'Previous slide');
  146. this.arrows.next = document.createElement('button');
  147. this.arrows.next.className = `${this.config.classes.arrow} ${this.config.classes.arrowNext}`;
  148. this.arrows.next.innerHTML = '›';
  149. this.arrows.next.setAttribute('aria-label', 'Next slide');
  150. this.slider.appendChild(this.arrows.prev);
  151. this.slider.appendChild(this.arrows.next);
  152. }
  153. bindEvents() {
  154. if (this.pagination) {
  155. this.pagination.addEventListener('click', (e) => {
  156. if (e.target.classList.contains(this.config.classes.paginationDot)) {
  157. const index = parseInt(e.target.dataset.index);
  158. this.goToSlide(index);
  159. }
  160. });
  161. }
  162. if (this.arrows.prev) {
  163. this.arrows.prev.addEventListener('click', () => this.prevSlide());
  164. }
  165. if (this.arrows.next) {
  166. this.arrows.next.addEventListener('click', () => this.nextSlide());
  167. }
  168. window.addEventListener('resize', () => this.handleResize());
  169. if (this.config.touchEnabled) {
  170. this.addTouchSupport();
  171. }
  172. if (this.config.pauseOnHover && this.config.autoplay) {
  173. this.slider.addEventListener('mouseenter', () => this.stopAutoplay());
  174. this.slider.addEventListener('mouseleave', () => this.startAutoplay());
  175. }
  176. this.slider.addEventListener('keydown', (e) => {
  177. if (e.key === 'ArrowLeft') this.prevSlide();
  178. if (e.key === 'ArrowRight') this.nextSlide();
  179. });
  180. }
  181. addTouchSupport() {
  182. let startX = 0;
  183. let startY = 0;
  184. let endX = 0;
  185. let endY = 0;
  186. this.container.addEventListener('touchstart', (e) => {
  187. startX = e.touches[0].clientX;
  188. startY = e.touches[0].clientY;
  189. });
  190. this.container.addEventListener('touchmove', (e) => {
  191. const currentX = e.touches[0].clientX;
  192. const currentY = e.touches[0].clientY;
  193. const diffX = Math.abs(currentX - startX);
  194. const diffY = Math.abs(currentY - startY);
  195. if (diffX > diffY) {
  196. e.preventDefault();
  197. }
  198. });
  199. this.container.addEventListener('touchend', (e) => {
  200. endX = e.changedTouches[0].clientX;
  201. endY = e.changedTouches[0].clientY;
  202. this.handleSwipe(startX, endX, startY, endY);
  203. });
  204. }
  205. handleSwipe(startX, endX, startY, endY) {
  206. const diffX = startX - endX;
  207. const diffY = startY - endY;
  208. if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > this.config.swipeThreshold) {
  209. if (diffX > 0) {
  210. this.nextSlide();
  211. } else {
  212. this.prevSlide();
  213. }
  214. }
  215. }
  216. handleResize() {
  217. const newSlidesPerView = this.getSlidesPerView();
  218. if (newSlidesPerView !== this.slidesPerView) {
  219. this.calculateDimensions();
  220. // Adjust index to valid range
  221. if (this.currentIndex > this.maxIndex) {
  222. this.currentIndex = this.maxIndex;
  223. }
  224. if (this.config.pagination) this.createPagination();
  225. this.updateSlider();
  226. if (this.config.onResize) this.config.onResize(this);
  227. }
  228. }
  229. updateSlider() {
  230. if (this.isTransitioning) return;
  231. const slideWidth = 100 / this.slidesPerView;
  232. let translateX;
  233. if (this.config.slideBy === 1) {
  234. // Move one slide at a time
  235. translateX = -(this.currentIndex * slideWidth);
  236. } else {
  237. // Move by groups (original behavior)
  238. translateX = -(this.currentIndex * 100);
  239. }
  240. this.slides.forEach(slide => {
  241. slide.style.flex = `0 0 ${slideWidth}%`;
  242. slide.style.maxWidth = `${slideWidth}%`;
  243. });
  244. this.container.style.transform = `translateX(${translateX}%)`;
  245. this.updateNavigation();
  246. if (this.config.onSlideChange) {
  247. this.config.onSlideChange(this.currentIndex, this);
  248. }
  249. }
  250. updateNavigation() {
  251. if (this.pagination) {
  252. const dots = this.pagination.querySelectorAll(`.${this.config.classes.paginationDot}`);
  253. dots.forEach((dot, index) => {
  254. dot.classList.toggle(this.config.classes.paginationActive, index === this.currentIndex);
  255. });
  256. }
  257. if (!this.config.loop && this.arrows.prev && this.arrows.next) {
  258. this.arrows.prev.disabled = this.currentIndex === 0;
  259. this.arrows.next.disabled = this.currentIndex === this.maxIndex;
  260. }
  261. }
  262. goToSlide(index, force = false) {
  263. if (this.isTransitioning && !force) return;
  264. if (this.config.loop) {
  265. if (index < 0) {
  266. this.currentIndex = this.maxIndex;
  267. } else if (index > this.maxIndex) {
  268. this.currentIndex = 0;
  269. } else {
  270. this.currentIndex = index;
  271. }
  272. } else {
  273. this.currentIndex = Math.max(0, Math.min(index, this.maxIndex));
  274. }
  275. this.updateSlider();
  276. this.restartAutoplay();
  277. }
  278. nextSlide() {
  279. this.goToSlide(this.currentIndex + 1);
  280. }
  281. prevSlide() {
  282. this.goToSlide(this.currentIndex - 1);
  283. }
  284. startAutoplay() {
  285. if (!this.config.autoplay) return;
  286. this.stopAutoplay();
  287. this.autoplayInterval = setInterval(() => {
  288. this.nextSlide();
  289. }, this.config.autoplayDelay);
  290. }
  291. stopAutoplay() {
  292. if (this.autoplayInterval) {
  293. clearInterval(this.autoplayInterval);
  294. this.autoplayInterval = null;
  295. }
  296. }
  297. restartAutoplay() {
  298. if (this.config.autoplay) {
  299. this.stopAutoplay();
  300. this.startAutoplay();
  301. }
  302. }
  303. destroy() {
  304. this.stopAutoplay();
  305. if (this.pagination) this.pagination.remove();
  306. if (this.arrows.prev) this.arrows.prev.remove();
  307. if (this.arrows.next) this.arrows.next.remove();
  308. }
  309. updateConfig(newConfig) {
  310. this.config = { ...this.config, ...newConfig };
  311. this.calculateDimensions();
  312. this.updateSlider();
  313. }
  314. getState() {
  315. return {
  316. currentIndex: this.currentIndex,
  317. totalSlides: this.totalSlides,
  318. slidesPerView: this.slidesPerView,
  319. maxIndex: this.maxIndex,
  320. isAutoplay: !!this.autoplayInterval
  321. };
  322. }
  323. }
  324. function createSlider(selector, options) {
  325. return new UniversalSlider(selector, options);
  326. }
  327. window.UniversalSlider = UniversalSlider;
  328. window.createSlider = createSlider;