Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[esl-carousel] inner API update + fixes #2595

Merged
merged 13 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/modules/esl-carousel/core/esl-carousel.events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ESLCarousel} from './esl-carousel';
import type {ESLCarouselDirection, ESLCarouselStaticState} from './nav/esl-carousel.nav.types';
import type {ESLCarouselDirection, ESLCarouselStaticState} from './esl-carousel.types';

/** {@link ESLCarouselSlideEvent} init object */
export interface ESLCarouselSlideEventInit {
Expand All @@ -8,17 +8,19 @@ export interface ESLCarouselSlideEventInit {
/** A list of indexes of slides that are active after the change */
indexesAfter: number[];
/** Direction of slide animation */
direction: ESLCarouselDirection | null;
direction?: ESLCarouselDirection;
/** Auxiliary request attribute that represents object that initiates slide change */
activator?: any;
}

/** {@link ESLCarousel} event that represents slide change event */
export class ESLCarouselSlideEvent extends Event implements ESLCarouselSlideEventInit {
/** {@link ESLCarouselSlideEvent} event type dispatched before slide change (pre-event) */
/** {@link ESLCarouselSlideEvent} cancelable event type dispatched before slide change (pre-event) */
public static readonly BEFORE = 'esl:before:slide-change';
/** {@link ESLCarouselSlideEvent} event type dispatched before carousel is going to change active slide (post-event) */
public static readonly CHANGE = 'esl:slide-change';
/** {@link ESLCarouselSlideEvent} event type dispatched after slide change (post-event) */
public static readonly AFTER = 'esl:slide-change';
public static readonly AFTER = 'esl:after:slide-change';

public override readonly target: ESLCarousel;
public readonly indexesBefore: number[];
Expand Down Expand Up @@ -58,7 +60,7 @@ export class ESLCarouselSlideEvent extends Event implements ESLCarouselSlideEven
return this.indexesAfter.map((index) => this.target.slideAt(index));
}

public static create(type: 'BEFORE' | 'AFTER', init: ESLCarouselSlideEventInit): ESLCarouselSlideEvent {
public static create(type: 'BEFORE' | 'CHANGE' | 'AFTER', init: ESLCarouselSlideEventInit): ESLCarouselSlideEvent {
return new ESLCarouselSlideEvent(ESLCarouselSlideEvent[type], init);
}
}
Expand Down
68 changes: 36 additions & 32 deletions src/modules/esl-carousel/core/esl-carousel.renderer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {memoize} from '../../esl-utils/decorators';
import {isEqual} from '../../esl-utils/misc/object';
import {SyntheticEventTarget} from '../../esl-utils/dom';
import {ESLCarouselDirection} from './esl-carousel.types';
import {ESLCarouselSlideEvent} from './esl-carousel.events';
import {normalize, sequence, indexToDirection} from './nav/esl-carousel.nav.utils';
import {indexToDirection, normalize, normalizeIndex, sequence} from './esl-carousel.utils';

import type {ESLCarousel, ESLCarouselActionParams} from './esl-carousel';
import type {ESLCarouselConfig, ESLCarouselDirection} from './nav/esl-carousel.nav.types';
import type {ESLCarousel} from './esl-carousel';
import type {ESLCarouselSlideEventInit} from './esl-carousel.events';
import type {ESLCarouselActionParams, ESLCarouselConfig, ESLCarouselNavInfo} from './esl-carousel.types';

export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
public static is: string;
Expand Down Expand Up @@ -91,50 +92,53 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
public onUnbind(): void {}
/** Processes drawing of the carousel {@link ESLCarousel}. */
public redraw(): void {}

/** Normalizes an index before navigation */
protected normalizeIndex(index: number, params?: ESLCarouselActionParams): number {
return normalizeIndex(index, this);
}
/** Normalizes a direction before navigation */
protected normalizeDirection(direction: ESLCarouselDirection | undefined, params?: ESLCarouselActionParams): ESLCarouselDirection {
return (this.loop ? params && params.direction : null) || direction || ESLCarouselDirection.NEXT;
}

/** Processes changing slides */
public async navigate(index: number, direction: ESLCarouselDirection, {activator}: ESLCarouselActionParams): Promise<void> {
const {activeIndex, activeIndexes} = this.$carousel;
public async navigate(to: ESLCarouselNavInfo, params: ESLCarouselActionParams): Promise<void> {
const index = this.normalizeIndex(to.index, params);
const direction = this.normalizeDirection(to.direction, params);

if (activeIndex === index && activeIndexes.length === this.count) return;
if (!this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('BEFORE', {
direction,
activator,
indexesBefore: activeIndexes,
indexesAfter: sequence(index, this.count, this.size)
}))) return;
const indexesAfter = sequence(index, this.count, this.size);
const indexesBefore = this.$carousel.activeIndexes;
if (indexesBefore.toString() === indexesAfter.toString()) return;

const details = {...params, direction, indexesBefore, indexesAfter};
if (!this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('BEFORE', details))) return;

this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('CHANGE', details));
this.setPreActive(index);

try {
await this.onBeforeAnimate(index, direction);
await this.onAnimate(index, direction);
await this.onAfterAnimate(index, direction);
await this.onBeforeAnimate(index, direction, params);
await this.onAnimate(index, direction, params);
await this.onAfterAnimate(index, direction, params);
} catch (e: unknown) {
console.error(e);
}

this.setActive(index, {direction, activator});
this.setActive(index, {direction, ...params});
}

/** Pre-processing animation action. */
public async onBeforeAnimate(index?: number, direction?: ESLCarouselDirection): Promise<void> {}
public async onBeforeAnimate(index: number, direction: ESLCarouselDirection, params: ESLCarouselActionParams): Promise<void> {}
/** Processes animation. */
public abstract onAnimate(index: number, direction: ESLCarouselDirection): Promise<void>;
public abstract onAnimate(index: number, direction: ESLCarouselDirection, params: ESLCarouselActionParams): Promise<void>;
/** Post-processing animation action. */
public async onAfterAnimate(index: number, direction: ESLCarouselDirection): Promise<void> {}

/**
* Moves slide by the passed offset in px.
* @param offset - offset in px
* @param from - start index (default: current active index)
*/
public abstract move(offset: number, from?: number): void;
/**
* Normalizes move offset to the "nearest stable" slide position.
* @param offset - offset in px
* @param from - start index (default: current active index)
*/
public abstract commit(offset?: number, from?: number): void;
public async onAfterAnimate(index: number, direction: ESLCarouselDirection, params: ESLCarouselActionParams): Promise<void> {}

/** Moves slide by the passed offset in px */
public abstract move(offset: number, from: number, params: ESLCarouselActionParams): void;
/** Normalizes move offset to the "nearest stable" slide position */
public abstract commit(offset: number, from: number, params: ESLCarouselActionParams): Promise<void>;

/** Sets active slides from passed index **/
public setActive(current: number, event?: Partial<ESLCarouselSlideEventInit>): void {
Expand Down
9 changes: 6 additions & 3 deletions src/modules/esl-carousel/core/esl-carousel.slide.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {CSSClassUtils} from '../../esl-utils/dom/class';
import {microtask} from '../../esl-utils/async/microtask';
import {attr, decorate, memoize, ready} from '../../esl-utils/decorators';
import {ExportNs} from '../../esl-utils/environment/export-ns';
import {attr, decorate, memoize} from '../../esl-utils/decorators';

import type {ESLCarousel} from './esl-carousel';

Expand All @@ -11,6 +12,7 @@ import type {ESLCarousel} from './esl-carousel';
*
* ESLCarouselSlide - a component that provides content for ESLCarousel {@link ESLCarousel}
*/
@ExportNs('Carousel.Slide')
export class ESLCarouselSlide extends ESLMixinElement {
public static override is = 'esl-carousel-slide';
public static override observedAttributes = ['active'];
Expand Down Expand Up @@ -49,7 +51,6 @@ export class ESLCarouselSlide extends ESLMixinElement {
return this.$host.closest(carouselTag) as ESLCarousel | undefined;
}

@ready
protected override connectedCallback(): void {
if (!this.$carousel) return;
this.$carousel?.addSlide && this.$carousel.addSlide(this.$host);
Expand All @@ -59,7 +60,9 @@ export class ESLCarouselSlide extends ESLMixinElement {
}

protected override disconnectedCallback(): void {
this.$carousel?.removeSlide && this.$carousel.removeSlide(this.$host);
// A disconnected callback is not directly related to slide removal from the carousel
// e.g. carousel itself can be removed from the DOM so child slides behave accordingly
this.$carousel?.update() && this.$carousel?.update();
memoize.clear(this, '$carousel');
super.disconnectedCallback();
}
Expand Down
60 changes: 36 additions & 24 deletions src/modules/esl-carousel/core/esl-carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,26 @@ import {ESLBaseElement} from '../../esl-base-element/core';
import {attr, boolAttr, ready, decorate, listen, memoize} from '../../esl-utils/decorators';
import {isMatches} from '../../esl-utils/dom/traversing';
import {microtask} from '../../esl-utils/async';
import {parseBoolean, sequentialUID} from '../../esl-utils/misc';
import {parseBoolean, parseTime, sequentialUID} from '../../esl-utils/misc';

import {CSSClassUtils} from '../../esl-utils/dom/class';
import {ESLTraversingQuery} from '../../esl-traversing-query/core';
import {ESLMediaRuleList} from '../../esl-media-query/core';
import {ESLResizeObserverTarget} from '../../esl-event-listener/core';

import {normalize, toIndex, canNavigate} from './nav/esl-carousel.nav.utils';
import {normalize, toIndex, canNavigate} from './esl-carousel.utils';

import {ESLCarouselSlide} from './esl-carousel.slide';
import {ESLCarouselRenderer} from './esl-carousel.renderer';
import {ESLCarouselChangeEvent} from './esl-carousel.events';

import type {
ESLCarouselState,
ESLCarouselDirection,
ESLCarouselSlideTarget,
ESLCarouselStaticState,
ESLCarouselConfig
} from './nav/esl-carousel.nav.types';

/** {@link ESLCarousel} action params interface */
export interface ESLCarouselActionParams {
/** Element requester of the change */
activator?: any;
/** Direction to move to. */
direction?: ESLCarouselDirection;
// TODO: implement
// noAnimation?: boolean;
}
ESLCarouselConfig,
ESLCarouselActionParams
} from './esl-carousel.types';

/**
* ESLCarousel component
Expand All @@ -43,7 +33,7 @@ export interface ESLCarouselActionParams {
@ExportNs('Carousel')
export class ESLCarousel extends ESLBaseElement {
public static override is = 'esl-carousel';
public static observedAttributes = ['media', 'type', 'loop', 'count', 'vertical', 'container'];
public static observedAttributes = ['media', 'type', 'loop', 'count', 'vertical', 'step-duration', 'container'];

/** Media query pattern used for {@link ESLMediaRuleList} of `type`, `loop` and `count` (default: `all`) */
@attr({defaultValue: 'all'}) public media: string;
Expand All @@ -56,6 +46,9 @@ export class ESLCarousel extends ESLBaseElement {
/** Orientation of the carousel (`horizontal` by default). Supports {@link ESLMediaRuleList} syntax */
@attr({defaultValue: 'false'}) public vertical: string | boolean;

/** Duration of the single slide transition */
@attr({defaultValue: '250'}) public stepDuration: string;

/** Container selector (supports traversing query). Carousel itself by default */
@attr({defaultValue: ''}) public container: string;
/** CSS class to add on the container when carousel is empty */
Expand Down Expand Up @@ -95,6 +88,11 @@ export class ESLCarousel extends ESLBaseElement {
public get verticalRule(): ESLMediaRuleList<boolean> {
return ESLMediaRuleList.parse(this.vertical as string, this.media, parseBoolean);
}
/** Duration of the single slide transition {@link ESLMediaRuleList} instance */
@memoize()
public get stepDurationRule(): ESLMediaRuleList<number> {
return ESLMediaRuleList.parse(this.stepDuration, this.media, parseTime);
}

/** Returns observed media rules */
public get observedRules(): ESLMediaRuleList[] {
Expand Down Expand Up @@ -169,6 +167,7 @@ export class ESLCarousel extends ESLBaseElement {
this.renderer.unbind();
memoize.clear(this, 'renderer');
this.renderer.bind();

this.updateStateMarkers();
this.dispatchEvent(ESLCarouselChangeEvent.create({initial, added, removed, config, oldConfig}));
}
Expand All @@ -184,17 +183,14 @@ export class ESLCarousel extends ESLBaseElement {

/** Appends slide instance to the current carousel */
public addSlide(slide: HTMLElement): void {
if (!slide) return;
slide.setAttribute(this.slideAttrName, '');
if (slide.parentNode === this.$slidesArea) return this.update();
console.debug('[ESL]: ESLCarousel moves slide to correct location', slide);
if (slide.parentNode) slide.remove();
Promise.resolve().then(() => this.$slidesArea.appendChild(slide));
this.$slidesArea.appendChild(slide);
}

/** Remove slide instance from the current carousel */
public removeSlide(slide: HTMLElement): void {
if (!slide) return;
if (slide.parentNode === this.$slidesArea) this.$slidesArea.removeChild(slide);
if (this.$slides.includes(slide)) this.update();
}
Expand Down Expand Up @@ -298,12 +294,28 @@ export class ESLCarousel extends ESLBaseElement {
}

/** Goes to the target according to passed params */
public goTo(target: HTMLElement | ESLCarouselSlideTarget, params: ESLCarouselActionParams = {}): Promise<void> {
public goTo(target: HTMLElement | ESLCarouselSlideTarget, params: Partial<ESLCarouselActionParams> = {}): Promise<void> {
if (target instanceof HTMLElement) return this.goTo(this.indexOf(target), params);
if (!this.renderer) return Promise.reject();
const nav = toIndex(target, this.state);
const direction = (this.loop ? params.direction : null) || nav.direction || 'next';
return this.renderer.navigate(nav.index, direction, params);
return this.renderer.navigate(toIndex(target, this.state), this.mergeParams(params));
}

/** Moves slides by the passed offset */
public move(offset: number, from: number = this.activeIndex, params: Partial<ESLCarouselActionParams> = {}): void {
if (!this.renderer) return;
this.renderer.move(offset, from, this.mergeParams(params));
}

/** Commits slides to the nearest stable position */
public commit(offset: number, from: number = this.activeIndex, params: Partial<ESLCarouselActionParams> = {}): Promise<void> {
if (!this.renderer) return Promise.reject();
return this.renderer.commit(offset, from, this.mergeParams(params));
}

/** Merges request params with default params */
protected mergeParams(params: Partial<ESLCarouselActionParams>): ESLCarouselActionParams {
const stepDuration = this.stepDurationRule.value || 0;
return {stepDuration, ...params};
}

/** @returns slide by index (supports not normalized indexes) */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export type ESLCarouselDirection = 'next' | 'prev';
/** Direction enum, can be used in calculation directly */
export enum ESLCarouselDirection {
NEXT = 1,
NONE = 0,
PREV = -1
}

export type ESLCarouselNavIndex = number | `${number}` | `+${number}` | `-${number}` | ESLCarouselDirection;

Expand Down Expand Up @@ -47,5 +52,15 @@ export type ESLCarouselNavInfo = {
/** Target index */
index: number;
/** Direction to reach the index */
direction: ESLCarouselDirection | null;
direction?: ESLCarouselDirection;
};

/** {@link ESLCarousel} action params interface */
export interface ESLCarouselActionParams {
/** Element that requests changes */
activator?: any;
/** Direction to move to */
direction?: ESLCarouselDirection;
/** Duration of a single slide transition in milliseconds. (Set to 0 to disable animation) */
stepDuration: number;
}
Loading
Loading