From 019715c7b3e520f8e7abf7025835cfdddf50f6db Mon Sep 17 00:00:00 2001 From: alesun Date: Wed, 24 Jan 2024 18:58:45 +0100 Subject: [PATCH 1/6] feat(esl-event-listener): add `isVertical` property to `ESLSwipeGestureEvent` --- .../esl-event-listener/core/targets/swipe.target.event.ts | 5 +++++ 1 file changed, 5 insertions(+) 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); From 480bac1f7a7f74d85b03c31aa15bb16a16912c49 Mon Sep 17 00:00:00 2001 From: alesun Date: Wed, 24 Jan 2024 19:01:53 +0100 Subject: [PATCH 2/6] feat(esl-carousel): `ESLCarouselTouchMixin` plugin is ready for usage with both: drag and touch support --- site/views/draft/multi-carousel.njk | 2 +- site/views/examples/carousel.njk | 6 +- src/modules/esl-carousel/README.md | 13 ++++ .../plugin/touch/esl-carousel.touch.mixin.ts | 72 +++++++++++++++---- 4 files changed, 76 insertions(+), 17 deletions(-) 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..031616f58 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..9f3ef5e7c 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,32 +1,56 @@ 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 {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 = (value: string): TouchType => { + value = value.toLowerCase(); + if (value === ESLCarouselTouchMixin.DRAG_TYPE || value === ESLCarouselTouchMixin.SWIPE_TYPE) return value; + return 'none'; +}; /** * {@link ESLCarousel} Touch handler mixin * * Usage: * ``` - * + * * - * + * * ``` */ @ExportNs('Carousel.Touch') export class ESLCarouselTouchMixin extends ESLCarouselPlugin { public static override is = 'esl-carousel-touch'; + public static readonly DRAG_TYPE = 'drag'; + public static readonly SWIPE_TYPE = 'swipe'; - /** Min distance in pixels to activate drugging mode */ + /** 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, defaultValue: 'drag'}) 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, toTouchType); + } /** Point to start from */ protected startPoint: Point = {x: 0, y: 0}; @@ -37,8 +61,6 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { 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 @@ -48,7 +70,10 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { } /** 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; @@ -67,7 +92,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { 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.$host.config.vertical ? point.y - this.startPoint.y : point.x - this.startPoint.x; if (!this.$host.hasAttribute('dragging')) { if (Math.abs(offset) < this.tolerance) return; @@ -90,11 +115,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.$host.config.vertical ? point.y - this.startPoint.y : point.x - this.startPoint.x; // 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 { From 92d77b5f0b75e0c94fe05ca540411d795e5bd974 Mon Sep 17 00:00:00 2001 From: Anastasiya Lesun <72765981+NastaLeo@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:23:41 +0100 Subject: [PATCH 3/6] style(esl-carousel): change examples Co-authored-by: ala'n (Alexey Stsefanovich) --- site/views/examples/carousel.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/views/examples/carousel.njk b/site/views/examples/carousel.njk index 031616f58..be9fd1f53 100644 --- a/site/views/examples/carousel.njk +++ b/site/views/examples/carousel.njk @@ -84,7 +84,7 @@ icon: examples/carousel.svg Previous Slide -
From 3199e9d02523308df00492fbff187f6578acf066 Mon Sep 17 00:00:00 2001 From: Anastasiya Lesun <72765981+NastaLeo@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:23:55 +0100 Subject: [PATCH 4/6] style(esl-carousel): change examples Co-authored-by: ala'n (Alexey Stsefanovich) --- site/views/examples/carousel.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/views/examples/carousel.njk b/site/views/examples/carousel.njk index be9fd1f53..9d5d06ca9 100644 --- a/site/views/examples/carousel.njk +++ b/site/views/examples/carousel.njk @@ -113,7 +113,7 @@ icon: examples/carousel.svg Previous Slide - +
{% for i in range(0, 4) -%} From 747213ebb168acd388dc1a37182d5a9b5dabb0b3 Mon Sep 17 00:00:00 2001 From: Anastasiya Lesun <72765981+NastaLeo@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:24:09 +0100 Subject: [PATCH 5/6] style(esl-carousel): change examples Co-authored-by: ala'n (Alexey Stsefanovich) --- .../esl-carousel/plugin/touch/esl-carousel.touch.mixin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9f3ef5e7c..c739fff19 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 @@ -28,7 +28,7 @@ const toTouchType = (value: string): TouchType => { * ``` * * - * + * * ``` */ @ExportNs('Carousel.Touch') From 4a86af54213a2ec23ca58ae3a19faf597a25de86 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 26 Jan 2024 03:41:50 +0100 Subject: [PATCH 6/6] refactor(esl-carousel): fix defaults and apply review notes --- site/views/examples/carousel.njk | 2 +- .../plugin/touch/esl-carousel.touch.mixin.ts | 37 ++++++++++++------- src/modules/esl-utils/misc/enum.ts | 33 +++++++++++++++++ 3 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/modules/esl-utils/misc/enum.ts diff --git a/site/views/examples/carousel.njk b/site/views/examples/carousel.njk index 9d5d06ca9..551bd9324 100644 --- a/site/views/examples/carousel.njk +++ b/site/views/examples/carousel.njk @@ -113,7 +113,7 @@ icon: examples/carousel.svg Previous Slide - +
{% for i in range(0, 4) -%} 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 c739fff19..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 @@ -7,19 +7,16 @@ import { 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 = (value: string): TouchType => { - value = value.toLowerCase(); - if (value === ESLCarouselTouchMixin.DRAG_TYPE || value === ESLCarouselTouchMixin.SWIPE_TYPE) return value; - return 'none'; -}; +export type TouchType = 'drag' | 'swipe' | 'none'; +const toTouchType: (str: string) => TouchType = buildEnumParser('none', 'drag', 'swipe'); /** * {@link ESLCarousel} Touch handler mixin @@ -34,6 +31,7 @@ const toTouchType = (value: string): TouchType => { @ExportNs('Carousel.Touch') export class ESLCarouselTouchMixin extends ESLCarouselPlugin { public static override is = 'esl-carousel-touch'; + public static readonly DRAG_TYPE = 'drag'; public static readonly SWIPE_TYPE = 'swipe'; @@ -41,7 +39,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { @prop(5) public tolerance: number; /** Condition to have drag and swipe support active. Supports {@link ESLMediaRuleList} */ - @attr({name: ESLCarouselTouchMixin.is, defaultValue: 'drag'}) public type: string; + @attr({name: ESLCarouselTouchMixin.is}) public type: string; /** Defines type of swipe */ @attr({name: 'esl-carousel-swipe-mode', defaultValue: 'group'}) public swipeType: 'group' | 'slide'; @@ -49,7 +47,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { /** @returns rule {@link ESLMediaRuleList} for touch types */ @memoize() public get typeRule(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.type, toTouchType); + return ESLMediaRuleList.parse(this.type || ESLCarouselTouchMixin.DRAG_TYPE, toTouchType); } /** Point to start from */ @@ -57,6 +55,15 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { /** 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 @@ -69,6 +76,12 @@ 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({ event: 'mousedown touchstart', @@ -91,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 = this.$host.config.vertical ? point.y - this.startPoint.y : point.x - this.startPoint.x; + const offset = this.getOffset(event); if (!this.$host.hasAttribute('dragging')) { if (Math.abs(offset) < this.tolerance) return; @@ -114,8 +126,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { if (this.$$attr('dragging', false) !== null) { event.preventDefault(); - const point = getTouchPoint(event); - const offset = this.$host.config.vertical ? point.y - this.startPoint.y : point.x - this.startPoint.x; + const offset = this.getOffset(event); // ignore single click offset !== 0 && this.$host.renderer.commit(offset); } @@ -130,7 +141,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin { 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'; + const direction = (e.direction === 'left' || e.direction === 'up') ? 'next' : 'prev'; this.$host?.goTo(`${this.swipeType}:${direction}`); } 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; + }; +}