@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
122 lines (121 loc) • 5.81 kB
JavaScript
import { ESLCarouselDirection } from './esl-carousel.types';
/** @returns sign of the value */
export const sign = (value) => value > 0 ? 1 : value < 0 ? -1 : 0;
/** @returns normalized slide index in bounds of [0, count] range */
export function normalize(index, size) {
return (size + (index % size)) % size;
}
/** @returns normalize first slide index according to the carousel mode */
export function normalizeIndex(index, { size, count, loop }) {
return loop && count < size ? normalize(index, size) : Math.max(0, Math.min(size - count, index));
}
/** @returns normalized sequence of slides starting from the current index */
export function sequence(current, count, size) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(normalize(current + i, size));
}
return result;
}
/** @returns closest direction to move from the slide `from` to slide `to` */
function calcDirection(from, to, size) {
const abs = Math.abs(from - to) % size;
return sign(to - from) * sign(size / 2 - abs);
}
/** @returns normalized numeric index from group index */
export function groupToIndex(group, count, size) {
const groupCount = Math.ceil(size / count);
const value = normalize(group, groupCount);
const index = value + 1 === groupCount ? size - count : count * value;
return normalize(index, size);
}
/** @returns numeric group index from slide index */
export function indexToGroup(index, count, size) {
const value = normalize(index, size);
const groupCount = Math.ceil(size / count);
const firstGroupIndex = value + count >= size ? groupCount - 1 : Math.floor(value / count);
const secondGroupIndex = (firstGroupIndex + 1) % groupCount;
// candidate groups have common slides
const isGroupIntersected = size - count + 1 < value + count && value + count <= size;
const commonCount = isGroupIntersected ? groupCount * count - size : 0;
const firstGroupCount = Math.min(size, (firstGroupIndex + 1) * count) - value;
const secondGroupCount = count - firstGroupCount + commonCount;
return (firstGroupCount >= secondGroupCount) ? firstGroupIndex : secondGroupIndex;
}
/** @returns closest direction to move to the passed index */
export function indexToDirection(index, { activeIndex, size, loop }) {
return loop ? calcDirection(activeIndex, index, size) : sign(index - activeIndex);
}
/** Splits target string into type and index parts */
function splitTarget(target) {
// Sanitize value
target = String(target).replace(/\s/, '');
// Split type and index part
const [type, index] = String(target).split(':');
// 'slide' type is used if no prefix provided
return index ? { type, index } : { type: 'slide', index: target };
}
/** Parses index value defining its value and type (absolute|relative) */
function parseIndex(index) {
if (typeof index === 'number')
return { value: index, isRelative: false };
index = index.trim();
if (index === 'next')
return { value: 1, isRelative: true, direction: ESLCarouselDirection.NEXT };
if (index === 'prev')
return { value: -1, isRelative: true, direction: ESLCarouselDirection.PREV };
if (index[0] === '+')
return { value: +index, isRelative: true, direction: ESLCarouselDirection.NEXT };
if (index[0] === '-')
return { value: +index, isRelative: true, direction: ESLCarouselDirection.PREV };
return { value: +index, isRelative: false };
}
/** @returns normalized numeric index from string with absolute or relative index */
function resolveSlideIndex(indexStr, cfg) {
const { value, isRelative, direction } = parseIndex(indexStr);
const target = value + (isRelative ? cfg.activeIndex : 0);
const index = normalizeIndex(target, cfg);
return { index, direction: direction || indexToDirection(index, cfg) };
}
/** @returns normalized numeric index from string with absolute or relative group index */
function resolveGroupIndex(indexStr, cfg) {
const { value, isRelative, direction } = parseIndex(indexStr);
if (!isRelative) {
const index = groupToIndex(value, cfg.count, cfg.size);
return { index, direction: indexToDirection(index, cfg) };
}
// TODO: extend navigation boundaries
if (value === -1 && cfg.activeIndex < cfg.count && cfg.activeIndex > 0) {
return { index: 0, direction: direction || ESLCarouselDirection.PREV };
}
if (value === 1 && normalize(cfg.activeIndex + cfg.count, cfg.size) > cfg.size - cfg.count) {
return { index: cfg.size - cfg.count, direction: direction || ESLCarouselDirection.NEXT };
}
const index = normalizeIndex(cfg.activeIndex + value * cfg.count, cfg);
return { index, direction: direction || indexToDirection(index, cfg) };
}
/** @returns normalized index from target definition and current state */
export function toIndex(target, cfg) {
if (typeof target === 'number') {
const index = normalizeIndex(target, cfg);
return { index, direction: indexToDirection(index, cfg) };
}
const { type, index } = splitTarget(target);
if (type === 'group')
return resolveGroupIndex(index, cfg);
if (type === 'slide')
return resolveSlideIndex(index, cfg);
return { index: cfg.activeIndex };
}
/**
* @returns whether the carousel can navigate to the target passed as {@link ESLCarouselSlideTarget}
* E.g.: carousel can't navigate to invalid target or to the next slide if it's the last slide and loop is disabled
*/
export function canNavigate(target, cfg) {
if (cfg.size <= cfg.count)
return false;
const { direction, index } = toIndex(target, cfg);
if (!cfg.loop && direction && index < direction * cfg.activeIndex)
return false;
return !!direction && index !== cfg.activeIndex;
}