Skip to content

Instantly share code, notes, and snippets.

@dpw1
Created January 5, 2026 09:37
Show Gist options
  • Select an option

  • Save dpw1/d3fa466530c2057c4720c82ce4f3a26e to your computer and use it in GitHub Desktop.

Select an option

Save dpw1/d3fa466530c2057c4720c82ce4f3a26e to your computer and use it in GitHub Desktop.
<script data-source="ezfycode.com">
window.ezfyProductSliderArrows = window.ezfyProductSliderArrows || {};
ezfyProductSliderArrows = (function () {
const SELECTORS = {
mainSlider: "slider-component[id*='GalleryViewer']",
mainSliderList: "slider-component[id*='GalleryViewer'] ul",
thumbnailSlider: "slider-component[id*='GalleryThumb']",
thumbnailSliderList: "slider-component[id*='GalleryThumb'] ul",
thumbnailsContainer: "ul[id*='Thumbnails']",
sliderComponent: "slider-component",
productContainer: ".product"
};
const ENABLE_CONSOLE_LOG = true;
function log(...args) {
if (ENABLE_CONSOLE_LOG) {
console.log(...args);
}
}
function _extractTextBetween(text, start, end) {
if (!start || !end) {
throw new Error(`Please add a "start" and "end" parameter`);
}
return text.split(start)[1].split(end)[0];
}
function _moveDOMElement(parent, child) {
document.querySelector(parent).appendChild(document.querySelector(child));
}
function _isProductPage() {
return /product/.test(window.location.href);
}
function _isCartPage() {
return /cart/.test(window.location.href);
}
function isCollectionsPage() {
return /\/collections\/.*(\/)?$/.test(window.location.pathname);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function _waitForElement(selector, delay = 50, tries = 100) {
const element = document.querySelector(selector);
if (!window[`__${selector}`]) {
window[`__${selector}`] = 0;
window[`__${selector}__delay`] = delay;
window[`__${selector}__tries`] = tries;
}
function _search() {
return new Promise((resolve) => {
window[`__${selector}`]++;
setTimeout(resolve, window[`__${selector}__delay`]);
});
}
if (element === null) {
if (window[`__${selector}`] >= window[`__${selector}__tries`]) {
window[`__${selector}`] = 0;
return Promise.resolve(null);
}
return _search().then(() => _waitForElement(selector));
} else {
return Promise.resolve(element);
}
}
function syncSliderOrder() {
const mainSlider = document.querySelector(SELECTORS.mainSliderList);
const thumbnailSlider = document.querySelector(SELECTORS.thumbnailSliderList);
if (!mainSlider || !thumbnailSlider) {
log('Main slider or thumbnail slider not found.');
return;
}
const mainSlides = Array.from(mainSlider.querySelectorAll('li'));
const thumbnailSlides = Array.from(thumbnailSlider.querySelectorAll('li'));
mainSlides.forEach((mainSlide, index) => {
const mediaId = mainSlide.getAttribute('data-media-id');
if (mediaId) {
const matchingThumbnail = thumbnailSlides.find(
(thumbnail) => thumbnail.getAttribute('data-target') === mediaId
);
if (matchingThumbnail) {
mainSlide.style.order = index + 1;
matchingThumbnail.style.order = index + 1;
}
}
});
log('Slider and thumbnail order synced successfully.');
}
function resetSlider() {
const $slider = document.querySelector(SELECTORS.sliderComponent);
const $ul = $slider.querySelector('ul');
if ($ul.children.length > 0) {
Array.from($ul.children).forEach((item) => {
item.classList.remove('is-active');
});
$ul.children[0].classList.add('is-active');
$ul.style.transition = 'transform 0ms';
$ul.style.transform = 'translateX(0px)';
}
}
function restart(){
resetSlider();
syncSliderOrder();
setupThumbnailClickHandler();
}
function updateThumbnail() {
log(`Starting updateThumbnail function.`);
const thumbnailsContainer = document.querySelector(SELECTORS.thumbnailsContainer);
if (!thumbnailsContainer) {
log(`No thumbnails container found. Exiting function.`);
return;
}
log(`Thumbnails container found.`);
const activeSlide = document.querySelector('.is-active');
if (!activeSlide) {
log(`No active slide found. Exiting function.`);
return;
}
log(`Active slide found.`);
const activeMediaPosition = activeSlide.getAttribute('data-media-id');
if (!activeMediaPosition) {
log(`Active slide lacks 'data-media-position' attribute. Exiting function.`);
return;
}
log(`Active slide has data-media-position: ${activeMediaPosition}.`);
const matchingThumbnail = Array.from(thumbnailsContainer.children).find(
(thumbnail) => thumbnail.getAttribute('data-target') === activeMediaPosition
);
if (!matchingThumbnail) {
log(`No matching thumbnail found for data-media-position: ${activeMediaPosition}. Exiting function.`);
return;
}
log(`Matching thumbnail found for data-media-position: ${activeMediaPosition}.`);
const thumbnailButton = matchingThumbnail.querySelector('button');
if (thumbnailButton) {
Array.from(thumbnailsContainer.querySelectorAll('button')).forEach((button) => {
button.removeAttribute('aria-current');
});
log(`Removed 'aria-current' from all thumbnail buttons.`);
thumbnailButton.setAttribute('aria-current', 'true');
log(`Set 'aria-current=true' on the matching thumbnail button.`);
log(`Clicked the matching thumbnail button.`);
} else {
log(`No button found inside the matching thumbnail. Exiting function.`);
}
}
{% assign default_svg = '<svg class="EzfyArrow-button-icon" viewBox="0 0 100 100"><path d="M 10,50 L 60,100 L 70,90 L 30,50 L 70,10 L 60,0 Z" class="arrow"></path></svg>' %}
{% assign custom_svg = section.settings.custom_svg | default: default_svg %}
function injectArrow(animationSpeed = 500) {
const html = `
<button class="EzfyArrow-button EzfyArrow-prev-next-button previous" type="button" aria-label="Previous">
{{ custom_svg }}
</button>
<button class="EzfyArrow-button EzfyArrow-prev-next-button next" type="button" aria-label="Next">
{{ custom_svg }}
</button>
`;
const $slider = document.querySelector(SELECTORS.sliderComponent);
const $ul = $slider.querySelector('ul');
if (!$slider.querySelector(`.EzfyArrow-button`)) {
$ul.insertAdjacentHTML('beforebegin', html);
}
const prevArrow = document.querySelector('.previous');
const nextArrow = document.querySelector('.next');
let currentIndex = 0;
const totalItems = $ul.children.length;
const itemWidth = $ul.querySelector('li').offsetWidth;
if (totalItems > 0) {
$ul.children[0].classList.add('is-active');
}
function handleArrowsDesktop(direction) {
window.ezfyArrowClicked = true;
const currentSlide = $ul.querySelector('.is-active');
let targetSlide;
if (direction === 'next') {
targetSlide = currentSlide.nextElementSibling || $ul.firstElementChild;
} else if (direction === 'prev') {
targetSlide = currentSlide.previousElementSibling || $ul.lastElementChild;
}
Array.from($ul.children).forEach((item) => {
item.classList.remove('is-active');
});
targetSlide.classList.add('is-active');
currentIndex = Array.from($ul.children).indexOf(targetSlide);
$ul.style.transition = `transform ${animationSpeed}ms ease`;
$ul.style.transform = `translateX(-${currentIndex * itemWidth}px)`;
setTimeout(() => {
window.ezfyArrowClicked = false;
}, animationSpeed);
}
if (!isMobile()) {
prevArrow.addEventListener('click', () => {
handleArrowsDesktop('prev');
setTimeout(() => {
updateThumbnail();
}, 50);
});
nextArrow.addEventListener('click', () => {
handleArrowsDesktop('next');
setTimeout(() => {
updateThumbnail();
}, 50);
});
}
}
function setupVariantChangeListeners() {
function handleVariantChange(event) {
const target = event.target;
if (target.tagName === 'SELECT') {
log('Dropdown changed:', target.value);
setTimeout(() => {
restart();
}, 50);
} else if (target.tagName === 'INPUT' && target.type === 'radio') {
log('Swatch changed:', target.value);
setTimeout(() => {
restart();
}, 50);
}
}
const productContainer = document.querySelector(SELECTORS.productContainer);
if (!productContainer) {
log('Product container not found.');
return;
}
productContainer.addEventListener('change', (event) => {
const target = event.target;
if (target.tagName === 'SELECT' || (target.tagName === 'INPUT' && target.type === 'radio')) {
handleVariantChange(event);
}
});
const mainSlider = document.querySelector(SELECTORS.mainSliderList);
const thumbnailSlider = document.querySelector(SELECTORS.thumbnailSliderList);
if (!mainSlider || !thumbnailSlider) {
log('Main slider or thumbnail slider not found.');
return;
}
const observerConfig = { childList: true, subtree: true };
const handleDomChanges = () => {
log('DOM changes detected. Restarting...');
restart();
};
const mainSliderObserver = new MutationObserver(handleDomChanges);
const thumbnailSliderObserver = new MutationObserver(handleDomChanges);
mainSliderObserver.observe(mainSlider, observerConfig);
thumbnailSliderObserver.observe(thumbnailSlider, observerConfig);
log('Variant change listeners and DOM observers set up successfully.');
}
function setupThumbnailClickHandler() {
const mainSlider = document.querySelector(SELECTORS.mainSliderList);
const thumbnailSlider = document.querySelector(SELECTORS.thumbnailSliderList);
if (!mainSlider || !thumbnailSlider) {
log('Main slider or thumbnail slider not found.');
return;
}
const mainSlides = Array.from(mainSlider.querySelectorAll('li'));
const thumbnailSlides = Array.from(thumbnailSlider.querySelectorAll('li'));
thumbnailSlides.forEach((thumbnail) => {
thumbnail.addEventListener('click', () => {
const targetId = thumbnail.getAttribute('data-target');
const matchingSlide = mainSlides.find(
(slide) => slide.getAttribute('data-media-id') === targetId
);
if (matchingSlide) {
mainSlides.forEach((slide) => slide.classList.remove('is-active'));
matchingSlide.classList.add('is-active');
const slideIndex = mainSlides.indexOf(matchingSlide);
const itemWidth = mainSlides[0].offsetWidth;
const translateXValue = -slideIndex * itemWidth;
mainSlider.style.transition = 'transform 500ms ease';
mainSlider.style.transform = `translateX(${translateXValue}px)`;
log(`Thumbnail clicked: Moved main slider to slide ${slideIndex + 1}`);
}
});
});
log('Thumbnail click handlers set up successfully.');
}
function setupSwipeDetection() {
const mainSlider = document.querySelector(SELECTORS.mainSliderList);
if (!mainSlider) {
log('Main slider not found for swipe detection');
return;
}
log('Setting up swipe detection on main slider');
let touchStartX = 0;
let touchEndX = 0;
let isTouching = false;
mainSlider.addEventListener('touchstart', (e) => {
isTouching = true;
touchStartX = e.changedTouches[0].screenX;
log('Touch started at X:', touchStartX);
}, { passive: true });
mainSlider.addEventListener('touchend', (e) => {
isTouching = false;
touchEndX = e.changedTouches[0].screenX;
log('Touch ended at X:', touchEndX);
const swipeDistance = touchEndX - touchStartX;
log('Swipe distance:', swipeDistance);
if (swipeDistance > 0) {
log('Right swipe detected');
} else if (swipeDistance < 0) {
log('Left swipe detected');
} else {
log('No horizontal swipe detected');
}
}, { passive: true });
window.ezfyScrollTimeout = null;
window.ezfyHasLoggedScrollEnd = false;
window.ezfyArrowClicked = false;
function forceSnap() {
// Recalculate slide positions fresh each time
const slides = mainSlider.querySelectorAll('li');
const slidePositions = Array.from(slides).map(slide => slide.offsetLeft);
const scrollLeft = mainSlider.scrollLeft;
let closest = slidePositions[0];
let minDistance = Math.abs(scrollLeft - closest);
for (let i = 1; i < slidePositions.length; i++) {
const distance = Math.abs(scrollLeft - slidePositions[i]);
if (distance < minDistance) {
closest = slidePositions[i];
minDistance = distance;
}
}
mainSlider.scrollTo({
left: closest,
behavior: 'smooth'
});
}
mainSlider.addEventListener('scroll', () => {
if (window.ezfyArrowClicked) {
return;
}
if (window.ezfyScrollTimeout) {
window.clearTimeout(window.ezfyScrollTimeout);
}
window.ezfyScrollTimeout = window.setTimeout(() => {
if (!window.ezfyHasLoggedScrollEnd && !isTouching) {
log('Scroll ended - slider has finished moving');
const slides = mainSlider.querySelectorAll('li');
const containerRect = mainSlider.getBoundingClientRect();
let mostVisibleSlide = null;
let maxVisibility = 0;
slides.forEach(slide => {
const slideRect = slide.getBoundingClientRect();
const visibleWidth = Math.min(slideRect.right, containerRect.right) -
Math.max(slideRect.left, containerRect.left);
const visibility = visibleWidth / slideRect.width;
if (visibility > maxVisibility) {
maxVisibility = visibility;
mostVisibleSlide = slide;
}
});
if (mostVisibleSlide) {
slides.forEach(slide => slide.classList.remove('is-active'));
mostVisibleSlide.classList.add('is-active');
log('Updated active slide to:', mostVisibleSlide.getAttribute('data-media-id'));
}
forceSnap();
window.ezfyHasLoggedScrollEnd = true;
}
}, 150);
window.ezfyHasLoggedScrollEnd = false;
}, { passive: true });
}
function isMobile() {
return window.innerWidth <= 749;
}
function setupMobileArrowHandler() {
if (!isMobile()) return;
const mainSlider = document.querySelector(SELECTORS.mainSliderList);
const prevArrow = document.querySelector('.EzfyArrow-button.previous');
const nextArrow = document.querySelector('.EzfyArrow-button.next');
if (!mainSlider || !prevArrow || !nextArrow) {
log('Required elements not found for mobile arrow handler');
return;
}
function scrollToSlide(direction) {
const slides = mainSlider.querySelectorAll('li');
const currentSlide = mainSlider.querySelector('.is-active');
const currentIndex = Array.from(slides).indexOf(currentSlide);
let targetSlide;
if (direction === 'next') {
targetSlide = currentSlide.nextElementSibling || slides[0];
} else {
targetSlide = currentSlide.previousElementSibling || slides[slides.length - 1];
}
slides.forEach(slide => slide.classList.remove('is-active'));
targetSlide.classList.add('is-active');
// Remove scroll snapping temporarily if needed
const previousSnap = mainSlider.style.scrollSnapType;
mainSlider.style.scrollSnapType = 'none';
// Calculate left offset
const offset = 0; // tweak if needed
const slideLeft = targetSlide.offsetLeft;
mainSlider.scrollTo({
left: slideLeft - offset,
behavior: 'smooth'
});
setTimeout(() => {
// Restore scroll snap
mainSlider.style.scrollSnapType = previousSnap;
updateThumbnail();
}, 300); // duration depends on smooth scroll behavior
}
prevArrow.addEventListener('click', () => {
scrollToSlide('prev');
});
nextArrow.addEventListener('click', () => {
scrollToSlide('next');
});
log('Mobile arrow handler set up successfully');
}
return {
restart:restart,
resetSlider:resetSlider,
syncSliderOrder:syncSliderOrder,
injectArrow: injectArrow,
init: function () {
document.addEventListener("DOMContentLoaded", function () {
injectArrow();
setupThumbnailClickHandler();
setupVariantChangeListeners();
setupSwipeDetection();
setupMobileArrowHandler();
});
},
};
})();
ezfyProductSliderArrows.init();
</script>
<style>
@keyframes fadeIn {
from {
opacity: 0.01;
}
to {
opacity: 1;
}
}
[id] [id*='GalleryViewer'] {
position: relative;
overflow: hidden;
touch-action: pan-x;
}
@media (max-width: 749px){
slider-component{
touch-action: pan-y;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
slider-component *{
touch-action: unset !important
}
}
.product-media-modal__content .globo-sw-media--hide{
display: unset !important;
}
[id] [id*='GalleryViewer'] ul{
display: flex;
transition: transform 500ms;
padding: 0px;
list-style: none;
transform: translateX(0px);
flex-wrap: nowrap;
touch-action: pan-x;
}
@media screen and (max-width: 749px) {
.ezfy-slider-arrows ~ * .grid--peek.slider .grid__item {
margin-left: unset !important;
}
}
@media screen and (min-width: 750px) {
[id] [id*='GalleryViewer'] ul {
transform: var(--transform-value, translateX(0px));
}
}
@media screen and (max-width: 749px) {
[id] [id*='GalleryViewer'] ul {
transform: translateX(0px) !important;
}
}
[id] [id*='GalleryViewer'] ul li{
width: 100%;
display: block !important;
touch-action: pan-x;
}
.EzfyArrow-button {
position: absolute;
background: {{ section.settings.arrow_bg_color }};
border: none;
color: {{ section.settings.arrow_color }};
opacity: 0.01;
animation: fadeIn 1s ease forwards;
z-index: 5;
width: {{ section.settings.arrow_size_desktop }}px;
height: {{ section.settings.arrow_size_desktop }}px;
border-radius: {{ section.settings.arrow_border_radius }}%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, color 0.2s ease;
touch-action: none;
cursor: pointer;
}
.EzfyArrow-button-icon {
fill: currentColor;
width: {{ section.settings.svg_size }}px;
height: {{ section.settings.svg_size }}px;
min-width: {{ section.settings.svg_size }}px;
min-height: {{ section.settings.svg_size }}px;
max-width: {{ section.settings.svg_size }}px;
max-height: {{ section.settings.svg_size }}px;
touch-action: none;
}
{% if section.settings.arrow_points_right %}
.EzfyArrow-button.next {
transform: translateY(-50%) rotate(180deg);
}
{% else %}
.EzfyArrow-button.previous {
transform: translateY(-50%) rotate(180deg);
}
{% endif %}
.EzfyArrow-prev-next-button {
top: 50%;
transform: translateY(-50%);
}
.EzfyArrow-prev-next-button.next { right: 10px; }
.EzfyArrow-prev-next-button.previous { left: 10px; }
@media screen and (max-width: 749px) {
.EzfyArrow-button {
width: {{ section.settings.arrow_size_mobile }}px;
height: {{ section.settings.arrow_size_mobile }}px;
}
.EzfyArrow-button-icon {
width: {{ section.settings.svg_size }}px;
height: {{ section.settings.svg_size }}px;
min-width: {{ section.settings.svg_size }}px;
min-height: {{ section.settings.svg_size }}px;
max-width: {{ section.settings.svg_size }}px;
max-height: {{ section.settings.svg_size }}px;
}
}
.EzfyArrow-button:hover {
background: {{ section.settings.arrow_bg_hover_color }};
color: {{ section.settings.arrow_hover_color }};
}
.EzfyArrow-button:focus {
outline: none;
}
.EzfyArrow-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gallery-viewer {
position: relative;
width: 100%;
overflow: hidden;
touch-action: pan-x;
}
.gallery-viewer-list {
display: flex;
transition: transform 0.5s ease;
touch-action: pan-x;
}
.gallery-viewer-item {
flex: 0 0 100%;
width: 100%;
touch-action: pan-x;
}
</style>
{% schema %}
{
"name": "EZFY Slider Arrows",
"class": "ezfy-slider-arrows",
"settings": [
{
"type": "header",
"content": "Arrow Size"
},
{
"type": "range",
"id": "arrow_size_desktop",
"min": 20,
"max": 100,
"step": 1,
"unit": "px",
"label": "Desktop Arrow Size",
"default": 44
},
{
"type": "range",
"id": "arrow_size_mobile",
"min": 16,
"max": 80,
"step": 1,
"unit": "px",
"label": "Mobile Arrow Size",
"default": 24
},
{
"type": "header",
"content": "SVG Dimensions"
},
{
"type": "range",
"id": "svg_size",
"min": 8,
"max": 80,
"step": 1,
"unit": "px",
"label": "SVG Size",
"default": 20
},
{
"type": "header",
"content": "Arrow Colors"
},
{
"type": "color",
"id": "arrow_color",
"label": "Arrow Color",
"default": "#333333"
},
{
"type": "color",
"id": "arrow_bg_color",
"label": "Arrow Background Color",
"default": "rgba(255, 255, 255, 0.75)"
},
{
"type": "color",
"id": "arrow_hover_color",
"label": "Arrow Hover Color",
"default": "#333333"
},
{
"type": "color",
"id": "arrow_bg_hover_color",
"label": "Arrow Background Hover Color",
"default": "#ffffff"
},
{
"type": "header",
"content": "Arrow Style"
},
{
"type": "range",
"id": "arrow_border_radius",
"min": 0,
"max": 50,
"step": 1,
"unit": "%",
"label": "Arrow Border Radius",
"default": 50
},
{
"type": "header",
"content": "Custom SVG"
},
{
"type": "textarea",
"id": "custom_svg",
"label": "Custom SVG Code",
"info": "Replace the default arrow SVG with your own. The SVG should be designed to work with the viewBox=\"0 0 100 100\". The default arrow uses a path with class=\"arrow\" for styling."
},
{
"type": "checkbox",
"id": "arrow_points_right",
"label": "Arrow points to the right",
"default": false,
"info": "If checked, the arrow points to the right. If unchecked, it points to the left."
}
],
"presets": [
{
"name": "EZFY Slider Arrows",
"category": "Custom by EZFY"
}
]
}
{% endschema %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment