Skip to content

Commit e32fce8

Browse files
committed
feat(esl-carousel): esl-carousel-autoplay plugin full support
Here is the full list of what new capabilities include: - Ability to pass multiple configuration. - Ability to control duration using time format. - Ability to pass selector for controls to toggle plugin state. - Ability to pass state marker classes to add to controls or/and container. - Dispatching of the state events. - Optional mixin/example - esl-carousel-autoplay-progress - to make simple autoplay progress UI (you can do progress controls based on CSS animation/transition out of the box without JS or use mixin source code as an example to crete custom control)
1 parent a548d87 commit e32fce8

File tree

4 files changed

+181
-32
lines changed

4 files changed

+181
-32
lines changed

packages/esl/src/esl-carousel/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export * from './plugin/keyboard/esl-carousel.keyboard.mixin';
2121

2222
// Autoplay
2323
export * from './plugin/autoplay/esl-carousel.autoplay.mixin';
24+
export * from './plugin/autoplay/esl-carousel.autoplay.event';
25+
export * from './plugin/autoplay/esl-carousel.autoplay.progress.mixin';
2426

2527
// Link Utility
2628
export * from './plugin/relation/esl-carousel.relation.mixin';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type {ESLCarousel} from '../../core/esl-carousel';
2+
import type {ESLCarouselAutoplayMixin} from './esl-carousel.autoplay.mixin';
3+
4+
interface ESLCarouselAutoplayEventInit {
5+
/** Whether autoplay plugin is enabled or disabled */
6+
enabled: boolean;
7+
/** Whether autoplay cycle is currently active */
8+
active: boolean;
9+
/** Duration of the current autoplay cycle in milliseconds */
10+
duration: number;
11+
}
12+
/**
13+
* ESLCarouselAutoplayEvent (esl:autoplay:change) event is dispatched by {@link ESLCarouselAutoplayMixin}
14+
* on the host {@link ESLCarousel} element when autoplay state changes.
15+
* It indicates whether autoplay is enabled or disabled, or notifies about the current autoplay cycle start.
16+
*/
17+
export class ESLCarouselAutoplayEvent extends Event implements ESLCarouselAutoplayEventInit {
18+
public static readonly NAME = 'esl:autoplay:change';
19+
20+
public override target: ESLCarousel;
21+
22+
public readonly enabled: boolean;
23+
public readonly active: boolean;
24+
public readonly duration: number;
25+
26+
protected constructor(init: ESLCarouselAutoplayEventInit) {
27+
super(ESLCarouselAutoplayEvent.NAME, {bubbles: false, cancelable: false});
28+
Object.assign(this, init);
29+
}
30+
31+
public static dispatch(plugin: ESLCarouselAutoplayMixin): boolean {
32+
const {enabled, active, duration} = plugin;
33+
const event = new ESLCarouselAutoplayEvent({enabled, active, duration});
34+
return plugin.$host.dispatchEvent(event);
35+
}
36+
}

packages/esl/src/esl-carousel/plugin/autoplay/esl-carousel.autoplay.mixin.ts

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import {ExportNs} from '../../../esl-utils/environment/export-ns';
2-
import {bind, listen, memoize, ready} from '../../../esl-utils/decorators';
2+
import {listen, memoize, ready} from '../../../esl-utils/decorators';
3+
import {parseTime} from '../../../esl-utils/misc/format';
4+
import {CSSClassUtils} from '../../../esl-utils/dom/class';
5+
import {ESLTraversingQuery} from '../../../esl-traversing-query/core';
36

47
import {ESLCarouselPlugin} from '../esl-carousel.plugin';
58
import {ESLCarouselSlideEvent} from '../../core/esl-carousel.events';
6-
import {parseTime} from '../../../esl-utils/misc/format';
9+
import {ESLCarouselAutoplayEvent} from './esl-carousel.autoplay.event';
710

811
export interface ESLCarouselAutoplayConfig {
912
/** Timeout to send next command to the host carousel */
10-
timeout: string | number;
13+
duration: string | number;
1114
/** Navigation command to send to the host carousel. Default: 'slide:next' */
1215
command: string;
16+
/** Selector for control to toggle plugin state */
17+
control?: string;
18+
/** Class to toggle on control element, when autoplay is active */
19+
controlCls?: string;
20+
/** Class to toggle on container element, when autoplay is active */
21+
containerCls?: string;
1322
}
1423

1524
/**
@@ -22,68 +31,122 @@ export interface ESLCarouselAutoplayConfig {
2231
export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin<ESLCarouselAutoplayConfig> {
2332
public static override is = 'esl-carousel-autoplay';
2433
public static override DEFAULT_CONFIG: ESLCarouselAutoplayConfig = {
25-
timeout: 5000,
34+
duration: 5000,
2635
command: 'slide:next'
2736
};
28-
public static override DEFAULT_CONFIG_KEY = 'timeout';
37+
public static override DEFAULT_CONFIG_KEY: keyof ESLCarouselAutoplayConfig = 'duration';
2938

30-
private _timeout: number | null = null;
39+
private _enabled: boolean = true;
40+
private _duration: number | null = null;
3141

42+
/** True if the autoplay timer is currently active */
3243
public get active(): boolean {
33-
return !!this._timeout;
44+
return !!this._duration;
3445
}
35-
public get timeout(): number {
36-
return parseTime(this.config.timeout);
46+
47+
/** True if the autoplay plugin is enabled */
48+
public get enabled(): boolean {
49+
return this._enabled;
50+
}
51+
protected set enabled(value: boolean) {
52+
this._enabled = value;
53+
CSSClassUtils.toggle(this.$controls, this.config.controlCls, this._enabled);
54+
const {$container} = this.$host;
55+
$container && CSSClassUtils.toggle($container, this.config.containerCls, this._enabled);
56+
}
57+
58+
/** The duration of the autoplay timer in milliseconds */
59+
public get duration(): number {
60+
return parseTime(this.config.duration);
61+
}
62+
63+
/** A list of control elements to toggle plugin state */
64+
@memoize()
65+
public get $controls(): HTMLElement[] {
66+
const sel = this.config.control;
67+
return sel ? ESLTraversingQuery.all(sel, this.$host) as HTMLElement[] : [];
3768
}
3869

3970
@ready
4071
protected override connectedCallback(): void {
41-
if (super.connectedCallback()) {
42-
this.start();
43-
}
72+
super.connectedCallback();
73+
this.start();
4474
}
4575

4676
protected override disconnectedCallback(): void {
4777
super.disconnectedCallback();
4878
this.stop();
4979
}
5080

81+
@listen({inherit: true})
5182
protected override onConfigChange(): void {
83+
super.onConfigChange();
84+
memoize.clear(this, ['$controls', 'duration']);
85+
// Full restart during config change
86+
this.stop();
5287
this.start();
5388
}
5489

55-
/** Activates the timer to send commands */
90+
/** Activates and restarts the autoplay carousel timer */
5691
public start(): void {
57-
this.stop();
58-
const {timeout} = this;
59-
if (timeout <= 0 || isNaN(+timeout)) return;
60-
this._timeout = window.setTimeout(this._onInterval, timeout);
92+
const {duration} = this;
93+
this.enabled = +duration > 0;
94+
if (this.enabled) this._onCycle();
6195
}
6296

63-
/** Deactivates the timer to send commands */
64-
public stop(): void {
65-
this._timeout && window.clearTimeout(this._timeout);
66-
this._timeout = null;
97+
/**
98+
* Deactivates the autoplay carousel timer.
99+
* @param system - If true, the plugin will be suspended but not disabled.
100+
*/
101+
public stop(system = false): void {
102+
if (!this.active && !this.enabled) return;
103+
if (!system) this.enabled = false;
104+
this._duration && window.clearTimeout(this._duration);
105+
this._duration = null;
106+
ESLCarouselAutoplayEvent.dispatch(this);
67107
}
68108

69-
/** Handles next timer interval */
70-
@bind
71-
protected _onInterval(): void {
72-
this.$host?.goTo(this.config.command);
73-
this._timeout = window.setTimeout(this._onInterval, this.timeout);
109+
/**
110+
* Starts a new autoplay cycle.
111+
* Produces cycle self call after a timeout with enabled command execution.
112+
*/
113+
protected async _onCycle(exec?: boolean): Promise<void> {
114+
this._duration && window.clearTimeout(this._duration);
115+
this._duration = null;
116+
if (exec) await this.$host?.goTo(this.config.command);
117+
if (!this.enabled || this.active) return;
118+
const {duration} = this;
119+
this._duration = window.setTimeout(() => this._onCycle(true), duration);
120+
ESLCarouselAutoplayEvent.dispatch(this);
74121
}
75122

76-
/** Handles auxiliary events to pause/resume timer */
77-
@listen(`mouseout mouseover focusin focusout ${ESLCarouselSlideEvent.AFTER}`)
123+
/** Handles click on control element to toggle plugin state */
124+
@listen({
125+
event: 'click',
126+
target: ($this: ESLCarouselAutoplayMixin)=> $this.$controls,
127+
condition: ($this: ESLCarouselAutoplayMixin)=> !!$this.$controls.length
128+
})
129+
protected _onToggle(e: Event): void {
130+
this.enabled ? this.stop() : this.start();
131+
e.preventDefault();
132+
}
133+
134+
/** Handles auxiliary events that represent user interaction to pause/resume timer */
135+
@listen('mouseleave mouseenter focusin focusout')
78136
protected _onInteract(e: Event): void {
79-
// Slide change can only delay the timer, but not start it
80-
if (e.type === ESLCarouselSlideEvent.AFTER && !this.active) return;
81-
if (['mouseover', 'focusin'].includes(e.type)) {
82-
this.stop();
137+
if (!this.enabled) return;
138+
if (['mouseenter', 'focusin'].includes(e.type)) {
139+
this.stop(true);
83140
} else {
84141
this.start();
85142
}
86143
}
144+
145+
/** Handles carousel slide change event to restart the timer */
146+
@listen(ESLCarouselSlideEvent.AFTER)
147+
protected _onSlideChange(): void {
148+
if (this.active) this.start();
149+
}
87150
}
88151

89152
declare global {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {ESLMixinElement} from '../../../esl-mixin-element/core';
2+
import {attr, boolAttr, listen} from '../../../esl-utils/decorators';
3+
import {afterNextRender} from '../../../esl-utils/async/raf';
4+
import {ESLCarouselAutoplayEvent} from './esl-carousel.autoplay.event';
5+
6+
/**
7+
* A mixin (custom attribute) element that manages the progress animation for the autoplay functionality
8+
* of an ESL Carousel. It listens for `ESLCarouselAutoplayEvent` events and updates
9+
* the animation state and autoplay status accordingly.
10+
* Uses three markers to represent the autoplay progress:
11+
* - `animate` attribute - appears on each cycle of active autoplay;
12+
* drops one frame before the next cycle to activate CSS animation
13+
* - `autoplay-enabled` attribute - indicates whether the autoplay plugin is enabled
14+
* - `--esl-autoplay-timeout` CSS variable - indicates the current autoplay cycle duration
15+
*/
16+
export class ESLCarouselAutoplayProgressMixin extends ESLMixinElement {
17+
public static override is = 'esl-carousel-autoplay-progress';
18+
19+
/**
20+
* {@link ESLTraversingQuery} string to find {@link ESLCarousel} instance with autoplay plugin.
21+
* Searching for the carousel in bounds of the `.esl-carousel-nav-container` element by default.
22+
*/
23+
@attr({
24+
name: 'target',
25+
defaultValue: '::parent(.esl-carousel-nav-container)::find(esl-carousel)'
26+
})
27+
public carousel: string;
28+
29+
/** Attribute to start animation representing autoplay cycle */
30+
@boolAttr() public animate: boolean;
31+
/** Autoplay enabled status marker attribute */
32+
@boolAttr() public autoplayEnabled: boolean;
33+
34+
protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
35+
this.$$on(this._onChange);
36+
}
37+
38+
@listen({
39+
event: ESLCarouselAutoplayEvent.NAME,
40+
target: ($this: ESLCarouselAutoplayProgressMixin) => $this.carousel
41+
})
42+
protected _onChange(e: ESLCarouselAutoplayEvent): void {
43+
this.autoplayEnabled = e.enabled;
44+
this.$host.style.setProperty('--esl-autoplay-timeout', `${e.duration}ms`);
45+
requestAnimationFrame(() => this.animate = false);
46+
e.active && afterNextRender(() => this.animate = true);
47+
}
48+
}

0 commit comments

Comments
 (0)