UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

265 lines (264 loc) 14 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()); }); }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _ESLDefaultCarouselRenderer_offset_accessor_storage; var ESLDefaultCarouselRenderer_1; import { ESLMediaQuery } from '../../esl-media-query/core'; import { bounds, normalize, normalizeIndex, sign } from '../core/esl-carousel.utils'; import { ESLCarouselRenderer } from '../core/esl-carousel.renderer'; /** * Default carousel renderer based on CSS Flexbox stage, order (flex), and stage animated movement via CSS transform. * Supports multiple slides per view, (infinite) loop mode, touch-move, vertical mode, slide siblings rendering. * * Provides default slide width, supports gap between slides. Does not rely on default slide width, potentially can be used with CSS custom slide width. */ let ESLDefaultCarouselRenderer = ESLDefaultCarouselRenderer_1 = class ESLDefaultCarouselRenderer extends ESLCarouselRenderer { constructor() { super(...arguments); /** Slides gap size */ this.gap = 0; /** First index of active slides. */ this.currentIndex = 0; _ESLDefaultCarouselRenderer_offset_accessor_storage.set(this, 0); } /** @returns shift size in pixels */ get offset() { return __classPrivateFieldGet(this, _ESLDefaultCarouselRenderer_offset_accessor_storage, "f"); } set offset(value) { __classPrivateFieldSet(this, _ESLDefaultCarouselRenderer_offset_accessor_storage, value, "f"); } /** Multiplier for the index move on the slide move */ get INDEX_MOVE_MULTIPLIER() { return 1; } /** @returns true if moving half of the slides before current is forbidden */ get lazyReorder() { const reserve = this.$carousel.getAttribute('lazy-reorder'); if (reserve === null) return false; return ESLMediaQuery.for(reserve).matches; } /** Actual slide size (uses average) */ get slideSize() { return this.$slides.reduce((size, $slide) => { return size + (this.vertical ? $slide.offsetHeight : $slide.offsetWidth); }, 0) / this.$slides.length; } /** * Processes binding of defined renderer to the carousel {@link ESLCarousel}. * Prepare to renderer animation. */ onBind() { this.currentIndex = bounds(this.$carousel.activeIndex, 0, this.size - this.count); this.redraw(true); } redraw(initial = false) { this.resize(); // Calculate initial offset based on current rendered state (available only on an initial render) const fallbackOffset = initial ? this.getOffset(this.getReserveCount()) : 0; this.reorder(); this.setActive(this.currentIndex); // Set initial offset based on pre-calculation initial && this.setTransformOffset(this.offset - fallbackOffset); // Update offset according to main algorithm (fix edge cases if the fallback offset is not correct) this.setTransformOffset(this.offset - this.getOffset(this.currentIndex)); } /** * Processes unbinding of defined renderer from the carousel {@link ESLCarousel}. * Clear animation. */ onUnbind() { this.$slides.forEach((el) => el.style.removeProperty('order')); this.$area.style.removeProperty('transform'); this.$area.style.removeProperty(ESLDefaultCarouselRenderer_1.SIZE_PROP); this.animating = false; this.$carousel.$$attr('active', false); } /** @returns slide offset by the slide index */ getOffset(index) { const slide = this.$slides[index]; if (!slide) return 0; return this.vertical ? slide.offsetTop : slide.offsetLeft; } /** Sets scene offset */ setTransformOffset(offset) { this.$area.style.transform = `translate3d(${this.vertical ? `0px, ${offset}px` : `${offset}px, 0px`}, 0px)`; } /** Animates scene offset to index */ animateTo(index, duration) { return __awaiter(this, void 0, void 0, function* () { this.currentIndex = this.normalizeIndex(index); const offset = -this.getOffset(this.currentIndex); this.animating = true; yield this.$area.animate({ transform: [`translate3d(${this.vertical ? `0px, ${offset}px` : `${offset}px, 0px`}, 0px)`] }, { duration, easing: 'linear' }).finished; this.offset = 0; // reset offset after animation this.animating = false; }); } /** Processes animation. */ onAnimate(nextIndex, direction, params) { return __awaiter(this, void 0, void 0, function* () { const { activeIndex, $slidesArea } = this.$carousel; this.currentIndex = activeIndex; if (!$slidesArea) return; const distance = normalize((nextIndex - activeIndex) * direction, this.size); const speed = Math.min(1, this.count / distance) * this.transitionDuration; this.$carousel.$$attr('active', true); while (this.currentIndex !== nextIndex) { yield this.onStepAnimate(direction * this.INDEX_MOVE_MULTIPLIER, speed); } // if no slide change performed, reset offset if (this.offset !== 0) yield this.onStepAnimate(0, speed); this.$carousel.$$attr('active', false); }); } /** Post-processing animation action. */ onAfterAnimate(nextIndex, direction, params) { const _super = Object.create(null, { onAfterAnimate: { get: () => super.onAfterAnimate } }); return __awaiter(this, void 0, void 0, function* () { // Make sure we end up in a defined state on transition end this.reorder(); this.setTransformOffset(-this.getOffset(this.currentIndex)); return _super.onAfterAnimate.call(this, nextIndex, direction, params); }); } /** Makes pre-processing the transition animation of one slide. */ onStepAnimate(indexOffset, duration) { return __awaiter(this, void 0, void 0, function* () { const index = normalize(this.currentIndex + indexOffset, this.size); // Make sure there is a slide in required direction this.reorder(indexOffset < 0); const offsetFrom = this.offset - this.getOffset(this.currentIndex); this.setTransformOffset(offsetFrom); yield this.animateTo(index, duration); }); } /** Handles the slides transition. */ move(offset, from, params) { this.$carousel.$$attr('active', true); const direction = sign(-offset); const slideSize = this.slideSize + this.gap; const amount = Math.abs(offset) / slideSize; const index = from + Math.floor(amount) * this.INDEX_MOVE_MULTIPLIER * direction; const next = from + Math.ceil(amount) * this.INDEX_MOVE_MULTIPLIER * direction; // Normalize index according to loop state this.currentIndex = normalizeIndex(index, this); // Block move before the first slide if loop is disabled if (this.currentIndex === 0 && !this.loop) offset = Math.min(0, offset); // Block move after the last slide if loop is disabled if (this.currentIndex + this.count >= this.size && !this.loop) offset = Math.max(0, offset); this.reorder(offset > 0); const offsetBefore = this.offset; this.offset = Math.round(offset % slideSize); const stageOffset = this.getOffset(this.currentIndex) - this.offset; this.setTransformOffset(-stageOffset); if (next !== index) { const nextIndex = normalize(next, this.size); this.setPreActive(nextIndex, Object.assign(Object.assign({}, params), { direction: sign(next - index) })); } if (this.currentIndex !== this.$carousel.activeIndex) { this.setActive(this.currentIndex, Object.assign(Object.assign({}, params), { direction })); } this.dispatchMoveEvent(offsetBefore, params); } /** Ends current transition and make permanent all changes performed in the transition. */ commit(params) { return __awaiter(this, void 0, void 0, function* () { const { offset } = this; const dir = sign(-offset); const slideSize = this.slideSize + this.gap; const amount = Math.abs(offset) / slideSize; const tolerance = ESLDefaultCarouselRenderer_1.NEXT_SLIDE_TOLERANCE; const direction = params.direction || sign((amount - Math.floor(amount)) - tolerance); const count = direction > 0 ? Math.ceil(amount) : Math.floor(amount); const index = this.currentIndex + count * this.INDEX_MOVE_MULTIPLIER * dir; yield this.animateTo(index, this.transitionDuration); this.$carousel.$$attr('active', false); yield this.onAfterAnimate(this.currentIndex, direction, params); }); } /** * @returns count of slides to be rendered (reserved) before the first slide */ getReserveCount(back) { const { size, count, loop, currentIndex } = this; const freeSlides = size - count; // no need to reorder if there are no free slides or loop is disabled if (!loop || !freeSlides) return 0; // if back option is not set, prefer to reserve slides with respect to semantic order if (typeof back !== 'boolean') back = !!currentIndex; // Check if reorder is forbidden (if back option is set - we should reserve at least one slide for animation) if (this.lazyReorder) return back ? 1 : 0; // otherwise, ensure that there are at least half of free slides reserved (if the back option is set - round up, otherwise - round down) return back ? Math.ceil(freeSlides / 2) : Math.floor(freeSlides / 2); } /** * Sets order style property for slides starting at index * @param back - if true, ensures that there is a slide rendered before the current one */ reorder(back) { const { size, loop, currentIndex, $slides } = this; const reserve = this.getReserveCount(back); const index = loop ? currentIndex : 0; for (let i = 0; i < size; ++i) { let offset = (size + i - index) % size; // inverses index for backward reserve if (offset >= size - reserve) offset -= size; $slides[i].style.order = String(offset); } } /** Sets min size for slides */ resize() { if (!this.$area) return; const areaStyles = getComputedStyle(this.$area); this.gap = parseFloat(this.vertical ? areaStyles.rowGap : areaStyles.columnGap); const areaSize = parseFloat(this.vertical ? areaStyles.height : areaStyles.width); const slideSize = Math.floor((areaSize - this.gap * (this.count - 1)) / this.count); this.$area.style.setProperty(ESLDefaultCarouselRenderer_1.SIZE_PROP, slideSize + 'px'); } }; _ESLDefaultCarouselRenderer_offset_accessor_storage = new WeakMap(); ESLDefaultCarouselRenderer.is = 'default'; ESLDefaultCarouselRenderer.classes = ['esl-carousel-default-renderer']; /** CSS variable name for slide auto size */ ESLDefaultCarouselRenderer.SIZE_PROP = '--esl-slide-size'; /** Tolerance to treat offset enough to move to the next slide. Relative (0-1) to slide width */ ESLDefaultCarouselRenderer.NEXT_SLIDE_TOLERANCE = 0.25; ESLDefaultCarouselRenderer = ESLDefaultCarouselRenderer_1 = __decorate([ ESLCarouselRenderer.register ], ESLDefaultCarouselRenderer); export { ESLDefaultCarouselRenderer };