Skip to content

Commit

Permalink
feat(esl-carousel): add grid renderer with capability to render mul…
Browse files Browse the repository at this point in the history
…ti row (column) carousel

Co-authored-by: Anna <abarmina@exadel.com>
  • Loading branch information
ala-n and abarmina committed Jun 24, 2024
1 parent 823fae8 commit a54a1ab
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/modules/esl-carousel/core.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

// Renderer Default
@import './renderers/esl-carousel.default.renderer.less';
@import './renderers/esl-carousel.grid.renderer.less';
1 change: 1 addition & 0 deletions src/modules/esl-carousel/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './plugin/wheel/esl-carousel.wheel.mixin';
// Renderer Default
import './renderers/esl-carousel.none.renderer';
import './renderers/esl-carousel.default.renderer';
import './renderers/esl-carousel.grid.renderer';
51 changes: 29 additions & 22 deletions src/modules/esl-carousel/renderers/esl-carousel.default.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {
const {activeIndex, $slidesArea} = this.$carousel;
this.currentIndex = activeIndex;
if (!$slidesArea) return;
while (this.currentIndex !== nextIndex) await this.onStepAnimate(direction);
while (this.currentIndex !== nextIndex) await this.onStepAnimate(direction === 'next' ? 1 : -1);
}

/** Post-processing animation action. */
Expand All @@ -93,11 +93,11 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {
}

/** Makes pre-processing the transition animation of one slide. */
protected async onStepAnimate(direction: ESLCarouselDirection): Promise<void> {
const index = normalize(this.currentIndex + (direction === 'next' ? 1 : -1), this.size);
protected async onStepAnimate(indexOffset: number): Promise<void> {
const index = normalize(this.currentIndex + indexOffset, this.size);

// Make sure there is a slide in required direction
this.reorder(direction === 'prev');
this.reorder(indexOffset < 0);

const offsetFrom = -this.getOffset(this.currentIndex);
this.setTransformOffset(offsetFrom);
Expand All @@ -115,19 +115,22 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {
this.$carousel.$$attr('animating', false);
}

protected indexByOffset(count: number): number {
return this.$carousel.activeIndex + count;
}

/** Handles the slides transition. */
public onMove(offset: number): void {
this.$carousel.toggleAttribute('active', true);

const sign = offset < 0 ? 1 : -1;
const slideSize = this.slideSize + this.gap;
const count = Math.floor(Math.abs(offset) / slideSize);
const index = this.$carousel.activeIndex + count * sign;
const index = this.indexByOffset(count * (offset < 0 ? 1 : -1));

// check left border of non-loop state
if (!this.loop && offset > 0 && index - 1 < 0) return;
if (!this.loop && offset > 0 && index <= 0) return;
// check right border of non-loop state
if (!this.loop && offset < 0 && index + 1 + this.count > this.$carousel.size) return;
if (!this.loop && offset < 0 && index + this.count >= this.size) return;

this.currentIndex = normalize(index, this.size);
this.reorder(offset > 0);
Expand All @@ -139,13 +142,12 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {
/** Ends current transition and make permanent all changes performed in the transition. */
// eslint-disable-next-line sonarjs/cognitive-complexity
public async commit(offset: number): Promise<void> {
const sign = offset < 0 ? 1 : -1;
const slideSize = this.slideSize + this.gap;

const amount = Math.abs(offset) / slideSize;
const tolerance = ESLDefaultCarouselRenderer.NEXT_SLIDE_TOLERANCE;
const count = (amount - Math.floor(amount)) > tolerance ? Math.ceil(amount) : Math.floor(amount);
const index = this.$carousel.activeIndex + count * sign;
const index = this.indexByOffset(count * (offset < 0 ? 1 : -1));

this.currentIndex = normalizeIndex(index, this);

Expand All @@ -160,26 +162,31 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {

this.reorder();
this.setTransformOffset(-this.getOffset(this.currentIndex));
this.setActive(this.currentIndex, {direction: sign > 0 ? 'next' : 'prev'});
this.setActive(this.currentIndex, {direction: offset < 0 ? 'next' : 'prev'});
this.$carousel.$$attr('active', false);
}

/**
* @returns count of slides to be rendered (reserved) before the first slide
*/
protected calcReserveCount(back?: boolean): number {
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;
// otherwise, ensure that there are at least half of free slides reserved (if the back option is set - round up, otherwise - round down)
return Math.min(count, 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
*/
protected reorder(back?: boolean): void {
const {size, count, loop, currentIndex, $slides} = this;

const reserve = ((): number => {
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;
// otherwise, ensure that there are at least half of free slides reserved (if the back option is set - round up, otherwise - round down)
return Math.min(count, back ? Math.ceil(freeSlides / 2) : Math.floor(freeSlides / 2));
})();
const {size, loop, currentIndex, $slides} = this;
const reserve = this.calcReserveCount(back);

const index = loop ? currentIndex : 0;
for (let i = 0; i < size; ++i) {
Expand Down
29 changes: 29 additions & 0 deletions src/modules/esl-carousel/renderers/esl-carousel.grid.renderer.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.esl-carousel-grid-renderer {
[esl-carousel-slides] {
display: grid;
grid-template-rows: 1fr 1fr;
grid-auto-columns: auto;
grid-auto-flow: column;

transition: none;
max-width: 100%;
max-height: 100%;
}

&.esl-carousel-vertical [esl-carousel-slides] {
grid-template-columns: 1fr 1fr;
grid-auto-rows: auto;
grid-auto-flow: row;
}

&[animating] [esl-carousel-slides] {
transition: transform 0.25s linear;
}

&.esl-carousel-horizontal [esl-carousel-slide] {
width: var(--esl-slide-size);
}
&.esl-carousel-vertical [esl-carousel-slide] {
height: var(--esl-slide-size);
}
}
89 changes: 89 additions & 0 deletions src/modules/esl-carousel/renderers/esl-carousel.grid.renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {prop, memoize} from '../../esl-utils/decorators';
import {ESLCarouselRenderer} from '../core/esl-carousel.renderer';
import {ESLDefaultCarouselRenderer} from './esl-carousel.default.renderer';

import type {ESLCarouselDirection} from '../core/nav/esl-carousel.nav.types';
import type {ESLCarouselActionParams} from '../core/esl-carousel';

@ESLCarouselRenderer.register
export class ESLGridCarouselRenderer extends ESLDefaultCarouselRenderer {
public static override is = 'grid';
public static override classes: string[] = ['esl-carousel-grid-renderer'];

/** Slide count per carousel dimension */
@prop(2, {readonly: true})
public readonly ROWS: number;

/** @returns fake slides collection to fill the last "row" in grid mode */
@memoize()
public get $fakeSlides(): HTMLElement[] {
const count = this.$carousel.$slides.length % this.ROWS;
if (count === 0) return [];
return Array.from({length: this.ROWS - count}, () => document.createElement('div'));
}

/** @returns all slides including {@link $fakeSlides} slides created in grid mode */
public override get $slides(): HTMLElement[] {
return (this.$carousel.$slides || []).concat(this.$fakeSlides);
}

/**
* Processes binding of defined renderer to the carousel {@link ESLCarousel}.
* Prepare to renderer animation.
*/
public override onBind(): void {
memoize.clear(this, '$fakeSlides');
this.$area.append(...this.$fakeSlides);
super.onBind();
}

/**
* Processes unbinding of defined renderer from the carousel {@link ESLCarousel}.
* Clear animation.
*/
public override onUnbind(): void {
this.$fakeSlides.forEach((el) => el.remove());
super.onUnbind();
}

/**
* Processes changing slides
* Normalize actual active index to the first slide in the current dimension ('row')
*/
public override async navigate(index: number, direction: ESLCarouselDirection, {activator}: ESLCarouselActionParams): Promise<void> {
await super.navigate(index - (index % this.ROWS), direction, {activator});
}

/** Processes animation. */
public override async onAnimate(nextIndex: number, direction: ESLCarouselDirection): Promise<void> {
const {activeIndex, $slidesArea} = this.$carousel;
this.currentIndex = activeIndex;
if (!$slidesArea) return;
const step = this.ROWS * (direction === 'next' ? 1 : -1);
while (this.currentIndex !== nextIndex) await this.onStepAnimate(step);
}

protected override indexByOffset(offset: number): number {
return super.indexByOffset(offset * this.ROWS);
}

/**
* @returns count of slides to be rendered (reserved) before the first slide does not include fake slides
*/
protected override calcReserveCount(back?: boolean): number {
const reserve = super.calcReserveCount(back);
return reserve - (reserve % this.ROWS);
}

/** Sets min size for slides */
protected override resize(): void {
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 count = Math.floor(this.count / this.ROWS);
this.slideSize = Math.floor((areaSize - this.gap * (count - 1)) / count);
this.$area.style.setProperty(ESLDefaultCarouselRenderer.SIZE_PROP, this.slideSize + 'px');
}
}

0 comments on commit a54a1ab

Please sign in to comment.