├── .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 |
10 |
11 |
12 | @foreach($items as $item)
13 | @continue(!isset($item))
14 |
{{ $item->render() }}
15 | @endforeach
16 |
17 |
18 | @if($nav)
19 |
20 |
25 |
30 |
31 | @endif
32 |
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 |
--------------------------------------------------------------------------------