@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
342 lines (338 loc) • 14.6 kB
JavaScript
/**
* Copyright IBM Corp. 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { registerSwipeEvents } from './swipeEvents.js';
/**
* Copyright IBM Corp. 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Initializes a carousel with the given configuration.
* @param carouselContainer - The HTMLElement representing the carousel container.
* @param config - Optional configuration object.
* @returns An object containing methods to control the carousel.
*/
const initCarousel = (carouselContainer, config) => {
const prefix = 'carousel';
let viewIndexStack = [0];
let previousViewIndexStack = [0];
const refs = {};
const minHeight = 10; // 10 rem
const { onViewChangeStart, onViewChangeEnd, excludeSwipeSupport } = config || {};
/**
* Registers an HTMLElement at a specific index in the refs array.
*
* @param {number} index - The index at which to register the HTMLElement.
* @param {HTMLElement} ref - The HTMLElement to register.
*
* @example
* registerRef(0, document.getElementById('myElement'));
*/
const registerRef = (index, ref) => {
refs[index] = ref;
};
/**
* Wraps all child elements of a given container into a new div with the specified class.
* If an element with the specified class already exists as a child of the container, the function does nothing.
*
* @param {HTMLElement} container - The container element to wrap child elements of.
* @param {string} wrapperClass - The class name to apply to the new wrapper div.
* @returns {void}
*/
const wrapAllItems = (container, wrapperClass) => {
if (container.querySelector(`.${wrapperClass}`)) {
return;
}
const wrapper = document.createElement('div');
wrapper.classList.add(`${wrapperClass}`);
while (container.firstChild) {
wrapper.appendChild(container.firstChild);
}
container.appendChild(wrapper);
};
const getHistory = () => {
return viewIndexStack.map((id) => ({
id,
elem: refs[id],
}));
};
/**
* Retrieves the current carousel response based on the view index stack and reference objects.
* @returns {CarouselResponse} - An object containing carousel response details.
*/
const getCallbackResponse = () => {
const totalRefs = Object.keys(refs).length;
const lastElementRef = refs[totalRefs - 1];
const historicalData = getHistory();
return {
currentIndex: viewIndexStack[0],
lastIndex: parseInt((lastElementRef === null || lastElementRef === void 0 ? void 0 : lastElementRef.dataset.index) || viewIndexStack[0].toString(), 10),
totalViews: totalRefs,
historyStack: historicalData,
};
};
/**
* Handles the start of a transition in the application.
* This function is responsible for capturing the current state of the view index stack
* and invoking a callback function if it exists.
*
* @function handleTransitionStart
* @returns {void}
*/
const handleTransitionStart = () => {
previousViewIndexStack = [...viewIndexStack];
const callbackData = getCallbackResponse();
onViewChangeStart === null || onViewChangeStart === void 0 ? void 0 : onViewChangeStart(callbackData);
};
/**
* Handles the 'transitionend' event for a given element.
* This function checks if the element has a 'data-index' attribute and if its value matches the current view index.
* If both conditions are met, it calls the 'onViewChangeEnd' callback with the response from 'getCallbackResponse'.
*
* @param {HTMLElement | null} el - The element to handle the 'transitionend' event for.
* @returns {void}
*/
const handleTransitionEnd = (el) => {
if (!el) {
return;
}
const tmpElementIndex = el.dataset.index;
if (tmpElementIndex &&
viewIndexStack[0] === parseInt(tmpElementIndex, 10)) {
const callbackData = getCallbackResponse();
onViewChangeEnd === null || onViewChangeEnd === void 0 ? void 0 : onViewChangeEnd(callbackData);
}
};
/**
* A utility function to sanitize an index value.
* This function ensures the index stays within the bounds of the refs array.
*
* @param {number} idx - The index to be sanitized.
* @returns {number} - The sanitized index.
*/
const sanitizeIndex = (idx) => {
const floorVal = 0;
const ceilVal = Object.keys(refs).length - 1;
return Math.max(floorVal, Math.min(idx, ceilVal));
};
/**
* Handles the 'transitionend' event for a given element.
* This function checks if the element has a 'data-index' attribute and if its value matches the current view index.
* If both conditions are met, it calls the 'onViewChangeEnd' callback with the response from 'getCallbackResponse'.
* @returns {void}
*/
const transitionToViewIndex = (idx) => {
const sanitizedIndex = sanitizeIndex(idx);
if (viewIndexStack[0] !== sanitizedIndex) {
handleTransitionStart();
viewIndexStack = [sanitizedIndex, ...viewIndexStack];
performAnimation(false);
}
};
const transitionComplete = (ref) => {
handleTransitionEnd(ref);
};
/**
* Attaches class names to an HTMLElement based on given conditions.
*
* @param {HTMLElement} viewItem - The HTML element to which class names will be added.
* @param {boolean} isInViewStack - Indicates if the view item is in the view stack.
* @param {boolean} isActive - Indicates if the view item is active.
* @param {boolean} isBeingRecycledOut - Indicates if the view item is being recycled out.
* @param {boolean} isBeingRecycledIn - Indicates if the view item is being recycled in.
* @returns {void}
*/
const attachClassNames = (viewItem, isInViewStack, isActive, isBeingRecycledOut, isBeingRecycledIn) => {
viewItem.classList.add(`${prefix}__view`);
viewItem.classList.toggle(`${prefix}__view-in-stack`, isInViewStack && !isActive);
viewItem.classList.toggle(`${prefix}__view-active`, isInViewStack && isActive);
if (isBeingRecycledIn && !isBeingRecycledOut) {
viewItem.classList.add(`${prefix}__view-recycle-in`);
}
if (!isBeingRecycledIn && isBeingRecycledOut) {
viewItem.classList.add(`${prefix}__view-recycle-out`);
}
};
const removeReCycleClasses = (viewItem) => {
viewItem.classList.remove(`${prefix}__view-recycle-in`, `${prefix}__view-recycle-out`);
};
const remToPx = (rem) => {
return (rem * parseFloat(getComputedStyle(document.documentElement).fontSize));
};
/**
* Updates the height of the items wrapper in a carousel based on the smallest item height and a threshold height.
* This function ensures that the items wrapper does not have a height smaller than the threshold, adjusting the item height if necessary.
*
* @param {number} itemHeightSmallest - The smallest height of an item in pixels.
*/
const updateHeightForWrapper = (itemHeightSmallest) => {
const thresholdHeight = remToPx(minHeight);
const containerHeight = carouselContainer.clientHeight;
if (containerHeight < thresholdHeight) {
if (itemHeightSmallest < thresholdHeight) {
itemHeightSmallest = thresholdHeight;
}
const itemsWrapper = carouselContainer.querySelector(`.${prefix}__itemsWrapper`);
if (itemsWrapper) {
itemsWrapper.style.blockSize = `${itemHeightSmallest}px`;
}
}
};
/**
* Performs animation on view items based on their state in the view index stack.
* @param {boolean} isInitial - A flag indicating if this is the initial animation.
*/
const performAnimation = (isInitial) => {
let itemHeightSmallest = 0;
Array.from(viewItems).forEach((viewItem, index) => {
const stackIndex = viewIndexStack.findIndex((idx) => idx === index);
const stackIndexInstanceCount = previousViewIndexStack.filter((viIdx) => viIdx === index).length;
const isBeingRecycledOut = previousViewIndexStack.length > viewIndexStack.length &&
previousViewIndexStack[0] === index &&
stackIndexInstanceCount > 0;
const isBeingRecycledIn = previousViewIndexStack.length < viewIndexStack.length &&
viewIndexStack[0] === index &&
stackIndexInstanceCount > 0;
const isInViewStack = stackIndex > -1;
const isActive = index === viewIndexStack[0];
attachClassNames(viewItem, isInViewStack, isActive, isBeingRecycledOut, isBeingRecycledIn);
if (isInitial) {
registerRef(index, viewItem);
setTimeout(() => {
if (!itemHeightSmallest ||
(viewItem.offsetHeight < itemHeightSmallest &&
itemHeightSmallest > remToPx(minHeight))) {
itemHeightSmallest = viewItem.offsetHeight;
}
viewItem.style.position = 'absolute';
updateHeightForWrapper(itemHeightSmallest);
});
const listener = (e) => {
removeReCycleClasses(viewItem);
if (e.target === refs[viewIndexStack[0]]) {
//transitionend will trigger twice for pervious card and current card
transitionComplete(viewItem);
}
};
// store reference on the element for later removal
viewItem._carouselListener = listener;
viewItem.addEventListener('animationend', listener);
viewItem.addEventListener('transitionend', listener);
viewItem.setAttribute('data-index', index.toString());
}
});
if (isInitial) {
handleTransitionEnd(Array.from(viewItems)[0]);
}
};
/**
* A utility function to navigate to the next view in the stack.
* This function increments the current view index and transitions to the new index.
*
* @returns {void} - This function does not return any value.
*/
const navigateNext = () => {
const targetViewIndex = viewIndexStack[0] + 1;
transitionToViewIndex(targetViewIndex);
};
/**
* Navigates to the previous view in the view stack.
* @function navigatePrev
* @description This function checks if there is a previous view in the stack. If so, it triggers a transition start, removes the current view from the stack, and performs an animation to transition to the previous view.
* @returns {void} - This function does not return a value.
*/
const navigatePrev = () => {
if (viewIndexStack.length - 1 >= 1) {
handleTransitionStart();
viewIndexStack = viewIndexStack.slice(1);
performAnimation(false);
}
};
/**
* A function that transitions the view to a specified index.
*
* @param {number} index - The index to transition to.
* @returns {void} - This function does not return a value.
*/
const goToIndex = (index) => {
transitionToViewIndex(index);
};
/**
* Retrieves the currently active item and its index from the view index stack and references.
* @returns An object containing the index and the corresponding item reference.
*/
const getActiveItem = () => {
return {
index: viewIndexStack[0],
item: refs[viewIndexStack[0]],
};
};
/**
* Resets the view index stack and performs an animation.
*
* @returns {void}
*/
const reset = () => {
viewIndexStack = [0];
performAnimation(false);
};
/**
* Removes event listeners for 'animationend' and 'transitionend' events from all elements with references stored in the `refs` object.
* Also registers swipe events if `excludeSwipeSupport` is false.
*/
const destroyEvents = () => {
Object.values(refs).forEach((el) => {
if (el && el._carouselListener) {
el.removeEventListener('animationend', el._carouselListener);
el.removeEventListener('transitionend', el._carouselListener);
}
});
if (!excludeSwipeSupport) {
registerSwipeEvents(carouselContainer, navigateNext, navigatePrev, true);
}
};
/**
* Retrieves carousel items from a given container element.
* If the container has a 'slot' element, it fetches all elements assigned to that slot.
* Otherwise, it fetches all direct children of the container.
*
* @param {HTMLElement} container - The container element from which to extract carousel items.
* @returns {HTMLElement[]} An array of HTMLElements representing the carousel items.
*
* @example
* const carouselContainer = document.querySelector('.carousel-container');
* const carouselItems = getCarouselItems(carouselContainer);
* console.log(carouselItems); // Logs the carousel items as HTMLElements
*/
const getCarouselItems = (container) => {
const slot = container.querySelector('slot');
return slot
? slot.assignedElements({ flatten: true })
: Array.from(container.children);
};
// initialize
wrapAllItems(carouselContainer, `${prefix}__itemsWrapper`);
const wrapper = carouselContainer.querySelector(`.${prefix}__itemsWrapper`);
const viewItems = getCarouselItems(wrapper);
carouselContainer.classList.add(`${prefix}__view-stack`);
performAnimation(true);
if (!excludeSwipeSupport) {
registerSwipeEvents(carouselContainer, navigateNext, navigatePrev, false);
}
return {
next: navigateNext,
prev: navigatePrev,
reset,
goToIndex,
getActiveItem,
destroyEvents,
allViews: refs,
};
};
export { initCarousel };
//# sourceMappingURL=carousel.js.map