-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(esl-event-listener): add
SwipeEventTarget
to subscribe swipe
…
… events using `ESLEventListener` Closes: #1809
- Loading branch information
1 parent
5993574
commit e7e69a2
Showing
5 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/modules/esl-event-listener/core/targets/swipe.event.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {overrideEvent} from '../../../esl-utils/dom/events/misc'; | ||
|
||
/** | ||
* Swipe directions that could be provided in {@link ESLSwipeGestureEvent} | ||
*/ | ||
export type SwipeDirection = 'left' | 'right' | 'up' | 'down'; | ||
|
||
/** | ||
* Event names that could be triggered by {@link ESLSwipeGestureTarget} | ||
*/ | ||
export type SwipeEventName = 'swipe' | 'swipe:left' | 'swipe:right' | 'swipe:up' | 'swipe:down'; | ||
|
||
/** | ||
* Describes swipe information provided with {@link ESLSwipeGestureEvent} | ||
*/ | ||
export interface ESLSwipeGestureEventInfo { | ||
target: Element; | ||
/** Swipe direction {@link SwipeDirection} */ | ||
direction: SwipeDirection; | ||
/** Distance between the points where pointerdown and pointerup events occurred along the x axis */ | ||
distanceX: number; | ||
/** Distance between the points where pointerdown and pointerup events occurred along the y axis */ | ||
distanceY: number; | ||
/** Original pointerdown event */ | ||
startEvent: PointerEvent; | ||
/** Original pointerup event */ | ||
endEvent: PointerEvent; | ||
} | ||
|
||
/** | ||
* Creates swipe event dispatched by {@link ESLSwipeGestureTarget} | ||
*/ | ||
export class ESLSwipeGestureEvent extends UIEvent { | ||
public override readonly target: Element; | ||
public swipeInfo: ESLSwipeGestureEventInfo; | ||
|
||
protected constructor(eventName: SwipeEventName, swipeInfo: ESLSwipeGestureEventInfo) { | ||
super(eventName, {bubbles: true, cancelable: true}); | ||
this.swipeInfo = swipeInfo; | ||
overrideEvent(this, 'target', swipeInfo.target); | ||
} | ||
|
||
/** Creates {@link ESLSwipeGestureEvent} from {@link ESLSwipeGestureTarget} */ | ||
public static fromConfig(eventName: SwipeEventName, swipeInfo: ESLSwipeGestureEventInfo): ESLSwipeGestureEvent { | ||
return new ESLSwipeGestureEvent(eventName, swipeInfo); | ||
} | ||
} |
167 changes: 167 additions & 0 deletions
167
src/modules/esl-event-listener/core/targets/swipe.target.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import {SyntheticEventTarget} from '../../../esl-utils/dom/events/target'; | ||
import {ESLMixinElement} from '../../../esl-mixin-element/ui/esl-mixin-element'; | ||
import {resolveDomTarget} from '../../../esl-utils/abstract/dom-target'; | ||
import {bind} from '../../../esl-utils/decorators/bind'; | ||
import {ESLEventUtils} from '../api'; | ||
import {resolveCSSSize} from '../../../esl-utils/dom/units'; | ||
import {ESLSwipeGestureEvent} from './swipe.event'; | ||
|
||
import type {CSSSize} from '../../../esl-utils/dom/units'; | ||
import type {SwipeDirection, ESLSwipeGestureEventInfo, SwipeEventName} from './swipe.event'; | ||
import type {ESLDomElementTarget} from '../../../esl-utils/abstract/dom-target'; | ||
|
||
/** | ||
* Describes parsed configuration of {@link ESLSwipeGestureTarget} | ||
*/ | ||
interface SwipeEventTargetConfig { | ||
threshold: CSSSize; | ||
timeout: number; | ||
} | ||
|
||
/** | ||
* Describes settings object that could be passed to {@link ESLSwipeGestureTarget.for} as optional parameter | ||
*/ | ||
export interface ESLSwipeGestureSetting { | ||
threshold?: CSSSize; | ||
timeout?: number; | ||
} | ||
|
||
/** | ||
* Diff between events coordinates and timestamp | ||
*/ | ||
interface EventsDiff { | ||
x: number; | ||
y: number; | ||
time: number; | ||
} | ||
|
||
/** | ||
* Synthetic target class that produces swipe events | ||
*/ | ||
export class ESLSwipeGestureTarget extends SyntheticEventTarget { | ||
protected static defaultConfig: SwipeEventTargetConfig = { | ||
threshold: '20px', | ||
timeout: 500 | ||
}; | ||
|
||
protected startEvent: PointerEvent; | ||
protected config: SwipeEventTargetConfig; | ||
protected target: Element; | ||
protected isGestureStarted: boolean = false; | ||
|
||
protected constructor(target: ESLDomElementTarget, settings: ESLSwipeGestureSetting) { | ||
super(); | ||
this.target = resolveDomTarget(target); | ||
this.config = Object.assign({}, ESLSwipeGestureTarget.defaultConfig, settings); | ||
} | ||
|
||
/** | ||
* @param $target - a target element to observe pointer events to detect a gesture | ||
* @param settings - optional config override (will be merged with a default one if passed) {@link ESLSwipeGestureSetting}. | ||
* @returns Returns the instance of ESLSwipeGestureTarget {@link ESLSwipeGestureTarget}. | ||
*/ | ||
public static for($target: ESLDomElementTarget, settings?: ESLSwipeGestureSetting): ESLSwipeGestureTarget { | ||
if ($target instanceof ESLMixinElement) return ESLSwipeGestureTarget.for($target.$host, settings); | ||
|
||
return new ESLSwipeGestureTarget($target, settings || {}); | ||
} | ||
|
||
/** | ||
* Saves swipe start event target, time when swipe started, pointerdown event and coordinates. | ||
* @param e - pointer event | ||
*/ | ||
@bind | ||
protected handleStart(e: PointerEvent): void { | ||
this.startEvent = e; | ||
this.isGestureStarted = true; | ||
} | ||
|
||
/** | ||
* @param e - pointer event (pointerdown) | ||
* @returns diff between pointerdown and pointer coordinates and timestamp {@link EventsDiff} | ||
*/ | ||
protected eventDiff(e: PointerEvent): EventsDiff { | ||
return { | ||
x: this.startEvent.clientX - e.clientX, | ||
y: this.startEvent.clientY - e.clientY, | ||
time: e.timeStamp - this.startEvent.timeStamp | ||
}; | ||
} | ||
|
||
/** | ||
* Triggers swipe event {@link SwipeEventName} with details {@link ESLSwipeGestureEvent} when pointerup event | ||
* occurred and threshold and distance match configuration | ||
* @param e - pointer event | ||
*/ | ||
@bind | ||
protected handleEnd(e: PointerEvent): void { | ||
// if the user released on a different target, cancel! | ||
if (!this.isGestureStarted || (this.startEvent?.target !== e.target)) return; | ||
|
||
const eventsDiff = this.eventDiff(e); | ||
const direction = this.resolveDirection(eventsDiff); | ||
if (direction) { | ||
const swipeInfo: ESLSwipeGestureEventInfo = { | ||
target: this.target, | ||
direction, | ||
distanceX: Math.abs(eventsDiff.x), | ||
distanceY: Math.abs(eventsDiff.y), | ||
startEvent: this.startEvent, | ||
endEvent: e | ||
}; | ||
|
||
// fire `swipe` event on the element that started the swipe | ||
this.dispatchEvent(ESLSwipeGestureEvent.fromConfig('swipe', swipeInfo)); | ||
// fire `swipe:${dir}` event on the element that started the swipe | ||
this.dispatchEvent(ESLSwipeGestureEvent.fromConfig(`swipe:${direction}` as SwipeEventName, swipeInfo)); | ||
} | ||
|
||
// Mark swipe as completed | ||
this.isGestureStarted = false; | ||
} | ||
|
||
/** | ||
* Returns swipe direction based on distance between swipe start and end points | ||
* @param diff - diff between pointerdown and pointer coordinates and timestamp {@link EventsDiff} | ||
* @returns direction of swipe {@link SwipeDirection} | ||
*/ | ||
protected resolveDirection(diff: EventsDiff): SwipeDirection | null { | ||
const swipeThreshold = (resolveCSSSize(this.config.threshold) || resolveCSSSize(ESLSwipeGestureTarget.defaultConfig.threshold)!); | ||
|
||
if (Math.abs(diff.x) > Math.abs(diff.y) && Math.abs(diff.x) > swipeThreshold && diff.time < this.config.timeout) { | ||
return diff.x > 0 ? 'left' : 'right'; | ||
} | ||
if (Math.abs(diff.y) > swipeThreshold && diff.time < this.config.timeout) { | ||
return diff.y > 0 ? 'up' : 'down'; | ||
} | ||
|
||
// Marker that there was not enough move characteristic to consider pointer move as touch swipe | ||
return null; | ||
} | ||
|
||
/** | ||
* Subscribes to pointerup and pointerdown event | ||
*/ | ||
public override addEventListener(callback: EventListener): void; | ||
public override addEventListener(event: SwipeEventName, callback: EventListener): void; | ||
public override addEventListener(event: any, callback: EventListener = event): void { | ||
super.addEventListener(event, callback); | ||
if (this.getEventListeners().length > 1) return; | ||
|
||
const {target} = this; | ||
ESLEventUtils.subscribe(this, {event: 'pointerdown', capture: false, target}, this.handleStart); | ||
ESLEventUtils.subscribe(this, {event: 'pointerup', capture: false, target}, this.handleEnd); | ||
} | ||
|
||
/** | ||
* Unsubscribes from the observed target {@link Element} changes | ||
*/ | ||
public override removeEventListener(callback: EventListener): void; | ||
public override removeEventListener(event: SwipeEventName, callback: EventListener): void; | ||
public override removeEventListener(event: any, callback: EventListener = event): void { | ||
super.removeEventListener(event, callback); | ||
if (this.getEventListeners().length > 0) return; | ||
|
||
ESLEventUtils.unsubscribe(this); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {resolveCSSSize} from '../units'; | ||
import type {CSSSize} from '../units'; | ||
|
||
describe('resolveCSSSize tests', () => { | ||
test('resolveCSSSize called with value \'25px\' should return 25', () => { | ||
const result = resolveCSSSize('25px'); | ||
expect(result).toStrictEqual(25); | ||
}); | ||
|
||
test('resolveCSSSize called with value \'25vw\' should return 25', () => { | ||
jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => 100); | ||
const result = resolveCSSSize('25vw'); | ||
expect(result).toStrictEqual(25); | ||
}); | ||
|
||
test('resolveCSSSize called with value \'25vh\' should return 25', () => { | ||
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 100); | ||
const result = resolveCSSSize('25vh'); | ||
expect(result).toStrictEqual(25); | ||
}); | ||
|
||
test('resolveCSSSize called with value \'25\' should return 25 as a number', () => { | ||
const result = resolveCSSSize('25'); | ||
expect(result).toStrictEqual(25); | ||
}); | ||
|
||
test('resolveCSSSize called with value \'undefined\' should return null', () => { | ||
const result = resolveCSSSize(('undefined' as CSSSize)); | ||
expect(result).toStrictEqual(null); | ||
}); | ||
|
||
test('resolveCSSSize called with value \'.5vw\' should return 1 (rounded value)', () => { | ||
jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => 100); | ||
const result = resolveCSSSize('.5vw'); | ||
expect(result).toStrictEqual(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
export type CSSSize = `${number}${'' | 'px' | 'vh' | 'vw'}`; | ||
|
||
/** | ||
* @param value - CSS value string in px, vh, vw or without units. {@link CSSSize} | ||
* @returns number in pixels. | ||
*/ | ||
export const resolveCSSSize = (value: CSSSize): number | null => { | ||
let units = 'px'; | ||
|
||
['px', 'vh', 'vw'].forEach((item) => { | ||
if (value.indexOf(item) > 0) { | ||
units = item; | ||
} | ||
}); | ||
|
||
const num = parseFloat(value.replace(units, '')); | ||
|
||
if (!num) { | ||
return null; | ||
} | ||
|
||
if (units === 'vh') { | ||
return Math.round((num / 100) * document.documentElement.clientHeight); // get percentage of viewport height in pixels | ||
} | ||
if (units === 'vw') { | ||
return Math.round((num / 100) * document.documentElement.clientWidth); // get percentage of viewport width in pixels | ||
} | ||
|
||
return num; | ||
}; |