Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Separate core and plugins into separate files
- Loading branch information
1 parent
a9d9f93
commit c349069
Showing
93 changed files
with
5,595 additions
and
3,847 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
const DEFAULT_TEXTS = { | ||
buttonPrevious: 'Previous items', | ||
buttonNext: 'Next items', | ||
}; | ||
const DEFAULT_ICONS = { | ||
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>', | ||
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>', | ||
}; | ||
const DEFAULT_CLASS_NAMES = { | ||
navContainer: 'overflow-slider__arrows', | ||
prevButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--prev', | ||
nextButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--next', | ||
}; | ||
function ArrowsPlugin(args) { | ||
return (slider) => { | ||
var _a, _b, _c, _d; | ||
const options = { | ||
texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), | ||
icons: Object.assign(Object.assign({}, DEFAULT_ICONS), (args === null || args === void 0 ? void 0 : args.icons) || []), | ||
classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), | ||
container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null, | ||
}; | ||
const nav = document.createElement('div'); | ||
nav.classList.add(options.classNames.navContainer); | ||
const prev = document.createElement('button'); | ||
prev.setAttribute('class', options.classNames.prevButton); | ||
prev.setAttribute('type', 'button'); | ||
prev.setAttribute('aria-label', options.texts.buttonPrevious); | ||
prev.setAttribute('aria-controls', (_b = slider.container.getAttribute('id')) !== null && _b !== void 0 ? _b : ''); | ||
prev.setAttribute('data-type', 'prev'); | ||
prev.innerHTML = options.icons.prev; | ||
prev.addEventListener('click', () => slider.moveToDirection('prev')); | ||
const next = document.createElement('button'); | ||
next.setAttribute('class', options.classNames.nextButton); | ||
next.setAttribute('type', 'button'); | ||
next.setAttribute('aria-label', options.texts.buttonNext); | ||
next.setAttribute('aria-controls', (_c = slider.container.getAttribute('id')) !== null && _c !== void 0 ? _c : ''); | ||
next.setAttribute('data-type', 'next'); | ||
next.innerHTML = options.icons.next; | ||
next.addEventListener('click', () => slider.moveToDirection('next')); | ||
// insert buttons to the nav | ||
nav.appendChild(prev); | ||
nav.appendChild(next); | ||
const update = () => { | ||
const scrollLeft = slider.container.scrollLeft; | ||
const scrollWidth = slider.container.scrollWidth; | ||
const clientWidth = slider.container.clientWidth; | ||
if (scrollLeft === 0) { | ||
prev.setAttribute('data-has-content', 'false'); | ||
} | ||
else { | ||
prev.setAttribute('data-has-content', 'true'); | ||
} | ||
if (scrollLeft + clientWidth >= scrollWidth) { | ||
next.setAttribute('data-has-content', 'false'); | ||
} | ||
else { | ||
next.setAttribute('data-has-content', 'true'); | ||
} | ||
}; | ||
if (options.container) { | ||
options.container.appendChild(nav); | ||
} | ||
else { | ||
(_d = slider.container.parentNode) === null || _d === void 0 ? void 0 : _d.insertBefore(nav, slider.container.nextSibling); | ||
} | ||
update(); | ||
slider.on('scroll', update); | ||
slider.on('contentsChanged', update); | ||
slider.on('containerSizeChanged', update); | ||
}; | ||
} | ||
|
||
export { ArrowsPlugin as default }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
function details(slider) { | ||
let instance; | ||
let hasOverflow = false; | ||
let slideCount = 0; | ||
let containerWidth = 0; | ||
let scrollableAreaWidth = 0; | ||
let amountOfPages = 0; | ||
let currentPage = 1; | ||
if (slider.container.scrollWidth > slider.container.clientWidth) { | ||
hasOverflow = true; | ||
} | ||
slideCount = Array.from(slider.container.querySelectorAll(':scope > *')).length; | ||
containerWidth = slider.container.offsetWidth; | ||
scrollableAreaWidth = slider.container.scrollWidth; | ||
amountOfPages = Math.ceil(scrollableAreaWidth / containerWidth); | ||
if (slider.container.scrollLeft >= 0) { | ||
currentPage = Math.floor(slider.container.scrollLeft / containerWidth); | ||
// consider as last page if the scrollLeft + containerWidth is equal to scrollWidth | ||
if (slider.container.scrollLeft + containerWidth === scrollableAreaWidth) { | ||
currentPage = amountOfPages - 1; | ||
} | ||
} | ||
instance = { | ||
hasOverflow, | ||
slideCount, | ||
containerWidth, | ||
scrollableAreaWidth, | ||
amountOfPages, | ||
currentPage, | ||
}; | ||
return instance; | ||
} | ||
|
||
export { details as default }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
function details(slider) { | ||
let instance; | ||
let hasOverflow = false; | ||
let slideCount = 0; | ||
let containerWidth = 0; | ||
let scrollableAreaWidth = 0; | ||
let amountOfPages = 0; | ||
let currentPage = 1; | ||
if (slider.container.scrollWidth > slider.container.clientWidth) { | ||
hasOverflow = true; | ||
} | ||
slideCount = Array.from(slider.container.querySelectorAll(':scope > *')).length; | ||
containerWidth = slider.container.offsetWidth; | ||
scrollableAreaWidth = slider.container.scrollWidth; | ||
amountOfPages = Math.ceil(scrollableAreaWidth / containerWidth); | ||
if (slider.container.scrollLeft >= 0) { | ||
currentPage = Math.floor(slider.container.scrollLeft / containerWidth); | ||
// consider as last page if the scrollLeft + containerWidth is equal to scrollWidth | ||
if (slider.container.scrollLeft + containerWidth === scrollableAreaWidth) { | ||
currentPage = amountOfPages - 1; | ||
} | ||
} | ||
instance = { | ||
hasOverflow, | ||
slideCount, | ||
containerWidth, | ||
scrollableAreaWidth, | ||
amountOfPages, | ||
currentPage, | ||
}; | ||
return instance; | ||
} | ||
|
||
export { details as default }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
import details from './details.esm.js'; | ||
import { generateId, objectsAreEqual } from './utils.esm.js'; | ||
|
||
function Slider(container, options, plugins) { | ||
let slider; | ||
let subs = {}; | ||
function init() { | ||
slider.container = container; | ||
// ensure container has id | ||
let containerId = container.getAttribute('id'); | ||
if (containerId === null) { | ||
containerId = generateId('overflow-slider'); | ||
container.setAttribute('id', containerId); | ||
} | ||
setDetails(true); | ||
slider.on('contentsChanged', () => setDetails()); | ||
slider.on('containerSizeChanged', () => setDetails()); | ||
let requestId = 0; | ||
const setDetailsDebounce = () => { | ||
if (requestId) { | ||
window.cancelAnimationFrame(requestId); | ||
} | ||
requestId = window.requestAnimationFrame(() => { | ||
setDetails(); | ||
}); | ||
}; | ||
slider.on('scroll', setDetailsDebounce); | ||
addEventListeners(); | ||
setDataAttributes(); | ||
setCSSVariables(); | ||
if (plugins) { | ||
for (const plugin of plugins) { | ||
plugin(slider); | ||
} | ||
} | ||
slider.on('detailsChanged', () => { | ||
setDataAttributes(); | ||
setCSSVariables(); | ||
}); | ||
slider.emit('created'); | ||
} | ||
function setDetails(isInit = false) { | ||
const oldDetails = slider.details; | ||
const newDetails = details(slider); | ||
slider.details = newDetails; | ||
if (!isInit && !objectsAreEqual(oldDetails, newDetails)) { | ||
slider.emit('detailsChanged'); | ||
} | ||
else if (isInit) { | ||
slider.emit('detailsChanged'); | ||
} | ||
} | ||
function addEventListeners() { | ||
// changes to DOM | ||
const observer = new MutationObserver(() => slider.emit('contentsChanged')); | ||
observer.observe(slider.container, { childList: true }); | ||
// container size changes | ||
const resizeObserver = new ResizeObserver(() => slider.emit('containerSizeChanged')); | ||
resizeObserver.observe(slider.container); | ||
// scroll event with debouncing | ||
slider.container.addEventListener('scroll', () => slider.emit('scroll')); | ||
// Listen for mouse down and touch start events on the document | ||
// This handles both mouse clicks and touch interactions | ||
let wasInteractedWith = false; | ||
slider.container.addEventListener('mousedown', () => { | ||
wasInteractedWith = true; | ||
}); | ||
slider.container.addEventListener('touchstart', () => { | ||
wasInteractedWith = true; | ||
}, { passive: true }); | ||
slider.container.addEventListener('focusin', (e) => { | ||
// move target parents as long as they are not the container | ||
// but only if focus didn't start from mouse or touch | ||
if (!wasInteractedWith) { | ||
let target = e.target; | ||
while (target.parentElement !== slider.container) { | ||
if (target.parentElement) { | ||
target = target.parentElement; | ||
} | ||
else { | ||
break; | ||
} | ||
} | ||
ensureSlideIsInView(target); | ||
} | ||
wasInteractedWith = false; | ||
}); | ||
} | ||
function setCSSVariables() { | ||
slider.container.style.setProperty('--slider-container-width', `${slider.details.containerWidth}px`); | ||
slider.container.style.setProperty('--slider-scrollable-width', `${slider.details.scrollableAreaWidth}px`); | ||
slider.container.style.setProperty('--slider-slides-count', `${slider.details.slideCount}`); | ||
} | ||
function setDataAttributes() { | ||
slider.container.setAttribute('data-has-overflow', slider.details.hasOverflow ? 'true' : 'false'); | ||
} | ||
function ensureSlideIsInView(slide) { | ||
const slideRect = slide.getBoundingClientRect(); | ||
const sliderRect = slider.container.getBoundingClientRect(); | ||
const containerWidth = slider.container.offsetWidth; | ||
const scrollLeft = slider.container.scrollLeft; | ||
const slideStart = slideRect.left - sliderRect.left + scrollLeft; | ||
const slideEnd = slideStart + slideRect.width; | ||
let scrollTarget = null; | ||
if (slideStart < scrollLeft) { | ||
scrollTarget = slideStart; | ||
} | ||
else if (slideEnd > scrollLeft + containerWidth) { | ||
scrollTarget = slideEnd - containerWidth; | ||
} | ||
if (scrollTarget) { | ||
slider.container.style.scrollSnapType = 'none'; | ||
slider.container.scrollLeft = scrollTarget; | ||
// @todo resume scroll snapping but at least proximity gives a lot of trouble | ||
// and it's not really needed for this use case but it would be nice to have | ||
// it back in case it's needed. We need to calculate scrollLeft some other way | ||
} | ||
} | ||
function moveToDirection(direction = "prev") { | ||
const scrollStrategy = slider.options.scrollStrategy; | ||
const scrollLeft = slider.container.scrollLeft; | ||
const sliderRect = slider.container.getBoundingClientRect(); | ||
const containerWidth = slider.container.offsetWidth; | ||
let targetScrollPosition = scrollLeft; | ||
if (direction === 'prev') { | ||
targetScrollPosition = Math.max(0, scrollLeft - slider.container.offsetWidth); | ||
} | ||
else if (direction === 'next') { | ||
targetScrollPosition = Math.min(slider.container.scrollWidth, scrollLeft + slider.container.offsetWidth); | ||
} | ||
if (scrollStrategy === 'fullSlide') { | ||
let fullSldeTargetScrollPosition = null; | ||
const slides = Array.from(slider.container.querySelectorAll(':scope > *')); | ||
let gapSize = 0; | ||
if (slides.length > 1) { | ||
const firstSlideRect = slides[0].getBoundingClientRect(); | ||
const secondSlideRect = slides[1].getBoundingClientRect(); | ||
gapSize = secondSlideRect.left - firstSlideRect.right; | ||
} | ||
// extend targetScrollPosition to include gap | ||
if (direction === 'prev') { | ||
fullSldeTargetScrollPosition = Math.max(0, targetScrollPosition - gapSize); | ||
} | ||
else { | ||
fullSldeTargetScrollPosition = Math.min(slider.container.scrollWidth, targetScrollPosition + gapSize); | ||
} | ||
if (direction === 'next') { | ||
let partialSlideFound = false; | ||
for (let slide of slides) { | ||
const slideRect = slide.getBoundingClientRect(); | ||
const slideStart = slideRect.left - sliderRect.left + scrollLeft; | ||
const slideEnd = slideStart + slideRect.width; | ||
if (slideStart < targetScrollPosition && slideEnd > targetScrollPosition) { | ||
fullSldeTargetScrollPosition = slideStart; | ||
partialSlideFound = true; | ||
break; | ||
} | ||
} | ||
if (!partialSlideFound) { | ||
fullSldeTargetScrollPosition = Math.min(targetScrollPosition, slider.container.scrollWidth - slider.container.offsetWidth); | ||
} | ||
if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition > scrollLeft) { | ||
targetScrollPosition = fullSldeTargetScrollPosition; | ||
} | ||
} | ||
else { | ||
let partialSlideFound = false; | ||
for (let slide of slides) { | ||
const slideRect = slide.getBoundingClientRect(); | ||
const slideStart = slideRect.left - sliderRect.left + scrollLeft; | ||
const slideEnd = slideStart + slideRect.width; | ||
if (slideStart < scrollLeft && slideEnd > scrollLeft) { | ||
fullSldeTargetScrollPosition = slideEnd - containerWidth; | ||
partialSlideFound = true; | ||
break; | ||
} | ||
} | ||
if (!partialSlideFound) { | ||
fullSldeTargetScrollPosition = Math.max(0, scrollLeft - containerWidth); | ||
} | ||
if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition < scrollLeft) { | ||
targetScrollPosition = fullSldeTargetScrollPosition; | ||
} | ||
} | ||
} | ||
slider.container.style.scrollBehavior = slider.options.scrollBehavior; | ||
slider.container.scrollLeft = targetScrollPosition; | ||
setTimeout(() => slider.container.style.scrollBehavior = '', 50); | ||
} | ||
function on(name, cb) { | ||
if (!subs[name]) { | ||
subs[name] = []; | ||
} | ||
subs[name].push(cb); | ||
} | ||
function emit(name) { | ||
var _a; | ||
if (subs && subs[name]) { | ||
subs[name].forEach(cb => { | ||
cb(slider); | ||
}); | ||
} | ||
const optionCallBack = (_a = slider === null || slider === void 0 ? void 0 : slider.options) === null || _a === void 0 ? void 0 : _a[name]; | ||
if (typeof optionCallBack === 'function') { | ||
optionCallBack(slider); | ||
} | ||
} | ||
slider = { | ||
emit, | ||
moveToDirection, | ||
on, | ||
options, | ||
}; | ||
init(); | ||
return slider; | ||
} | ||
|
||
export { Slider as default }; |
Oops, something went wrong.