Skip to content

Commit cbb364e

Browse files
authored
Merge pull request #2524 from exadel-inc/feat/carousel-rework-3
feat(esl-carousel): rework carousel plugins API to use json attr + smart media query
2 parents 466b66c + 834063b commit cbb364e

21 files changed

+255
-145
lines changed

site/src/common/close.less

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
.close {
2+
--icon-color: #000;
3+
24
float: right;
35
font-size: 1.5rem;
46
font-weight: 700;
57
line-height: 1;
6-
color: #000;
7-
text-shadow: 0 1px 0 #fff;
8+
color: var(--icon-color);
9+
text-shadow: 0 1px 0 invert(var(--icon-color));
810
opacity: 0.5;
911

1012
&:hover {
11-
color: #000;
13+
color: var(--icon-color);
1214
text-decoration: none;
1315
}
1416

@@ -18,11 +20,14 @@
1820
}
1921

2022
&.inverse {
21-
color: #fff;
22-
text-shadow: 0 1px 0 #000;
23-
&:hover {
24-
color: #fff;
25-
}
23+
--icon-color: #fff;
24+
}
25+
&-remove:hover {
26+
--icon-color: #fb2020;
27+
}
28+
29+
&-icon::before {
30+
content: '×';
2631
}
2732

2833
.img-container & {

site/views/examples/carousel/default.sample.njk

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ tags: carousel-sample
2121
<li esl-carousel-slide {{ 'active' if loop.first }}>
2222
<div class="card">
2323
<div class="card-image img-container img-container-16-9">
24-
<button type="button" class="close inverse" aria-label="Close" onclick="this.closest('li').remove()">
25-
<span aria-hidden="true">×</span>
26-
</button>
2724
<esl-image lazy
2825
mode="cover"
2926
data-alt="{{ 'Carousel slide ' + loop.index }}"
3027
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
28+
<button type="button" class="close close-icon close-remove inverse"
29+
title="Remove" aria-label="Remove"
30+
onclick="this.closest('li').remove()"></button>
3131
</div>
3232
<div class="card-content p-3">
3333
<h5>Item {{ loop.index }}</h5>

site/views/examples/carousel/multirow.sample.njk

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ tags: carousel-sample
2424
mode="cover"
2525
data-alt="{{ 'Carousel slide ' + loop.index }}"
2626
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
27+
<button type="button" class="close close-icon close-remove inverse"
28+
title="Remove" aria-label="Remove"
29+
onclick="this.closest('li').remove()"></button>
2730
</div>
2831
<div class="card-content p-3">
2932
<h5>Item {{ loop.index }}</h5>

site/views/examples/carousel/single.sample.njk

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ tags: carousel-sample
1010
</button>
1111

1212
<esl-carousel demo-options-target
13-
esl-carousel-touch="@touch => swipe"
13+
esl-carousel-touch="none | @touch => swipe"
1414
loop="true">
1515
<ul esl-carousel-slides>
1616
{% for i in range(0, 4) -%}

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {isMatches} from '../../esl-utils/dom/traversing';
55
import {microtask} from '../../esl-utils/async';
66
import {parseBoolean, sequentialUID} from '../../esl-utils/misc';
77

8+
import {CSSClassUtils} from '../../esl-utils/dom/class';
9+
import {ESLTraversingQuery} from '../../esl-traversing-query/core';
810
import {ESLMediaRuleList} from '../../esl-media-query/core';
911
import {ESLResizeObserverTarget} from '../../esl-event-listener/core';
1012

@@ -21,8 +23,6 @@ import type {
2123
ESLCarouselStaticState,
2224
ESLCarouselConfig
2325
} from './nav/esl-carousel.nav.types';
24-
import {CSSClassUtils} from '../../esl-utils/dom/class';
25-
import {ESLTraversingQuery} from '../../esl-traversing-query/core/esl-traversing-query';
2626

2727
/** {@link ESLCarousel} action params interface */
2828
export interface ESLCarouselActionParams {
@@ -78,22 +78,22 @@ export class ESLCarousel extends ESLBaseElement {
7878
/** Renderer type {@link ESLMediaRuleList} instance */
7979
@memoize()
8080
public get typeRule(): ESLMediaRuleList<string> {
81-
return ESLMediaRuleList.parseTuple(this.media, this.type);
81+
return ESLMediaRuleList.parse(this.type, this.media);
8282
}
8383
/** Loop marker {@link ESLMediaRuleList} instance */
8484
@memoize()
8585
public get loopRule(): ESLMediaRuleList<boolean> {
86-
return ESLMediaRuleList.parseTuple(this.media, this.loop as string, parseBoolean);
86+
return ESLMediaRuleList.parse(this.loop as string, this.media, parseBoolean);
8787
}
8888
/** Count of visible slides {@link ESLMediaRuleList} instance */
8989
@memoize()
9090
public get countRule(): ESLMediaRuleList<number> {
91-
return ESLMediaRuleList.parseTuple(this.media, this.count as string, parseInt);
91+
return ESLMediaRuleList.parse(this.count as string, this.media, parseInt);
9292
}
9393
/** Orientation of the carousel {@link ESLMediaRuleList} instance */
9494
@memoize()
9595
public get verticalRule(): ESLMediaRuleList<boolean> {
96-
return ESLMediaRuleList.parseTuple(this.media, this.vertical as string, parseBoolean);
96+
return ESLMediaRuleList.parse(this.vertical as string, this.media, parseBoolean);
9797
}
9898

9999
/** Returns observed media rules */

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

+22-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
import {ExportNs} from '../../../esl-utils/environment/export-ns';
2-
import {attr, bind, listen, ready} from '../../../esl-utils/decorators';
2+
import {bind, listen, ready} from '../../../esl-utils/decorators';
33

44
import {ESLCarouselPlugin} from '../esl-carousel.plugin';
55
import {ESLCarouselSlideEvent} from '../../core/esl-carousel.events';
66

7+
export interface ESLCarouselAutoplayConfig {
8+
/** Timeout to send next command to the host carousel */
9+
timeout: number;
10+
/** Navigation command to send to the host carousel. Default: 'slide:next' */
11+
command: string;
12+
}
13+
714
/**
815
* {@link ESLCarousel} autoplay (auto-advance) plugin mixin
916
* Automatically switch slides by timeout
1017
*
1118
* @author Alexey Stsefanovich (ala'n)
1219
*/
1320
@ExportNs('Carousel.Autoplay')
14-
export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin {
21+
export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin<ESLCarouselAutoplayConfig> {
1522
public static override is = 'esl-carousel-autoplay';
16-
17-
/** Timeout to send next command to the host carousel */
18-
@attr({defaultValue: '5000', name: ESLCarouselAutoplayMixin.is})
19-
public timeout: number;
20-
21-
/** Navigation command to send to the host carousel. Default: 'slide:next' */
22-
@attr({defaultValue: 'slide:next', name: ESLCarouselAutoplayMixin.is + '-command'})
23-
public command: string;
23+
public static override DEFAULT_CONFIG_KEY = 'timeout';
2424

2525
private _timeout: number | null = null;
2626

27+
public get active(): boolean {
28+
return !!this._timeout;
29+
}
30+
2731
@ready
2832
protected override connectedCallback(): void {
2933
if (super.connectedCallback()) {
@@ -36,33 +40,34 @@ export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin {
3640
this.stop();
3741
}
3842

39-
protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
43+
protected override onConfigChange(): void {
4044
this.start();
4145
}
4246

4347
/** Activates the timer to send commands */
4448
public start(): void {
4549
this.stop();
46-
this._timeout = window.setTimeout(this._onInterval, this.timeout);
50+
this._timeout = window.setTimeout(this._onInterval, this.config.timeout);
4751
}
4852

4953
/** Deactivates the timer to send commands */
5054
public stop(): void {
51-
if (typeof this._timeout === 'number') {
52-
window.clearTimeout(this._timeout);
53-
}
55+
this._timeout && window.clearTimeout(this._timeout);
56+
this._timeout = null;
5457
}
5558

5659
/** Handles next timer interval */
5760
@bind
5861
protected _onInterval(): void {
59-
this.$host?.goTo(this.command);
60-
this._timeout = window.setTimeout(this._onInterval, this.timeout);
62+
this.$host?.goTo(this.config.command);
63+
this._timeout = window.setTimeout(this._onInterval, this.config.timeout);
6164
}
6265

6366
/** Handles auxiliary events to pause/resume timer */
6467
@listen(`mouseout mouseover focusin focusout ${ESLCarouselSlideEvent.AFTER}`)
6568
protected _onInteract(e: Event): void {
69+
// Slide change can only delay the timer, but not start it
70+
if (e.type === ESLCarouselSlideEvent.AFTER && !this.active) return;
6671
if (['mouseover', 'focusin'].includes(e.type)) {
6772
this.stop();
6873
} else {

src/modules/esl-carousel/plugin/esl-carousel.plugin.ts

+53-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,52 @@
1-
import {ESLMixinElement} from '../../esl-mixin-element/ui/esl-mixin-element';
1+
import {ESLMixinElement} from '../../esl-mixin-element/core';
2+
import {bind, ready, memoize} from '../../esl-utils/decorators';
3+
import {evaluate} from '../../esl-utils/misc/format';
4+
import {ESLMediaRuleList} from '../../esl-media-query/core';
25
import {ESLCarousel} from '../core/esl-carousel';
3-
import {ready} from '../../esl-utils/decorators/ready';
46

57
/** Base mixin plugin of {@link ESLCarousel} */
6-
export abstract class ESLCarouselPlugin extends ESLMixinElement {
8+
export abstract class ESLCarouselPlugin<Config> extends ESLMixinElement {
9+
/** Config key to be used if passed non object value */
10+
protected static DEFAULT_CONFIG_KEY: string = '';
11+
712
/** {@link ESLCarousel} host instance */
813
public override $host: ESLCarousel;
914

15+
/** Plugin configuration attribute value */
16+
public get configValue(): string {
17+
const plugin = (this.constructor as typeof ESLCarouselPlugin);
18+
return this.$$attr(plugin.is) || '';
19+
}
20+
public set configValue(value: string) {
21+
const plugin = (this.constructor as typeof ESLCarouselPlugin);
22+
this.$$attr(plugin.is, value);
23+
}
24+
25+
/** Plugin configuration query */
26+
@memoize()
27+
public get configQuery(): ESLMediaRuleList<Config | null> {
28+
return ESLMediaRuleList.parse(this.configValue, this.$host.media, this.parseConfig);
29+
}
30+
31+
/** Active plugin configuration object */
32+
public get config(): Config {
33+
return this.configQuery.value || {} as Config;
34+
}
35+
36+
/**
37+
* Parses plugin media query value term to the config object.
38+
* Provides the capability to pass a config a stringified non-strict JSON or as a string (mapped to single option configuration).
39+
*
40+
* Uses {@link ESLCarouselPlugin.DEFAULT_CONFIG_KEY} to map string value to the config object.
41+
*/
42+
@bind
43+
protected parseConfig(value: string): Config | null {
44+
if (!value) return null;
45+
if (value.trim().startsWith('{')) return evaluate(value, {});
46+
const {DEFAULT_CONFIG_KEY} = (this.constructor as typeof ESLCarouselPlugin);
47+
return {[DEFAULT_CONFIG_KEY]: value} as Config;
48+
}
49+
1050
@ready
1151
protected override connectedCallback(): boolean | void {
1252
const {$host} = this;
@@ -21,6 +61,16 @@ export abstract class ESLCarouselPlugin extends ESLMixinElement {
2161
}
2262
}
2363

64+
protected override attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
65+
if (attrName === (this.constructor as typeof ESLCarouselPlugin).is) {
66+
memoize.clear(this, 'configQuery');
67+
this.onConfigChange();
68+
}
69+
}
70+
71+
/** Callback to be executed on plugin configuration query change (attribute change) */
72+
protected onConfigChange(): void {}
73+
2474
/** Register mixin-plugin in ESLMixinRegistry */
2575
public static override register(): void {
2676
ESLCarousel.registered.then(() => super.register());

src/modules/esl-carousel/plugin/keyboard/esl-carousel.keyboard.mixin.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import {ExportNs} from '../../../esl-utils/environment/export-ns';
22

33
import {ESLCarouselPlugin} from '../esl-carousel.plugin';
4-
import {attr, listen} from '../../../esl-utils/decorators';
4+
import {listen} from '../../../esl-utils/decorators';
55
import {ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP} from '../../../esl-utils/dom/keys';
66

7+
export interface ESLCarouselKeyboardConfig {
8+
/** Prefix for command to request next/prev navigation */
9+
command: 'slide' | 'group' | 'none';
10+
}
11+
712
/**
813
* {@link ESLCarousel} Keyboard arrow support
914
*
1015
* @author Alexey Stsefanovich (ala'n)
1116
*/
1217
@ExportNs('Carousel.Keyboard')
13-
export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin {
18+
export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin<ESLCarouselKeyboardConfig> {
1419
public static override is = 'esl-carousel-keyboard';
15-
16-
/** Prefix to request next/prev navigation */
17-
@attr({name: ESLCarouselKeyboardMixin.is}) public type: 'slide' | 'group';
20+
public static override DEFAULT_CONFIG_KEY = 'command';
1821

1922
/** @returns key code for next navigation */
2023
protected get nextKey(): string {
@@ -28,9 +31,9 @@ export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin {
2831
/** Handles `keydown` event */
2932
@listen('keydown')
3033
protected _onKeydown(event: KeyboardEvent): void {
31-
if (!this.$host || this.$host.animating) return;
32-
if (event.key === this.nextKey) this.$host.goTo(`${this.type || 'slide'}:next`);
33-
if (event.key === this.prevKey) this.$host.goTo(`${this.type || 'slide'}:prev`);
34+
if (!this.$host || this.$host.animating || this.config.command === 'none') return;
35+
if (event.key === this.nextKey) this.$host.goTo(`${this.config.command || 'slide'}:next`);
36+
if (event.key === this.prevKey) this.$host.goTo(`${this.config.command || 'slide'}:prev`);
3437
}
3538
}
3639

src/modules/esl-carousel/plugin/nav/esl-carousel.nav.arrows.less

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
// variable to make clickable area larger
55
--esl-carousel-arrow-padding: 0px;
66
--esl-carousel-arrow-size: 40px;
7-
--esl-carousel-arrow-offset: calc(
8-
(var(--esl-carousel-arrow-size) + var(--esl-carousel-arrow-padding) + var(--esl-carousel-side-space)) * -1
9-
);
7+
/* stylelint-disable-next-line */
8+
--esl-carousel-arrow-offset: calc((var(--esl-carousel-arrow-size) + var(--esl-carousel-arrow-padding) + var(--esl-carousel-side-space)) * -1);
109
--esl-carousel-arrow-bg: grey;
1110

1211
.esl-carousel-arrow {

src/modules/esl-carousel/plugin/relation/esl-carousel.relation.mixin.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
import {ExportNs} from '../../../esl-utils/environment/export-ns';
2-
import {attr, listen, memoize} from '../../../esl-utils/decorators';
3-
import {parseBoolean} from '../../../esl-utils/misc/format';
2+
import {listen, memoize} from '../../../esl-utils/decorators';
43
import {ESLTraversingQuery} from '../../../esl-traversing-query/core';
54

65
import {ESLCarousel} from '../../core/esl-carousel';
76
import {ESLCarouselPlugin} from '../esl-carousel.plugin';
87
import {ESLCarouselSlideEvent} from '../../core/esl-carousel.events';
98

9+
export interface ESLCarouselRelateToConfig {
10+
/** Target carousel selector */
11+
target: string;
12+
/** Proactive mode to relate to the target immediately */
13+
proactive: boolean;
14+
}
15+
1016
/**
1117
* Slide Carousel Link plugin mixin to bind carousel positions
1218
*/
1319
@ExportNs('Carousel.RelateTo')
14-
export class ESLCarouselRelateToMixin extends ESLCarouselPlugin {
20+
export class ESLCarouselRelateToMixin extends ESLCarouselPlugin<ESLCarouselRelateToConfig> {
1521
public static override is = 'esl-carousel-relate-to';
16-
17-
@attr({name: ESLCarouselRelateToMixin.is})
18-
public target: string;
19-
20-
@attr({name: ESLCarouselRelateToMixin.is + '-proactive', parser: parseBoolean})
21-
public proactive: boolean;
22+
public static override DEFAULT_CONFIG_KEY = 'target';
2223

2324
protected get event(): string {
24-
return this.proactive ? ESLCarouselSlideEvent.BEFORE : ESLCarouselSlideEvent.AFTER;
25+
return this.config.proactive ? ESLCarouselSlideEvent.BEFORE : ESLCarouselSlideEvent.AFTER;
2526
}
2627

2728
/** @returns ESLCarousel target to share state changes */
2829
@memoize()
2930
public get $target(): ESLCarousel | null {
30-
const $target = ESLTraversingQuery.first(this.target);
31+
const $target = ESLTraversingQuery.first(this.config.target);
3132
return ($target instanceof ESLCarousel) ? $target : null;
3233
}
3334

34-
protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void {
35+
protected override onConfigChange(): void {
36+
// Listener event change is not handled by resubscribe automatically
3537
this.$$off(this._onSlideChange);
3638
memoize.clear(this, '$target');
3739
this.$$on(this._onSlideChange);

0 commit comments

Comments
 (0)