@evermade/overflow-slider 2.0.2 → 3.1.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/.nvmrc +1 -1
- package/README.md +26 -4
- package/dist/core/details.esm.js +4 -4
- package/dist/core/details.min.js +1 -1
- package/dist/core/overflow-slider.esm.js +2 -0
- package/dist/core/overflow-slider.min.js +1 -1
- package/dist/core/slider.esm.js +202 -29
- package/dist/core/slider.min.js +1 -1
- package/dist/core/utils.esm.js +15 -1
- package/dist/core/utils.min.js +1 -1
- package/dist/overflow-slider.css +1 -1
- package/dist/plugins/arrows/arrows/index.esm.js +15 -8
- package/dist/plugins/arrows/arrows/index.min.js +1 -1
- package/dist/plugins/dots/dots/index.esm.js +21 -22
- package/dist/plugins/dots/dots/index.min.js +1 -1
- package/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
- package/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
- package/dist/plugins/fade/fade/index.esm.js +81 -0
- package/dist/plugins/fade/fade/index.min.js +1 -0
- package/dist/plugins/full-width/full-width/index.esm.js +1 -0
- package/dist/plugins/full-width/full-width/index.min.js +1 -1
- package/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +4 -6
- package/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
- package/dist/plugins/thumbnails/thumbnails/index.esm.js +1 -0
- package/docs/assets/demo.css +16 -0
- package/docs/assets/demo.js +44 -7
- package/docs/dist/core/details.esm.js +4 -4
- package/docs/dist/core/details.min.js +1 -1
- package/docs/dist/core/overflow-slider.esm.js +2 -0
- package/docs/dist/core/overflow-slider.min.js +1 -1
- package/docs/dist/core/slider.esm.js +202 -29
- package/docs/dist/core/slider.min.js +1 -1
- package/docs/dist/core/utils.esm.js +15 -1
- package/docs/dist/core/utils.min.js +1 -1
- package/docs/dist/overflow-slider.css +1 -1
- package/docs/dist/plugins/arrows/arrows/index.esm.js +15 -8
- package/docs/dist/plugins/arrows/arrows/index.min.js +1 -1
- package/docs/dist/plugins/dots/dots/index.esm.js +21 -22
- package/docs/dist/plugins/dots/dots/index.min.js +1 -1
- package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
- package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
- package/docs/dist/plugins/fade/fade/index.esm.js +81 -0
- package/docs/dist/plugins/fade/fade/index.min.js +1 -0
- package/docs/dist/plugins/full-width/full-width/index.esm.js +1 -0
- package/docs/dist/plugins/full-width/full-width/index.min.js +1 -1
- package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +4 -6
- package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
- package/docs/dist/plugins/thumbnails/thumbnails/index.esm.js +1 -0
- package/docs/index.html +39 -7
- package/package.json +5 -1
- package/src/core/details.ts +4 -4
- package/src/core/overflow-slider.ts +2 -0
- package/src/core/slider.ts +226 -36
- package/src/core/types.ts +33 -1
- package/src/core/utils.ts +19 -1
- package/src/overflow-slider.scss +2 -1
- package/src/plugins/arrows/index.ts +16 -8
- package/src/plugins/dots/index.ts +21 -23
- package/src/plugins/drag-scrolling/index.ts +34 -5
- package/src/plugins/fade/index.ts +101 -0
- package/src/plugins/fade/styles.scss +27 -0
- package/src/plugins/full-width/index.ts +1 -0
- package/src/plugins/scroll-indicator/index.ts +4 -6
- package/src/plugins/thumbnails/index.ts +1 -1
package/src/core/slider.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Slider, SliderOptions, SliderPlugin } from './types';
|
|
2
2
|
import details from './details';
|
|
3
|
-
import { generateId, objectsAreEqual } from './utils';
|
|
3
|
+
import { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum } from './utils';
|
|
4
4
|
|
|
5
5
|
export default function Slider( container: HTMLElement, options : SliderOptions, plugins? : SliderPlugin[] ) {
|
|
6
6
|
let slider: Slider;
|
|
@@ -39,6 +39,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
39
39
|
addEventListeners();
|
|
40
40
|
setDataAttributes();
|
|
41
41
|
setCSSVariables();
|
|
42
|
+
|
|
42
43
|
if (plugins) {
|
|
43
44
|
for (const plugin of plugins) {
|
|
44
45
|
plugin(slider);
|
|
@@ -78,7 +79,96 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
78
79
|
resizeObserver.observe( slider.container );
|
|
79
80
|
|
|
80
81
|
// scroll event with debouncing
|
|
81
|
-
|
|
82
|
+
let scrollTimeout: ReturnType<typeof setTimeout>;
|
|
83
|
+
let nativeScrollTimeout: ReturnType<typeof setTimeout>;
|
|
84
|
+
let programmaticScrollTimeout: ReturnType<typeof setTimeout>;
|
|
85
|
+
|
|
86
|
+
let scrollLeft = slider.container.scrollLeft;
|
|
87
|
+
let nativeScrollLeft = slider.container.scrollLeft;
|
|
88
|
+
let programmaticScrollLeft = slider.container.scrollLeft;
|
|
89
|
+
|
|
90
|
+
let isScrolling = false;
|
|
91
|
+
let isUserScrolling = false;
|
|
92
|
+
let isProgrammaticScrolling = false;
|
|
93
|
+
|
|
94
|
+
// any scroll
|
|
95
|
+
slider.container.addEventListener('scroll', () => {
|
|
96
|
+
const newScrollLeft = slider.container.scrollLeft;
|
|
97
|
+
if ( Math.floor( scrollLeft ) !== Math.floor( newScrollLeft ) ) {
|
|
98
|
+
if (!isScrolling) {
|
|
99
|
+
isScrolling = true;
|
|
100
|
+
slider.emit('scrollStart');
|
|
101
|
+
}
|
|
102
|
+
scrollLeft = newScrollLeft;
|
|
103
|
+
clearTimeout(scrollTimeout);
|
|
104
|
+
scrollTimeout = setTimeout(() => {
|
|
105
|
+
isScrolling = false;
|
|
106
|
+
slider.emit('scrollEnd');
|
|
107
|
+
}, 50);
|
|
108
|
+
slider.emit('scroll');
|
|
109
|
+
}
|
|
110
|
+
// keep up nativeScrolling to take into account scroll-snap
|
|
111
|
+
if ( isUserScrolling ) {
|
|
112
|
+
nativeScrollHandler();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// user initted scroll (touchmove, mouse wheel, etc.)
|
|
117
|
+
const nativeScrollHandler = () => {
|
|
118
|
+
const newScrollLeft = slider.container.scrollLeft;
|
|
119
|
+
if ( Math.floor( nativeScrollLeft ) !== Math.floor( newScrollLeft ) && ! isProgrammaticScrolling ) {
|
|
120
|
+
if (!isUserScrolling) {
|
|
121
|
+
slider.emit('nativeScrollStart');
|
|
122
|
+
isUserScrolling = true;
|
|
123
|
+
}
|
|
124
|
+
slider.emit('nativeScroll');
|
|
125
|
+
nativeScrollLeft = newScrollLeft;
|
|
126
|
+
clearTimeout(nativeScrollTimeout);
|
|
127
|
+
nativeScrollTimeout = setTimeout(() => {
|
|
128
|
+
isUserScrolling = false;
|
|
129
|
+
slider.emit('nativeScrollEnd');
|
|
130
|
+
// update programmaticScrollLeft to match nativeScrollLeft
|
|
131
|
+
// this prevents programmaticScroll triggering with no real change to scrollLeft
|
|
132
|
+
programmaticScrollLeft = nativeScrollLeft;
|
|
133
|
+
}, 50);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
slider.container.addEventListener('touchmove', nativeScrollHandler);
|
|
138
|
+
slider.container.addEventListener('mousewheel', nativeScrollHandler);
|
|
139
|
+
slider.container.addEventListener('wheel', nativeScrollHandler);
|
|
140
|
+
|
|
141
|
+
// programmatic scroll (scrollTo, etc.)
|
|
142
|
+
slider.on('programmaticScrollStart', () => {
|
|
143
|
+
isProgrammaticScrolling = true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
slider.container.addEventListener('scroll', () => {
|
|
147
|
+
const newScrollLeft = slider.container.scrollLeft;
|
|
148
|
+
if ( Math.floor( programmaticScrollLeft ) !== Math.floor( newScrollLeft ) && !isUserScrolling && isProgrammaticScrolling) {
|
|
149
|
+
programmaticScrollLeft = newScrollLeft;
|
|
150
|
+
clearTimeout(programmaticScrollTimeout);
|
|
151
|
+
programmaticScrollTimeout = setTimeout(() => {
|
|
152
|
+
isProgrammaticScrolling = false;
|
|
153
|
+
slider.emit('programmaticScrollEnd');
|
|
154
|
+
// update nativeScrollLeft to match programmaticScrollLeft
|
|
155
|
+
// this prevents nativeScroll triggering with no real change to scrollLeft
|
|
156
|
+
nativeScrollLeft = programmaticScrollLeft;
|
|
157
|
+
}, 50);
|
|
158
|
+
slider.emit('programmaticScroll');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Fix issues on scroll snapping not working on programmatic scroll (it's not smooth)
|
|
163
|
+
// by disabling scroll snap if scrolling is programmatic
|
|
164
|
+
slider.on( 'programmaticScrollStart', () => {
|
|
165
|
+
slider.container.style.scrollSnapType = 'none';
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
// restore scroll snap if user scroll starts
|
|
169
|
+
slider.on( 'nativeScrollStart', () => {
|
|
170
|
+
slider.container.style.scrollSnapType = '';
|
|
171
|
+
} );
|
|
82
172
|
|
|
83
173
|
// Listen for mouse down and touch start events on the document
|
|
84
174
|
// This handles both mouse clicks and touch interactions
|
|
@@ -128,26 +218,20 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
128
218
|
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
129
219
|
const slideEnd = slideStart + slideRect.width;
|
|
130
220
|
let scrollTarget = null;
|
|
131
|
-
if (slideStart < scrollLeft) {
|
|
221
|
+
if ( Math.floor( slideStart ) < Math.floor( scrollLeft ) ) {
|
|
132
222
|
scrollTarget = slideStart;
|
|
133
|
-
} else if (slideEnd > scrollLeft + containerWidth) {
|
|
223
|
+
} else if ( Math.floor( slideEnd ) > Math.floor( scrollLeft ) + Math.floor( containerWidth ) ) {
|
|
134
224
|
scrollTarget = slideEnd - containerWidth;
|
|
135
|
-
} else if (slideStart === 0) {
|
|
225
|
+
} else if ( Math.floor( slideStart ) === 0) {
|
|
136
226
|
scrollTarget = 0;
|
|
227
|
+
} else {
|
|
228
|
+
scrollTarget = slideStart;
|
|
137
229
|
}
|
|
138
230
|
if (scrollTarget !== null) {
|
|
139
|
-
slider.container.style.scrollSnapType = 'none';
|
|
140
|
-
// seems like in order for scroll behavior: smooth to work, we need to wait a bit to disable scrollSnapType
|
|
141
231
|
setTimeout((scrollTarget) => {
|
|
232
|
+
slider.emit('programmaticScrollStart');
|
|
142
233
|
slider.container.scrollTo({ left: scrollTarget, behavior: behavior });
|
|
143
234
|
}, 50, scrollTarget);
|
|
144
|
-
setTimeout(() => {
|
|
145
|
-
// leave snapping off to fix issues with slide moving back on focus
|
|
146
|
-
if ( behavior == 'smooth' ) {
|
|
147
|
-
slider.container.style.scrollSnapType = '';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
}, 500);
|
|
151
235
|
}
|
|
152
236
|
};
|
|
153
237
|
|
|
@@ -156,24 +240,34 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
156
240
|
const scrollLeft = slider.container.scrollLeft;
|
|
157
241
|
const slides = slider.slides;
|
|
158
242
|
let activeSlideIdx = 0;
|
|
243
|
+
let scrolledPastLastSlide = false;
|
|
159
244
|
|
|
160
245
|
for (let i = 0; i < slides.length; i++) {
|
|
161
246
|
const slideRect = slides[i].getBoundingClientRect();
|
|
162
247
|
const slideStart = slideRect.left - sliderRect.left + scrollLeft + getGapSize();
|
|
163
248
|
|
|
164
|
-
if (slideStart
|
|
249
|
+
if ( Math.floor( slideStart ) >= Math.floor( scrollLeft ) ) {
|
|
165
250
|
activeSlideIdx = i;
|
|
166
251
|
break;
|
|
167
252
|
}
|
|
253
|
+
if ( i === slides.length - 1 ) {
|
|
254
|
+
scrolledPastLastSlide = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if ( scrolledPastLastSlide ) {
|
|
259
|
+
activeSlideIdx = slides.length - 1;
|
|
168
260
|
}
|
|
169
261
|
|
|
170
262
|
const oldActiveSlideIdx = slider.activeSlideIdx;
|
|
171
263
|
slider.activeSlideIdx = activeSlideIdx;
|
|
264
|
+
|
|
172
265
|
if (oldActiveSlideIdx !== activeSlideIdx) {
|
|
173
266
|
slider.emit('activeSlideChanged');
|
|
174
267
|
}
|
|
175
268
|
}
|
|
176
269
|
|
|
270
|
+
|
|
177
271
|
function moveToSlide( idx: number ) {
|
|
178
272
|
const slide = slider.slides[idx];
|
|
179
273
|
if (slide) {
|
|
@@ -181,15 +275,32 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
181
275
|
}
|
|
182
276
|
};
|
|
183
277
|
|
|
278
|
+
function getInclusiveScrollWidth() : number {
|
|
279
|
+
return slider.container.scrollWidth + getOutermostChildrenEdgeMarginSum(slider.container);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function getInclusiveClientWidth() : number {
|
|
283
|
+
return slider.container.clientWidth + getOutermostChildrenEdgeMarginSum(slider.container);
|
|
284
|
+
}
|
|
285
|
+
|
|
184
286
|
function getGapSize() : number {
|
|
185
287
|
let gapSize = 0;
|
|
186
288
|
if (slider.slides.length > 1) {
|
|
187
289
|
const firstSlideRect = slider.slides[0].getBoundingClientRect();
|
|
188
290
|
const secondSlideRect = slider.slides[1].getBoundingClientRect();
|
|
189
|
-
gapSize = secondSlideRect.left - firstSlideRect.right;
|
|
291
|
+
gapSize = Math.floor( secondSlideRect.left - firstSlideRect.right );
|
|
190
292
|
}
|
|
191
293
|
return gapSize;
|
|
192
|
-
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function getLeftOffset() : number {
|
|
297
|
+
let offset = 0;
|
|
298
|
+
const fullWidthOffset = slider.container.getAttribute('data-full-width-offset');
|
|
299
|
+
if (fullWidthOffset) {
|
|
300
|
+
offset = parseInt(fullWidthOffset);
|
|
301
|
+
}
|
|
302
|
+
return Math.floor( offset );
|
|
303
|
+
};
|
|
193
304
|
|
|
194
305
|
function moveToDirection(direction = "prev") {
|
|
195
306
|
const scrollStrategy = slider.options.scrollStrategy;
|
|
@@ -200,16 +311,16 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
200
311
|
if (direction === 'prev') {
|
|
201
312
|
targetScrollPosition = Math.max(0, scrollLeft - slider.container.offsetWidth);
|
|
202
313
|
} else if (direction === 'next') {
|
|
203
|
-
targetScrollPosition = Math.min(slider.
|
|
314
|
+
targetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), scrollLeft + slider.container.offsetWidth);
|
|
204
315
|
}
|
|
205
316
|
if (scrollStrategy === 'fullSlide') {
|
|
206
|
-
let
|
|
317
|
+
let fullSlideTargetScrollPosition = null;
|
|
207
318
|
|
|
208
319
|
// extend targetScrollPosition to include gap
|
|
209
320
|
if (direction === 'prev') {
|
|
210
|
-
|
|
321
|
+
fullSlideTargetScrollPosition = Math.max(0, targetScrollPosition - getGapSize());
|
|
211
322
|
} else {
|
|
212
|
-
|
|
323
|
+
fullSlideTargetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), targetScrollPosition + getGapSize());
|
|
213
324
|
}
|
|
214
325
|
|
|
215
326
|
if (direction === 'next') {
|
|
@@ -218,49 +329,125 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
218
329
|
const slideRect = slide.getBoundingClientRect();
|
|
219
330
|
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
220
331
|
const slideEnd = slideStart + slideRect.width;
|
|
221
|
-
if (slideStart < targetScrollPosition && slideEnd > targetScrollPosition) {
|
|
222
|
-
|
|
332
|
+
if ( Math.floor( slideStart ) < Math.floor( targetScrollPosition ) && Math.floor( slideEnd ) > Math.floor( targetScrollPosition ) ) {
|
|
333
|
+
fullSlideTargetScrollPosition = slideStart;
|
|
223
334
|
partialSlideFound = true;
|
|
224
335
|
break;
|
|
225
336
|
}
|
|
226
337
|
}
|
|
227
|
-
if (!partialSlideFound) {
|
|
228
|
-
|
|
338
|
+
if ( ! partialSlideFound ) {
|
|
339
|
+
fullSlideTargetScrollPosition = Math.min(targetScrollPosition, slider.getInclusiveScrollWidth() - slider.container.offsetWidth);
|
|
229
340
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
341
|
+
if ( fullSlideTargetScrollPosition ) {
|
|
342
|
+
if ( Math.floor( fullSlideTargetScrollPosition ) > Math.floor( scrollLeft ) ) {
|
|
343
|
+
// make sure fullSlideTargetScrollPosition is possible considering the container width
|
|
344
|
+
const maxScrollPosition = Math.floor( slider.getInclusiveScrollWidth() ) - Math.floor( containerWidth );
|
|
345
|
+
targetScrollPosition = Math.min( fullSlideTargetScrollPosition, maxScrollPosition );
|
|
346
|
+
} else {
|
|
347
|
+
// cannot snap to slide, move one page worth of distance
|
|
348
|
+
targetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), scrollLeft + containerWidth);
|
|
349
|
+
}
|
|
232
350
|
}
|
|
351
|
+
|
|
233
352
|
} else {
|
|
234
353
|
let partialSlideFound = false;
|
|
235
354
|
for (let slide of slider.slides) {
|
|
236
355
|
const slideRect = slide.getBoundingClientRect();
|
|
237
356
|
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
238
357
|
const slideEnd = slideStart + slideRect.width;
|
|
239
|
-
if (slideStart < scrollLeft && slideEnd > scrollLeft) {
|
|
240
|
-
|
|
358
|
+
if ( Math.floor( slideStart ) < Math.floor( scrollLeft ) && Math.floor( slideEnd ) > Math.floor( scrollLeft ) ) {
|
|
359
|
+
fullSlideTargetScrollPosition = slideEnd - containerWidth;
|
|
241
360
|
partialSlideFound = true;
|
|
242
361
|
break;
|
|
243
362
|
}
|
|
244
363
|
}
|
|
245
|
-
if (!partialSlideFound) {
|
|
246
|
-
|
|
364
|
+
if ( ! partialSlideFound ) {
|
|
365
|
+
fullSlideTargetScrollPosition = Math.max(0, scrollLeft - containerWidth);
|
|
247
366
|
}
|
|
248
|
-
if (
|
|
249
|
-
targetScrollPosition =
|
|
367
|
+
if ( fullSlideTargetScrollPosition && Math.floor( fullSlideTargetScrollPosition ) < Math.floor( scrollLeft ) ) {
|
|
368
|
+
targetScrollPosition = fullSlideTargetScrollPosition;
|
|
250
369
|
}
|
|
251
370
|
}
|
|
252
371
|
}
|
|
372
|
+
|
|
373
|
+
// add left offset
|
|
374
|
+
const offsettedTargetScrollPosition = targetScrollPosition - getLeftOffset();
|
|
375
|
+
if ( Math.floor( offsettedTargetScrollPosition ) >= 0) {
|
|
376
|
+
targetScrollPosition = offsettedTargetScrollPosition;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
slider.emit('programmaticScrollStart');
|
|
253
380
|
slider.container.style.scrollBehavior = slider.options.scrollBehavior;
|
|
254
381
|
slider.container.scrollLeft = targetScrollPosition;
|
|
255
382
|
setTimeout(() => slider.container.style.scrollBehavior = '', 50);
|
|
256
|
-
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
function snapToClosestSlide(direction = "prev") {
|
|
386
|
+
const isMovingForward = direction === 'next';
|
|
387
|
+
const slideReference = [];
|
|
388
|
+
for (let i = 0; i < slider.slides.length; i++) {
|
|
389
|
+
const slide = slider.slides[i];
|
|
390
|
+
const slideWidth = slide.offsetWidth;
|
|
391
|
+
const slideStart = slide.offsetLeft;
|
|
392
|
+
const slideEnd = slideStart + slideWidth;
|
|
393
|
+
const slideMiddle = slideStart + slideWidth / 2;
|
|
394
|
+
const trigger = Math.min(slideMiddle, slideStart + slider.options.emulateScrollSnapMaxThreshold);
|
|
395
|
+
slideReference.push({
|
|
396
|
+
start: slideStart,
|
|
397
|
+
middle: slideMiddle,
|
|
398
|
+
end: slideEnd,
|
|
399
|
+
width: slideWidth,
|
|
400
|
+
trigger: trigger,
|
|
401
|
+
slide: slide,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
let snapTarget = null;
|
|
405
|
+
const scrollPosition = slider.container.scrollLeft;
|
|
406
|
+
if (isMovingForward) {
|
|
407
|
+
for (let i = 0; i < slideReference.length; i++) {
|
|
408
|
+
const item = slideReference[i];
|
|
409
|
+
if ( i === 0 && Math.floor( scrollPosition ) <= Math.floor( item.trigger ) ) {
|
|
410
|
+
snapTarget = 0;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if ( Math.floor( slider.container.scrollLeft ) <= Math.floor( item.trigger ) ) {
|
|
414
|
+
snapTarget = item.start;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
for (let i = slideReference.length - 1; i >= 0; i--) {
|
|
420
|
+
const item = slideReference[i];
|
|
421
|
+
if ( i === slideReference.length - 1 && Math.floor( scrollPosition ) >= Math.floor( item.trigger ) ) {
|
|
422
|
+
snapTarget = item.start;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if ( Math.floor( slider.container.scrollLeft ) >= Math.floor( item.trigger ) ) {
|
|
426
|
+
snapTarget = item.start;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if ( snapTarget !== null ) {
|
|
432
|
+
const offsettedSnapTarget = snapTarget - getLeftOffset();
|
|
433
|
+
if ( Math.floor( offsettedSnapTarget ) >= 0 ) {
|
|
434
|
+
snapTarget = offsettedSnapTarget;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const scrollBehavior = slider.options.scrollBehavior || 'smooth';
|
|
438
|
+
slider.container.scrollTo({
|
|
439
|
+
left: snapTarget,
|
|
440
|
+
behavior: scrollBehavior as ScrollBehavior
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
};
|
|
257
444
|
|
|
258
445
|
function on(name: string, cb: any) {
|
|
259
446
|
if (!subs[name]) {
|
|
260
447
|
subs[name] = [];
|
|
261
448
|
}
|
|
262
449
|
subs[name].push(cb);
|
|
263
|
-
}
|
|
450
|
+
};
|
|
264
451
|
|
|
265
452
|
function emit(name: string) {
|
|
266
453
|
if (subs && subs[name]) {
|
|
@@ -272,12 +459,15 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
|
|
|
272
459
|
if (typeof optionCallBack === 'function') {
|
|
273
460
|
optionCallBack(slider);
|
|
274
461
|
}
|
|
275
|
-
}
|
|
462
|
+
};
|
|
276
463
|
|
|
277
464
|
slider = <Slider>{
|
|
278
465
|
emit,
|
|
279
466
|
moveToDirection,
|
|
280
467
|
moveToSlide,
|
|
468
|
+
snapToClosestSlide,
|
|
469
|
+
getInclusiveScrollWidth,
|
|
470
|
+
getInclusiveClientWidth,
|
|
281
471
|
on,
|
|
282
472
|
options,
|
|
283
473
|
};
|
package/src/core/types.ts
CHANGED
|
@@ -5,9 +5,14 @@ export type Slider<O = {}, C = {}, H extends string = string> = {
|
|
|
5
5
|
moveToDirection: (
|
|
6
6
|
direction: 'prev' | 'next'
|
|
7
7
|
) => void
|
|
8
|
+
snapToClosestSlide: (
|
|
9
|
+
direction: 'prev' | 'next'
|
|
10
|
+
) => void
|
|
8
11
|
moveToSlide: (
|
|
9
12
|
index: number
|
|
10
13
|
) => void
|
|
14
|
+
getInclusiveScrollWidth: () => number
|
|
15
|
+
getInclusiveClientWidth: () => number
|
|
11
16
|
on: (
|
|
12
17
|
name: H | SliderHooks,
|
|
13
18
|
cb: (props: Slider<O, C, H>) => void
|
|
@@ -21,6 +26,8 @@ export type SliderOptions = {
|
|
|
21
26
|
scrollBehavior: string;
|
|
22
27
|
scrollStrategy: string;
|
|
23
28
|
slidesSelector: string;
|
|
29
|
+
emulateScrollSnap: boolean;
|
|
30
|
+
emulateScrollSnapMaxThreshold: number;
|
|
24
31
|
[key: string]: any;
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -38,7 +45,16 @@ export type SliderHooks =
|
|
|
38
45
|
| HOOK_CONTENTS_CHANGED
|
|
39
46
|
| HOOK_DETAILS_CHANGED
|
|
40
47
|
| HOOK_CONTAINER_SIZE_CHANGED
|
|
41
|
-
| HOOK_ACTIVE_SLIDE_CHANGED
|
|
48
|
+
| HOOK_ACTIVE_SLIDE_CHANGED
|
|
49
|
+
| HOOK_SCROLL_START
|
|
50
|
+
| HOOK_SCROLL
|
|
51
|
+
| HOOK_SCROLL_END
|
|
52
|
+
| HOOK_NATIVE_SCROLL_START
|
|
53
|
+
| HOOK_NATIVE_SCROLL
|
|
54
|
+
| HOOK_NATIVE_SCROLL_END
|
|
55
|
+
| HOOK_PROGRAMMATIC_SCROLL_START
|
|
56
|
+
| HOOK_PROGRAMMATIC_SCROLL
|
|
57
|
+
| HOOK_PROGRAMMATIC_SCROLL_END;
|
|
42
58
|
|
|
43
59
|
export type HOOK_CREATED = 'created';
|
|
44
60
|
export type HOOK_DETAILS_CHANGED = 'detailsChanged';
|
|
@@ -46,4 +62,20 @@ export type HOOK_CONTENTS_CHANGED = 'contentsChanged';
|
|
|
46
62
|
export type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged';
|
|
47
63
|
export type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged';
|
|
48
64
|
|
|
65
|
+
// any type of scroll
|
|
66
|
+
export type HOOK_SCROLL_START = 'scrollStart';
|
|
67
|
+
export type HOOK_SCROLL = 'scroll';
|
|
68
|
+
export type HOOK_SCROLL_END = 'scrollEnd';
|
|
69
|
+
|
|
70
|
+
// user initted scroll (touch, mouse wheel, etc.)
|
|
71
|
+
export type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart';
|
|
72
|
+
export type HOOK_NATIVE_SCROLL = 'nativeScroll';
|
|
73
|
+
export type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd';
|
|
74
|
+
|
|
75
|
+
// programmatic scroll (e.g. el.scrollTo)
|
|
76
|
+
export type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
|
|
77
|
+
export type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
|
|
78
|
+
export type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
|
|
79
|
+
|
|
80
|
+
|
|
49
81
|
export type SliderPlugin = (slider: Slider) => void;
|
package/src/core/utils.ts
CHANGED
|
@@ -21,4 +21,22 @@ function objectsAreEqual( obj1: any, obj2: any ) {
|
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
function getOutermostChildrenEdgeMarginSum( el: HTMLElement ): number {
|
|
25
|
+
if (el.children.length === 0) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// get the first child and its left margin
|
|
30
|
+
const firstChild = el.children[0];
|
|
31
|
+
const firstChildStyle = getComputedStyle(firstChild);
|
|
32
|
+
const firstChildMarginLeft = parseFloat(firstChildStyle.marginLeft);
|
|
33
|
+
|
|
34
|
+
// Get the last child and its right margin
|
|
35
|
+
const lastChild = el.children[el.children.length - 1];
|
|
36
|
+
const lastChildStyle = getComputedStyle(lastChild);
|
|
37
|
+
const lastChildMarginRight = parseFloat(lastChildStyle.marginRight);
|
|
38
|
+
|
|
39
|
+
return firstChildMarginLeft + lastChildMarginRight;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum };
|
package/src/overflow-slider.scss
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
.overflow-slider {
|
|
10
10
|
overflow: auto;
|
|
11
11
|
width: 100%;
|
|
12
|
-
overflow: auto;
|
|
13
12
|
display: grid;
|
|
14
13
|
grid-auto-flow: column;
|
|
15
14
|
grid-template-columns: max-content;
|
|
16
15
|
max-width: max-content;
|
|
16
|
+
position: relative;
|
|
17
17
|
// hide native scrollbars
|
|
18
18
|
&::-webkit-scrollbar {
|
|
19
19
|
display: none;
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
@import 'plugins/arrows/styles.scss';
|
|
34
34
|
@import 'plugins/dots/styles.scss';
|
|
35
|
+
@import 'plugins/fade/styles.scss';
|
|
35
36
|
@import 'plugins/drag-scrolling/styles.scss';
|
|
36
37
|
@import 'plugins/scroll-indicator/styles.scss';
|
|
37
38
|
@import 'plugins/skip-links/styles.scss';
|
|
@@ -66,7 +66,11 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
|
|
|
66
66
|
prev.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? '');
|
|
67
67
|
prev.setAttribute( 'data-type', 'prev' );
|
|
68
68
|
prev.innerHTML = options.icons.prev;
|
|
69
|
-
prev.addEventListener( 'click', () =>
|
|
69
|
+
prev.addEventListener( 'click', () => {
|
|
70
|
+
if ( prev.getAttribute('data-has-content') === 'true' ) {
|
|
71
|
+
slider.moveToDirection( 'prev' );
|
|
72
|
+
}
|
|
73
|
+
} );
|
|
70
74
|
|
|
71
75
|
const next = document.createElement( 'button' );
|
|
72
76
|
next.setAttribute( 'class', options.classNames.nextButton );
|
|
@@ -75,7 +79,11 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
|
|
|
75
79
|
next.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? '');
|
|
76
80
|
next.setAttribute( 'data-type', 'next' );
|
|
77
81
|
next.innerHTML = options.icons.next;
|
|
78
|
-
next.addEventListener( 'click', () =>
|
|
82
|
+
next.addEventListener( 'click', () => {
|
|
83
|
+
if ( next.getAttribute('data-has-content') === 'true' ) {
|
|
84
|
+
slider.moveToDirection( 'next' );
|
|
85
|
+
}
|
|
86
|
+
} );
|
|
79
87
|
|
|
80
88
|
// insert buttons to the nav
|
|
81
89
|
nav.appendChild( prev );
|
|
@@ -83,15 +91,15 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
|
|
|
83
91
|
|
|
84
92
|
const update = () => {
|
|
85
93
|
const scrollLeft = slider.container.scrollLeft;
|
|
86
|
-
const scrollWidth = slider.
|
|
87
|
-
const clientWidth = slider.
|
|
88
|
-
const buffer =
|
|
89
|
-
if ( scrollLeft === 0 ) {
|
|
94
|
+
const scrollWidth = slider.getInclusiveScrollWidth();
|
|
95
|
+
const clientWidth = slider.getInclusiveClientWidth();
|
|
96
|
+
const buffer = 0;
|
|
97
|
+
if ( Math.floor( scrollLeft ) === 0 ) {
|
|
90
98
|
prev.setAttribute( 'data-has-content', 'false' );
|
|
91
99
|
} else {
|
|
92
100
|
prev.setAttribute( 'data-has-content', 'true' );
|
|
93
101
|
}
|
|
94
|
-
if ( scrollLeft + clientWidth >= scrollWidth
|
|
102
|
+
if ( Math.floor( scrollLeft + clientWidth ) >= Math.floor( scrollWidth ) ) {
|
|
95
103
|
next.setAttribute( 'data-has-content', 'false' );
|
|
96
104
|
} else {
|
|
97
105
|
next.setAttribute( 'data-has-content', 'true' );
|
|
@@ -110,7 +118,7 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
update();
|
|
113
|
-
slider.on( '
|
|
121
|
+
slider.on( 'scrollEnd', update );
|
|
114
122
|
slider.on( 'contentsChanged', update );
|
|
115
123
|
slider.on( 'containerSizeChanged', update );
|
|
116
124
|
};
|
|
@@ -45,8 +45,8 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
|
|
|
45
45
|
|
|
46
46
|
const dotsList = document.createElement( 'ul' );
|
|
47
47
|
|
|
48
|
-
const pages = slider.details.
|
|
49
|
-
const
|
|
48
|
+
const pages = slider.details.slideCount;
|
|
49
|
+
const currentItem = slider.activeSlideIdx;
|
|
50
50
|
|
|
51
51
|
if ( pages <= 1 ) {
|
|
52
52
|
return;
|
|
@@ -58,22 +58,22 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
|
|
|
58
58
|
dot.setAttribute( 'type', 'button' );
|
|
59
59
|
dot.setAttribute( 'class', options.classNames.dotsItem );
|
|
60
60
|
dot.setAttribute( 'aria-label', options.texts.dotDescription.replace( '%d', ( i + 1 ).toString() ).replace( '%d', pages.toString() ) );
|
|
61
|
-
dot.setAttribute( 'aria-pressed', ( i ===
|
|
62
|
-
dot.setAttribute( 'data-
|
|
61
|
+
dot.setAttribute( 'aria-pressed', ( i === currentItem ).toString() );
|
|
62
|
+
dot.setAttribute( 'data-item', ( i + 1 ).toString() );
|
|
63
63
|
dotListItem.appendChild( dot );
|
|
64
64
|
dotsList.appendChild( dotListItem );
|
|
65
65
|
dot.addEventListener( 'click', () => activateDot( i + 1 ) );
|
|
66
66
|
dot.addEventListener( 'focus', () => pageFocused = i + 1 );
|
|
67
67
|
dot.addEventListener( 'keydown', ( e ) => {
|
|
68
|
-
const
|
|
69
|
-
if ( !
|
|
68
|
+
const currentItemItem = dots.querySelector( `[aria-pressed="true"]` );
|
|
69
|
+
if ( ! currentItemItem ) {
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
const
|
|
72
|
+
const currentItem = parseInt( currentItemItem.getAttribute( 'data-item' ) ?? '1' );
|
|
73
73
|
if ( e.key === 'ArrowLeft' ) {
|
|
74
|
-
const previousPage =
|
|
74
|
+
const previousPage = currentItem - 1;
|
|
75
75
|
if ( previousPage > 0 ) {
|
|
76
|
-
const matchingDot = dots.querySelector( `[data-
|
|
76
|
+
const matchingDot = dots.querySelector( `[data-item="${previousPage}"]` );
|
|
77
77
|
if ( matchingDot ) {
|
|
78
78
|
( <HTMLElement>matchingDot ).focus();
|
|
79
79
|
}
|
|
@@ -81,9 +81,9 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if ( e.key === 'ArrowRight' ) {
|
|
84
|
-
const nextPage =
|
|
84
|
+
const nextPage = currentItem + 1;
|
|
85
85
|
if ( nextPage <= pages ) {
|
|
86
|
-
const matchingDot = dots.querySelector( `[data-
|
|
86
|
+
const matchingDot = dots.querySelector( `[data-item="${nextPage}"]` );
|
|
87
87
|
if ( matchingDot ) {
|
|
88
88
|
( <HTMLElement>matchingDot ).focus();
|
|
89
89
|
}
|
|
@@ -97,20 +97,19 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
|
|
|
97
97
|
|
|
98
98
|
// return focus to same page after rebuild
|
|
99
99
|
if ( pageFocused ) {
|
|
100
|
-
const matchingDot = dots.querySelector( `[data-
|
|
100
|
+
const matchingDot = dots.querySelector( `[data-item="${pageFocused}"]` );
|
|
101
101
|
if ( matchingDot ) {
|
|
102
102
|
( <HTMLElement>matchingDot ).focus();
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
const activateDot = (
|
|
108
|
-
const scrollTargetPosition = slider.details.containerWidth * ( page - 1 );
|
|
109
|
-
slider.container.style.scrollBehavior = slider.options.scrollBehavior;
|
|
110
|
-
slider.container.
|
|
111
|
-
slider.container.
|
|
112
|
-
slider.
|
|
113
|
-
slider.container.style.scrollSnapType = '';
|
|
107
|
+
const activateDot = ( item: number ) => {
|
|
108
|
+
// const scrollTargetPosition = slider.details.containerWidth * ( page - 1 );
|
|
109
|
+
// slider.container.style.scrollBehavior = slider.options.scrollBehavior;
|
|
110
|
+
// slider.container.scrollLeft = scrollTargetPosition;
|
|
111
|
+
// slider.container.style.scrollBehavior = '';
|
|
112
|
+
slider.moveToSlide( item - 1 );
|
|
114
113
|
};
|
|
115
114
|
|
|
116
115
|
buildDots();
|
|
@@ -121,9 +120,8 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
|
|
|
121
120
|
slider.container.parentNode?.insertBefore( dots, slider.container.nextSibling );
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
slider.on( '
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
slider.on( 'scrollEnd', buildDots );
|
|
124
|
+
slider.on( 'contentsChanged', buildDots );
|
|
125
|
+
slider.on( 'containerSizeChanged', buildDots );
|
|
128
126
|
};
|
|
129
127
|
};
|