Created
September 25, 2024 19:20
-
-
Save tak-dcxi/dddf0347d97c2d8787b83c8f4f95f9aa to your computer and use it in GitHub Desktop.
initializeCarousel
This file contains hidden or 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
| // @sample https://codepen.io/tak-dcxi/pen/dyBbBgd | |
| export type CarouselSelectors = { | |
| scrollerSelector: string | undefined; | |
| dirButtonSelector: string | undefined; | |
| progressSelector: string | undefined; | |
| }; | |
| const DEFAULT_MOVE = 1; | |
| const DEFAULT_GAP = 0; | |
| const initializeCarousel = ( | |
| element: HTMLElement, | |
| selectors: CarouselSelectors | |
| ): void => { | |
| const scroller = element.querySelector<HTMLElement>( | |
| `${selectors.scrollerSelector}` | |
| ); | |
| if (!scroller) return; | |
| const items = [...scroller.children] as HTMLElement[]; | |
| if (items.length === 0) return; | |
| const dirButtons = element.querySelectorAll<HTMLButtonElement>( | |
| `${selectors.dirButtonSelector}` | |
| ); | |
| if (dirButtons.length === 0) return; | |
| const progressBar = element.querySelector<HTMLProgressElement>( | |
| `${selectors.progressSelector}` | |
| ); | |
| if (!progressBar) return; | |
| const controller = new AbortController(); | |
| const { signal } = controller; | |
| const debouncedFunctions = debounce(() => { | |
| detectScrollEdge(scroller); | |
| updateDirButtonState(scroller, dirButtons); | |
| updateInertAttribute(scroller, items); | |
| updateProgressBar(scroller, progressBar); | |
| }); | |
| scroller.addEventListener("scroll", debouncedFunctions, { signal }); | |
| window.addEventListener("resize", debouncedFunctions, { signal }); | |
| dirButtons.forEach((button) => | |
| button.addEventListener( | |
| "click", | |
| () => { | |
| scroller.scrollLeft = calcScrollLeft(element, scroller, items, button); | |
| }, | |
| { signal } | |
| ) | |
| ); | |
| setAttributes(element, items); | |
| debouncedFunctions(); | |
| }; | |
| const setAttributes = (element: HTMLElement, items: HTMLElement[]): void => { | |
| element.setAttribute("aria-roledescription", "カルーセル"); | |
| const totalItems = items.length; | |
| items.forEach((item, index) => { | |
| item.setAttribute("role", "group"); | |
| item.setAttribute("aria-roledescription", "スライド"); | |
| item.setAttribute("aria-label", `${index + 1} of ${totalItems}`); | |
| }); | |
| }; | |
| const detectScrollEdge = (scroller: HTMLElement): void => { | |
| const scrollLeft = scroller.scrollLeft; | |
| const scrollRight = | |
| scroller.scrollWidth - (scrollLeft + scroller.clientWidth); | |
| const edge = scrollLeft <= 0 ? "start" : scrollRight <= 1 ? "end" : ""; | |
| if (scroller.getAttribute("data-edge") !== edge) { | |
| scroller.setAttribute("data-edge", edge); | |
| } | |
| }; | |
| const calcScrollLeft = ( | |
| element: HTMLElement, | |
| scroller: HTMLElement, | |
| items: HTMLElement[], | |
| button: HTMLElement | |
| ): number => { | |
| const dir = button.getAttribute("data-dir") === "prev" ? -1 : 1; | |
| const styles = getComputedStyle(element); | |
| const move = parseInt(styles.getPropertyValue("--_move"), 10) || DEFAULT_MOVE; | |
| const gap = parseInt(styles.getPropertyValue("--_gap"), 10) || DEFAULT_GAP; | |
| const itemWidth = items[0].getBoundingClientRect().width; | |
| const totalItemsSize = itemWidth * move; | |
| const totalGap = gap * move; | |
| return scroller.scrollLeft + dir * (totalItemsSize + totalGap); | |
| }; | |
| const updateDirButtonState = ( | |
| scroller: HTMLElement, | |
| dirButtons: NodeListOf<HTMLButtonElement> | |
| ): void => { | |
| const edge = scroller.getAttribute("data-edge"); | |
| dirButtons.forEach((button) => { | |
| const dir = button.getAttribute("data-dir"); | |
| const isDisabled = | |
| (edge === "start" && dir === "prev") || | |
| (edge === "end" && dir === "next"); | |
| button.setAttribute("aria-disabled", String(isDisabled)); | |
| }); | |
| }; | |
| const updateInertAttribute = ( | |
| scroller: HTMLElement, | |
| items: HTMLElement[] | |
| ): void => { | |
| const scrollerRect = scroller.getBoundingClientRect(); | |
| items.forEach((item) => { | |
| const itemRect = item.getBoundingClientRect(); | |
| const isVisible = | |
| itemRect.left < scrollerRect.right && itemRect.right > scrollerRect.left; | |
| item.inert = !isVisible; | |
| }); | |
| }; | |
| const updateProgressBar = ( | |
| scroller: HTMLElement, | |
| progressBar: HTMLProgressElement | |
| ): void => { | |
| const scrollPercentage = | |
| (scroller.scrollLeft / (scroller.scrollWidth - scroller.clientWidth)) * 100; | |
| progressBar.value = scrollPercentage; | |
| }; | |
| const debounce = <T extends any[], R>( | |
| callback: (...args: T) => R | |
| ): ((...args: T) => void) => { | |
| let timeout: number | undefined; | |
| return (...args: T): void => { | |
| if (timeout !== undefined) cancelAnimationFrame(timeout); | |
| timeout = requestAnimationFrame(() => callback.apply(this, args)); | |
| }; | |
| }; | |
| // export default initializeCarousel | |
| document.addEventListener("DOMContentLoaded", () => { | |
| const carouselElement = document.querySelector<HTMLElement>(".carousel"); | |
| if (carouselElement) { | |
| initializeCarousel(carouselElement, { | |
| scrollerSelector: ".scroller", | |
| dirButtonSelector: ".dir-button", | |
| progressSelector: ".progress-bar" | |
| }); | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment