Skip to content

Commit

Permalink
Merge pull request #2524 from exadel-inc/feat/carousel-rework-3
Browse files Browse the repository at this point in the history
feat(esl-carousel): rework carousel plugins API to use json attr + smart media query
  • Loading branch information
ala-n committed Jul 22, 2024
2 parents 466b66c + 834063b commit cbb364e
Show file tree
Hide file tree
Showing 21 changed files with 255 additions and 145 deletions.
21 changes: 13 additions & 8 deletions site/src/common/close.less
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
.close {
--icon-color: #000;

float: right;
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
color: var(--icon-color);
text-shadow: 0 1px 0 invert(var(--icon-color));
opacity: 0.5;

&:hover {
color: #000;
color: var(--icon-color);
text-decoration: none;
}

Expand All @@ -18,11 +20,14 @@
}

&.inverse {
color: #fff;
text-shadow: 0 1px 0 #000;
&:hover {
color: #fff;
}
--icon-color: #fff;
}
&-remove:hover {
--icon-color: #fb2020;
}

&-icon::before {
content: '×';
}

.img-container & {
Expand Down
6 changes: 3 additions & 3 deletions site/views/examples/carousel/default.sample.njk
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ tags: carousel-sample
<li esl-carousel-slide {{ 'active' if loop.first }}>
<div class="card">
<div class="card-image img-container img-container-16-9">
<button type="button" class="close inverse" aria-label="Close" onclick="this.closest('li').remove()">
<span aria-hidden="true">×</span>
</button>
<esl-image lazy
mode="cover"
data-alt="{{ 'Carousel slide ' + loop.index }}"
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
<button type="button" class="close close-icon close-remove inverse"
title="Remove" aria-label="Remove"
onclick="this.closest('li').remove()"></button>
</div>
<div class="card-content p-3">
<h5>Item {{ loop.index }}</h5>
Expand Down
3 changes: 3 additions & 0 deletions site/views/examples/carousel/multirow.sample.njk
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ tags: carousel-sample
mode="cover"
data-alt="{{ 'Carousel slide ' + loop.index }}"
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
<button type="button" class="close close-icon close-remove inverse"
title="Remove" aria-label="Remove"
onclick="this.closest('li').remove()"></button>
</div>
<div class="card-content p-3">
<h5>Item {{ loop.index }}</h5>
Expand Down
2 changes: 1 addition & 1 deletion site/views/examples/carousel/single.sample.njk
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ tags: carousel-sample
</button>

<esl-carousel demo-options-target
esl-carousel-touch="@touch => swipe"
esl-carousel-touch="none | @touch => swipe"
loop="true">
<ul esl-carousel-slides>
{% for i in range(0, 4) -%}
Expand Down
12 changes: 6 additions & 6 deletions src/modules/esl-carousel/core/esl-carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {isMatches} from '../../esl-utils/dom/traversing';
import {microtask} from '../../esl-utils/async';
import {parseBoolean, 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';

Expand All @@ -21,8 +23,6 @@ import type {
ESLCarouselStaticState,
ESLCarouselConfig
} from './nav/esl-carousel.nav.types';
import {CSSClassUtils} from '../../esl-utils/dom/class';
import {ESLTraversingQuery} from '../../esl-traversing-query/core/esl-traversing-query';

/** {@link ESLCarousel} action params interface */
export interface ESLCarouselActionParams {
Expand Down Expand Up @@ -78,22 +78,22 @@ export class ESLCarousel extends ESLBaseElement {
/** Renderer type {@link ESLMediaRuleList} instance */
@memoize()
public get typeRule(): ESLMediaRuleList<string> {
return ESLMediaRuleList.parseTuple(this.media, this.type);
return ESLMediaRuleList.parse(this.type, this.media);
}
/** Loop marker {@link ESLMediaRuleList} instance */
@memoize()
public get loopRule(): ESLMediaRuleList<boolean> {
return ESLMediaRuleList.parseTuple(this.media, this.loop as string, parseBoolean);
return ESLMediaRuleList.parse(this.loop as string, this.media, parseBoolean);
}
/** Count of visible slides {@link ESLMediaRuleList} instance */
@memoize()
public get countRule(): ESLMediaRuleList<number> {
return ESLMediaRuleList.parseTuple(this.media, this.count as string, parseInt);
return ESLMediaRuleList.parse(this.count as string, this.media, parseInt);
}
/** Orientation of the carousel {@link ESLMediaRuleList} instance */
@memoize()
public get verticalRule(): ESLMediaRuleList<boolean> {
return ESLMediaRuleList.parseTuple(this.media, this.vertical as string, parseBoolean);
return ESLMediaRuleList.parse(this.vertical as string, this.media, parseBoolean);
}

/** Returns observed media rules */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import {ExportNs} from '../../../esl-utils/environment/export-ns';
import {attr, bind, listen, ready} from '../../../esl-utils/decorators';
import {bind, listen, ready} from '../../../esl-utils/decorators';

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

export interface ESLCarouselAutoplayConfig {
/** Timeout to send next command to the host carousel */
timeout: number;
/** Navigation command to send to the host carousel. Default: 'slide:next' */
command: string;
}

/**
* {@link ESLCarousel} autoplay (auto-advance) plugin mixin
* Automatically switch slides by timeout
*
* @author Alexey Stsefanovich (ala'n)
*/
@ExportNs('Carousel.Autoplay')
export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin {
export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin<ESLCarouselAutoplayConfig> {
public static override is = 'esl-carousel-autoplay';

/** Timeout to send next command to the host carousel */
@attr({defaultValue: '5000', name: ESLCarouselAutoplayMixin.is})
public timeout: number;

/** Navigation command to send to the host carousel. Default: 'slide:next' */
@attr({defaultValue: 'slide:next', name: ESLCarouselAutoplayMixin.is + '-command'})
public command: string;
public static override DEFAULT_CONFIG_KEY = 'timeout';

private _timeout: number | null = null;

public get active(): boolean {
return !!this._timeout;
}

@ready
protected override connectedCallback(): void {
if (super.connectedCallback()) {
Expand All @@ -36,33 +40,34 @@ export class ESLCarouselAutoplayMixin extends ESLCarouselPlugin {
this.stop();
}

protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
protected override onConfigChange(): void {
this.start();
}

/** Activates the timer to send commands */
public start(): void {
this.stop();
this._timeout = window.setTimeout(this._onInterval, this.timeout);
this._timeout = window.setTimeout(this._onInterval, this.config.timeout);
}

/** Deactivates the timer to send commands */
public stop(): void {
if (typeof this._timeout === 'number') {
window.clearTimeout(this._timeout);
}
this._timeout && window.clearTimeout(this._timeout);
this._timeout = null;
}

/** Handles next timer interval */
@bind
protected _onInterval(): void {
this.$host?.goTo(this.command);
this._timeout = window.setTimeout(this._onInterval, this.timeout);
this.$host?.goTo(this.config.command);
this._timeout = window.setTimeout(this._onInterval, this.config.timeout);
}

/** Handles auxiliary events to pause/resume timer */
@listen(`mouseout mouseover focusin focusout ${ESLCarouselSlideEvent.AFTER}`)
protected _onInteract(e: Event): void {
// Slide change can only delay the timer, but not start it
if (e.type === ESLCarouselSlideEvent.AFTER && !this.active) return;
if (['mouseover', 'focusin'].includes(e.type)) {
this.stop();
} else {
Expand Down
56 changes: 53 additions & 3 deletions src/modules/esl-carousel/plugin/esl-carousel.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import {ESLMixinElement} from '../../esl-mixin-element/ui/esl-mixin-element';
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {bind, ready, memoize} from '../../esl-utils/decorators';
import {evaluate} from '../../esl-utils/misc/format';
import {ESLMediaRuleList} from '../../esl-media-query/core';
import {ESLCarousel} from '../core/esl-carousel';
import {ready} from '../../esl-utils/decorators/ready';

/** Base mixin plugin of {@link ESLCarousel} */
export abstract class ESLCarouselPlugin extends ESLMixinElement {
export abstract class ESLCarouselPlugin<Config> extends ESLMixinElement {
/** Config key to be used if passed non object value */
protected static DEFAULT_CONFIG_KEY: string = '';

/** {@link ESLCarousel} host instance */
public override $host: ESLCarousel;

/** Plugin configuration attribute value */
public get configValue(): string {
const plugin = (this.constructor as typeof ESLCarouselPlugin);
return this.$$attr(plugin.is) || '';
}
public set configValue(value: string) {
const plugin = (this.constructor as typeof ESLCarouselPlugin);
this.$$attr(plugin.is, value);
}

/** Plugin configuration query */
@memoize()
public get configQuery(): ESLMediaRuleList<Config | null> {
return ESLMediaRuleList.parse(this.configValue, this.$host.media, this.parseConfig);
}

/** Active plugin configuration object */
public get config(): Config {
return this.configQuery.value || {} as Config;
}

/**
* Parses plugin media query value term to the config object.
* Provides the capability to pass a config a stringified non-strict JSON or as a string (mapped to single option configuration).
*
* Uses {@link ESLCarouselPlugin.DEFAULT_CONFIG_KEY} to map string value to the config object.
*/
@bind
protected parseConfig(value: string): Config | null {
if (!value) return null;
if (value.trim().startsWith('{')) return evaluate(value, {});
const {DEFAULT_CONFIG_KEY} = (this.constructor as typeof ESLCarouselPlugin);
return {[DEFAULT_CONFIG_KEY]: value} as Config;
}

@ready
protected override connectedCallback(): boolean | void {
const {$host} = this;
Expand All @@ -21,6 +61,16 @@ export abstract class ESLCarouselPlugin extends ESLMixinElement {
}
}

protected override attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
if (attrName === (this.constructor as typeof ESLCarouselPlugin).is) {
memoize.clear(this, 'configQuery');
this.onConfigChange();
}
}

/** Callback to be executed on plugin configuration query change (attribute change) */
protected onConfigChange(): void {}

/** Register mixin-plugin in ESLMixinRegistry */
public static override register(): void {
ESLCarousel.registered.then(() => super.register());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import {ExportNs} from '../../../esl-utils/environment/export-ns';

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

export interface ESLCarouselKeyboardConfig {
/** Prefix for command to request next/prev navigation */
command: 'slide' | 'group' | 'none';
}

/**
* {@link ESLCarousel} Keyboard arrow support
*
* @author Alexey Stsefanovich (ala'n)
*/
@ExportNs('Carousel.Keyboard')
export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin {
export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin<ESLCarouselKeyboardConfig> {
public static override is = 'esl-carousel-keyboard';

/** Prefix to request next/prev navigation */
@attr({name: ESLCarouselKeyboardMixin.is}) public type: 'slide' | 'group';
public static override DEFAULT_CONFIG_KEY = 'command';

/** @returns key code for next navigation */
protected get nextKey(): string {
Expand All @@ -28,9 +31,9 @@ export class ESLCarouselKeyboardMixin extends ESLCarouselPlugin {
/** Handles `keydown` event */
@listen('keydown')
protected _onKeydown(event: KeyboardEvent): void {
if (!this.$host || this.$host.animating) return;
if (event.key === this.nextKey) this.$host.goTo(`${this.type || 'slide'}:next`);
if (event.key === this.prevKey) this.$host.goTo(`${this.type || 'slide'}:prev`);
if (!this.$host || this.$host.animating || this.config.command === 'none') return;
if (event.key === this.nextKey) this.$host.goTo(`${this.config.command || 'slide'}:next`);
if (event.key === this.prevKey) this.$host.goTo(`${this.config.command || 'slide'}:prev`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
// variable to make clickable area larger
--esl-carousel-arrow-padding: 0px;
--esl-carousel-arrow-size: 40px;
--esl-carousel-arrow-offset: calc(
(var(--esl-carousel-arrow-size) + var(--esl-carousel-arrow-padding) + var(--esl-carousel-side-space)) * -1
);
/* stylelint-disable-next-line */
--esl-carousel-arrow-offset: calc((var(--esl-carousel-arrow-size) + var(--esl-carousel-arrow-padding) + var(--esl-carousel-side-space)) * -1);
--esl-carousel-arrow-bg: grey;

.esl-carousel-arrow {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
import {ExportNs} from '../../../esl-utils/environment/export-ns';
import {attr, listen, memoize} from '../../../esl-utils/decorators';
import {parseBoolean} from '../../../esl-utils/misc/format';
import {listen, memoize} from '../../../esl-utils/decorators';
import {ESLTraversingQuery} from '../../../esl-traversing-query/core';

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

export interface ESLCarouselRelateToConfig {
/** Target carousel selector */
target: string;
/** Proactive mode to relate to the target immediately */
proactive: boolean;
}

/**
* Slide Carousel Link plugin mixin to bind carousel positions
*/
@ExportNs('Carousel.RelateTo')
export class ESLCarouselRelateToMixin extends ESLCarouselPlugin {
export class ESLCarouselRelateToMixin extends ESLCarouselPlugin<ESLCarouselRelateToConfig> {
public static override is = 'esl-carousel-relate-to';

@attr({name: ESLCarouselRelateToMixin.is})
public target: string;

@attr({name: ESLCarouselRelateToMixin.is + '-proactive', parser: parseBoolean})
public proactive: boolean;
public static override DEFAULT_CONFIG_KEY = 'target';

protected get event(): string {
return this.proactive ? ESLCarouselSlideEvent.BEFORE : ESLCarouselSlideEvent.AFTER;
return this.config.proactive ? ESLCarouselSlideEvent.BEFORE : ESLCarouselSlideEvent.AFTER;
}

/** @returns ESLCarousel target to share state changes */
@memoize()
public get $target(): ESLCarousel | null {
const $target = ESLTraversingQuery.first(this.target);
const $target = ESLTraversingQuery.first(this.config.target);
return ($target instanceof ESLCarousel) ? $target : null;
}

protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void {
protected override onConfigChange(): void {
// Listener event change is not handled by resubscribe automatically
this.$$off(this._onSlideChange);
memoize.clear(this, '$target');
this.$$on(this._onSlideChange);
Expand Down
Loading

0 comments on commit cbb364e

Please sign in to comment.