├── .gitignore ├── composer.json ├── public ├── css │ └── own-carousel.min.css └── js │ └── own-carousel.min.js ├── resources ├── css │ └── own-carousel.min.css ├── views │ └── components │ │ └── slider.blade.php └── js │ └── own-carousel.min.js ├── src ├── Providers │ └── MoonshineCarouselServiceProvider.php └── Components │ └── Slider.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | composer.lock 4 | /vendor/ 5 | /.idea/ 6 | /.vscode/ 7 | .vscode/ 8 | .idea/ 9 | .env 10 | .env.* -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "name": "webmatherfacker/moonshine-carousel", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-4": { 7 | "Webmatherfacker\\MoonshineCarousel\\": "src/" 8 | } 9 | }, 10 | "authors": [ 11 | { 12 | "name": "ghost" 13 | } 14 | ], 15 | "conflict": { 16 | "moonshine/moonshine": "<3.0" 17 | }, 18 | "scripts": { 19 | }, 20 | "extra": { 21 | "laravel": { 22 | "providers": [ 23 | "Webmatherfacker\\MoonshineCarousel\\Providers\\MoonshineCarouselServiceProvider" 24 | ] 25 | } 26 | }, 27 | "minimum-stability": "dev", 28 | "require": {} 29 | } 30 | -------------------------------------------------------------------------------- /public/css/own-carousel.min.css: -------------------------------------------------------------------------------- 1 | :root{--width:0;--margin:0}.own-carousel__outer{position:relative;overflow:hidden;user-select:none}.own-carousel{display:flex}.own-carousel__item{flex-shrink:0;overflow:hidden;flex-basis:var(--width)}.own-carousel__item:not(:first-child){margin-left:var(--margin)} .control__prev, .control__next{display: flex;justify-content: center;align-items: center;width: 35px;height: 35px;position: absolute;background: rgb(120, 67, 233);border-radius: 50%}.control__prev svg path, .control__next svg path{fill: #fff}.control__prev{left: -18px}.control__next{right: -18px}.own-carousel__container{position: relative;width: 100%}.own-carousel__control{display: flex;top: calc(50% - 1rem);width: 100%;color: #fff;position: absolute;justify-content: space-between} 2 | -------------------------------------------------------------------------------- /resources/css/own-carousel.min.css: -------------------------------------------------------------------------------- 1 | :root{--width:0;--margin:0}.own-carousel__outer{position:relative;overflow:hidden;user-select:none}.own-carousel{display:flex}.own-carousel__item{flex-shrink:0;overflow:hidden;flex-basis:var(--width)}.own-carousel__item:not(:first-child){margin-left:var(--margin)} .control__prev, .control__next{display: flex;justify-content: center;align-items: center;width: 35px;height: 35px;position: absolute;background: rgb(120, 67, 233);border-radius: 50%}.control__prev svg path, .control__next svg path{fill: #fff}.control__prev{left: -18px}.control__next{right: -18px}.own-carousel__container{position: relative;width: 100%}.own-carousel__control{display: flex;top: calc(50% - 1rem);width: 100%;color: #fff;position: absolute;justify-content: space-between} 2 | -------------------------------------------------------------------------------- /src/Providers/MoonshineCarouselServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__ . '/../../resources/views', 'slider'); 22 | 23 | $this->publishes([ 24 | __DIR__ . '/../../public' => public_path('vendor/webmatherfacker/moonshine-carousel'), 25 | ], ['moonshine-carousel', 'laravel-assets']); 26 | 27 | 28 | $assets->add([ 29 | Css::make('/vendor/webmatherfacker/moonshine-carousel/css/own-carousel.min.css'), 30 | Js::make('/vendor/webmatherfacker/moonshine-carousel/js/own-carousel.min.js'), 31 | ]); 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /src/Components/Slider.php: -------------------------------------------------------------------------------- 1 | items = is_callable($items) 36 | ? $items() 37 | : $items; 38 | 39 | return $this; 40 | } 41 | 42 | public function itemPerRow(int $count): Slider 43 | { 44 | $this->itemPerRow = $count; 45 | 46 | return $this; 47 | } 48 | public function editItemWidth($itemWidthPercent): Slider 49 | { 50 | $this->itemWidth = $itemWidthPercent; 51 | 52 | return $this; 53 | } 54 | public function loop(): Slider 55 | { 56 | $this->loop = true; 57 | 58 | return $this; 59 | } 60 | public function nav(): Slider 61 | { 62 | $this->navigation = true; 63 | 64 | return $this; 65 | } 66 | 67 | protected function viewData(): array 68 | { 69 | return [ 70 | 'items' => $this->items, 71 | 'itemPerRow' => $this->itemPerRow, 72 | 'itemWidth' => $this->itemWidth, 73 | 'loop' => $this->loop, 74 | 'nav' => $this->navigation 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /resources/views/components/slider.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'items' => null, 3 | 'itemPerRow' => 3, 4 | 'itemWidth' => 20, 5 | 'loop' => false, 6 | 'nav' => false, 7 | ]) 8 | 9 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moonshine Slider - слайдер 2 | [![Software License][ico-license]](LICENSE) 3 | 4 | [![Laravel][ico-laravel]](Laravel) [![PHP][ico-php]](PHP) 5 | 6 | Moonshine Slider компонент для отображения слайдером элементов административной панели [MoonShine](https://moonshine-laravel.com/). 7 | 8 | ## Содержание 9 | * [Установка](#установка) 10 | * [Использование](#использование) 11 | * [Лицензия](#лицензия) 12 | 13 | ## Установка 14 | Команда для установки: 15 | ```bash 16 | composer require webmatherfacker/moonshine-carousel 17 | ``` 18 | ## Использование 19 | ```php 20 | addItems([ 25 | ValueMetric::make('Articles') 26 | ->value(100), 27 | ValueMetric::make('Orders') 28 | ->value(150), 29 | ValueMetric::make('Products') 30 | ->value(250), 31 | ValueMetric::make('Users') 32 | ->value(350), 33 | ValueMetric::make('Sales') 34 | ->value(500), 35 | ValueMetric::make('Countries') 36 | ->value(195) 37 | ]), 38 | ``` 39 | 40 | Перечень методов: 41 | 42 | `itemPerRow($count)` количество отображаемых элементов на странице. По умолчанию `3` 43 | 44 | Использование: 45 | ```php 46 | Slider::make()->addItems(...)->itemPerRow(3), 47 | ``` 48 | 49 | `editItemWidth($percent)` ширина каждого элемента карусели. По умолчанию `20` 50 | > ⚠️ **Предупреждение**
51 | > Разрыв между каждым элементом будет рассчитан автоматически. 52 | Например: если ваш itemPerRow равен 4, а ItemWidth равен 24, то разрыв между каждым элементом будет (100-24 * 4) / 3. Не допускайте, чтобы ваш разрыв был отрицательным! 53 | 54 | Использование: 55 | ```php 56 | Slider::make()->addItems(...)->editItemWidth(20), 57 | ``` 58 | 59 | `loop` позволяет включить зацикливание слайдера, чтобы прокрутка повторялась сначала. По умолчанию `false` 60 | 61 | Использование: 62 | ```php 63 | Slider::make()->addItems(...)->loop(), 64 | ``` 65 | 66 | 67 | `nav` позволяет включить кнопки управления. По умолчанию `false` 68 | 69 | Использование: 70 | ```php 71 | Slider::make()->addItems(...)->nav(), 72 | ``` 73 | 74 | ## Лицензия 75 | [Лицензия MIT](LICENSE). 76 | 77 | 78 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 79 | [ico-laravel]: https://img.shields.io/badge/Laravel-10+-FF2D20?style=for-the-badge&logo=laravel 80 | [ico-php]: https://img.shields.io/badge/PHP-8.2+-777BB4?style=for-the-badge&logo=php -------------------------------------------------------------------------------- /public/js/own-carousel.min.js: -------------------------------------------------------------------------------- 1 | Object.prototype.ownCarousel = function (options) { 2 | const { 3 | itemPerRow = 4, 4 | itemWidth = 24, 5 | loop = true, 6 | responsive = {}, 7 | draggable = true, 8 | mouseWheel = false, 9 | autoplay = 0, 10 | stopAutoplayWhenHover = true, 11 | nav = false, 12 | isRTL = false, 13 | } = options; 14 | //extract arguments 15 | this.carousel = this.querySelector(".own-carousel"); 16 | this.carouselOuter = this.querySelector(".own-carousel__outer"); 17 | this.itemWidthBig = this.itemWidth = itemWidth; 18 | this.itemPerRowBig = this.itemPerRow = itemPerRow; 19 | this.responsive = responsive; 20 | this.gapWidth = (100 - itemPerRow * itemWidth) / (itemPerRow - 1) || 0; // prevent divide 0 21 | this.style.setProperty("--width", `${itemWidth}%`); 22 | this.style.setProperty("--margin", `${this.gapWidth}%`); 23 | this.index = 0; 24 | this.carouselItem = this.carousel.children; 25 | this.imgWidth = this.carouselItem[0].getBoundingClientRect().width; 26 | this.numberOfItem = this.carouselItem.length; 27 | this.step = 28 | this.imgWidth + (this.gapWidth / this.itemWidth) * this.imgWidth; //calculate the step to translate 29 | this.stepToMoveAutoplay = isRTL ? -1 : 1; 30 | 31 | if (loop) { 32 | //if loop is true, clone carousel item 33 | this.index = itemPerRow; 34 | this.carousel.style.transform = `translate3d(${ 35 | -this.index * this.step 36 | }px,0,0)`; 37 | for (let i = 0; i < this.itemPerRow; i++) { 38 | let cloneNode = this.carouselItem[i].cloneNode(true); 39 | this.carousel.insertAdjacentElement("beforeend", cloneNode); 40 | } 41 | let count = 0; 42 | while (count != this.itemPerRow) { 43 | let cloneNode = 44 | this.carouselItem[this.numberOfItem - 1].cloneNode(true); 45 | this.carousel.insertAdjacentElement("afterbegin", cloneNode); 46 | count++; 47 | } 48 | } 49 | 50 | this.translateSlide = () => { 51 | this.carousel.style.transform = `translate3d(${ 52 | -this.index * this.step 53 | }px,0,0)`; 54 | //just a function used many time, to translate slide 55 | }; 56 | 57 | this.resetSlide = (step) => { 58 | //reset slide, to make it loop 59 | if (this.index + step < 1) this.index = this.numberOfItem + 1; 60 | if (this.index + step >= this.numberOfItem + this.itemPerRow) 61 | this.index = this.itemPerRow - 1; 62 | this.carousel.style.transition = "none"; 63 | this.translateSlide(); 64 | //reset it silently by changing transition to none 65 | //then move it, i had delay 20ms because if we change inline style transition too fast, it will not work properly 66 | setTimeout(() => { 67 | this.carousel.style.transition = "all 0.25s"; 68 | this.index += step; 69 | this.translateSlide(); 70 | }, 20); 71 | }; 72 | 73 | this.moveSlide = (step) => { 74 | this.carousel.style.transition = "all 0.25s"; 75 | //every time we move slide, add transition 76 | if (loop) { 77 | if ( 78 | this.index + step < 1 || 79 | this.index + step >= this.numberOfItem + this.itemPerRow 80 | ) { 81 | this.resetSlide(step); 82 | } else { 83 | this.index += step; 84 | this.translateSlide(); 85 | } 86 | } else { 87 | if ( 88 | this.index + step < 0 || 89 | this.index + step > this.numberOfItem - this.itemPerRow 90 | ) 91 | return; 92 | else { 93 | this.index += step; 94 | this.translateSlide(); 95 | } 96 | } 97 | }; 98 | 99 | if (nav) { 100 | this.querySelector(".control__prev").addEventListener("click", () => { 101 | this.moveSlide(-1); 102 | }); 103 | this.querySelector(".control__next").addEventListener("click", () => { 104 | this.moveSlide(1); 105 | }); 106 | } 107 | 108 | if (draggable) { 109 | //if draggable is true, add draggable-support event, variables,... 110 | let firstPos = (currentPos = 0); 111 | 112 | let dragStartHandle = (e) => { 113 | this.carousel.style.transition = "none"; 114 | if (e.type === "touchstart") { 115 | currentPos = e.touches[0].clientX; 116 | document.addEventListener("touchmove", dragHandle); 117 | document.addEventListener("touchend", dragEndHandle); 118 | } else { 119 | this.carouselOuter.style.cursor = "grab"; 120 | currentPos = e.clientX; 121 | document.addEventListener("mousemove", dragHandle); 122 | document.addEventListener("mouseup", dragEndHandle); 123 | } 124 | firstPos = currentPos; 125 | //add necessary listener, style, reset first and current position 126 | if (autoplay) { 127 | clearInterval(intervalId); 128 | clearTimeout(timeoutId); 129 | } 130 | }; 131 | 132 | let dragHandle = (e) => { 133 | let currentMove = parseFloat( 134 | this.carousel.style.transform.slice(12) 135 | ); //get the x-axis of transform3d 136 | let currentIndex = -currentMove / this.step; 137 | 138 | let x = e.type === "touchmove" ? e.touches[0].clientX : e.clientX; //get current coordinate 139 | let distanceMoved = x - currentPos; 140 | if (loop) { 141 | if (currentIndex <= 0) { 142 | this.index = this.numberOfItem; 143 | this.translateSlide(); 144 | } else if ( 145 | currentIndex >= 146 | this.numberOfItem + this.itemPerRow 147 | ) { 148 | this.index = this.itemPerRow; 149 | this.translateSlide(); 150 | } else 151 | this.carousel.style.transform = `translate3d(${ 152 | currentMove + distanceMoved 153 | }px,0,0)`; 154 | } else { 155 | if ( 156 | currentIndex < 0 || 157 | currentIndex > this.numberOfItem - this.itemPerRow 158 | ) 159 | this.carousel.style.transform = `translate3d(${ 160 | currentMove + distanceMoved / 5 161 | }px,0,0)`; 162 | //when user drag out of bound, decrease speed 163 | else 164 | this.carousel.style.transform = `translate3d(${ 165 | currentMove + distanceMoved 166 | }px,0,0)`; 167 | } 168 | currentPos = x; 169 | }; 170 | 171 | this.checkIndex = (currentMove) => { 172 | let temp = currentMove; 173 | while (temp >= this.step) { 174 | temp -= this.step; 175 | } 176 | if ( 177 | (temp > 50 && firstPos - currentPos >= 0) || 178 | (this.step - temp < 50 && firstPos - currentPos <= 0) 179 | ) 180 | return Math.ceil(currentMove / this.step); 181 | return Math.floor(currentMove / this.step); 182 | }; // this function is used to check current index to determine move next or previous 183 | 184 | let dragEndHandle = (e) => { 185 | if (e.type === "touchend") { 186 | document.removeEventListener("touchmove", dragHandle); 187 | document.removeEventListener("touchend", dragEndHandle); 188 | } else { 189 | document.removeEventListener("mousemove", dragHandle); 190 | document.removeEventListener("mouseup", dragEndHandle); 191 | this.carouselOuter.style.cursor = "auto"; 192 | } 193 | //remove unnecessary event listener and style 194 | let currentMove = parseFloat( 195 | this.carousel.style.transform.slice(12) 196 | ); 197 | this.index = this.checkIndex(-currentMove); 198 | if (!loop) { 199 | if (this.index > this.numberOfItem - this.itemPerRow) 200 | this.index = this.numberOfItem - this.itemPerRow; 201 | if (this.index < 0) this.index = 0; 202 | } 203 | this.carousel.style.transition = "all 0.25s"; 204 | this.translateSlide(); 205 | if (autoplay) { 206 | clearInterval(intervalId); 207 | clearTimeout(timeoutId); 208 | timeoutId = setTimeout(() => { 209 | intervalId = setInterval( 210 | this.moveSlide, 211 | autoplay, 212 | this.stepToMoveAutoplay 213 | ); 214 | }, 2000); 215 | } 216 | }; 217 | 218 | this.carouselOuter.addEventListener("mousedown", dragStartHandle); 219 | this.carouselOuter.addEventListener("touchstart", dragStartHandle); 220 | // i had to create carouselOuter because carousel will be hidden when slide is working 221 | } 222 | 223 | if (mouseWheel) { 224 | this.carouselOuter.addEventListener("wheel", (e) => { 225 | e.preventDefault(); 226 | if (e.deltaY > 0) this.moveSlide(-1); 227 | else this.moveSlide(1); 228 | }); 229 | } 230 | 231 | let intervalId; 232 | let timeoutId; 233 | 234 | if (autoplay) { 235 | timeoutId = setTimeout(() => { 236 | intervalId = setInterval( 237 | this.moveSlide, 238 | autoplay, 239 | this.stepToMoveAutoplay 240 | ); 241 | }, 3000); 242 | if (stopAutoplayWhenHover) { 243 | this.carouselOuter.addEventListener("mouseenter", () => { 244 | clearTimeout(timeoutId); 245 | clearInterval(intervalId); 246 | }); 247 | this.carouselOuter.addEventListener("mouseleave", () => { 248 | clearInterval(intervalId); 249 | clearTimeout(timeoutId); 250 | timeoutId = setTimeout(() => { 251 | intervalId = setInterval( 252 | this.moveSlide, 253 | autoplay, 254 | this.stepToMoveAutoplay 255 | ); 256 | }, 2000); 257 | }); 258 | } 259 | } 260 | }; 261 | 262 | function debounce(fn, delay) { 263 | let id = null; 264 | return function (args) { 265 | clearTimeout(id); 266 | id = null; 267 | id = setTimeout(function () { 268 | fn.call(this, args); 269 | }, delay); 270 | }; 271 | } 272 | 273 | function responsive() { 274 | let windowWidth = window.innerWidth; 275 | let flag = false; 276 | let crsContainer = document.querySelectorAll(".own-carousel__container"); 277 | let containerArray = Array.from(crsContainer); 278 | containerArray.forEach((item) => { 279 | for (let property in item.responsive) { 280 | if (property >= windowWidth) { 281 | item.itemPerRow = item.responsive[property][0]; 282 | item.itemWidth = item.responsive[property][1]; 283 | flag = true; 284 | break; 285 | } 286 | } 287 | if (!flag) { 288 | //all property are smaller the window width, happen when we increase window width, reset these property to highest value 289 | item.itemPerRow = item.itemPerRowBig; 290 | item.itemWidth = item.itemWidthBig; 291 | } 292 | item.gapWidth = 293 | (100 - item.itemPerRow * item.itemWidth) / (item.itemPerRow - 1) || 294 | 0; 295 | item.style.setProperty("--width", `${item.itemWidth}%`); 296 | item.style.setProperty("--margin", `${item.gapWidth}%`); 297 | }); 298 | //divide into 2 phase to avoid wrong calculating for imgWidth 299 | containerArray.forEach((item) => { 300 | item.imgWidth = item.carouselItem[0].getBoundingClientRect().width; 301 | item.step = 302 | item.imgWidth + (item.gapWidth / item.itemWidth) * item.imgWidth; 303 | //change important property for responsive 304 | item.moveSlide(0); 305 | //fit carousel in correct position 306 | }); 307 | } 308 | 309 | window.addEventListener("resize", debounce(responsive, 500)); 310 | //reduce the number of execution for performance 311 | 312 | 313 | document.addEventListener('DOMContentLoaded', () => { 314 | const containers = document.querySelectorAll('.own-carousel__container'); 315 | containers.forEach(container => { 316 | const itemPerRow = parseInt(container.dataset.row); 317 | const itemWidth = parseInt(container.dataset.width); 318 | const loopAttributeValue = container.dataset.loop; 319 | const navAttributeValue = container.dataset.nav; 320 | const loop = loopAttributeValue === '1'; 321 | const nav = navAttributeValue === '1'; 322 | container.ownCarousel({ 323 | itemPerRow: itemPerRow, 324 | itemWidth: itemWidth, 325 | responsive: { 326 | 1000: [4, 24], 327 | 800: [3, 33], 328 | 600: [2, 49], 329 | 400: [1, 100] 330 | }, 331 | loop: loop, 332 | autoplay: 0, 333 | nav: nav 334 | }); 335 | }); 336 | 337 | window.addEventListener('resize', debounce(responsive, 500)); 338 | responsive(); 339 | }); 340 | -------------------------------------------------------------------------------- /resources/js/own-carousel.min.js: -------------------------------------------------------------------------------- 1 | Object.prototype.ownCarousel = function (options) { 2 | const { 3 | itemPerRow = 4, 4 | itemWidth = 24, 5 | loop = true, 6 | responsive = {}, 7 | draggable = true, 8 | mouseWheel = false, 9 | autoplay = 0, 10 | stopAutoplayWhenHover = true, 11 | nav = false, 12 | isRTL = false, 13 | } = options; 14 | //extract arguments 15 | this.carousel = this.querySelector(".own-carousel"); 16 | this.carouselOuter = this.querySelector(".own-carousel__outer"); 17 | this.itemWidthBig = this.itemWidth = itemWidth; 18 | this.itemPerRowBig = this.itemPerRow = itemPerRow; 19 | this.responsive = responsive; 20 | this.gapWidth = (100 - itemPerRow * itemWidth) / (itemPerRow - 1) || 0; // prevent divide 0 21 | this.style.setProperty("--width", `${itemWidth}%`); 22 | this.style.setProperty("--margin", `${this.gapWidth}%`); 23 | this.index = 0; 24 | this.carouselItem = this.carousel.children; 25 | this.imgWidth = this.carouselItem[0].getBoundingClientRect().width; 26 | this.numberOfItem = this.carouselItem.length; 27 | this.step = 28 | this.imgWidth + (this.gapWidth / this.itemWidth) * this.imgWidth; //calculate the step to translate 29 | this.stepToMoveAutoplay = isRTL ? -1 : 1; 30 | 31 | if (loop) { 32 | //if loop is true, clone carousel item 33 | this.index = itemPerRow; 34 | this.carousel.style.transform = `translate3d(${ 35 | -this.index * this.step 36 | }px,0,0)`; 37 | for (let i = 0; i < this.itemPerRow; i++) { 38 | let cloneNode = this.carouselItem[i].cloneNode(true); 39 | this.carousel.insertAdjacentElement("beforeend", cloneNode); 40 | } 41 | let count = 0; 42 | while (count != this.itemPerRow) { 43 | let cloneNode = 44 | this.carouselItem[this.numberOfItem - 1].cloneNode(true); 45 | this.carousel.insertAdjacentElement("afterbegin", cloneNode); 46 | count++; 47 | } 48 | } 49 | 50 | this.translateSlide = () => { 51 | this.carousel.style.transform = `translate3d(${ 52 | -this.index * this.step 53 | }px,0,0)`; 54 | //just a function used many time, to translate slide 55 | }; 56 | 57 | this.resetSlide = (step) => { 58 | //reset slide, to make it loop 59 | if (this.index + step < 1) this.index = this.numberOfItem + 1; 60 | if (this.index + step >= this.numberOfItem + this.itemPerRow) 61 | this.index = this.itemPerRow - 1; 62 | this.carousel.style.transition = "none"; 63 | this.translateSlide(); 64 | //reset it silently by changing transition to none 65 | //then move it, i had delay 20ms because if we change inline style transition too fast, it will not work properly 66 | setTimeout(() => { 67 | this.carousel.style.transition = "all 0.25s"; 68 | this.index += step; 69 | this.translateSlide(); 70 | }, 20); 71 | }; 72 | 73 | this.moveSlide = (step) => { 74 | this.carousel.style.transition = "all 0.25s"; 75 | //every time we move slide, add transition 76 | if (loop) { 77 | if ( 78 | this.index + step < 1 || 79 | this.index + step >= this.numberOfItem + this.itemPerRow 80 | ) { 81 | this.resetSlide(step); 82 | } else { 83 | this.index += step; 84 | this.translateSlide(); 85 | } 86 | } else { 87 | if ( 88 | this.index + step < 0 || 89 | this.index + step > this.numberOfItem - this.itemPerRow 90 | ) 91 | return; 92 | else { 93 | this.index += step; 94 | this.translateSlide(); 95 | } 96 | } 97 | }; 98 | 99 | if (nav) { 100 | this.querySelector(".control__prev").addEventListener("click", () => { 101 | this.moveSlide(-1); 102 | }); 103 | this.querySelector(".control__next").addEventListener("click", () => { 104 | this.moveSlide(1); 105 | }); 106 | } 107 | 108 | if (draggable) { 109 | //if draggable is true, add draggable-support event, variables,... 110 | let firstPos = (currentPos = 0); 111 | 112 | let dragStartHandle = (e) => { 113 | this.carousel.style.transition = "none"; 114 | if (e.type === "touchstart") { 115 | currentPos = e.touches[0].clientX; 116 | document.addEventListener("touchmove", dragHandle); 117 | document.addEventListener("touchend", dragEndHandle); 118 | } else { 119 | this.carouselOuter.style.cursor = "grab"; 120 | currentPos = e.clientX; 121 | document.addEventListener("mousemove", dragHandle); 122 | document.addEventListener("mouseup", dragEndHandle); 123 | } 124 | firstPos = currentPos; 125 | //add necessary listener, style, reset first and current position 126 | if (autoplay) { 127 | clearInterval(intervalId); 128 | clearTimeout(timeoutId); 129 | } 130 | }; 131 | 132 | let dragHandle = (e) => { 133 | let currentMove = parseFloat( 134 | this.carousel.style.transform.slice(12) 135 | ); //get the x-axis of transform3d 136 | let currentIndex = -currentMove / this.step; 137 | 138 | let x = e.type === "touchmove" ? e.touches[0].clientX : e.clientX; //get current coordinate 139 | let distanceMoved = x - currentPos; 140 | if (loop) { 141 | if (currentIndex <= 0) { 142 | this.index = this.numberOfItem; 143 | this.translateSlide(); 144 | } else if ( 145 | currentIndex >= 146 | this.numberOfItem + this.itemPerRow 147 | ) { 148 | this.index = this.itemPerRow; 149 | this.translateSlide(); 150 | } else 151 | this.carousel.style.transform = `translate3d(${ 152 | currentMove + distanceMoved 153 | }px,0,0)`; 154 | } else { 155 | if ( 156 | currentIndex < 0 || 157 | currentIndex > this.numberOfItem - this.itemPerRow 158 | ) 159 | this.carousel.style.transform = `translate3d(${ 160 | currentMove + distanceMoved / 5 161 | }px,0,0)`; 162 | //when user drag out of bound, decrease speed 163 | else 164 | this.carousel.style.transform = `translate3d(${ 165 | currentMove + distanceMoved 166 | }px,0,0)`; 167 | } 168 | currentPos = x; 169 | }; 170 | 171 | this.checkIndex = (currentMove) => { 172 | let temp = currentMove; 173 | while (temp >= this.step) { 174 | temp -= this.step; 175 | } 176 | if ( 177 | (temp > 50 && firstPos - currentPos >= 0) || 178 | (this.step - temp < 50 && firstPos - currentPos <= 0) 179 | ) 180 | return Math.ceil(currentMove / this.step); 181 | return Math.floor(currentMove / this.step); 182 | }; // this function is used to check current index to determine move next or previous 183 | 184 | let dragEndHandle = (e) => { 185 | if (e.type === "touchend") { 186 | document.removeEventListener("touchmove", dragHandle); 187 | document.removeEventListener("touchend", dragEndHandle); 188 | } else { 189 | document.removeEventListener("mousemove", dragHandle); 190 | document.removeEventListener("mouseup", dragEndHandle); 191 | this.carouselOuter.style.cursor = "auto"; 192 | } 193 | //remove unnecessary event listener and style 194 | let currentMove = parseFloat( 195 | this.carousel.style.transform.slice(12) 196 | ); 197 | this.index = this.checkIndex(-currentMove); 198 | if (!loop) { 199 | if (this.index > this.numberOfItem - this.itemPerRow) 200 | this.index = this.numberOfItem - this.itemPerRow; 201 | if (this.index < 0) this.index = 0; 202 | } 203 | this.carousel.style.transition = "all 0.25s"; 204 | this.translateSlide(); 205 | if (autoplay) { 206 | clearInterval(intervalId); 207 | clearTimeout(timeoutId); 208 | timeoutId = setTimeout(() => { 209 | intervalId = setInterval( 210 | this.moveSlide, 211 | autoplay, 212 | this.stepToMoveAutoplay 213 | ); 214 | }, 2000); 215 | } 216 | }; 217 | 218 | this.carouselOuter.addEventListener("mousedown", dragStartHandle); 219 | this.carouselOuter.addEventListener("touchstart", dragStartHandle); 220 | // i had to create carouselOuter because carousel will be hidden when slide is working 221 | } 222 | 223 | if (mouseWheel) { 224 | this.carouselOuter.addEventListener("wheel", (e) => { 225 | e.preventDefault(); 226 | if (e.deltaY > 0) this.moveSlide(-1); 227 | else this.moveSlide(1); 228 | }); 229 | } 230 | 231 | let intervalId; 232 | let timeoutId; 233 | 234 | if (autoplay) { 235 | timeoutId = setTimeout(() => { 236 | intervalId = setInterval( 237 | this.moveSlide, 238 | autoplay, 239 | this.stepToMoveAutoplay 240 | ); 241 | }, 3000); 242 | if (stopAutoplayWhenHover) { 243 | this.carouselOuter.addEventListener("mouseenter", () => { 244 | clearTimeout(timeoutId); 245 | clearInterval(intervalId); 246 | }); 247 | this.carouselOuter.addEventListener("mouseleave", () => { 248 | clearInterval(intervalId); 249 | clearTimeout(timeoutId); 250 | timeoutId = setTimeout(() => { 251 | intervalId = setInterval( 252 | this.moveSlide, 253 | autoplay, 254 | this.stepToMoveAutoplay 255 | ); 256 | }, 2000); 257 | }); 258 | } 259 | } 260 | }; 261 | 262 | function debounce(fn, delay) { 263 | let id = null; 264 | return function (args) { 265 | clearTimeout(id); 266 | id = null; 267 | id = setTimeout(function () { 268 | fn.call(this, args); 269 | }, delay); 270 | }; 271 | } 272 | 273 | function responsive() { 274 | let windowWidth = window.innerWidth; 275 | let flag = false; 276 | let crsContainer = document.querySelectorAll(".own-carousel__container"); 277 | let containerArray = Array.from(crsContainer); 278 | containerArray.forEach((item) => { 279 | for (let property in item.responsive) { 280 | if (property >= windowWidth) { 281 | item.itemPerRow = item.responsive[property][0]; 282 | item.itemWidth = item.responsive[property][1]; 283 | flag = true; 284 | break; 285 | } 286 | } 287 | if (!flag) { 288 | //all property are smaller the window width, happen when we increase window width, reset these property to highest value 289 | item.itemPerRow = item.itemPerRowBig; 290 | item.itemWidth = item.itemWidthBig; 291 | } 292 | item.gapWidth = 293 | (100 - item.itemPerRow * item.itemWidth) / (item.itemPerRow - 1) || 294 | 0; 295 | item.style.setProperty("--width", `${item.itemWidth}%`); 296 | item.style.setProperty("--margin", `${item.gapWidth}%`); 297 | }); 298 | //divide into 2 phase to avoid wrong calculating for imgWidth 299 | containerArray.forEach((item) => { 300 | item.imgWidth = item.carouselItem[0].getBoundingClientRect().width; 301 | item.step = 302 | item.imgWidth + (item.gapWidth / item.itemWidth) * item.imgWidth; 303 | //change important property for responsive 304 | item.moveSlide(0); 305 | //fit carousel in correct position 306 | }); 307 | } 308 | 309 | window.addEventListener("resize", debounce(responsive, 500)); 310 | //reduce the number of execution for performance 311 | 312 | 313 | document.addEventListener('DOMContentLoaded', () => { 314 | const containers = document.querySelectorAll('.own-carousel__container'); 315 | containers.forEach(container => { 316 | const itemPerRow = parseInt(container.dataset.row); 317 | const itemWidth = parseInt(container.dataset.width); 318 | const loopAttributeValue = container.dataset.loop; 319 | const navAttributeValue = container.dataset.nav; 320 | const loop = loopAttributeValue === '1'; 321 | const nav = navAttributeValue === '1'; 322 | container.ownCarousel({ 323 | itemPerRow: itemPerRow, 324 | itemWidth: itemWidth, 325 | responsive: { 326 | 1000: [4, 24], 327 | 800: [3, 33], 328 | 600: [2, 49], 329 | 400: [1, 100] 330 | }, 331 | loop: loop, 332 | autoplay: 0, 333 | nav: nav 334 | }); 335 | }); 336 | 337 | window.addEventListener('resize', debounce(responsive, 500)); 338 | responsive(); 339 | }); 340 | --------------------------------------------------------------------------------