@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
435 lines (434 loc) • 17 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 { ExportNs } from '../../esl-utils/environment/export-ns';
import { ESLBaseElement } from '../../esl-base-element/core';
import { attr, boolAttr, safe, 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, toCamelCase } from '../../esl-utils/misc';
import { CSSClassUtils } from '../../esl-utils/dom/class';
import { ESLMediaRuleList } from '../../esl-media-query/core';
import { ESLResizeObserverTarget } from '../../esl-event-listener/core';
import { normalize, toIndex, isCurrent, canNavigate } from './esl-carousel.utils';
import { ESLCarouselSlide } from './esl-carousel.slide';
import { ESLCarouselChangeEvent } from './esl-carousel.events';
import { ESLCarouselRendererRegistry } from './esl-carousel.renderer.registry';
/**
* 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 this.renderer.state;
}
/** @returns currently active renderer */
get renderer() {
return ESLCarouselRendererRegistry.instance.create(this, this.configCurrent);
}
/** @returns normalized focus policy with legacy `no-inert` support */
get focusPolicyCurrent() {
const policy = this.$$attr('focus-policy');
if (policy === 'active' || policy === 'none' || policy === 'reveal')
return policy;
if (policy === null && this.hasAttribute('no-inert'))
return 'none';
return 'active';
}
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();
}
if (attrName === 'focus-policy' || attrName === 'no-inert') {
return this.updateSlidesA11yState();
}
memoize.clear(this, `${toCamelCase(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);
}
/** Updates slide accessibility state according to current focus policy */
updateSlidesA11yState() {
this.$slides.forEach(($slide) => { var _a; return (_a = ESLCarouselSlide.get($slide)) === null || _a === void 0 ? void 0 : _a.updateActiveState(); });
}
/** Appends slide instance to the current carousel */
addSlide(slide) {
slide.setAttribute(this.slideAttrName, '');
if (slide.parentNode === this.$slidesArea)
return this.update();
if (slide.parentNode)
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();
}
_onFocusIn(e) {
const target = e.target;
if (this.focusPolicyCurrent !== 'reveal' || !(target instanceof Node))
return;
const $slide = this.$slides.find(($s) => $s.contains(target));
if (!$slide || this.isActive($slide))
return;
this.goTo($slide, { activator: e }).catch(console.debug);
}
onShowRequest(e) {
const detail = e.detail || {};
if (!isMatches(this, detail.match))
return;
const index = this.$slides.findIndex(($s) => $s.contains(e.target));
if (index !== -1 && !this.isActive(index))
this.goTo(index).catch(console.debug);
}
/** @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 this.$$find(this.container);
}
/** @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 additional shift of the stage in pixels */
get offset() {
return this.renderer.offset || 0;
}
/** @returns index of first (the most left in the loop) active slide */
get activeIndex() {
return this.renderer.activeIndex;
}
/** @returns list of active slide indexes. */
get activeIndexes() {
return this.renderer.activeIndexes;
}
/** Goes to the target according to passed params */
goTo(target_1) {
return __awaiter(this, arguments, void 0, function* (target, params = {}) {
if (target instanceof HTMLElement)
return this.goTo(this.indexOf(target), params);
if (!this.renderer)
throw new Error('Renderer is not available');
const index = toIndex(target, this.renderer);
if (isNaN(index.index))
throw new Error(`Invalid target index passed ${target}`);
return this.renderer.navigate(index, 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(params = {}) {
if (!this.renderer)
return Promise.reject();
return this.renderer.commit(this.mergeParams(params));
}
/** Merges request params with default params */
mergeParams(params) {
const stepDuration = this.stepDurationRule.value || 0;
const indexesBefore = this.activeIndexes;
return Object.assign({ stepDuration, indexesBefore }, 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.
* @see canNavigate
*/
canNavigate(target) {
return canNavigate(target, this.renderer);
}
/**
* @returns if the passed navigation target refers to a currently active slide (or group) in this carousel.
* @see isCurrent
*/
isCurrent(target) {
return isCurrent(target, this.renderer);
}
/** @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', 'focus-policy', 'no-inert'];
__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()
], ESLCarousel.prototype, "stepDuration", void 0);
__decorate([
attr({ defaultValue: '' })
], ESLCarousel.prototype, "container", void 0);
__decorate([
attr({ defaultValue: 'active' })
], ESLCarousel.prototype, "focusPolicy", 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(),
safe(ESLMediaRuleList.empty())
], ESLCarousel.prototype, "typeRule", null);
__decorate([
memoize(),
safe(ESLMediaRuleList.empty())
], ESLCarousel.prototype, "loopRule", null);
__decorate([
memoize(),
safe(ESLMediaRuleList.empty())
], ESLCarousel.prototype, "countRule", null);
__decorate([
memoize(),
safe(ESLMediaRuleList.empty())
], ESLCarousel.prototype, "verticalRule", null);
__decorate([
memoize(),
safe(ESLMediaRuleList.empty())
], 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: ESLCarouselRendererRegistry.instance })
], ESLCarousel.prototype, "_onRegistryUpdate", null);
__decorate([
listen({ event: 'resize', target: ESLResizeObserverTarget.for })
], ESLCarousel.prototype, "_onResize", null);
__decorate([
listen('focusin')
], ESLCarousel.prototype, "_onFocusIn", 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 };