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