UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

183 lines (182 loc) 8.81 kB
import { ESLCarouselDirection } from './esl-carousel.types'; /** @returns stringified sign of the value */ export const dir = (value) => value > 0 ? '+1' : value < 0 ? '-1' : ''; /** @returns sign of the value */ export const sign = (value) => value > 0 ? 1 : value < 0 ? -1 : 0; export const bounds = (value, min, max) => Math.max(min, Math.min(max, value)); /** @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) : bounds(index, 0, size - count); } /** @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 numeric index from group index */ export function groupToIndex(group, count, size) { const groupCount = Math.ceil(size / count); if (group < 0 || group >= groupCount) return NaN; const index = group + 1 === groupCount ? size - count : count * group; 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+/g, ''); // Short form of next/prev considered as slide target if (target === 'next' || target === 'prev') return { index: target, type: 'slide' }; // Split type and index part const [type, index] = String(target).split(':'); return index ? { type, index } : { 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 : -1); if (!isRelative && (target < 0 || target >= cfg.size)) return { index: NaN }; const index = isRelative ? normalizeIndex(target, cfg) : bounds(target, 0, cfg.size - cfg.count); 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 - 1, 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 numeric index from simple index definition */ function resolveIndex(target, cfg) { const i = normalize(+target, cfg.size); const index = cfg.loop ? i : bounds(Math.max(i, +target), 0, cfg.size - cfg.count); const direction = indexToDirection(index, cfg); return { index, direction }; } /** @returns normalized first index from target definition and current state */ export function toIndex(target, cfg) { const { type, index } = splitTarget(target); if (type === 'group') return resolveGroupIndex(index, cfg); if (type === 'slide') return resolveSlideIndex(index, cfg); return resolveIndex(index, cfg); } /** * @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 (isNaN(index)) return false; if (!cfg.loop && direction && index < direction * cfg.activeIndex) return false; return index !== cfg.activeIndex || Math.abs(cfg.offset) > 0; } /** * Checks whether given (0-based) slide index is currently active. * * @param index - 0-based slide index to check. * @param state - current carousel state (size, count, activeIndex, loop). * @returns true if index corresponds to an active slide of the current view; otherwise false. */ export function isCurrentIndex(index, { count, size, activeIndex, loop }) { if (!isFinite(index)) return false; // NaN / non-finite if (index < 0 || index >= size || count <= 0 || size <= 0) return false; // Boundaries const diff = index - activeIndex; if (loop) return (diff + size) % size < count; return diff >= 0 && diff < count; } /** * Determines if a navigation target refers to a currently active slide (or group) of the carousel. * * Supported target syntaxes (absolute only considered "current"): * - Numeric (short form): `0`, `1`, `2`, `-1` (negative only meaningful in loop mode, normalized by size). * - Slide explicit: `slide:1`, `slide:2`, ... (1-based). Internally converted to 0-based index (value - 1). * - Group explicit: `group:1`, `group:2`, ... (1-based). A group is considered current if its FIRST slide is active. * * Relative syntaxes are NEVER considered current and always return false * * Semantics: * - For a slide target we only check the single referenced slide. * - For a group target we resolve the first slide of the group via {@link groupToIndex} and test it. * - For numeric (short) targets in loop mode we normalize the raw value modulo size; in non-loop mode value must be within [0, size). * - No validation beyond what is needed for determining activity; invalid (out-of-range) absolute slide/group indexes yield false. * * @param target - navigation target specification. * @param state - current carousel state. * @returns true if target points to an active slide/group; otherwise false. */ export function isCurrent(target, state) { const { type, index } = splitTarget(target); const { value, isRelative } = parseIndex(index); if (type && isRelative) return false; if (type === 'slide') return isCurrentIndex(value - 1, state); if (type === 'group') return isCurrentIndex(groupToIndex(value - 1, state.count, state.size), state); return isCurrentIndex(normalize(value, state.size), state); }