@frsource/tiny-carousel-core
Version:
Core module for @frsource/tiny-carousel, based on CSS scroll snap feature 🚀
187 lines (156 loc) • 5.5 kB
text/typescript
import './index.scss';
import { on, findXSnapIndex, off, addScrollEndListener } from '@frsource/tiny-carousel-utils';
import type { DeepPartial, OmitFirstItem } from './helpers';
export type PluginDefinition<C extends unknown[] | undefined = undefined> = C extends unknown[]
? {
install(instance: TinyCarousel, ...args: C): void;
}
: {
install(instance: TinyCarousel): void;
};
export interface Config {
/**
* Index of the item which should be activated during carousel initialization
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
active: number;
/**
* To be changed together with `$class-name` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
className: string;
/**
* To be changed together with `$class-name-overflow` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
classNameOverflow: string;
/**
* To be changed together with `$item-class-name` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
itemClassName: string;
/**
* To be changed together with `$frs-hide-scroll` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
hideScrollClassName: string;
/**
* Allows toggling the overflow behavior on or off. Default: true
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
overflow: boolean;
items: HTMLElement[];
}
/**
* Used to distinguish whether the css class was added by TinyCarousel or not
*/
const TINYCAROUSEL_ADDED_CLASS = 'tcACls';
const _defaultConfig: Config = {
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
itemClassName: 'frs-tc-item',
hideScrollClassName: 'frs-hide-scroll', // default frs-hide-scrollbar classname, for private use only
overflow: true,
items: [],
};
export class TinyCarousel {
public config: Config;
private _active?: number;
private _resetActive: TinyCarousel['resetActive'];
private _handleOverflow: TinyCarousel['handleOverflow'];
static get defaultConfig () {
return Object.assign({}, _defaultConfig);
}
static updateDefaultConfig (config: DeepPartial<Config>) {
Object.assign(_defaultConfig, config);
}
constructor(public carouselElement: HTMLElement, config: DeepPartial<Config> = {}) {
this.config = Object.assign({}, _defaultConfig, config);
this._resetActive = this.resetActive.bind(this);
this._handleOverflow = this.handleOverflow.bind(this);
}
// make it public if needed (and document it)
private handleOverflow () {
if (
!this.config.overflow ||
!this.config.items.length
) return;
if (
this.active === -1 ||
this.active === this.config.items.length
) this.goTo(this.active);
}
use<PD extends PluginDefinition>(pluginDefinition: PD, ...args: OmitFirstItem<Parameters<PD['install']>>) {
(pluginDefinition as PluginDefinition<unknown[]>).install(this, ...args);
return this;
}
init() {
on(this.carouselElement, 'scroll', this._resetActive, { passive: true });
this._handleOverflow = addScrollEndListener(this.carouselElement, this._handleOverflow);
if (!this.config.items.length) this.config.items = this.findPossibleItems();
this.carouselElement.classList.add(this.config.className);
this.carouselElement.classList.add(this.config.hideScrollClassName);
if (this.config.overflow) this.carouselElement.classList.add(this.config.classNameOverflow);
this.config.items.forEach(({ classList, dataset }) => {
if (!classList.contains(this.config.itemClassName)) {
dataset[TINYCAROUSEL_ADDED_CLASS] = '';
classList.add(this.config.itemClassName)
}
});
this.goTo(this.config.active);
return this;
}
destroy() {
off(this.carouselElement, 'scroll', this._resetActive);
off(this.carouselElement, 'scroll', this._handleOverflow);
return this;
}
/*
* Returns an index of the active item or -1 if items array is empty.
*/
get active () {
if (this._active !== void 0) return this._active;
this._active = findXSnapIndex(
this.carouselElement,
this.config.items,
this.config.overflow
);
return this._active;
}
goTo (n: number) {
const len = this.config.items.length;
if (len) {
if (this.config.overflow) {
// treat negative numbers as counter starting at the end of items array
while (n < 0) n += len;
// treat numbers >= items length as overflow - start counting the rest from the beginning
while (n >= len) n -= len;
} else {
if (n < 0) n = 0;
else if (n >= len) n = len - 1;
}
this.carouselElement.scrollLeft = this.config.items[n].offsetLeft;
}
return this;
}
next () {
return this.goTo(this.active + 1);
}
prev () {
return this.goTo(this.active - 1);
}
resetActive () {
this._active = void 0;
}
findPossibleItems () {
const children = Array.from(this.carouselElement.children) as HTMLElement[];
const { itemClassName } = this.config;
const filtredChildren = children.filter(
child =>
!(TINYCAROUSEL_ADDED_CLASS in child.dataset) && child.classList.contains(itemClassName),
);
if (filtredChildren.length) return filtredChildren;
return children;
}
}