{% for i in range(0, 4) -%}
diff --git a/src/modules/esl-carousel/README.md b/src/modules/esl-carousel/README.md
index 05a5e0590..f300866fa 100644
--- a/src/modules/esl-carousel/README.md
+++ b/src/modules/esl-carousel/README.md
@@ -31,3 +31,16 @@ The elements are interrelated and don't make sense on their own. This is because
### ESLCarouselSlide Attributes | Properties:
- `active` (boolean) - an active state marker
+
+### ESLCarouselTouchMixin
+**ESLCarouselTouchMixin** is an ESL mixin attribute `esl-carousel-touch` that provide for `ESLCarousel` user support of `drag` and `swipe` events handling.
+By default, `drag` event is specified, but there is possibility to declare other configuration.
+
+#### `ESLCarouselTouchMixin` Attributes | Properties:
+- `esl-carousel-touch` - attribute defined by [ESLMediaRuleList](../esl-media-query/core/esl-media-rule-list.ts) to describe how touch events will be applied.
+- `esl-carousel-swipe-mode` (`group` by default) - attribute to precise supportable type in case swipe event is allowed (`group` or `slide`).
+
+#### Use cases
+```html
+
+```
diff --git a/src/modules/esl-carousel/plugin/touch/esl-carousel.touch.mixin.ts b/src/modules/esl-carousel/plugin/touch/esl-carousel.touch.mixin.ts
index e89e79dfc..fdd9d460c 100644
--- a/src/modules/esl-carousel/plugin/touch/esl-carousel.touch.mixin.ts
+++ b/src/modules/esl-carousel/plugin/touch/esl-carousel.touch.mixin.ts
@@ -1,44 +1,73 @@
import {ExportNs} from '../../../esl-utils/environment/export-ns';
-import {attr, prop, listen} from '../../../esl-utils/decorators';
-import {getTouchPoint, isMouseEvent, isTouchEvent} from '../../../esl-utils/dom';
-import {ESLMediaQuery} from '../../../esl-media-query/core';
+import {attr, prop, listen, memoize} from '../../../esl-utils/decorators';
+import {
+ ESLSwipeGestureEvent,
+ ESLSwipeGestureTarget,
+ getTouchPoint,
+ isMouseEvent,
+ isTouchEvent
+} from '../../../esl-utils/dom';
+import {buildEnumParser} from '../../../esl-utils/misc/enum';
+import {ESLMediaRuleList} from '../../../esl-media-query/core';
import {ESLCarouselPlugin} from '../esl-carousel.plugin';
import type {Point} from '../../../esl-utils/dom';
+export type TouchType = 'drag' | 'swipe' | 'none';
+const toTouchType: (str: string) => TouchType = buildEnumParser('none', 'drag', 'swipe');
+
/**
* {@link ESLCarousel} Touch handler mixin
*
* Usage:
* ```
- *
+ *
*
- *
+ *
* ```
*/
@ExportNs('Carousel.Touch')
export class ESLCarouselTouchMixin extends ESLCarouselPlugin {
public static override is = 'esl-carousel-touch';
- /** Min distance in pixels to activate drugging mode */
+ public static readonly DRAG_TYPE = 'drag';
+ public static readonly SWIPE_TYPE = 'swipe';
+
+ /** Min distance in pixels to activate dragging mode */
@prop(5) public tolerance: number;
- /** {@link ESLMediaQuery} condition to have touch support active */
- @attr({name: ESLCarouselTouchMixin.is}) public media: string;
+ /** Condition to have drag and swipe support active. Supports {@link ESLMediaRuleList} */
+ @attr({name: ESLCarouselTouchMixin.is}) public type: string;
+
+ /** Defines type of swipe */
+ @attr({name: 'esl-carousel-swipe-mode', defaultValue: 'group'}) public swipeType: 'group' | 'slide';
+
+ /** @returns rule {@link ESLMediaRuleList} for touch types */
+ @memoize()
+ public get typeRule(): ESLMediaRuleList {
+ return ESLMediaRuleList.parse(this.type || ESLCarouselTouchMixin.DRAG_TYPE, toTouchType);
+ }
/** Point to start from */
protected startPoint: Point = {x: 0, y: 0};
/** Marker whether touch event is started */
protected isTouchStarted = false;
+ protected override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
+ if (name === ESLCarouselTouchMixin.is) {
+ this.$$off(this._onTypeChanged);
+ memoize.clear(this, 'typeRule');
+ this.$$on(this._onTypeChanged);
+ this._onTypeChanged();
+ }
+ }
+
/** @returns marker whether the event should be ignored. */
protected isIgnoredEvent(event: TouchEvent | PointerEvent | MouseEvent): boolean | undefined {
// No nav required
if (this.$host.size <= this.$host.config.count) return true;
- // Check for media condition
- if (!ESLMediaQuery.for(this.media).matches) return true;
// Multi-touch gesture
if (isTouchEvent(event) && event.touches.length !== 1) return true;
// Non-primary mouse button initiate drug event
@@ -47,8 +76,17 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin {
return !!(event.target as HTMLElement).closest('input, textarea, [editable]');
}
+ /** @returns offset between start point and passed event point */
+ protected getOffset(event: TouchEvent | PointerEvent | MouseEvent): number {
+ const point = getTouchPoint(event);
+ return this.$host.config.vertical ? point.y - this.startPoint.y : point.x - this.startPoint.x;
+ }
+
/** Handles `mousedown` / `touchstart` event to manage thumb drag start and scroll clicks */
- @listen('mousedown touchstart')
+ @listen({
+ event: 'mousedown touchstart',
+ condition: (that: ESLCarouselTouchMixin) => that.typeRule.value === ESLCarouselTouchMixin.DRAG_TYPE
+ })
protected _onPointerDown(event: MouseEvent | TouchEvent): void {
if (this.isTouchStarted || !this.$host.renderer || this.$host.animating) return;
@@ -66,8 +104,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin {
/** Processes `mousemove` and `touchmove` events. */
protected _onPointerMove(event: TouchEvent | PointerEvent | MouseEvent): void {
if (!this.isTouchStarted) return;
- const point = getTouchPoint(event);
- const offset = point.x - this.startPoint.x;
+ const offset = this.getOffset(event);
if (!this.$host.hasAttribute('dragging')) {
if (Math.abs(offset) < this.tolerance) return;
@@ -89,12 +126,32 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin {
if (this.$$attr('dragging', false) !== null) {
event.preventDefault();
- const point = getTouchPoint(event);
- const offset = point.x - this.startPoint.x;
+ const offset = this.getOffset(event);
// ignore single click
offset !== 0 && this.$host.renderer.commit(offset);
}
}
+
+ /** Handles `swipe` event */
+ @listen({
+ event: ESLSwipeGestureEvent.type,
+ target: ESLSwipeGestureTarget.for,
+ condition: (that: ESLCarouselTouchMixin)=> that.typeRule.value === ESLCarouselTouchMixin.SWIPE_TYPE
+ })
+ protected _onSwipe(e: ESLSwipeGestureEvent): void {
+ if (!this.$host || this.$host.animating) return;
+ if (this.$host.config.vertical !== e.isVertical) return;
+ const direction = (e.direction === 'left' || e.direction === 'up') ? 'next' : 'prev';
+ this.$host?.goTo(`${this.swipeType}:${direction}`);
+ }
+
+ @listen({event: 'change', target: (that: ESLCarouselTouchMixin) => that.typeRule})
+ protected _onTypeChanged(): void {
+ this.$$off(this._onPointerDown);
+ this.$$off(this._onSwipe);
+ this.$$on(this._onPointerDown);
+ this.$$on(this._onSwipe);
+ }
}
declare global {
diff --git a/src/modules/esl-event-listener/core/targets/swipe.target.event.ts b/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
index 96b641fb7..f1fc78fb1 100644
--- a/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
+++ b/src/modules/esl-event-listener/core/targets/swipe.target.event.ts
@@ -44,6 +44,11 @@ export class ESLSwipeGestureEvent extends UIEvent implements ESLSwipeGestureEven
public readonly startEvent: PointerEvent;
public readonly duration: number;
+ /** @returns whether swipe direction is vertical or not */
+ public get isVertical(): boolean {
+ return this.direction === 'up' || this.direction === 'down';
+ }
+
protected constructor(target: Element, swipeInfo: ESLSwipeGestureEventInfo) {
super(ESLSwipeGestureEvent.type, {bubbles: false, cancelable: true});
overrideEvent(this, 'target', target);
diff --git a/src/modules/esl-utils/misc/enum.ts b/src/modules/esl-utils/misc/enum.ts
new file mode 100644
index 000000000..280ebd555
--- /dev/null
+++ b/src/modules/esl-utils/misc/enum.ts
@@ -0,0 +1,33 @@
+type EnumParser = (str: string) => V;
+/** Parses string to 2-value union type */
+export function buildEnumParser<
+ T0 extends string,
+ T1 extends string
+>(def: T0, v1: T1): EnumParser;
+/** Parses string to 3-value union type */
+export function buildEnumParser<
+ T0 extends string,
+ T1 extends string,
+ T2 extends string
+>(def: T0, v1: T1, v2: T2): EnumParser;
+/** Parses string to 4-value union type */
+export function buildEnumParser<
+ T0 extends string,
+ T1 extends string,
+ T2 extends string,
+ T3 extends string
+>(def: T0, v1: T1, v2: T2, v3: T3): EnumParser;
+/** Parses string to 5-value union type */
+export function buildEnumParser<
+ T0 extends string,
+ T1 extends string,
+ T2 extends string,
+ T3 extends string,
+ T4 extends string
+>(def: T0, v1: T1, v2: T2, v3: T3, v4: T4): EnumParser;
+export function buildEnumParser(def: string, ...values: string[]): EnumParser {
+ return (str: string): string => {
+ const value = str.trim().toLowerCase();
+ return values.includes(value) ? value : def;
+ };
+}