@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
231 lines (230 loc) • 10.3 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { memoize } from '../../esl-utils/decorators';
import { isEqual } from '../../esl-utils/misc/object';
import { parseTime } from '../../esl-utils/misc/format';
import { promisifyTimeout } from '../../esl-utils/async/promise/timeout';
import { SyntheticEventTarget } from '../../esl-utils/dom';
import { ESLCarouselDirection } from './esl-carousel.types';
import { ESLCarouselSlideEvent } from './esl-carousel.events';
import { ESLCarouselNavRejection } from './esl-carousel.errors';
import { normalize, normalizeIndex, sequence } from './esl-carousel.utils';
export class ESLCarouselRenderer {
constructor($carousel, options) {
/** (visible) slide count per view */
this.count = 0;
/** cyclic carousel rendering mode */
this.loop = false;
/** vertical carousel rendering mode */
this.vertical = false;
/** marker if the renderer is applied to the carousel */
this._bound = false;
this.$carousel = $carousel;
this.count = options.count;
this.loop = options.loop;
this.vertical = options.vertical;
}
/** @returns marker if the renderer is applied to the carousel */
get bound() {
return this._bound;
}
/** @returns renderer type name */
get type() {
return this.constructor.is;
}
/** @returns slide total count or 0 if the renderer is not bound */
get size() {
return this._bound ? this.$slides.length : 0;
}
/** @returns renderer config */
get config() {
const { type, size, count, loop, vertical } = this;
return { type, size, count, loop, vertical };
}
/** @returns {@link ESLCarousel} `$slidesArea` */
get $area() {
return this.$carousel.$slidesArea;
}
/** @returns {@link ESLCarousel} `$slides` */
get $slides() {
return this.$carousel.$slides || [];
}
get animating() {
return this.$carousel.hasAttribute('animating');
}
set animating(value) {
this.$carousel.toggleAttribute('animating', value);
}
get transitionDuration() {
const name = ESLCarouselRenderer.TRANSITION_DURATION_PROP;
const duration = getComputedStyle(this.$area).getPropertyValue(name);
return parseTime(duration);
}
set transitionDuration(value) {
if (typeof value === 'number' && value > 0) {
this.$carousel.style.setProperty(ESLCarouselRenderer.TRANSITION_DURATION_PROP, `${value}ms`);
}
else {
this.$carousel.style.removeProperty(ESLCarouselRenderer.TRANSITION_DURATION_PROP);
}
}
get transitionDuration$$() {
return promisifyTimeout(this.transitionDuration);
}
equal(config) {
return isEqual(this.config, config);
}
bind() {
this._bound = true;
const type = this.constructor;
const orientationCls = `esl-carousel-${this.vertical ? 'vertical' : 'horizontal'}`;
this.$carousel.classList.add(orientationCls, ...type.classes);
this.onBind();
}
unbind() {
if (!this._bound)
return;
const type = this.constructor;
const orientationCls = ['esl-carousel-vertical', 'esl-carousel-horizontal'];
this.$carousel.classList.remove(...orientationCls, ...type.classes);
this.onUnbind();
this._bound = false;
}
/** Processes binding of defined renderer to the carousel {@link ESLCarousel}. */
onBind() { }
/** Processes unbinding of defined renderer from the carousel {@link ESLCarousel}. */
onUnbind() { }
/** Processes drawing of the carousel {@link ESLCarousel}. */
redraw() { }
/** Normalizes an index before navigation */
normalizeIndex(index, params) {
return normalizeIndex(index, this);
}
/** Normalizes a direction before navigation */
normalizeDirection(direction, params) {
return (this.loop ? params && params.direction : null) || direction || ESLCarouselDirection.NEXT;
}
/** Processes changing slides */
navigate(to, params) {
return __awaiter(this, void 0, void 0, function* () {
const index = this.normalizeIndex(to.index, params);
const direction = this.normalizeDirection(to.direction, params);
if (index === this.$carousel.activeIndex)
return; // skip if index is already active
if (!this.dispatchChangeEvent('BEFORE', index, Object.assign(Object.assign({}, params), { direction })))
return;
try {
this.transitionDuration = params.stepDuration;
yield this.onBeforeAnimate(index, direction, params);
yield this.onAnimate(index, direction, params);
yield this.onAfterAnimate(index, direction, params);
}
catch (e) {
if (e instanceof Error)
throw e;
}
finally {
this.transitionDuration = null;
}
});
}
/** Pre-processing animation action. */
onBeforeAnimate(index, direction, params) {
return __awaiter(this, void 0, void 0, function* () {
if (this.animating)
throw new ESLCarouselNavRejection(index);
this.setPreActive(index, Object.assign(Object.assign({}, params), { direction, final: true }));
});
}
/** Post-processing animation action. */
onAfterAnimate(index, direction, params) {
return __awaiter(this, void 0, void 0, function* () {
this.setActive(index, Object.assign(Object.assign({}, params), { direction }));
});
}
/** Sets active slides from passed index **/
setActive(index, event) {
const indexesBefore = this.$carousel.activeIndexes;
const count = Math.min(this.count, this.size);
for (let i = 0; i < this.size; i++) {
const position = normalize(i + index, this.size);
const $slide = this.$slides[position];
$slide.toggleAttribute('active', i < count);
$slide.toggleAttribute('pre-active', false);
$slide.toggleAttribute('next', i === count && (this.loop || position !== 0));
$slide.toggleAttribute('prev', i === this.size - 1 && i >= count && (this.loop || position !== this.size - 1));
}
event && this.dispatchChangeEvent('AFTER', index, Object.assign(Object.assign({}, event), { indexesBefore }));
}
/** Sets pre-active (slides that are going to be active) slides from the passed index **/
setPreActive(index, event) {
let changed = false;
const count = Math.min(this.count, this.size);
for (let i = 0; i < this.size; ++i) {
const position = normalize(i + index, this.size);
const $slide = this.$slides[position];
if ($slide.hasAttribute('active'))
continue; // skip already active slides
changed = changed || ($slide.hasAttribute('pre-active') !== (i < count));
$slide.toggleAttribute('pre-active', i < count);
}
if (event && changed)
this.dispatchChangeEvent('CHANGE', index, event);
}
/** Dispatches a change event with the given type and index */
dispatchChangeEvent(name, index, event) {
const indexesBefore = event.indexesBefore || this.$carousel.activeIndexes;
const count = Math.min(this.count, this.size);
const indexesAfter = sequence(index, count, this.size);
const details = Object.assign(Object.assign({}, event), { indexesBefore, indexesAfter });
return this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create(name, details));
}
// Register API
static get registry() {
return new ESLCarouselRendererRegistry();
}
static register(view = this) {
ESLCarouselRenderer.registry.register(view);
}
}
/** CSS variable name to set transition duration */
ESLCarouselRenderer.TRANSITION_DURATION_PROP = '--esl-carousel-step-duration';
ESLCarouselRenderer.classes = [];
__decorate([
memoize()
], ESLCarouselRenderer, "registry", null);
export class ESLCarouselRendererRegistry extends SyntheticEventTarget {
constructor() {
super(...arguments);
this.store = new Map();
}
create(carousel, config) {
let Renderer = this.store.get(config.type);
if (!Renderer)
[Renderer] = this.store.values(); // take first Renderer in store
return new Renderer(carousel, config);
}
register(view) {
if (!view || !view.is)
throw Error('[ESL]: CarouselRendererRegistry: incorrect registration request');
if (this.store.has(view.is))
throw Error(`View with name ${view.is} already defined`);
this.store.set(view.is, view);
const detail = { name: view.is, view };
const event = new CustomEvent('change', { detail });
this.dispatchEvent(event);
}
}