Skip to content

Instantly share code, notes, and snippets.

@ten1seven
Created September 29, 2021 20:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ten1seven/74fc8393fd10a3e1d3ed442f384d4dfc to your computer and use it in GitHub Desktop.
Save ten1seven/74fc8393fd10a3e1d3ed442f384d4dfc to your computer and use it in GitHub Desktop.
Carousel rotator JavaScript from Urban Land Institute
import debounce from 'lodash.debounce'
export default class Rotator {
constructor(el) {
this.variables(el)
this.setup()
this.events()
}
variables(el) {
this.el = el
// container
this.container = this.el.querySelector('[data-rotator-container]')
this.containerWidth = this.container.offsetWidth
// buttons
this.nextButton = this.el.querySelector('[data-rotator-next]')
this.prevButton = this.el.querySelector('[data-rotator-prev]')
this.pauseButton = this.el.querySelector('[data-rotator-pause]')
// pages
this.bundlePages()
this.computePages()
// child pages
this.hasChildren = false
this.childContainers = this.el.querySelectorAll('[data-child-rotator-container]')
if (this.childContainers.length > 0) {
this.bundleChildPages()
this.computeChildPages()
}
// auto rotate
this.rotate = this.el.hasAttribute('data-rotator-auto') && this.el.getAttribute('data-rotator-auto') === 'true' ? true : null
this.rotateSpeed = this.el.hasAttribute('data-rotator-speed') ? parseInt(this.el.getAttribute('data-rotator-speed')) : null
}
setup() {
this.goToNextPage()
this.showButtons()
if (this.rotate && this.rotateSpeed > 0) {
this.rotateInterval = setInterval(this.goToNextPage, this.rotateSpeed)
}
}
events() {
this.nextButton.addEventListener('click', this.clickNext)
this.prevButton.addEventListener('click', this.clickPrev)
if (this.pauseButton) {
this.pauseButton.addEventListener('click', this.clickPause)
}
window.addEventListener('resize', debounce(this.resize, 200))
}
bundlePages = () => {
this.pages = []
for (let i = 0, len = this.container.childNodes.length; i < len; i++) {
const page = this.container.childNodes[i]
if (page.nodeName !== '#text') {
page.style.transition = 'opacity 200ms linear'
this.pages.push(page)
}
}
}
bundleChildPages = () => {
this.childPages = []
for (let i = 0, len = this.childContainers.length; i < len; i++) {
const childContainer = this.childContainers[i]
let childPages = []
for (let i = 0, len = childContainer.childNodes.length; i < len; i++) {
const childPage = childContainer.childNodes[i]
if (childPage.nodeName !== '#text') {
childPage.style.transition = 'opacity 200ms linear'
childPages.push(childPage)
}
}
this.childPages.push(childPages)
}
}
computePages = () => {
this.currentPage = -1
this.currentPages = []
this.pageWidth = this.pages[0].offsetWidth
this.pagesToShow = Math.max(Math.floor(this.containerWidth / this.pageWidth), 1)
this.totalPages = Math.ceil(this.pages.length / this.pagesToShow)
this.prevPage = 0
this.nextPage = this.pagesToShow
}
computeChildPages = () => {
this.hasChildren = true
this.currentPage = 0 // bump up 1 to account for child page rotating first
this.currentChildPage = -1
this.currentChildPages = []
this.childPagesWidth = this.childPages[0][0].offsetWidth
this.childPagesToShow = Math.max(Math.floor(this.containerWidth / this.childPagesWidth), 1)
this.totalChildPages = Math.ceil(this.childPages[0].length / this.childPagesToShow)
this.prevChildPage = 0
this.nextChildPage = this.childPagesToShow
}
showButtons = () => {
const morePages = this.totalPages > 1 || (this.hasChildren && this.totalChildPages && this.totalChildPages > 1)
// remove buttons if only one slide
if (morePages) {
this.nextButton.style.display = 'block'
this.prevButton.style.display = 'block'
if (this.pauseButton) {
this.pauseButton.style.display = 'block'
}
} else {
this.nextButton.style.display = 'none'
this.prevButton.style.display = 'none'
if (this.pauseButton) {
this.pauseButton.style.display = 'none'
}
}
}
resetPages = callback => {
for (let i=0, len=this.pages.length; i < len; i++) {
const page = this.pages[i]
page.style.display = 'block'
}
callback()
}
resetChildPages = callback => {
for (let i=0, ilen=this.childPages.length; i < ilen; i++) {
for (let j=0, jlen=this.childPages[i].length; j < jlen; j++) {
const childPage = this.childPages[i][j]
childPage.style.display = 'block'
}
}
callback()
}
resize = () => {
const newContainerWidth = this.container.offsetWidth
if (this.containerWidth !== newContainerWidth) {
this.containerWidth = this.container.offsetWidth
clearInterval(this.rotateInterval)
this.resetPages(this.computePages)
if (this.childContainers.length > 0) {
this.resetChildPages(this.computeChildPages)
}
this.setup()
}
}
clickPause = () => {
clearInterval(this.rotateInterval)
// .remove doesn't work in IE 11 - but we don't
// _really_ need to remove this anyway
if (this.pauseButton && this.pauseButton.remove) {
this.pauseButton.remove()
}
}
clickNext = () => {
this.clickPause()
this.goToNextPage()
}
clickPrev = () => {
this.clickPause()
this.goToPrevPage()
}
goToNextPage = () => {
if (this.hasChildren) {
if (this.currentChildPage <= 0) {
this.totalChildPages = Math.ceil(this.childPages[this.currentPage].length / this.childPagesToShow)
}
this.currentChildPage++
if (this.currentChildPage >= this.totalChildPages) {
this.currentChildPage = 0
this.currentPage++
if (this.currentPage >= this.totalPages) {
this.currentPage = 0
}
}
this.prevChildPage = this.childPagesToShow * this.currentChildPage
this.nextChildPage = this.childPagesToShow * (this.currentChildPage + 1)
this.updateSlides(this.childPages[this.currentPage], this.prevChildPage, this.nextChildPage)
} else {
this.currentPage++
if (this.currentPage === this.totalPages) {
this.currentPage = 0
}
}
this.prevPage = this.pagesToShow * this.currentPage
this.nextPage = this.pagesToShow * (this.currentPage + 1)
this.updateSlides(this.pages, this.prevPage, this.nextPage)
}
goToPrevPage = () => {
if (this.hasChildren) {
if (this.currentChildPage <= 0) {
this.currentPage--
if (this.currentPage < 0) {
this.currentPage = this.totalPages - 1
}
this.totalChildPages = Math.ceil(this.childPages[this.currentPage].length / this.childPagesToShow)
this.currentChildPage = this.totalChildPages
}
this.prevChildPage = this.childPagesToShow * this.currentChildPage
this.nextChildPage = this.childPagesToShow * (this.currentChildPage - 1)
this.currentChildPage--
this.updateSlides(this.childPages[this.currentPage], this.nextChildPage, this.prevChildPage)
} else {
this.currentPage--
if (this.currentPage < 0) {
this.currentPage = this.totalPages - 1
}
}
this.prevPage = this.pagesToShow * this.currentPage
this.nextPage = this.pagesToShow * (this.currentPage + 1)
this.updateSlides(this.pages, this.prevPage, this.nextPage)
}
updateSlides = (items, firstPage, lastPage) => {
let currentItems = []
if (lastPage <= firstPage) {
for (let i = firstPage; i < items.length; i++) {
currentItems.push(i)
}
for (let i = 0; i < lastPage; i++) {
currentItems.push(i)
}
} else {
for (let i = firstPage; i < lastPage; i++) {
currentItems.push(i)
}
}
for (let i = 0, len = items.length; i < len; i++) {
const item = items[i]
if (currentItems.indexOf(i) > -1) {
setTimeout(() => {
item.style.display = 'block'
}, 200)
setTimeout(() => {
item.style.opacity = '1'
}, 400)
} else {
item.style.opacity = '0'
setTimeout(() => {
item.style.display = 'none'
}, 200)
}
}
}
}
/*
Usage:
======
<div
data-module="rotator"
data-rotator-auto="true/false"
data-rotator-speed="number"
>
<button data-rotator-pause></button>
<button data-rotator-prev></button>
Has width and overflow-x-auto
<div data-rotator-container>
-- items to rotate --
<div data-child-rotator>
<div data-child-rotator-container>
-- child items to rotate --
</div>
</div>
</div>
<button data-rotator-next></button>
</div>
options
--
data-rotator-auto - auto rotate?
data-rotator-speed - auto rotate speed
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment