UNPKG

@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
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 };