@evermade/overflow-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/.editorconfig +22 -0
- package/.github/workflows/npm-publish.yml +33 -0
- package/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/changelog.md +5 -0
- package/dist/index.esm.js +694 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.js +709 -0
- package/dist/index.min.js +2 -0
- package/dist/overflow-slider.css +1 -0
- package/docs/assets/demo.css +513 -0
- package/docs/assets/demo.js +113 -0
- package/docs/dist/overflow-slider.css +1 -0
- package/docs/dist/overflow-slider.esm.js +694 -0
- package/docs/index.html +230 -0
- package/package.json +55 -0
- package/rollup.config.js +45 -0
- package/src/core/details.ts +43 -0
- package/src/core/slider.ts +234 -0
- package/src/core/types.ts +41 -0
- package/src/core/utils.ts +24 -0
- package/src/index.ts +18 -0
- package/src/overflow-slider.scss +213 -0
- package/src/overflow-slider.ts +40 -0
- package/src/plugins/arrows.ts +107 -0
- package/src/plugins/dots.ts +129 -0
- package/src/plugins/drag-scrolling.ts +78 -0
- package/src/plugins/scroll-indicator.ts +152 -0
- package/src/plugins/skip-links.ts +61 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
function details(slider) {
|
|
2
|
+
let instance;
|
|
3
|
+
let hasOverflow = false;
|
|
4
|
+
let slideCount = 0;
|
|
5
|
+
let containerWidth = 0;
|
|
6
|
+
let scrollableAreaWidth = 0;
|
|
7
|
+
let amountOfPages = 0;
|
|
8
|
+
let currentPage = 1;
|
|
9
|
+
if (slider.container.scrollWidth > slider.container.clientWidth) {
|
|
10
|
+
hasOverflow = true;
|
|
11
|
+
}
|
|
12
|
+
slideCount = Array.from(slider.container.querySelectorAll(':scope > *')).length;
|
|
13
|
+
containerWidth = slider.container.offsetWidth;
|
|
14
|
+
scrollableAreaWidth = slider.container.scrollWidth;
|
|
15
|
+
amountOfPages = Math.ceil(scrollableAreaWidth / containerWidth);
|
|
16
|
+
if (slider.container.scrollLeft >= 0) {
|
|
17
|
+
currentPage = Math.floor(slider.container.scrollLeft / containerWidth);
|
|
18
|
+
// consider as last page if the scrollLeft + containerWidth is equal to scrollWidth
|
|
19
|
+
if (slider.container.scrollLeft + containerWidth === scrollableAreaWidth) {
|
|
20
|
+
currentPage = amountOfPages - 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
instance = {
|
|
24
|
+
hasOverflow,
|
|
25
|
+
slideCount,
|
|
26
|
+
containerWidth,
|
|
27
|
+
scrollableAreaWidth,
|
|
28
|
+
amountOfPages,
|
|
29
|
+
currentPage,
|
|
30
|
+
};
|
|
31
|
+
return instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateId(prefix, i = 1) {
|
|
35
|
+
const id = `${prefix}-${i}`;
|
|
36
|
+
if (document.getElementById(id)) {
|
|
37
|
+
return generateId(prefix, i + 1);
|
|
38
|
+
}
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
function objectsAreEqual(obj1, obj2) {
|
|
42
|
+
const keys1 = Object.keys(obj1);
|
|
43
|
+
const keys2 = Object.keys(obj2);
|
|
44
|
+
if (keys1.length !== keys2.length) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
for (let key of keys1) {
|
|
48
|
+
if (obj2.hasOwnProperty(key) === false || obj1[key] !== obj2[key]) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Slider(container, options, plugins) {
|
|
56
|
+
let slider;
|
|
57
|
+
let subs = {};
|
|
58
|
+
function init() {
|
|
59
|
+
slider.container = container;
|
|
60
|
+
// ensure container has id
|
|
61
|
+
let containerId = container.getAttribute('id');
|
|
62
|
+
if (containerId === null) {
|
|
63
|
+
containerId = generateId('overflow-slider');
|
|
64
|
+
container.setAttribute('id', containerId);
|
|
65
|
+
}
|
|
66
|
+
setDetails(true);
|
|
67
|
+
slider.on('contentsChanged', () => setDetails());
|
|
68
|
+
slider.on('containerSizeChanged', () => setDetails());
|
|
69
|
+
let requestId = 0;
|
|
70
|
+
const setDetailsDebounce = () => {
|
|
71
|
+
if (requestId) {
|
|
72
|
+
window.cancelAnimationFrame(requestId);
|
|
73
|
+
}
|
|
74
|
+
requestId = window.requestAnimationFrame(() => {
|
|
75
|
+
setDetails();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
slider.on('scroll', setDetailsDebounce);
|
|
79
|
+
addEventListeners();
|
|
80
|
+
setDataAttributes();
|
|
81
|
+
setCSSVariables();
|
|
82
|
+
if (plugins) {
|
|
83
|
+
for (const plugin of plugins) {
|
|
84
|
+
plugin(slider);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
slider.on('detailsChanged', () => {
|
|
88
|
+
setDataAttributes();
|
|
89
|
+
setCSSVariables();
|
|
90
|
+
});
|
|
91
|
+
slider.emit('created');
|
|
92
|
+
}
|
|
93
|
+
function setDetails(isInit = false) {
|
|
94
|
+
const oldDetails = slider.details;
|
|
95
|
+
const newDetails = details(slider);
|
|
96
|
+
slider.details = newDetails;
|
|
97
|
+
if (!isInit && !objectsAreEqual(oldDetails, newDetails)) {
|
|
98
|
+
slider.emit('detailsChanged');
|
|
99
|
+
}
|
|
100
|
+
else if (isInit) {
|
|
101
|
+
slider.emit('detailsChanged');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function addEventListeners() {
|
|
105
|
+
// changes to DOM
|
|
106
|
+
const observer = new MutationObserver(() => slider.emit('contentsChanged'));
|
|
107
|
+
observer.observe(slider.container, { childList: true });
|
|
108
|
+
// container size changes
|
|
109
|
+
const resizeObserver = new ResizeObserver(() => slider.emit('containerSizeChanged'));
|
|
110
|
+
resizeObserver.observe(slider.container);
|
|
111
|
+
// scroll event with debouncing
|
|
112
|
+
slider.container.addEventListener('scroll', () => slider.emit('scroll'));
|
|
113
|
+
// Listen for mouse down and touch start events on the document
|
|
114
|
+
// This handles both mouse clicks and touch interactions
|
|
115
|
+
let wasInteractedWith = false;
|
|
116
|
+
slider.container.addEventListener('mousedown', () => {
|
|
117
|
+
wasInteractedWith = true;
|
|
118
|
+
});
|
|
119
|
+
slider.container.addEventListener('touchstart', () => {
|
|
120
|
+
wasInteractedWith = true;
|
|
121
|
+
}, { passive: true });
|
|
122
|
+
slider.container.addEventListener('focusin', (e) => {
|
|
123
|
+
// move target parents as long as they are not the container
|
|
124
|
+
// but only if focus didn't start from mouse or touch
|
|
125
|
+
if (!wasInteractedWith) {
|
|
126
|
+
let target = e.target;
|
|
127
|
+
while (target.parentElement !== slider.container) {
|
|
128
|
+
if (target.parentElement) {
|
|
129
|
+
target = target.parentElement;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ensureSlideIsInView(target);
|
|
136
|
+
}
|
|
137
|
+
wasInteractedWith = false;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function setCSSVariables() {
|
|
141
|
+
slider.container.style.setProperty('--slider-container-width', `${slider.details.containerWidth}px`);
|
|
142
|
+
slider.container.style.setProperty('--slider-scrollable-width', `${slider.details.scrollableAreaWidth}px`);
|
|
143
|
+
slider.container.style.setProperty('--slider-slides-count', `${slider.details.slideCount}`);
|
|
144
|
+
}
|
|
145
|
+
function setDataAttributes() {
|
|
146
|
+
slider.container.setAttribute('data-has-overflow', slider.details.hasOverflow ? 'true' : 'false');
|
|
147
|
+
}
|
|
148
|
+
function ensureSlideIsInView(slide) {
|
|
149
|
+
const slideRect = slide.getBoundingClientRect();
|
|
150
|
+
const sliderRect = slider.container.getBoundingClientRect();
|
|
151
|
+
const containerWidth = slider.container.offsetWidth;
|
|
152
|
+
const scrollLeft = slider.container.scrollLeft;
|
|
153
|
+
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
154
|
+
const slideEnd = slideStart + slideRect.width;
|
|
155
|
+
let scrollTarget = null;
|
|
156
|
+
if (slideStart < scrollLeft) {
|
|
157
|
+
scrollTarget = slideStart;
|
|
158
|
+
}
|
|
159
|
+
else if (slideEnd > scrollLeft + containerWidth) {
|
|
160
|
+
scrollTarget = slideEnd - containerWidth;
|
|
161
|
+
}
|
|
162
|
+
if (scrollTarget) {
|
|
163
|
+
slider.container.style.scrollSnapType = 'none';
|
|
164
|
+
slider.container.scrollLeft = scrollTarget;
|
|
165
|
+
// @todo resume scroll snapping but at least proximity gives a lot of trouble
|
|
166
|
+
// and it's not really needed for this use case but it would be nice to have
|
|
167
|
+
// it back in case it's needed. We need to calculate scrollLeft some other way
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function moveToDirection(direction = "prev") {
|
|
171
|
+
const scrollStrategy = slider.options.scrollStrategy;
|
|
172
|
+
const scrollLeft = slider.container.scrollLeft;
|
|
173
|
+
const sliderRect = slider.container.getBoundingClientRect();
|
|
174
|
+
const containerWidth = slider.container.offsetWidth;
|
|
175
|
+
let targetScrollPosition = scrollLeft;
|
|
176
|
+
if (direction === 'prev') {
|
|
177
|
+
targetScrollPosition = Math.max(0, scrollLeft - slider.container.offsetWidth);
|
|
178
|
+
}
|
|
179
|
+
else if (direction === 'next') {
|
|
180
|
+
targetScrollPosition = Math.min(slider.container.scrollWidth, scrollLeft + slider.container.offsetWidth);
|
|
181
|
+
}
|
|
182
|
+
if (scrollStrategy === 'fullSlide') {
|
|
183
|
+
let fullSldeTargetScrollPosition = null;
|
|
184
|
+
const slides = Array.from(slider.container.querySelectorAll(':scope > *'));
|
|
185
|
+
let gapSize = 0;
|
|
186
|
+
if (slides.length > 1) {
|
|
187
|
+
const firstSlideRect = slides[0].getBoundingClientRect();
|
|
188
|
+
const secondSlideRect = slides[1].getBoundingClientRect();
|
|
189
|
+
gapSize = secondSlideRect.left - firstSlideRect.right;
|
|
190
|
+
}
|
|
191
|
+
// extend targetScrollPosition to include gap
|
|
192
|
+
if (direction === 'prev') {
|
|
193
|
+
fullSldeTargetScrollPosition = Math.max(0, targetScrollPosition - gapSize);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
fullSldeTargetScrollPosition = Math.min(slider.container.scrollWidth, targetScrollPosition + gapSize);
|
|
197
|
+
}
|
|
198
|
+
if (direction === 'next') {
|
|
199
|
+
let partialSlideFound = false;
|
|
200
|
+
for (let slide of slides) {
|
|
201
|
+
const slideRect = slide.getBoundingClientRect();
|
|
202
|
+
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
203
|
+
const slideEnd = slideStart + slideRect.width;
|
|
204
|
+
if (slideStart < targetScrollPosition && slideEnd > targetScrollPosition) {
|
|
205
|
+
fullSldeTargetScrollPosition = slideStart;
|
|
206
|
+
partialSlideFound = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!partialSlideFound) {
|
|
211
|
+
fullSldeTargetScrollPosition = Math.min(targetScrollPosition, slider.container.scrollWidth - slider.container.offsetWidth);
|
|
212
|
+
}
|
|
213
|
+
if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition > scrollLeft) {
|
|
214
|
+
targetScrollPosition = fullSldeTargetScrollPosition;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
let partialSlideFound = false;
|
|
219
|
+
for (let slide of slides) {
|
|
220
|
+
const slideRect = slide.getBoundingClientRect();
|
|
221
|
+
const slideStart = slideRect.left - sliderRect.left + scrollLeft;
|
|
222
|
+
const slideEnd = slideStart + slideRect.width;
|
|
223
|
+
if (slideStart < scrollLeft && slideEnd > scrollLeft) {
|
|
224
|
+
fullSldeTargetScrollPosition = slideEnd - containerWidth;
|
|
225
|
+
partialSlideFound = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!partialSlideFound) {
|
|
230
|
+
fullSldeTargetScrollPosition = Math.max(0, scrollLeft - containerWidth);
|
|
231
|
+
}
|
|
232
|
+
if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition < scrollLeft) {
|
|
233
|
+
targetScrollPosition = fullSldeTargetScrollPosition;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
slider.container.style.scrollBehavior = 'smooth';
|
|
238
|
+
slider.container.scrollLeft = targetScrollPosition;
|
|
239
|
+
setTimeout(() => slider.container.style.scrollBehavior = '', 50);
|
|
240
|
+
}
|
|
241
|
+
function on(name, cb) {
|
|
242
|
+
if (!subs[name]) {
|
|
243
|
+
subs[name] = [];
|
|
244
|
+
}
|
|
245
|
+
subs[name].push(cb);
|
|
246
|
+
}
|
|
247
|
+
function emit(name) {
|
|
248
|
+
var _a;
|
|
249
|
+
if (subs && subs[name]) {
|
|
250
|
+
subs[name].forEach(cb => {
|
|
251
|
+
cb(slider);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const optionCallBack = (_a = slider === null || slider === void 0 ? void 0 : slider.options) === null || _a === void 0 ? void 0 : _a[name];
|
|
255
|
+
if (typeof optionCallBack === 'function') {
|
|
256
|
+
optionCallBack(slider);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
slider = {
|
|
260
|
+
emit,
|
|
261
|
+
moveToDirection,
|
|
262
|
+
on,
|
|
263
|
+
options,
|
|
264
|
+
};
|
|
265
|
+
init();
|
|
266
|
+
return slider;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function OverflowSlider(container, options, plugins) {
|
|
270
|
+
try {
|
|
271
|
+
// check that container HTML element
|
|
272
|
+
if (!(container instanceof Element)) {
|
|
273
|
+
throw new Error(`Container must be HTML element, found ${typeof container}`);
|
|
274
|
+
}
|
|
275
|
+
const defaults = {
|
|
276
|
+
scrollBehavior: "smooth",
|
|
277
|
+
scrollStrategy: "fullSlide",
|
|
278
|
+
};
|
|
279
|
+
const sliderOptions = Object.assign(Object.assign({}, defaults), options);
|
|
280
|
+
// disable smooth scrolling if user prefers reduced motion
|
|
281
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
282
|
+
sliderOptions.scrollBehavior = "auto";
|
|
283
|
+
}
|
|
284
|
+
return Slider(container, sliderOptions, plugins);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
console.error(e);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const DEFAULT_TEXTS$2 = {
|
|
292
|
+
skipList: 'Skip list'
|
|
293
|
+
};
|
|
294
|
+
const DEFAULT_CLASS_NAMES$3 = {
|
|
295
|
+
skipLink: 'screen-reader-text',
|
|
296
|
+
skipLinkTarget: 'overflow-slider__skip-link-target',
|
|
297
|
+
};
|
|
298
|
+
function SkipLinksPlugin(args) {
|
|
299
|
+
return (slider) => {
|
|
300
|
+
var _a, _b, _c, _d, _e, _f;
|
|
301
|
+
const options = {
|
|
302
|
+
texts: Object.assign(Object.assign({}, DEFAULT_TEXTS$2), (args === null || args === void 0 ? void 0 : args.texts) || []),
|
|
303
|
+
classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES$3), (args === null || args === void 0 ? void 0 : args.classNames) || []),
|
|
304
|
+
containerBefore: (_a = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _a !== void 0 ? _a : null,
|
|
305
|
+
containerAfter: (_b = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _b !== void 0 ? _b : null,
|
|
306
|
+
};
|
|
307
|
+
const skipId = generateId('overflow-slider-skip');
|
|
308
|
+
const skipLinkEl = document.createElement('a');
|
|
309
|
+
skipLinkEl.setAttribute('href', `#${skipId}`);
|
|
310
|
+
skipLinkEl.textContent = options.texts.skipList;
|
|
311
|
+
skipLinkEl.classList.add(options.classNames.skipLink);
|
|
312
|
+
const skipTargetEl = document.createElement('div');
|
|
313
|
+
skipTargetEl.setAttribute('id', skipId);
|
|
314
|
+
skipTargetEl.setAttribute('tabindex', '-1');
|
|
315
|
+
if (options.containerBefore) {
|
|
316
|
+
(_c = options.containerBefore.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(skipLinkEl, options.containerBefore);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
(_d = slider.container.parentNode) === null || _d === void 0 ? void 0 : _d.insertBefore(skipLinkEl, slider.container);
|
|
320
|
+
}
|
|
321
|
+
if (options.containerAfter) {
|
|
322
|
+
(_e = options.containerAfter.parentNode) === null || _e === void 0 ? void 0 : _e.insertBefore(skipTargetEl, options.containerAfter.nextSibling);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
(_f = slider.container.parentNode) === null || _f === void 0 ? void 0 : _f.insertBefore(skipTargetEl, slider.container.nextSibling);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const DEFAULT_TEXTS$1 = {
|
|
331
|
+
buttonPrevious: 'Previous items',
|
|
332
|
+
buttonNext: 'Next items',
|
|
333
|
+
};
|
|
334
|
+
const DEFAULT_ICONS = {
|
|
335
|
+
prev: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.6 3.4l-7.6 7.6 7.6 7.6 1.4-1.4-5-5h12.6v-2h-12.6l5-5z"/></svg>',
|
|
336
|
+
next: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.4 3.4l-1.4 1.4 5 5h-12.6v2h12.6l-5 5 1.4 1.4 7.6-7.6z"/></svg>',
|
|
337
|
+
};
|
|
338
|
+
const DEFAULT_CLASS_NAMES$2 = {
|
|
339
|
+
navContainer: 'overflow-slider__arrows',
|
|
340
|
+
prevButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--prev',
|
|
341
|
+
nextButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--next',
|
|
342
|
+
};
|
|
343
|
+
function ArrowsPlugin(args) {
|
|
344
|
+
return (slider) => {
|
|
345
|
+
var _a, _b, _c, _d;
|
|
346
|
+
const options = {
|
|
347
|
+
texts: Object.assign(Object.assign({}, DEFAULT_TEXTS$1), (args === null || args === void 0 ? void 0 : args.texts) || []),
|
|
348
|
+
icons: Object.assign(Object.assign({}, DEFAULT_ICONS), (args === null || args === void 0 ? void 0 : args.icons) || []),
|
|
349
|
+
classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES$2), (args === null || args === void 0 ? void 0 : args.classNames) || []),
|
|
350
|
+
container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null,
|
|
351
|
+
};
|
|
352
|
+
const nav = document.createElement('div');
|
|
353
|
+
nav.classList.add(options.classNames.navContainer);
|
|
354
|
+
const prev = document.createElement('button');
|
|
355
|
+
prev.setAttribute('class', options.classNames.prevButton);
|
|
356
|
+
prev.setAttribute('type', 'button');
|
|
357
|
+
prev.setAttribute('aria-label', options.texts.buttonPrevious);
|
|
358
|
+
prev.setAttribute('aria-controls', (_b = slider.container.getAttribute('id')) !== null && _b !== void 0 ? _b : '');
|
|
359
|
+
prev.setAttribute('data-type', 'prev');
|
|
360
|
+
prev.innerHTML = options.icons.prev;
|
|
361
|
+
prev.addEventListener('click', () => slider.moveToDirection('prev'));
|
|
362
|
+
const next = document.createElement('button');
|
|
363
|
+
next.setAttribute('class', options.classNames.nextButton);
|
|
364
|
+
next.setAttribute('type', 'button');
|
|
365
|
+
next.setAttribute('aria-label', options.texts.buttonNext);
|
|
366
|
+
next.setAttribute('aria-controls', (_c = slider.container.getAttribute('id')) !== null && _c !== void 0 ? _c : '');
|
|
367
|
+
next.setAttribute('data-type', 'next');
|
|
368
|
+
next.innerHTML = options.icons.next;
|
|
369
|
+
next.addEventListener('click', () => slider.moveToDirection('next'));
|
|
370
|
+
// insert buttons to the nav
|
|
371
|
+
nav.appendChild(prev);
|
|
372
|
+
nav.appendChild(next);
|
|
373
|
+
const update = () => {
|
|
374
|
+
const scrollLeft = slider.container.scrollLeft;
|
|
375
|
+
const scrollWidth = slider.container.scrollWidth;
|
|
376
|
+
const clientWidth = slider.container.clientWidth;
|
|
377
|
+
if (scrollLeft === 0) {
|
|
378
|
+
prev.setAttribute('data-has-content', 'false');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
prev.setAttribute('data-has-content', 'true');
|
|
382
|
+
}
|
|
383
|
+
if (scrollLeft + clientWidth >= scrollWidth) {
|
|
384
|
+
next.setAttribute('data-has-content', 'false');
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
next.setAttribute('data-has-content', 'true');
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
if (options.container) {
|
|
391
|
+
options.container.appendChild(nav);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
(_d = slider.container.parentNode) === null || _d === void 0 ? void 0 : _d.insertBefore(nav, slider.container.nextSibling);
|
|
395
|
+
}
|
|
396
|
+
update();
|
|
397
|
+
slider.on('scroll', update);
|
|
398
|
+
slider.on('contentsChanged', update);
|
|
399
|
+
slider.on('containerSizeChanged', update);
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const DEFAULT_CLASS_NAMES$1 = {
|
|
404
|
+
scrollIndicator: 'overflow-slider__scroll-indicator',
|
|
405
|
+
scrollIndicatorBar: 'overflow-slider__scroll-indicator-bar',
|
|
406
|
+
scrollIndicatorButton: 'overflow-slider__scroll-indicator-button',
|
|
407
|
+
};
|
|
408
|
+
function ScrollIndicatorPlugin(args) {
|
|
409
|
+
return (slider) => {
|
|
410
|
+
var _a, _b, _c;
|
|
411
|
+
const options = {
|
|
412
|
+
classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES$1), (args === null || args === void 0 ? void 0 : args.classNames) || []),
|
|
413
|
+
container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null,
|
|
414
|
+
};
|
|
415
|
+
const scrollbarContainer = document.createElement('div');
|
|
416
|
+
scrollbarContainer.setAttribute('class', options.classNames.scrollIndicator);
|
|
417
|
+
scrollbarContainer.setAttribute('tabindex', '0');
|
|
418
|
+
scrollbarContainer.setAttribute('role', 'scrollbar');
|
|
419
|
+
scrollbarContainer.setAttribute('aria-controls', (_b = slider.container.getAttribute('id')) !== null && _b !== void 0 ? _b : '');
|
|
420
|
+
scrollbarContainer.setAttribute('aria-orientation', 'horizontal');
|
|
421
|
+
scrollbarContainer.setAttribute('aria-valuemax', '100');
|
|
422
|
+
scrollbarContainer.setAttribute('aria-valuemin', '0');
|
|
423
|
+
scrollbarContainer.setAttribute('aria-valuenow', '0');
|
|
424
|
+
const scrollbar = document.createElement('div');
|
|
425
|
+
scrollbar.setAttribute('class', options.classNames.scrollIndicatorBar);
|
|
426
|
+
const scrollbarButton = document.createElement('div');
|
|
427
|
+
scrollbarButton.setAttribute('class', options.classNames.scrollIndicatorButton);
|
|
428
|
+
scrollbarButton.setAttribute('data-is-grabbed', 'false');
|
|
429
|
+
scrollbar.appendChild(scrollbarButton);
|
|
430
|
+
scrollbarContainer.appendChild(scrollbar);
|
|
431
|
+
const setDataAttributes = () => {
|
|
432
|
+
scrollbarContainer.setAttribute('data-has-overflow', slider.details.hasOverflow.toString());
|
|
433
|
+
};
|
|
434
|
+
setDataAttributes();
|
|
435
|
+
const getScrollbarButtonLeftOffset = () => {
|
|
436
|
+
const scrollbarRatio = slider.container.offsetWidth / slider.container.scrollWidth;
|
|
437
|
+
return slider.container.scrollLeft * scrollbarRatio;
|
|
438
|
+
};
|
|
439
|
+
// scrollbarbutton width and position is calculated based on the scroll position and available width
|
|
440
|
+
let requestId = 0;
|
|
441
|
+
const update = () => {
|
|
442
|
+
if (requestId) {
|
|
443
|
+
window.cancelAnimationFrame(requestId);
|
|
444
|
+
}
|
|
445
|
+
requestId = window.requestAnimationFrame(() => {
|
|
446
|
+
const scrollbarButtonWidth = (slider.container.offsetWidth / slider.container.scrollWidth) * 100;
|
|
447
|
+
const scrollLeftInPortion = getScrollbarButtonLeftOffset();
|
|
448
|
+
scrollbarButton.style.width = `${scrollbarButtonWidth}%`;
|
|
449
|
+
scrollbarButton.style.transform = `translateX(${scrollLeftInPortion}px)`;
|
|
450
|
+
// aria-valuenow
|
|
451
|
+
const scrollLeft = slider.container.scrollLeft;
|
|
452
|
+
const scrollWidth = slider.container.scrollWidth;
|
|
453
|
+
const containerWidth = slider.container.offsetWidth;
|
|
454
|
+
const scrollPercentage = (scrollLeft / (scrollWidth - containerWidth)) * 100;
|
|
455
|
+
scrollbarContainer.setAttribute('aria-valuenow', Math.round(Number.isNaN(scrollPercentage) ? 0 : scrollPercentage).toString());
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
// insert to DOM
|
|
459
|
+
if (options.container) {
|
|
460
|
+
options.container.appendChild(scrollbarContainer);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
(_c = slider.container.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(scrollbarContainer, slider.container.nextSibling);
|
|
464
|
+
}
|
|
465
|
+
// update the scrollbar when the slider is scrolled
|
|
466
|
+
update();
|
|
467
|
+
slider.on('scroll', update);
|
|
468
|
+
slider.on('contentsChanged', update);
|
|
469
|
+
slider.on('containerSizeChanged', update);
|
|
470
|
+
slider.on('detailsChanged', setDataAttributes);
|
|
471
|
+
// handle arrow keys while focused
|
|
472
|
+
scrollbarContainer.addEventListener('keydown', (e) => {
|
|
473
|
+
if (e.key === 'ArrowLeft') {
|
|
474
|
+
slider.moveToDirection('prev');
|
|
475
|
+
}
|
|
476
|
+
else if (e.key === 'ArrowRight') {
|
|
477
|
+
slider.moveToDirection('next');
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// handle click to before or after the scrollbar button
|
|
481
|
+
scrollbarContainer.addEventListener('click', (e) => {
|
|
482
|
+
const scrollbarButtonWidth = scrollbarButton.offsetWidth;
|
|
483
|
+
const scrollbarButtonLeft = getScrollbarButtonLeftOffset();
|
|
484
|
+
const scrollbarButtonRight = scrollbarButtonLeft + scrollbarButtonWidth;
|
|
485
|
+
const clickX = e.pageX - scrollbarContainer.offsetLeft;
|
|
486
|
+
if (clickX < scrollbarButtonLeft) {
|
|
487
|
+
slider.moveToDirection('prev');
|
|
488
|
+
}
|
|
489
|
+
else if (clickX > scrollbarButtonRight) {
|
|
490
|
+
slider.moveToDirection('next');
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
// make scrollbar button draggable via mouse/touch and update the scroll position
|
|
494
|
+
let isMouseDown = false;
|
|
495
|
+
let startX = 0;
|
|
496
|
+
let scrollLeft = 0;
|
|
497
|
+
scrollbarButton.addEventListener('mousedown', (e) => {
|
|
498
|
+
isMouseDown = true;
|
|
499
|
+
startX = e.pageX - scrollbarContainer.offsetLeft;
|
|
500
|
+
scrollLeft = slider.container.scrollLeft;
|
|
501
|
+
// change cursor to grabbing
|
|
502
|
+
scrollbarButton.style.cursor = 'grabbing';
|
|
503
|
+
slider.container.style.scrollSnapType = 'none';
|
|
504
|
+
scrollbarButton.setAttribute('data-is-grabbed', 'true');
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
});
|
|
508
|
+
window.addEventListener('mouseup', () => {
|
|
509
|
+
isMouseDown = false;
|
|
510
|
+
scrollbarButton.style.cursor = '';
|
|
511
|
+
slider.container.style.scrollSnapType = '';
|
|
512
|
+
scrollbarButton.setAttribute('data-is-grabbed', 'false');
|
|
513
|
+
});
|
|
514
|
+
window.addEventListener('mousemove', (e) => {
|
|
515
|
+
if (!isMouseDown) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
const x = e.pageX - scrollbarContainer.offsetLeft;
|
|
520
|
+
const scrollingFactor = slider.container.scrollWidth / scrollbarContainer.offsetWidth;
|
|
521
|
+
const walk = (x - startX) * scrollingFactor;
|
|
522
|
+
slider.container.scrollLeft = scrollLeft + walk;
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK = 20;
|
|
528
|
+
function DragScrollingPlugin(args) {
|
|
529
|
+
var _a;
|
|
530
|
+
const options = {
|
|
531
|
+
draggedDistanceThatPreventsClick: (_a = args === null || args === void 0 ? void 0 : args.draggedDistanceThatPreventsClick) !== null && _a !== void 0 ? _a : DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK,
|
|
532
|
+
};
|
|
533
|
+
return (slider) => {
|
|
534
|
+
let isMouseDown = false;
|
|
535
|
+
let startX = 0;
|
|
536
|
+
let scrollLeft = 0;
|
|
537
|
+
const hasOverflow = () => {
|
|
538
|
+
return slider.details.hasOverflow;
|
|
539
|
+
};
|
|
540
|
+
slider.container.addEventListener('mousedown', (e) => {
|
|
541
|
+
if (!hasOverflow()) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
isMouseDown = true;
|
|
545
|
+
startX = e.pageX - slider.container.offsetLeft;
|
|
546
|
+
scrollLeft = slider.container.scrollLeft;
|
|
547
|
+
// change cursor to grabbing
|
|
548
|
+
slider.container.style.cursor = 'grabbing';
|
|
549
|
+
slider.container.style.scrollSnapType = 'none';
|
|
550
|
+
// prevent pointer events on the slides
|
|
551
|
+
// const slides = slider.container.querySelectorAll( ':scope > *' );
|
|
552
|
+
// slides.forEach((slide) => {
|
|
553
|
+
// (<HTMLElement>slide).style.pointerEvents = 'none';
|
|
554
|
+
// });
|
|
555
|
+
// prevent focus going to the slides
|
|
556
|
+
// e.preventDefault();
|
|
557
|
+
// e.stopPropagation();
|
|
558
|
+
});
|
|
559
|
+
window.addEventListener('mouseup', () => {
|
|
560
|
+
if (!hasOverflow()) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
isMouseDown = false;
|
|
564
|
+
slider.container.style.cursor = '';
|
|
565
|
+
slider.container.style.scrollSnapType = '';
|
|
566
|
+
setTimeout(() => {
|
|
567
|
+
const slides = slider.container.querySelectorAll(':scope > *');
|
|
568
|
+
slides.forEach((slide) => {
|
|
569
|
+
slide.style.pointerEvents = '';
|
|
570
|
+
});
|
|
571
|
+
}, 50);
|
|
572
|
+
});
|
|
573
|
+
window.addEventListener('mousemove', (e) => {
|
|
574
|
+
if (!hasOverflow()) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (!isMouseDown) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
e.preventDefault();
|
|
581
|
+
const x = e.pageX - slider.container.offsetLeft;
|
|
582
|
+
const walk = (x - startX);
|
|
583
|
+
slider.container.scrollLeft = scrollLeft - walk;
|
|
584
|
+
// if walk is more than 30px, don't allow click event
|
|
585
|
+
// e.preventDefault();
|
|
586
|
+
const absWalk = Math.abs(walk);
|
|
587
|
+
const slides = slider.container.querySelectorAll(':scope > *');
|
|
588
|
+
const pointerEvents = absWalk > options.draggedDistanceThatPreventsClick ? 'none' : '';
|
|
589
|
+
slides.forEach((slide) => {
|
|
590
|
+
slide.style.pointerEvents = pointerEvents;
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const DEFAULT_TEXTS = {
|
|
597
|
+
dotDescription: 'Page %d of %d',
|
|
598
|
+
};
|
|
599
|
+
const DEFAULT_CLASS_NAMES = {
|
|
600
|
+
dotsContainer: 'overflow-slider__dots',
|
|
601
|
+
dotsItem: 'overflow-slider__dot-item',
|
|
602
|
+
};
|
|
603
|
+
function DotsPlugin(args) {
|
|
604
|
+
return (slider) => {
|
|
605
|
+
var _a, _b;
|
|
606
|
+
const options = {
|
|
607
|
+
texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []),
|
|
608
|
+
classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []),
|
|
609
|
+
container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null,
|
|
610
|
+
};
|
|
611
|
+
const dots = document.createElement('div');
|
|
612
|
+
dots.classList.add(options.classNames.dotsContainer);
|
|
613
|
+
let pageFocused = null;
|
|
614
|
+
const buildDots = () => {
|
|
615
|
+
dots.setAttribute('data-has-content', slider.details.hasOverflow.toString());
|
|
616
|
+
dots.innerHTML = '';
|
|
617
|
+
const dotsList = document.createElement('ul');
|
|
618
|
+
const pages = slider.details.amountOfPages;
|
|
619
|
+
const currentPage = slider.details.currentPage;
|
|
620
|
+
if (pages <= 1) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
for (let i = 0; i < pages; i++) {
|
|
624
|
+
const dotListItem = document.createElement('li');
|
|
625
|
+
const dot = document.createElement('button');
|
|
626
|
+
dot.setAttribute('type', 'button');
|
|
627
|
+
dot.setAttribute('class', options.classNames.dotsItem);
|
|
628
|
+
dot.setAttribute('aria-label', options.texts.dotDescription.replace('%d', (i + 1).toString()).replace('%d', pages.toString()));
|
|
629
|
+
dot.setAttribute('aria-pressed', (i === currentPage).toString());
|
|
630
|
+
dot.setAttribute('data-page', (i + 1).toString());
|
|
631
|
+
dotListItem.appendChild(dot);
|
|
632
|
+
dotsList.appendChild(dotListItem);
|
|
633
|
+
dot.addEventListener('click', () => activateDot(i + 1));
|
|
634
|
+
dot.addEventListener('focus', () => pageFocused = i + 1);
|
|
635
|
+
dot.addEventListener('keydown', (e) => {
|
|
636
|
+
var _a;
|
|
637
|
+
const currentPageItem = dots.querySelector(`[aria-pressed="true"]`);
|
|
638
|
+
if (!currentPageItem) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const currentPage = parseInt((_a = currentPageItem.getAttribute('data-page')) !== null && _a !== void 0 ? _a : '1');
|
|
642
|
+
if (e.key === 'ArrowLeft') {
|
|
643
|
+
const previousPage = currentPage - 1;
|
|
644
|
+
if (previousPage > 0) {
|
|
645
|
+
const matchingDot = dots.querySelector(`[data-page="${previousPage}"]`);
|
|
646
|
+
if (matchingDot) {
|
|
647
|
+
matchingDot.focus();
|
|
648
|
+
}
|
|
649
|
+
activateDot(previousPage);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (e.key === 'ArrowRight') {
|
|
653
|
+
const nextPage = currentPage + 1;
|
|
654
|
+
if (nextPage <= pages) {
|
|
655
|
+
const matchingDot = dots.querySelector(`[data-page="${nextPage}"]`);
|
|
656
|
+
if (matchingDot) {
|
|
657
|
+
matchingDot.focus();
|
|
658
|
+
}
|
|
659
|
+
activateDot(nextPage);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
dots.appendChild(dotsList);
|
|
665
|
+
// return focus to same page after rebuild
|
|
666
|
+
if (pageFocused) {
|
|
667
|
+
const matchingDot = dots.querySelector(`[data-page="${pageFocused}"]`);
|
|
668
|
+
if (matchingDot) {
|
|
669
|
+
matchingDot.focus();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const activateDot = (page) => {
|
|
674
|
+
const scrollTargetPosition = slider.details.containerWidth * (page - 1);
|
|
675
|
+
slider.container.style.scrollBehavior = slider.options.scrollBehavior;
|
|
676
|
+
slider.container.style.scrollSnapType = 'none';
|
|
677
|
+
slider.container.scrollLeft = scrollTargetPosition;
|
|
678
|
+
slider.container.style.scrollBehavior = '';
|
|
679
|
+
slider.container.style.scrollSnapType = '';
|
|
680
|
+
};
|
|
681
|
+
buildDots();
|
|
682
|
+
if (options.container) {
|
|
683
|
+
options.container.appendChild(dots);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
(_b = slider.container.parentNode) === null || _b === void 0 ? void 0 : _b.insertBefore(dots, slider.container.nextSibling);
|
|
687
|
+
}
|
|
688
|
+
slider.on('detailsChanged', () => {
|
|
689
|
+
buildDots();
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export { ArrowsPlugin, DotsPlugin, DragScrollingPlugin, OverflowSlider, ScrollIndicatorPlugin, SkipLinksPlugin };
|