pure-swipe-slider 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kellas
4
+ Based on Swipe.js by Brad Birdsall (Copyright (c) 2013, MIT License)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # Pure Swipe Slider Web Component
2
+
3
+ [![Bundle Size](https://img.shields.io/badge/gzipped_size-%3C_4_KB-blue)](https://github.com/alexstep/swipe-slider)
4
+ [![Dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen)](https://github.com/alexstep/swipe-slider)
5
+ [![License](https://img.shields.io/badge/license-MIT-yellow)](./LICENSE)
6
+ [![NPM Version](https://img.shields.io/npm/v/pure-swipe-slider)](https://www.npmjs.com/package/pure-swipe-slider)
7
+
8
+ [**Live Demo**](https://alexstep.github.io/swipe-slider/demo.html)
9
+
10
+ A tiny, zero-dependency swipe slider as a Web Component (< 4 KB gzipped).
11
+ Framework-agnostic, mobile-first, event-driven.
12
+
13
+ Built on top of Swipe.js with modernized internals, Pointer Events, GPU-accelerated transforms, and a clean Custom Events interface.
14
+
15
+ ## Two ways to use
16
+
17
+ - `<swipe-slider>` — Web Component for 99% of cases
18
+ - `swipe3.js` — low-level engine for advanced / legacy layouts
19
+
20
+ ## Features
21
+
22
+ - **Full API access**: call `next()`, `prev()`, `slide()` directly on the element
23
+ - **Unified input handling**: touch, mouse, wheel, pen via Pointer Events
24
+ - **Mobile-optimized**: smooth 60fps animations with iOS overscroll handling
25
+ - **Framework-ready**: standard HTML tag with Custom Events
26
+ - **Flexible content**: images, dates, complex layouts, and dynamic slides
27
+ - **Performance-first**: GPU acceleration and modern optimizations
28
+ - **No build step**: works directly in modern browsers
29
+
30
+ ## Installation
31
+
32
+ ### From NPM
33
+
34
+ ```bash
35
+ npm install pure-swipe-slider
36
+ ```
37
+
38
+ ### Quick Start
39
+
40
+ Simply import the component with default tag registration:
41
+
42
+ ```javascript
43
+ import 'pure-swipe-slider/register.js';
44
+ ```
45
+
46
+ ### Custom Tag Registration
47
+
48
+ Import the class separately to register with your own tag name:
49
+
50
+ ```javascript
51
+ import SwipeSlider from 'pure-swipe-slider';
52
+
53
+ // Register with custom tag name
54
+ customElements.define('my-slider', SwipeSlider);
55
+ ```
56
+
57
+ Then use it in HTML:
58
+
59
+ ```html
60
+ <my-slider>
61
+ <div>Slide 1</div>
62
+ <div>Slide 2</div>
63
+ </my-slider>
64
+ ```
65
+
66
+ ## Class Usage
67
+
68
+ The `SwipeSlider` class can be extended for custom behavior:
69
+
70
+ ```javascript
71
+ import SwipeSlider from './swipe-slider.js';
72
+
73
+ class MyCustomSlider extends SwipeSlider {
74
+ connectedCallback() {
75
+ super.connectedCallback();
76
+ // Custom initialization
77
+ this.style.border = '2px solid blue';
78
+ }
79
+ }
80
+
81
+ // Register custom class
82
+ customElements.define('my-custom-slider', MyCustomSlider);
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ ### Basic Example
88
+
89
+ ```html
90
+ <swipe-slider draggable mousewheel>
91
+ <div>Slide 1</div>
92
+ <div>Slide 2</div>
93
+ <div>Slide 3</div>
94
+ </swipe-slider>
95
+ ```
96
+
97
+ With default tag registration:
98
+
99
+ ```javascript
100
+ import './register.js';
101
+ ```
102
+
103
+ ### With Event Listeners
104
+
105
+ ```javascript
106
+ const slider = document.querySelector('swipe-slider');
107
+
108
+ slider.addEventListener('swipe:change', (e) => {
109
+ const { index, element, direction } = e.detail;
110
+ console.log(`Swiped to index ${index} in direction ${direction}`);
111
+ });
112
+ ```
113
+
114
+ ## API Reference
115
+
116
+ ### Attributes
117
+
118
+ Boolean attributes follow HTML semantics: presence = `true`, absence = `false`.
119
+
120
+ | Attribute | Type | Default | Description |
121
+ |-----------|------|---------|-------------|
122
+ | `start-slide` | Number | `0` | Initial slide index. |
123
+ | `speed` | Number | `400` | Transition speed in milliseconds. |
124
+ | `draggable` | Boolean | `false` | Enable mouse dragging. |
125
+ | `mousewheel` | Boolean | `true` | Enable mouse wheel navigation. Use `no-mousewheel` to disable. |
126
+ | `disable-scroll` | Boolean | `false` | Disable vertical scrolling while swiping. |
127
+ | `stop-propagation` | Boolean | `false` | Stop event propagation. |
128
+ | `passive-events` | Boolean | `false` | Use passive event listeners (may improve performance but breaks preventDefault). |
129
+ | `loop` | Boolean | `false` | Enable infinite circular loop (moves slides dynamically). |
130
+ | `auto-height` | Boolean \| Number | `false` | Enable automatic height adjustment. Optional value sets min-height. |
131
+
132
+ ### Events
133
+
134
+ All events are dispatched as CustomEvent with `{ bubbles: true, composed: true }`.
135
+
136
+ | Event | Detail | Description |
137
+ |-------|--------|-------------|
138
+ | `swipe:change` | `{ index, element, direction }` | Fired when the active slide changes. |
139
+ | `swipe:transition-end` | `{ index, element }` | Fired when the transition finishes. |
140
+ | `swipe:drag-start` | `{ index, element }` | Fired when dragging starts. |
141
+ | `swipe:drag-end` | `{ index, element }` | Fired when dragging ends. |
142
+ | `swipe:move` | `none` | Fired during movement (high-frequency event, use sparingly). |
143
+
144
+ ### Methods
145
+
146
+ All `swipe.js` methods are proxied:
147
+
148
+ - `slide(to, speed)`
149
+ - `prev()`
150
+ - `next()`
151
+ - `getPos()`
152
+ - `getNumSlides()`
153
+ - `kill()`
154
+ - `setup(options)`
155
+ - `appendSlide(element)`
156
+ - `prependSlide(element)`
157
+ - `adjustHeight(element)`
158
+
159
+ ## Direct Usage (swipe3.js)
160
+
161
+ For advanced use cases or when you don't need the web component wrapper, you can use `swipe3.js` directly:
162
+
163
+ ### Import
164
+
165
+ ```javascript
166
+ import Swipe from './swipe3.js';
167
+ ```
168
+
169
+ ### HTML Structure
170
+
171
+ ```html
172
+ <div id="slider-container">
173
+ <div class="slides">
174
+ <div>Slide 1</div>
175
+ <div>Slide 2</div>
176
+ <div>Slide 3</div>
177
+ </div>
178
+ </div>
179
+ ```
180
+
181
+ ### JavaScript Usage
182
+
183
+ ```javascript
184
+ // Initialize
185
+ const container = document.getElementById('slider-container');
186
+ const swipeInstance = Swipe(container, {
187
+ speed: 400, // Transition speed in ms
188
+ startSlide: 0, // Initial slide index
189
+ draggable: true, // Enable mouse dragging
190
+ mousewheel: true, // Enable mouse wheel navigation
191
+ disableScroll: false, // Prevent vertical scrolling during swipe
192
+ stopPropagation: false, // Stop event propagation
193
+ passive: false, // Use passive event listeners
194
+
195
+ // Callbacks
196
+ callback(index, element, direction) {
197
+ console.log(`Swiped to slide ${index} in direction ${direction}`);
198
+ },
199
+
200
+ transitionEnd(index, element) {
201
+ console.log(`Transition ended at slide ${index}`);
202
+ },
203
+
204
+ dragStart(index, element) {
205
+ console.log(`Drag started at slide ${index}`);
206
+ },
207
+
208
+ dragEnd(index, element) {
209
+ console.log(`Drag ended at slide ${index}`);
210
+ },
211
+
212
+ runMove() {
213
+ console.log('Moving during drag');
214
+ }
215
+ });
216
+
217
+ // Use API methods
218
+ swipeInstance.next(); // Go to next slide
219
+ swipeInstance.prev(); // Go to previous slide
220
+ swipeInstance.slide(2); // Go to slide at index 2
221
+ swipeInstance.getPos(); // Get current slide index
222
+ swipeInstance.getNumSlides(); // Get total number of slides
223
+
224
+ // Add slides dynamically
225
+ const newSlide = document.createElement('div');
226
+ newSlide.textContent = 'New Slide';
227
+ swipeInstance.appendSlide(newSlide);
228
+
229
+ // Cleanup when done
230
+ swipeInstance.kill();
231
+ ```
232
+
233
+ ### Required CSS
234
+
235
+ When using directly, you'll need to add basic CSS:
236
+
237
+ ```css
238
+ #slider-container {
239
+ position: relative;
240
+ overflow: hidden;
241
+ width: 100%;
242
+ }
243
+
244
+ .slides {
245
+ position: relative;
246
+ white-space: nowrap;
247
+ }
248
+
249
+ .slides > * {
250
+ display: inline-block;
251
+ vertical-align: top;
252
+ white-space: normal;
253
+ }
254
+ ```
255
+
256
+ ### Options Reference
257
+
258
+ | Option | Type | Default | Description |
259
+ |--------|------|---------|-------------|
260
+ | `speed` | Number | `400` | Transition speed in milliseconds |
261
+ | `startSlide` | Number | `0` | Initial slide index |
262
+ | `draggable` | Boolean | `false` | Enable mouse dragging |
263
+ | `mousewheel` | Boolean | `true` | Enable mouse wheel navigation |
264
+ | `disableScroll` | Boolean | `false` | Disable vertical scrolling while swiping |
265
+ | `stopPropagation` | Boolean | `false` | Stop event propagation |
266
+ | `ignore` | String | `null` | CSS selector for elements to ignore (e.g., "button, a") |
267
+ | `passive` | Boolean | `false` | Use passive event listeners (may improve performance but breaks preventDefault) |
268
+
269
+ ### Callback Functions
270
+
271
+ All callbacks receive `(index, element)` parameters where:
272
+ - `index`: Current slide index (0-based)
273
+ - `element`: Current slide DOM element
274
+
275
+ | Callback | Parameters | Description |
276
+ |----------|------------|-------------|
277
+ | `callback` | `(index, element, direction)` | Fired when slide changes (direction: -1 for prev, 1 for next) |
278
+ | `transitionEnd` | `(index, element)` | Fired when transition animation completes |
279
+ | `dragStart` | `(index, element)` | Fired when dragging starts |
280
+ | `dragEnd` | `(index, element)` | Fired when dragging ends |
281
+ | `runMove` | `none` | Fired during dragging movement |
282
+
283
+ ### API Methods
284
+
285
+ | Method | Parameters | Description |
286
+ |--------|------------|-------------|
287
+ | `slide(to, speed?)` | `to`: slide index, `speed?`: optional transition speed | Navigate to specific slide |
288
+ | `prev()` | none | Go to previous slide |
289
+ | `next()` | none | Go to next slide |
290
+ | `getPos()` | none | Get current slide index |
291
+ | `getNumSlides()` | none | Get total number of slides |
292
+ | `appendSlide(element)` | `element`: DOM element | Add slide at the end |
293
+ | `prependSlide(element)` | `element`: DOM element | Add slide at the beginning |
294
+ | `setup(options?)` | `options?`: new options object | Reinitialize with new options |
295
+ | `kill()` | none | Destroy instance and cleanup events/styles |
296
+
297
+ ## CSS Customization
298
+
299
+ The component provides a minimal CSS structure with a built-in height transition (0.2s linear) for `auto-height` mode. You can customize the look using standard CSS:
300
+
301
+ ```css
302
+ swipe-slider {
303
+ /* container styles */
304
+ }
305
+
306
+ .swipe-slider-wrapper > * {
307
+ /* slide styles */
308
+ }
309
+ ```
310
+
311
+ ## When this is not a good fit
312
+
313
+ - If you need virtualized slides (1000+ items)
314
+ - If you rely heavily on React/Vue-specific lifecycles
315
+ - If you need autoplay / pagination / thumbnails out of the box
316
+ - If you need complex state management or data binding
317
+
318
+ ## License
319
+
320
+ MIT
321
+
322
+ ## Performance Optimizations (swipe3.js)
323
+
324
+ The internal `swipe3.js` engine includes these optimizations for smooth mobile performance:
325
+
326
+ | Optimization | Description |
327
+ |-------------|-------------|
328
+ | GPU-accelerated transforms | `translate3d` for hardware acceleration instead of `translateX` |
329
+ | will-change hints | Browser hints to create compositor layers for smoother animations |
330
+ | Pointer Events API | Unified touch/mouse/pen input handling with fallbacks |
331
+ | Passive event listeners | Non-blocking touch listeners where possible (via `passive-events` attr) |
332
+ | Reduced layout thrashing | Batched DOM reads/writes to minimize reflows |
333
+ | Modern ES6+ syntax | Latest JavaScript features for better performance and maintainability |
334
+ | Size (Gzipped) | < 4 KB for the entire component (JS + CSS) |
335
+
336
+ ## Size
337
+
338
+ | Metric | JavaScript | CSS | Total |
339
+ |--------|------------|-----|-------|
340
+ | Minified | 9.8 KB | 0.3 KB | **10.1 KB** |
341
+ | Minified + Gzip | 3.2 KB | 0.2 KB | **3.4 KB** |
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pure-swipe-slider",
3
+ "version": "1.0.0",
4
+ "description": "A tiny, zero-dependency swipe slider web component (< 4 KB gzipped).",
5
+ "main": "swipe-slider.js",
6
+ "module": "swipe-slider.js",
7
+ "files": [
8
+ "swipe-slider.js",
9
+ "swipe3.js",
10
+ "swipe-slider.css",
11
+ "register.js",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/alexstep/swipe-slider.git"
21
+ },
22
+ "keywords": [
23
+ "web-component",
24
+ "swipe",
25
+ "slider",
26
+ "carousel",
27
+ "touch",
28
+ "mobile-first",
29
+ "pointer-events"
30
+ ],
31
+ "author": "alexstep",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/alexstep/swipe-slider/issues"
35
+ },
36
+ "homepage": "https://github.com/alexstep/swipe-slider#readme"
37
+ }
package/register.js ADDED
@@ -0,0 +1,4 @@
1
+ import './swipe-slider.css'
2
+ import SwipeSlider from './swipe-slider.js'
3
+
4
+ customElements.define('swipe-slider', SwipeSlider)
@@ -0,0 +1,24 @@
1
+ swipe-slider {
2
+ display: block;
3
+ width: 100%;
4
+ min-width: 0;
5
+ overflow: hidden;
6
+ position: relative;
7
+ transition: height 0.2s linear;
8
+ will-change: height;
9
+
10
+ .swipe-slider-wrapper {
11
+ overflow: hidden;
12
+ position: relative;
13
+ height: 100%;
14
+ touch-action: pan-y;
15
+
16
+ & > * {
17
+ float: left;
18
+ width: 100%;
19
+ position: relative;
20
+ box-sizing: border-box;
21
+ will-change: transform;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,168 @@
1
+ import Swipe from './swipe3.js'
2
+
3
+ /**
4
+ * Universal Swipe Slider Web Component
5
+ * Wrapper around swipe.js
6
+ */
7
+ export default class SwipeSlider extends HTMLElement {
8
+ constructor() {
9
+ super()
10
+ this.swipe = null
11
+ }
12
+
13
+ connectedCallback() {
14
+ this.init()
15
+ }
16
+ disconnectedCallback() {
17
+ this.kill()
18
+ }
19
+
20
+ _emit(name, detail = {}) {
21
+ this.dispatchEvent(new CustomEvent(`swipe:${name}`, { detail, bubbles: true }))
22
+ }
23
+
24
+ _idx(el, i) {
25
+ const li = el?.dataset?.logicalIndex
26
+ return li !== undefined ? +li : i
27
+ }
28
+
29
+ init(options = {}) {
30
+ if (this.swipe) this.kill()
31
+
32
+ const get = a => this.getAttribute(a)
33
+ const has = a => this.hasAttribute(a)
34
+ const autoH = get('auto-height')
35
+ const isLoop = has('loop')
36
+
37
+ const sOpts = {
38
+ startSlide: +(get('start-slide') || 0),
39
+ speed: +(get('speed') || 400),
40
+ draggable: has('draggable'),
41
+ mousewheel: !has('no-mousewheel'),
42
+ disableScroll: has('disable-scroll'),
43
+ stopPropagation: has('stop-propagation'),
44
+ passive: has('passive-events'),
45
+ ...options,
46
+ callback: (i, el, dir) => {
47
+ if (autoH !== null) this.adjustHeight(el)
48
+
49
+ if (isLoop && this.swipe) {
50
+ const total = this.swipe.getNumSlides()
51
+ const method = i === total - 1 ? 'appendSlide' : i === 0 ? 'prependSlide' : null
52
+ if (method) {
53
+ const wrapper = this.querySelector('.swipe-slider-wrapper')
54
+ const slide = method[0] === 'a' ? wrapper?.firstElementChild : wrapper?.lastElementChild
55
+ if (slide) this[method](slide)
56
+ }
57
+ }
58
+
59
+ this._emit('change', { index: this._idx(el, i), element: el, direction: dir })
60
+ options.callback?.(i, el, dir)
61
+ },
62
+ transitionEnd: (i, el) => {
63
+ this._emit('transition-end', { index: i, element: el })
64
+ options.transitionEnd?.(i, el)
65
+ },
66
+ dragStart: (i, el) => {
67
+ this._emit('drag-start', { index: i, element: el })
68
+ options.dragStart?.(i, el)
69
+ },
70
+ dragEnd: (i, el) => {
71
+ this._emit('drag-end', { index: i, element: el })
72
+ options.dragEnd?.(i, el)
73
+ },
74
+ runMove: () => {
75
+ this._emit('move')
76
+ options.runMove?.()
77
+ },
78
+ }
79
+
80
+ let wrapper = this.querySelector('.swipe-slider-wrapper')
81
+ if (!wrapper) {
82
+ wrapper = document.createElement('div')
83
+ wrapper.className = 'swipe-slider-wrapper'
84
+ let i = 0
85
+ while (this.firstChild) {
86
+ const child = this.firstChild
87
+ if (child instanceof HTMLElement) child.dataset.logicalIndex = String(i++)
88
+ wrapper.appendChild(child)
89
+ }
90
+ this.appendChild(wrapper)
91
+ } else {
92
+ ;[...wrapper.children].forEach((c, i) => {
93
+ if (c instanceof HTMLElement && !c.dataset.logicalIndex) c.dataset.logicalIndex = String(i)
94
+ })
95
+ }
96
+
97
+ if (isLoop && wrapper.children.length > 1) {
98
+ const first = wrapper.firstElementChild
99
+ const last = wrapper.lastElementChild
100
+ if (sOpts.startSlide === 0 && last) {
101
+ wrapper.prepend(last)
102
+ sOpts.startSlide = 1
103
+ } else if (sOpts.startSlide === wrapper.children.length - 1 && first) {
104
+ wrapper.append(first)
105
+ sOpts.startSlide = wrapper.children.length - 2
106
+ }
107
+ }
108
+
109
+ requestAnimationFrame(() => {
110
+ // @ts-ignore
111
+ this.swipe = new Swipe(this, sOpts)
112
+ if (autoH !== null) {
113
+ const slides = this.querySelectorAll('.swipe-slider-wrapper > *')
114
+ const active = this.swipe?.getPos()
115
+ if (active !== undefined && slides[active]) this.adjustHeight(slides[active])
116
+ }
117
+ this.dataset.ready = 'true'
118
+ })
119
+ }
120
+
121
+ adjustHeight(el) {
122
+ if (!el) return
123
+ const min = +(this.getAttribute('auto-height') || 0)
124
+ this.style.height = Math.max(el.offsetHeight, min) + 'px'
125
+ }
126
+
127
+ slide(to, s) {
128
+ return this.swipe?.slide(to, s)
129
+ }
130
+ prev() {
131
+ if (this.hasAttribute('loop') && this.swipe?.getPos() === 0) {
132
+ const s = this.querySelector('.swipe-slider-wrapper')?.lastElementChild
133
+ if (s instanceof HTMLElement) this.prependSlide(s)
134
+ }
135
+ return this.swipe?.prev()
136
+ }
137
+ next() {
138
+ if (this.hasAttribute('loop') && this.swipe?.getPos() === this.swipe?.getNumSlides() - 1) {
139
+ const s = this.querySelector('.swipe-slider-wrapper')?.firstElementChild
140
+ if (s instanceof HTMLElement) this.appendSlide(s)
141
+ }
142
+ return this.swipe?.next()
143
+ }
144
+ getPos() {
145
+ const i = this.swipe ? this.swipe.getPos() : 0
146
+ const slides = this.querySelectorAll('.swipe-slider-wrapper > *')
147
+ return this._idx(slides[i], i)
148
+ }
149
+ getNumSlides() {
150
+ return this.swipe?.getNumSlides()
151
+ }
152
+ kill() {
153
+ if (this.swipe) {
154
+ this.swipe.kill()
155
+ this.swipe = null
156
+ }
157
+ delete this.dataset.ready
158
+ }
159
+ setup(o) {
160
+ this.swipe?.setup(o)
161
+ }
162
+ appendSlide(s) {
163
+ this.swipe?.appendSlide(s)
164
+ }
165
+ prependSlide(s) {
166
+ this.swipe?.prependSlide(s)
167
+ }
168
+ }
package/swipe3.js ADDED
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Swipe 3.0 - Optimized for mobile performance
3
+ * Based on https://github.com/thebird/Swipe
4
+ */
5
+
6
+ const root = typeof self === 'object' && self.self === self ? self : typeof globalThis === 'object' ? globalThis : this
7
+
8
+ function debounce(fn, delay = 150) {
9
+ let timeoutId = null
10
+
11
+ function debounced(...args) {
12
+ if (timeoutId) clearTimeout(timeoutId)
13
+ timeoutId = setTimeout(() => {
14
+ timeoutId = null
15
+ fn.apply(this, args)
16
+ }, delay)
17
+ }
18
+
19
+ debounced.cancel = () => {
20
+ if (timeoutId) {
21
+ clearTimeout(timeoutId)
22
+ timeoutId = null
23
+ }
24
+ }
25
+
26
+ return debounced
27
+ }
28
+
29
+ function isCancelable(event) {
30
+ return event && (typeof event.cancelable !== 'boolean' || event.cancelable)
31
+ }
32
+
33
+ const supports = {
34
+ pointerEvents: 'PointerEvent' in root,
35
+ touch: 'ontouchstart' in root,
36
+ }
37
+
38
+ const EASING = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
39
+
40
+ function Swipe(container, options = {}) {
41
+ if (!container) return null
42
+
43
+ const config = {
44
+ speed: 400,
45
+ startSlide: 0,
46
+ draggable: false,
47
+ mousewheel: true,
48
+ disableScroll: false,
49
+ stopPropagation: false,
50
+ ignore: null,
51
+ ...options,
52
+ }
53
+
54
+ let start = {}
55
+ let delta = {}
56
+ let isScrolling
57
+ let index = parseInt(config.startSlide, 10) || 0
58
+
59
+ const element = container.children[0]
60
+ if (!element) return null
61
+
62
+ let slides, slidePos, width, length
63
+
64
+ const slideDir = (() => {
65
+ const dir = root.getComputedStyle?.(container, null)?.getPropertyValue('direction')
66
+ return dir === 'rtl' ? 'right' : 'left'
67
+ })()
68
+
69
+ const debouncedSetup = debounce(setup, 150)
70
+ let wheelEndTimeout = null
71
+
72
+ const run = (name, ...args) => config[name]?.(...args)
73
+
74
+ // Unified event handlers
75
+ const events = {
76
+ handleEvent(event) {
77
+ switch (event.type) {
78
+ case 'pointerdown':
79
+ this.onStart(event, event.clientX, event.clientY, event.pointerId)
80
+ break
81
+ case 'pointermove':
82
+ if (event.isPrimary && event.pointerId === start.pointerId) {
83
+ this.onMove(event, event.clientX, event.clientY)
84
+ }
85
+ break
86
+ case 'pointerup':
87
+ case 'pointercancel':
88
+ case 'pointerleave':
89
+ this.onEnd(event, 'pointer')
90
+ break
91
+
92
+ case 'touchstart':
93
+ if (event.touches[0]) {
94
+ this.onStart(event, event.touches[0].pageX, event.touches[0].pageY)
95
+ }
96
+ break
97
+ case 'touchmove':
98
+ if (event.touches.length === 1 && !(event.scale && event.scale !== 1)) {
99
+ this.onMove(event, event.touches[0].pageX, event.touches[0].pageY)
100
+ }
101
+ break
102
+ case 'touchend':
103
+ this.onEnd(event, 'touch')
104
+ break
105
+
106
+ case 'mousedown':
107
+ event.preventDefault()
108
+ this.onStart(event, event.pageX, event.pageY)
109
+ break
110
+ case 'mousemove':
111
+ this.onMove(event, event.pageX, event.pageY)
112
+ break
113
+ case 'mouseup':
114
+ case 'mouseleave':
115
+ this.onEnd(event, 'mouse')
116
+ break
117
+
118
+ case 'transitionend':
119
+ if (parseInt(event.target.getAttribute('data-index'), 10) === index) {
120
+ run('transitionEnd', index, slides[index])
121
+ }
122
+ break
123
+ case 'resize':
124
+ debouncedSetup()
125
+ break
126
+ case 'wheel':
127
+ this.onWheel(event)
128
+ break
129
+ }
130
+
131
+ if (config.stopPropagation) event.stopPropagation()
132
+ },
133
+
134
+ captured: false,
135
+
136
+ onStart(event, x, y, pointerId) {
137
+ if (event.type === 'pointerdown' && !event.isPrimary) return
138
+ if (config.ignore && event.target.matches(config.ignore)) return
139
+
140
+ start = { x, y, time: Date.now(), pointerId }
141
+ isScrolling = undefined
142
+ delta = {}
143
+ this.captured = false
144
+
145
+ if (event.type === 'pointerdown') {
146
+ element.addEventListener('pointermove', this, { passive: false })
147
+ element.addEventListener('pointerup', this)
148
+ element.addEventListener('pointercancel', this)
149
+ element.addEventListener('pointerleave', this)
150
+ } else if (event.type === 'touchstart') {
151
+ element.addEventListener('touchmove', this, { passive: false })
152
+ element.addEventListener('touchend', this)
153
+ } else {
154
+ element.addEventListener('mousemove', this)
155
+ element.addEventListener('mouseup', this)
156
+ element.addEventListener('mouseleave', this)
157
+ }
158
+
159
+ run('dragStart', index, slides[index])
160
+ },
161
+
162
+ onMove(event, x, y) {
163
+ delta = { x: x - start.x, y: y - start.y }
164
+
165
+ if (isScrolling === undefined) {
166
+ const sensitivity = 10
167
+ isScrolling = Math.abs(delta.y) > Math.abs(delta.x) + sensitivity
168
+
169
+ if (!isScrolling && !this.captured && event.type === 'pointermove') {
170
+ element.setPointerCapture(event.pointerId)
171
+ this.captured = true
172
+ }
173
+ }
174
+
175
+ if (!isScrolling) {
176
+ if (isCancelable(event)) event.preventDefault()
177
+ run('runMove')
178
+
179
+ const resistedDelta = applyResistance(delta.x)
180
+ translate(index - 1, resistedDelta + slidePos[index - 1], 0)
181
+ translate(index, resistedDelta + slidePos[index], 0)
182
+ translate(index + 1, resistedDelta + slidePos[index + 1], 0)
183
+ } else if (config.disableScroll && isCancelable(event)) {
184
+ event.preventDefault()
185
+ }
186
+ },
187
+
188
+ onEnd(event, type) {
189
+ if (type === 'pointer' && !event.isPrimary) return
190
+
191
+ if (this.captured) {
192
+ element.releasePointerCapture(event.pointerId)
193
+ this.captured = false
194
+ }
195
+
196
+ if (type === 'pointer') {
197
+ element.removeEventListener('pointermove', this)
198
+ element.removeEventListener('pointerup', this)
199
+ element.removeEventListener('pointercancel', this)
200
+ element.removeEventListener('pointerleave', this)
201
+ } else if (type === 'touch') {
202
+ element.removeEventListener('touchmove', this)
203
+ element.removeEventListener('touchend', this)
204
+ } else {
205
+ element.removeEventListener('mousemove', this)
206
+ element.removeEventListener('mouseup', this)
207
+ element.removeEventListener('mouseleave', this)
208
+ }
209
+
210
+ this.finishSwipe()
211
+ },
212
+
213
+ onWheel(event) {
214
+ let deltaX = event.deltaX
215
+ let deltaY = event.deltaY
216
+ if (event.deltaMode === 1) {
217
+ deltaX *= 40
218
+ deltaY *= 40
219
+ } else if (event.deltaMode === 2) {
220
+ deltaX *= width
221
+ deltaY *= width
222
+ }
223
+
224
+ if (Math.abs(deltaX) < 3) return
225
+ if (isCancelable(event)) event.preventDefault()
226
+
227
+ if (!delta.x) start.time = Date.now()
228
+
229
+ let slower = 0.7
230
+ const absDelta = Math.abs(delta.x || 0)
231
+ if (absDelta > width * 0.5) slower = 0.3
232
+ if (absDelta > width) slower = 0.1
233
+ if (absDelta > width * 1.5) slower = 0.05
234
+ if (absDelta > width * 2) slower = 0.01
235
+
236
+ delta = {
237
+ x: (delta.x || 0) - deltaX * slower,
238
+ y: (delta.y || 0) - deltaY * 0.5,
239
+ }
240
+
241
+ const resistedDelta = applyResistance(delta.x)
242
+ translate(index - 1, resistedDelta + slidePos[index - 1], 0)
243
+ translate(index, resistedDelta + slidePos[index], 0)
244
+ translate(index + 1, resistedDelta + slidePos[index + 1], 0)
245
+
246
+ if (wheelEndTimeout) clearTimeout(wheelEndTimeout)
247
+ wheelEndTimeout = setTimeout(() => {
248
+ this.finishSwipe()
249
+ delta = {}
250
+ wheelEndTimeout = null
251
+ }, 50)
252
+ },
253
+
254
+ finishSwipe() {
255
+ const duration = Date.now() - start.time
256
+ const absX = Math.abs(delta.x || 0)
257
+ const isValidSlide = (duration < 250 && absX > 20) || absX > width / 2
258
+ const isPastBounds = (!index && delta.x > 0) || (index === slides.length - 1 && delta.x < 0)
259
+ const direction = delta.x ? Math.abs(delta.x) / delta.x : 0
260
+
261
+ if (!isScrolling && delta.x) {
262
+ if (isValidSlide && !isPastBounds) {
263
+ if (direction < 0) {
264
+ move(index - 1, -width, 0)
265
+ move(index, slidePos[index] - width, config.speed)
266
+ move(circle(index + 1), slidePos[circle(index + 1)] - width, config.speed)
267
+ index = circle(index + 1)
268
+ } else {
269
+ move(index + 1, width, 0)
270
+ move(index, slidePos[index] + width, config.speed)
271
+ move(circle(index - 1), slidePos[circle(index - 1)] + width, config.speed)
272
+ index = circle(index - 1)
273
+ }
274
+ run('callback', index, slides[index], direction)
275
+ } else {
276
+ move(index - 1, -width, config.speed)
277
+ move(index, 0, config.speed)
278
+ move(index + 1, width, config.speed)
279
+ }
280
+ }
281
+
282
+ run('dragEnd', index, slides[index])
283
+ },
284
+ }
285
+
286
+ function applyResistance(deltaX) {
287
+ const atStart = !index && deltaX > 0
288
+ const atEnd = index === slides.length - 1 && deltaX < 0
289
+ if (atStart || atEnd) {
290
+ return deltaX / (Math.abs(deltaX) / width + 1)
291
+ }
292
+ return deltaX
293
+ }
294
+
295
+ function circle(idx) {
296
+ return (slides.length + (idx % slides.length)) % slides.length
297
+ }
298
+
299
+ function move(idx, dist, speed) {
300
+ translate(idx, dist, speed)
301
+ slidePos[idx] = dist
302
+ }
303
+
304
+ function translate(idx, dist, speed) {
305
+ const slide = slides[idx]
306
+ if (!slide?.style) return
307
+
308
+ slide.style.transitionDuration = speed + 'ms'
309
+ slide.style.transitionTimingFunction = EASING
310
+ slide.style.transform = `translate3d(${dist}px, 0, 0)`
311
+ }
312
+
313
+ function setup(opts) {
314
+ if (opts) Object.assign(config, opts)
315
+
316
+ slides = element.children
317
+ length = slides.length
318
+
319
+ if (!length) return
320
+
321
+ width = container.getBoundingClientRect().width || container.offsetWidth
322
+
323
+ slidePos = new Array(length)
324
+ element.style.width = length * width * 2 + 'px'
325
+
326
+ for (let i = length - 1; i >= 0; i--) {
327
+ const slide = slides[i]
328
+
329
+ slide.style.willChange = 'transform'
330
+ slide.style.width = width + 'px'
331
+ slide.setAttribute('data-index', i)
332
+ slide.style[slideDir] = i * -width + 'px'
333
+
334
+ if (slideDir === 'right') {
335
+ slide.style.float = 'right'
336
+ }
337
+
338
+ const initialDist = index > i ? -width : index < i ? width : 0
339
+ move(i, initialDist, 0)
340
+ }
341
+
342
+ detachEvents()
343
+ attachEvents()
344
+ }
345
+
346
+ function attachEvents() {
347
+ if (supports.pointerEvents) {
348
+ element.addEventListener('pointerdown', events)
349
+ } else if (supports.touch) {
350
+ element.addEventListener('touchstart', events)
351
+ }
352
+
353
+ if (config.draggable && !supports.pointerEvents) {
354
+ element.addEventListener('mousedown', events)
355
+ }
356
+
357
+ if (config.mousewheel) {
358
+ element.addEventListener('wheel', events, { passive: false })
359
+ }
360
+
361
+ element.addEventListener('transitionend', events)
362
+ root.addEventListener('resize', events)
363
+ }
364
+
365
+ function detachEvents() {
366
+ if (supports.pointerEvents) {
367
+ element.removeEventListener('pointerdown', events)
368
+ element.removeEventListener('pointermove', events)
369
+ element.removeEventListener('pointerup', events)
370
+ element.removeEventListener('pointercancel', events)
371
+ } else {
372
+ element.removeEventListener('touchstart', events)
373
+ element.removeEventListener('touchmove', events)
374
+ element.removeEventListener('touchend', events)
375
+ }
376
+
377
+ element.removeEventListener('mousedown', events)
378
+ element.removeEventListener('mousemove', events)
379
+ element.removeEventListener('mouseup', events)
380
+ element.removeEventListener('mouseleave', events)
381
+
382
+ element.removeEventListener('wheel', events)
383
+ element.removeEventListener('transitionend', events)
384
+ root.removeEventListener('resize', events)
385
+ }
386
+
387
+ const getPos = () => index
388
+
389
+ function slideTo(to, slideSpeed) {
390
+ to = typeof to === 'number' ? to : parseInt(to, 10)
391
+
392
+ if (index === to) return
393
+
394
+ const direction = Math.abs(index - to) / (index - to)
395
+ let diff = Math.abs(index - to) - 1
396
+
397
+ while (diff--) {
398
+ move(circle((to > index ? to : index) - diff - 1), width * direction, 0)
399
+ }
400
+
401
+ to = circle(to)
402
+
403
+ move(index, width * direction, slideSpeed ?? config.speed)
404
+ move(to, 0, slideSpeed ?? config.speed)
405
+
406
+ index = to
407
+
408
+ requestAnimationFrame(() => {
409
+ run('callback', index, slides[index], direction)
410
+ })
411
+ }
412
+
413
+ function prev() {
414
+ slideTo(index - 1)
415
+ }
416
+
417
+ function next() {
418
+ if (index < slides.length - 1) {
419
+ slideTo(index + 1)
420
+ }
421
+ }
422
+
423
+ function kill() {
424
+ element.style.width = ''
425
+ element.style[slideDir] = ''
426
+
427
+ for (let i = slides.length - 1; i >= 0; i--) {
428
+ const slide = slides[i]
429
+
430
+ if (slide.getAttribute('data-cloned')) {
431
+ slide.parentElement?.removeChild(slide)
432
+ continue
433
+ }
434
+
435
+ slide.style.width = ''
436
+ slide.style[slideDir] = ''
437
+ slide.style.transitionDuration = ''
438
+ slide.style.transform = ''
439
+ slide.style.willChange = ''
440
+ }
441
+
442
+ detachEvents()
443
+ debouncedSetup.cancel()
444
+
445
+ if (wheelEndTimeout) {
446
+ clearTimeout(wheelEndTimeout)
447
+ wheelEndTimeout = null
448
+ }
449
+ }
450
+
451
+ setup()
452
+
453
+ return {
454
+ setup,
455
+ slide: slideTo,
456
+ prev,
457
+ next,
458
+ getPos,
459
+ getNumSlides: () => length,
460
+ setIndex: newIndex => {
461
+ index = newIndex
462
+ },
463
+ appendSlide: slide => {
464
+ element.appendChild(slide)
465
+ setup()
466
+ },
467
+ prependSlide: slide => {
468
+ element.prepend(slide)
469
+ index++
470
+ setup()
471
+ },
472
+ kill,
473
+ }
474
+ }
475
+
476
+ export default Swipe