@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
389 lines (388 loc) • 14.7 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;
};
import { ExportNs } from '../../esl-utils/environment/export-ns';
import { ESLBaseElement } from '../../esl-base-element/core';
import { attr, boolAttr, ready, decorate, listen, memoize } from '../../esl-utils/decorators';
import { isMatches } from '../../esl-utils/dom/traversing';
import { microtask } from '../../esl-utils/async';
import { parseBoolean, parseTime, sequentialUID } from '../../esl-utils/misc';
import { CSSClassUtils } from '../../esl-utils/dom/class';
import { ESLTraversingQuery } from '../../esl-traversing-query/core';
import { ESLMediaRuleList } from '../../esl-media-query/core';
import { ESLResizeObserverTarget } from '../../esl-event-listener/core';
import { normalize, toIndex, canNavigate } from './esl-carousel.utils';
import { ESLCarouselSlide } from './esl-carousel.slide';
import { ESLCarouselRenderer } from './esl-carousel.renderer';
import { ESLCarouselChangeEvent } from './esl-carousel.events';
/**
* ESLCarousel component
* @author Julia Murashko, Alexey Stsefanovich (ala'n)
*
* ESLCarousel - a slideshow component for cycling through slides.
*/
let ESLCarousel = class ESLCarousel extends ESLBaseElement {
/** Marker/mixin attribute to define slide element */
get slideAttrName() {
return this.tagName + '-slide';
}
/** Renderer type {@link ESLMediaRuleList} instance */
get typeRule() {
return ESLMediaRuleList.parse(this.type, this.media);
}
/** Loop marker {@link ESLMediaRuleList} instance */
get loopRule() {
return ESLMediaRuleList.parse(this.loop, this.media, parseBoolean);
}
/** Count of visible slides {@link ESLMediaRuleList} instance */
get countRule() {
return ESLMediaRuleList.parse(this.count, this.media, parseInt);
}
/** Orientation of the carousel {@link ESLMediaRuleList} instance */
get verticalRule() {
return ESLMediaRuleList.parse(this.vertical, this.media, parseBoolean);
}
/** Duration of the single slide transition {@link ESLMediaRuleList} instance */
get stepDurationRule() {
return ESLMediaRuleList.parse(this.stepDuration, this.media, parseTime);
}
/** Returns observed media rules */
get observedRules() {
return [this.typeRule, this.loopRule, this.countRule, this.verticalRule];
}
/** Carousel instance current {@link ESLCarouselStaticState} */
get config() {
return this.renderer.config;
}
/** Carousel instance configured {@link ESLCarouselStaticState} */
get configCurrent() {
return {
type: this.typeRule.value || 'default',
size: this.$slides.length,
count: this.countRule.value || 1,
loop: !!this.loopRule.value,
vertical: !!this.verticalRule.value
};
}
/** Carousel instance current {@link ESLCarouselState} */
get state() {
return Object.assign({}, this.renderer.config, {
activeIndex: this.activeIndex
});
}
/** @returns currently active renderer */
get renderer() {
return ESLCarouselRenderer.registry.create(this, this.configCurrent);
}
connectedCallback() {
super.connectedCallback();
this.update();
this.updateA11y();
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (!this.connected)
return;
if (attrName === 'container') {
memoize.clear(this, '$container');
return this.updateStateMarkers();
}
memoize.clear(this, `${attrName}Rule`);
this.update();
}
disconnectedCallback() {
super.disconnectedCallback();
memoize.clear(this, ['$container', '$slides', '$slidesArea', 'typeRule', 'loopRule', 'countRule', 'verticalRule']);
}
/** Updates the config and the state that is associated with */
update() {
const config = this.configCurrent;
const oldConfig = this.config;
const initial = !this.renderer.bound;
const $oldSlides = initial ? [] : this.$slides;
memoize.clear(this, '$slides');
const added = this.$slides.filter((slide) => !$oldSlides.includes(slide));
const removed = $oldSlides.filter((slide) => !this.$slides.includes(slide));
if (!added.length && !removed.length && this.renderer.equal(config))
return;
this.renderer.unbind();
memoize.clear(this, 'renderer');
this.renderer.bind();
this.updateStateMarkers();
this.dispatchEvent(ESLCarouselChangeEvent.create({ initial, added, removed, config, oldConfig }));
}
updateStateMarkers() {
this.$$attr('empty', !this.size);
this.$$attr('single-slide', this.size === 1);
this.$$attr('incomplete', this.size <= this.renderer.count);
if (!this.$container)
return;
CSSClassUtils.toggle(this.$container, this.containerEmptyClass, this.empty, this);
CSSClassUtils.toggle(this.$container, this.containerIncompleteClass, this.incomplete, this);
}
/** Appends slide instance to the current carousel */
addSlide(slide) {
slide.setAttribute(this.slideAttrName, '');
if (slide.parentNode === this.$slidesArea)
return this.update();
console.debug('[ESL]: ESLCarousel moves slide to correct location', slide);
this.$slidesArea.appendChild(slide);
}
/** Remove slide instance from the current carousel */
removeSlide(slide) {
if (slide.parentNode === this.$slidesArea)
this.$slidesArea.removeChild(slide);
if (this.$slides.includes(slide))
this.update();
}
updateA11y() {
if (!this.role) {
this.setAttribute('role', 'region');
this.setAttribute('aria-roledescription', 'Carousel');
}
if (!this.id)
this.id = sequentialUID('esl-carousel-');
if (!this.$slidesArea.id)
this.$slidesArea.id = `${this.id}-slides`;
if (!this.$slidesArea.role)
this.$slidesArea.role = 'list';
}
_onRuleUpdate() {
this.update();
}
_onRegistryUpdate() {
this.update();
}
_onResize() {
this.renderer && this.renderer.redraw();
}
onShowRequest(e) {
const detail = e.detail || {};
if (!isMatches(this, detail.match))
return;
const index = this.$slides.findIndex(($slide) => $slide.contains(e.target));
if (index !== -1 && !this.isActive(index))
this.goTo(index);
}
/** @returns slides that are processed by the current carousel. */
get $slides() {
const { slideAttrName } = this;
const els = this.$slidesArea ? [...this.$slidesArea.children] : [];
return els.filter((el) => el.hasAttribute(slideAttrName));
}
/**
* @returns carousel container
*/
get $container() {
return ESLTraversingQuery.first(this.container, this);
}
/** @returns carousel slides area */
get $slidesArea() {
const $provided = this.querySelector(`[${this.tagName}-slides]`);
if ($provided)
return $provided;
const $container = document.createElement('div');
$container.setAttribute(this.tagName + '-slides', '');
this.appendChild($container);
return $container;
}
/** @returns first active slide */
get $activeSlide() {
return this.$slides[this.activeIndex];
}
/** @returns list of active slides. */
get $activeSlides() {
return this.activeIndexes.map((index) => this.$slides[index]);
}
/** @returns count of slides. */
get size() {
return this.$slides.length || 0;
}
/** @returns index of first (the most left in the loop) active slide */
get activeIndex() {
if (this.size <= 0)
return -1;
if (this.isActive(0)) {
for (let i = this.size - 1; i > 0; --i) {
if (!this.isActive(i))
return normalize(i + 1, this.size);
}
}
return this.$slides.findIndex(this.isActive, this);
}
/** @returns list of active slide indexes. */
get activeIndexes() {
const start = this.activeIndex;
if (start < 0)
return [];
const indexes = [];
for (let i = 0; i < this.size; i++) {
const index = normalize(i + start, this.size);
if (this.isActive(index))
indexes.push(index);
}
return indexes;
}
/** Goes to the target according to passed params */
goTo(target, params = {}) {
if (target instanceof HTMLElement)
return this.goTo(this.indexOf(target), params);
if (!this.renderer)
return Promise.reject();
return this.renderer.navigate(toIndex(target, this.state), this.mergeParams(params));
}
/** Moves slides by the passed offset */
move(offset, from = this.activeIndex, params = {}) {
if (!this.renderer)
return;
this.renderer.move(offset, from, this.mergeParams(params));
}
/** Commits slides to the nearest stable position */
commit(offset, from = this.activeIndex, params = {}) {
if (!this.renderer)
return Promise.reject();
return this.renderer.commit(offset, from, this.mergeParams(params));
}
/** Merges request params with default params */
mergeParams(params) {
const stepDuration = this.stepDurationRule.value || 0;
return Object.assign({ stepDuration }, params);
}
/** @returns slide by index (supports not normalized indexes) */
slideAt(index) {
return this.$slides[normalize(index, this.$slides.length)];
}
/** @returns index of the passed slide */
indexOf(slide) {
return this.$slides.indexOf(slide);
}
/** @returns if the passed slide target can be reached */
canNavigate(target) {
return canNavigate(target, this.state);
}
/** @returns if the passed element (or slide on a passed index) is an active slide */
isActive(el) {
if (typeof el === 'number')
return this.isActive(this.$slides[el]);
return el && el.hasAttribute('active');
}
/** @returns if the passed element (or slide on a passed index) is a slide in pre-active state */
isPreActive(el) {
if (typeof el === 'number')
return this.isPreActive(this.$slides[el]);
return el && el.hasAttribute('pre-active');
}
/** @returns if the passed element (or slide on a passed index) is a next slide */
isNext(el) {
if (typeof el === 'number')
return this.isNext(this.$slides[el]);
return el && el.hasAttribute('next');
}
/** @returns if the passed element (or slide on a passed index) is a prev slide */
isPrev(el) {
if (typeof el === 'number')
return this.isPrev(this.$slides[el]);
return el && el.hasAttribute('prev');
}
/**
* Registers component in the {@link customElements} registry
* @param tagName - custom tag name to register custom element
*/
static register(tagName) {
super.register(tagName);
ESLCarouselSlide.is = this.is + '-slide';
ESLCarouselSlide.register();
}
};
ESLCarousel.is = 'esl-carousel';
ESLCarousel.observedAttributes = ['media', 'type', 'loop', 'count', 'vertical', 'step-duration', 'container'];
__decorate([
attr({ defaultValue: 'all' })
], ESLCarousel.prototype, "media", void 0);
__decorate([
attr({ defaultValue: 'default' })
], ESLCarousel.prototype, "type", void 0);
__decorate([
attr({ defaultValue: 'false' })
], ESLCarousel.prototype, "loop", void 0);
__decorate([
attr({ defaultValue: '1' })
], ESLCarousel.prototype, "count", void 0);
__decorate([
attr({ defaultValue: 'false' })
], ESLCarousel.prototype, "vertical", void 0);
__decorate([
attr({ defaultValue: '250' })
], ESLCarousel.prototype, "stepDuration", void 0);
__decorate([
attr({ defaultValue: '' })
], ESLCarousel.prototype, "container", void 0);
__decorate([
attr({ defaultValue: '' })
], ESLCarousel.prototype, "containerEmptyClass", void 0);
__decorate([
attr({ defaultValue: '' })
], ESLCarousel.prototype, "containerIncompleteClass", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLCarousel.prototype, "animating", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLCarousel.prototype, "empty", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLCarousel.prototype, "singleSlide", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLCarousel.prototype, "incomplete", void 0);
__decorate([
memoize()
], ESLCarousel.prototype, "typeRule", null);
__decorate([
memoize()
], ESLCarousel.prototype, "loopRule", null);
__decorate([
memoize()
], ESLCarousel.prototype, "countRule", null);
__decorate([
memoize()
], ESLCarousel.prototype, "verticalRule", null);
__decorate([
memoize()
], ESLCarousel.prototype, "stepDurationRule", null);
__decorate([
memoize()
], ESLCarousel.prototype, "renderer", null);
__decorate([
ready
], ESLCarousel.prototype, "connectedCallback", null);
__decorate([
decorate(microtask)
], ESLCarousel.prototype, "update", null);
__decorate([
listen({ event: 'change', target: ($this) => $this.observedRules })
], ESLCarousel.prototype, "_onRuleUpdate", null);
__decorate([
listen({ event: 'change', target: ESLCarouselRenderer.registry })
], ESLCarousel.prototype, "_onRegistryUpdate", null);
__decorate([
listen({ event: 'resize', target: ESLResizeObserverTarget.for })
], ESLCarousel.prototype, "_onResize", null);
__decorate([
listen('esl:show:request')
], ESLCarousel.prototype, "onShowRequest", null);
__decorate([
memoize()
], ESLCarousel.prototype, "$slides", null);
__decorate([
memoize()
], ESLCarousel.prototype, "$container", null);
__decorate([
memoize()
], ESLCarousel.prototype, "$slidesArea", null);
ESLCarousel = __decorate([
ExportNs('Carousel')
], ESLCarousel);
export { ESLCarousel };