generated from adobe/aem-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #561 from hlxsites/503-implement-carousel-ui-block
503 implement carousel UI block
- Loading branch information
Showing
4 changed files
with
502 additions
and
3 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,118 @@ | ||
import { button, div, span } from '../../scripts/dom-builder.js'; | ||
import Carousel from '../../scripts/Carousel.js'; | ||
|
||
const SLIDE_DELAY = 5000; | ||
const SLIDE_TRANSITION = 500; | ||
|
||
function configureNavigation(element) { | ||
const elementControls = element.querySelector('.carousel-controls'); | ||
const previousBtn = button({ type: 'button', class: 'flex items-center justify-center h-full cursor-pointer group focus:outline-none', 'data-carousel-prev': '' }); | ||
previousBtn.element = element; | ||
previousBtn.innerHTML = ` | ||
<span | ||
class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-danaherpurple-50 group-hover:danaherpurple-25" | ||
> | ||
<svg class="w-3 h-3 text-danaherpurple-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> | ||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M5 1 1 5l4 4" /> | ||
</svg> | ||
<span class="sr-only">Previous</span> | ||
</span> | ||
`; | ||
const nextBtn = button({ type: 'button', class: 'flex items-center justify-center h-full cursor-pointer group focus:outline-none', 'data-carousel-next': '' }); | ||
nextBtn.element = element; | ||
nextBtn.innerHTML = ` | ||
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-danaherpurple-50 group-hover:danaherpurple-25"> | ||
<svg class="w-3 h-3 text-danaherpurple-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> | ||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="m1 9 4-4-4-4" /> | ||
</svg> | ||
<span class="sr-only">Next</span> | ||
</span> | ||
`; | ||
elementControls.prepend(previousBtn); | ||
elementControls.append(nextBtn); | ||
return elementControls; | ||
} | ||
|
||
function configurePagination(element) { | ||
const paginateWrapper = div({ class: 'relative lg:max-w-7xl mx-auto' }, div({ class: 'carousel-controls absolute z-10 flex items-center gap-x-4 -translate-x-1/2 bottom-5 left-24 lg:left-28 space-x-3' })); | ||
paginateWrapper.querySelector('.carousel-controls').append(span({ class: 'carousel-paginate text-base font-bold' }, `1/${element.querySelectorAll('.carousel-slider').length}`)); | ||
element.append(paginateWrapper); | ||
} | ||
|
||
export default function decorate(block) { | ||
const uuid = crypto.randomUUID(4).substring(0, 6); | ||
block.classList.add(...'relative min-h-[30rem] md:min-h-[37rem] overflow-hidden'.split(' ')); | ||
const groupElements = [...block.children].reduce((prev, curr) => { | ||
prev.push([...curr.children]); | ||
return prev; | ||
}, []); | ||
block.innerHTML = ''; | ||
const slides = groupElements.map((ele, eleIndex) => { | ||
if (ele.length > 1) { | ||
const carouselSlider = div({ class: `carousel-slider duration-${SLIDE_TRANSITION} ease-in-out absolute inset-0 transition-transform transform z-10`, 'data-carousel-item': '' }); | ||
ele.map((el, index) => { | ||
let changedBtn = 0; | ||
if (index === 0) { | ||
el.classList.add(...'lg:w-1/2 px-0 lg:px-8 xl:pr-10 space-y-6 pb-10 pt-6 md:pt-4 lg:py-20'.split(' ')); | ||
const heading = el.querySelector('h2'); | ||
if (heading) { | ||
heading.classList.add(...'text-2xl md:text-4xl tracking-wide md:tracking-tight font-medium md:font-normal leading-8 md:leading-[55px]'.split(' ')); | ||
if (heading.previousElementSibling) { | ||
heading.previousElementSibling.classList.add(...'text-danaherpurple-500'.split(' ')); | ||
} | ||
if (heading.nextElementSibling) { | ||
heading.nextElementSibling.classList.add(...'text-xl font-extralight tracking-tight leading-7'.split(' ')); | ||
} | ||
} | ||
if (el.querySelector('.button-container')) { | ||
const actions = div({ class: 'flex flex-col md:flex-row gap-5' }); | ||
el.querySelectorAll('.button-container').forEach((btnContainer) => { | ||
btnContainer.querySelectorAll('.btn').forEach((elBtn) => { | ||
elBtn.className = `btn btn-lg ${(changedBtn === 0) ? 'btn-primary-purple' : 'btn-outline-trending-brand'} rounded-full px-6`; | ||
actions.append(elBtn); | ||
changedBtn = 1; | ||
}); | ||
}); | ||
el.append(actions); | ||
} | ||
carouselSlider.append(div({ class: 'max-w-7xl mx-auto w-full md:h-auto overflow-hidden lg:text-left' }, el)); | ||
} else { | ||
el.classList.add(...'relative h-full w-full lg:absolute lg:inset-y-0 lg:right-0 lg:h-full lg:w-1/2'.split(' ')); | ||
el.querySelector('img').classList.add(...'relative lg:absolute block w-full lg:w-1/2 lg:h-full lg:-translate-y-1/2 lg:top-1/2 lg:left-1/2 object-contain lg:object-cover'.split(' ')); | ||
carouselSlider.append(el.querySelector('img').parentElement); | ||
} | ||
changedBtn = 0; | ||
return index; | ||
}); | ||
block.append(carouselSlider); | ||
return { position: parseInt(eleIndex, 10), el: carouselSlider }; | ||
} | ||
return null; | ||
}).filter((item) => item); | ||
if (block.parentElement.className.includes('carousel-wrapper')) { | ||
let controls; | ||
block.parentElement.classList.add(...'relative w-full'.split(' ')); | ||
block.parentElement.setAttribute('data-carousel', 'slide'); | ||
block.parentElement.setAttribute('id', uuid); | ||
if (block.children.length > 1) { | ||
configurePagination(block.parentElement); | ||
controls = configureNavigation(block.parentElement); | ||
} | ||
const options = { | ||
defaultPosition: 0, | ||
interval: SLIDE_DELAY, | ||
onChange: (elIndex) => { | ||
if (block.children.length > 1) block.parentElement.querySelector('.carousel-paginate').innerHTML = `${elIndex + 1}/${slides.length}`; | ||
}, | ||
}; | ||
const carousel = new Carousel(block, slides, options); | ||
if (block.children.length > 1) { | ||
controls.querySelector('button[data-carousel-prev]').addEventListener('click', carousel.prev); | ||
controls.querySelector('button[data-carousel-next]').addEventListener('click', carousel.next); | ||
carousel.loop(); | ||
} else if (block.children.length === 1) { | ||
block.children[0].classList.remove(...'translate-x-full z-[1]'.split(' ')); | ||
block.children[0].classList.add(...'translate-x-0 z-[2]'.split(' ')); | ||
} | ||
} | ||
} |
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,189 @@ | ||
/* eslint no-underscore-dangle: 0 */ | ||
const DEFAULT = { | ||
defaultPosition: 0, | ||
indicators: { | ||
items: [], | ||
activeClasses: 'bg-white dark:bg-gray-800', | ||
inactiveClasses: | ||
'bg-white/50 dark:bg-gray-800/50 hover:bg-white dark:hover:bg-gray-800', | ||
}, | ||
interval: 3000, | ||
onNext: () => {}, | ||
onPrev: () => {}, | ||
onChange: () => {}, | ||
}; | ||
|
||
export default function Carousel(carouselEl, items, options) { | ||
/** | ||
* Clears the cycling interval | ||
*/ | ||
const pause = () => clearInterval(this._intervalInstance); | ||
|
||
/** | ||
* Get the currently active item | ||
*/ | ||
const getActiveItem = () => this._activeItem; | ||
|
||
/** | ||
* Set the currently active item and data attribute | ||
* @param {*} position | ||
*/ | ||
const setActiveItem = (item) => { | ||
this._activeItem = item; | ||
const { position } = item; | ||
|
||
// update the indicators if available | ||
if (this._indicators.length) { | ||
this._indicators = this._indicators.map((indicator) => { | ||
indicator.el.setAttribute('aria-current', 'false'); | ||
indicator.el.classList.remove(...this._options.indicators.activeClasses.split(' ')); | ||
indicator.el.classList.add(...this._options.indicators.inactiveClasses.split(' ')); | ||
return indicator; | ||
}); | ||
this._indicators[position].el.classList.add(...this._options.indicators.activeClasses.split(' ')); | ||
this._indicators[position].el.classList.remove(...this._options.indicators.inactiveClasses.split(' ')); | ||
this._indicators[position].el.setAttribute('aria-current', 'true'); | ||
} | ||
}; | ||
|
||
/** | ||
* Set an interval to loop through the carousel items | ||
*/ | ||
this.loop = () => { | ||
if (typeof window !== 'undefined') { | ||
this._intervalInstance = window.setInterval(() => { | ||
this.next(); | ||
}, this._intervalDuration); | ||
} | ||
}; | ||
|
||
/** | ||
* This method applies the transform classes | ||
* based on the left, middle, and right rotation carousel items | ||
* @param {*} rotationItems | ||
*/ | ||
const rotate = (rotationItems) => { | ||
// reset | ||
this._slides = this._slides.map((item) => { | ||
item.el.classList.add('hidden'); | ||
return item; | ||
}); | ||
|
||
// left item (previously active) | ||
rotationItems.left.el.classList.remove(...'-translate-x-full translate-x-full translate-x-0 hidden z-[2]'.split(' ')); | ||
rotationItems.left.el.classList.add(...'-translate-x-full z-[1]'.split(' ')); | ||
|
||
// currently active item | ||
rotationItems.middle.el.classList.remove(...'-translate-x-full translate-x-full translate-x-0 hidden z-[1]'.split(' ')); | ||
rotationItems.middle.el.classList.add(...'translate-x-0 z-[2]'.split(' ')); | ||
|
||
// right item (upcoming active) | ||
rotationItems.right.el.classList.remove(...'-translate-x-full translate-x-full translate-x-0 hidden z-[2]'.split(' ')); | ||
rotationItems.right.el.classList.add(...'translate-x-full z-[1]'.split(' ')); | ||
}; | ||
|
||
/** | ||
* Slide to the element based on id | ||
* @param {*} position | ||
*/ | ||
const slideTo = (position) => { | ||
const nextItem = this._slides[position]; | ||
const rotationItems = { | ||
left: | ||
nextItem.position === 0 | ||
? this._slides[this._slides.length - 1] | ||
: this._slides[nextItem.position - 1], | ||
middle: nextItem, | ||
right: | ||
nextItem.position === this._slides.length - 1 | ||
? this._slides[0] | ||
: this._slides[nextItem.position + 1], | ||
}; | ||
rotate(rotationItems); | ||
setActiveItem(nextItem); | ||
if (this._intervalInstance) { | ||
pause(); | ||
this.loop(); | ||
} | ||
|
||
this._options.onChange(position); | ||
}; | ||
|
||
/** | ||
* Based on the currently active item it will go to the next position | ||
*/ | ||
this.next = () => { | ||
const activeItem = getActiveItem(); | ||
const { position } = activeItem; | ||
let nextItem = null; | ||
|
||
// check if last item | ||
if (position === this._slides.length - 1) { | ||
// eslint-disable-next-line prefer-destructuring | ||
nextItem = this._slides[0]; | ||
} else nextItem = this._slides[position + 1]; | ||
|
||
slideTo(nextItem.position); | ||
|
||
// callback function | ||
this._options.onNext(nextItem.position); | ||
}; | ||
|
||
/** | ||
* Based on the currently active item it will go to the previous position | ||
*/ | ||
this.prev = () => { | ||
const activeItem = getActiveItem(); | ||
let prevItem = null; | ||
|
||
// check if first item | ||
if (activeItem.position === 0) { | ||
prevItem = this._slides[this._slides.length - 1]; | ||
} else { | ||
prevItem = this._slides[activeItem.position - 1]; | ||
} | ||
|
||
slideTo(prevItem.position); | ||
|
||
// callback function | ||
this._options.onPrev(prevItem.position); | ||
}; | ||
|
||
const getItem = (position) => this._slides[position]; | ||
|
||
const init = () => { | ||
if (this._slides.length && !this._initialized) { | ||
this._slides = this._slides.map((item) => { | ||
item.el.classList.add(...'absolute inset-0 transition-transform transform'.split(' ')); | ||
return item; | ||
}); | ||
|
||
// if no active item is set then first position is default | ||
if (getActiveItem()) slideTo(getActiveItem().position); | ||
else slideTo(0); | ||
|
||
this._indicators = this._indicators.map((indicator, position) => { | ||
indicator.el.addEventListener('click', () => { | ||
slideTo(position); | ||
}); | ||
return indicator; | ||
}); | ||
|
||
this._initialized = true; | ||
} | ||
}; | ||
|
||
this._element = carouselEl; | ||
this._slides = items; | ||
this._options = { | ||
...DEFAULT, | ||
...options, | ||
indicators: { ...DEFAULT.indicators, ...options.indicators }, | ||
}; | ||
this._activeItem = getItem(this._options.defaultPosition); | ||
this._indicators = this._options.indicators.items; | ||
this._intervalDuration = this._options.interval; | ||
this._intervalInstance = null; | ||
this._initialized = false; | ||
init(); | ||
} |
Oops, something went wrong.