diff --git a/site/views/draft/multi-carousel.njk b/site/views/draft/multi-carousel.njk index 06225929e..26a2bca81 100644 --- a/site/views/draft/multi-carousel.njk +++ b/site/views/draft/multi-carousel.njk @@ -177,7 +177,7 @@ icon: examples/carousel.svg diff --git a/site/views/examples/carousel.njk b/site/views/examples/carousel.njk index 9c8e5ec2a..551bd9324 100644 --- a/site/views/examples/carousel.njk +++ b/site/views/examples/carousel.njk @@ -54,7 +54,7 @@ icon: examples/carousel.svg Previous Slide - @@ -84,7 +84,7 @@ icon: examples/carousel.svg Previous Slide -
@@ -113,7 +113,7 @@ icon: examples/carousel.svg Previous Slide - +
{% 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; + }; +}