Merge pull request #12 from brianpooe/claude/angular-signals-upgrade-01LkzyeFbjsqKer2XzATwcxH
refactor: Implement clean Angular architecture with explicit folder s…
This commit is contained in:
commit
d5a96e8d77
15 changed files with 2065 additions and 2031 deletions
|
|
@ -1,13 +1,10 @@
|
|||
import { Component, ElementRef, HostBinding, OnInit, OnDestroy, computed, input, output, signal, effect, inject } from '@angular/core';
|
||||
|
||||
import { Properties } from './interfaces';
|
||||
import { defaultProperties } from './properties';
|
||||
import { IvyPinch } from './ivypinch';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BrightnessService } from './services/brightness.service';
|
||||
import { ZoomStateService } from './services/zoom-state.service';
|
||||
import { ZoomControlsComponent } from './components/zoom-controls/zoom-controls.component';
|
||||
import { BrightnessControlsComponent } from './components/brightness-controls/brightness-controls.component';
|
||||
|
||||
import { Properties, defaultProperties } from '../../../models';
|
||||
import { IvyPinch, BrightnessService, ZoomStateService } from '../../../services';
|
||||
import { ZoomControlsComponent } from '../../presentational/zoom-controls/zoom-controls.component';
|
||||
import { BrightnessControlsComponent } from '../../presentational/brightness-controls/brightness-controls.component';
|
||||
|
||||
interface ComponentProperties extends Properties {
|
||||
disabled?: boolean;
|
||||
|
|
@ -25,8 +22,8 @@ export const _defaultComponentProperties: ComponentProperties = {
|
|||
@Component({
|
||||
selector: 'pinch-zoom, [pinch-zoom]',
|
||||
exportAs: 'pinchZoom',
|
||||
templateUrl: './pinch-zoom.component.html',
|
||||
styleUrls: ['./pinch-zoom.component.sass'],
|
||||
templateUrl: './pinch-zoom.container.html',
|
||||
styleUrls: ['./pinch-zoom.container.sass'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, ZoomControlsComponent, BrightnessControlsComponent],
|
||||
providers: [BrightnessService, ZoomStateService],
|
||||
|
|
@ -1,2 +1,6 @@
|
|||
export * from './zoom-controls/zoom-controls.component';
|
||||
export * from './brightness-controls/brightness-controls.component';
|
||||
// Container (Smart) Components
|
||||
export * from './containers/pinch-zoom/pinch-zoom.container';
|
||||
|
||||
// Presentational (Dumb) Components
|
||||
export * from './presentational/zoom-controls/zoom-controls.component';
|
||||
export * from './presentational/brightness-controls/brightness-controls.component';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,8 @@
|
|||
// Core models
|
||||
export * from './properties.model';
|
||||
export * from './interfaces.model';
|
||||
|
||||
// Feature models
|
||||
export * from './zoom-config.model';
|
||||
export * from './transform-state.model';
|
||||
export * from './brightness-state.model';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Properties } from './interfaces';
|
||||
import { Properties } from './interfaces.model';
|
||||
|
||||
export const defaultProperties: Properties = {
|
||||
transitionDuration: 200,
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export * from './brightness.service';
|
||||
export * from './zoom-state.service';
|
||||
export * from './ivy-pinch.service';
|
||||
export * from './touches.service';
|
||||
|
|
|
|||
1562
projects/ngx-pinch-zoom/src/lib/services/ivy-pinch.service.ts
Normal file
1562
projects/ngx-pinch-zoom/src/lib/services/ivy-pinch.service.ts
Normal file
File diff suppressed because it is too large
Load diff
465
projects/ngx-pinch-zoom/src/lib/services/touches.service.ts
Normal file
465
projects/ngx-pinch-zoom/src/lib/services/touches.service.ts
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
/**
|
||||
* Properties interface for Touches gesture detection.
|
||||
*/
|
||||
export interface TouchProperties {
|
||||
element: HTMLElement;
|
||||
listeners?: 'auto' | 'mouse and touch';
|
||||
touchListeners?: TouchListeners;
|
||||
mouseListeners?: MouseListeners;
|
||||
otherListeners?: OtherListeners;
|
||||
resize?: boolean;
|
||||
}
|
||||
|
||||
export type EventType =
|
||||
| undefined
|
||||
| 'touchstart'
|
||||
| 'touchend'
|
||||
| 'touchmove'
|
||||
| 'mousedown'
|
||||
| 'mouseup'
|
||||
| 'mousemove'
|
||||
| 'pan'
|
||||
| 'pinch'
|
||||
| 'horizontal-swipe'
|
||||
| 'vertical-swipe'
|
||||
| 'tap'
|
||||
| 'longtap'
|
||||
| 'wheel'
|
||||
| 'double-tap'
|
||||
| 'resize';
|
||||
export type TouchHandler = 'handleTouchstart' | 'handleTouchmove' | 'handleTouchend';
|
||||
export type MouseHandler = 'handleMousedown' | 'handleMousemove' | 'handleMouseup' | 'handleWheel';
|
||||
export type OtherHandler = 'handleResize';
|
||||
|
||||
export type TouchListeners = Partial<Record<'touchstart' | 'touchmove' | 'touchend', TouchHandler>>;
|
||||
export type MouseListeners = Partial<Record<'mousedown' | 'mousemove' | 'mouseup' | 'wheel', MouseHandler>>;
|
||||
export type OtherListeners = Partial<Record<'resize', OtherHandler>>;
|
||||
|
||||
/**
|
||||
* Touch and mouse gesture detection utility class.
|
||||
* Instantiated directly with 'new Touches(properties)'.
|
||||
* Not an Angular service - does not use dependency injection.
|
||||
*/
|
||||
export class Touches {
|
||||
private properties: TouchProperties;
|
||||
private element: HTMLElement;
|
||||
private elementPosition: DOMRect;
|
||||
private eventType: EventType = undefined;
|
||||
private handlers: TouchListeners | MouseListeners | OtherListeners = {};
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private lastTap = 0;
|
||||
private doubleTapTimeout: number = 0;
|
||||
private doubleTapMinTimeout = 300;
|
||||
private tapMinTimeout = 200;
|
||||
private touchstartTime = 0;
|
||||
private i: number = 0;
|
||||
private isMousedown = false;
|
||||
|
||||
// Store bound event handlers to prevent memory leaks
|
||||
private boundHandlers = new Map<string, EventListener>();
|
||||
|
||||
private _touchListeners: Record<'touchstart' | 'touchmove' | 'touchend', TouchHandler> = {
|
||||
touchstart: 'handleTouchstart',
|
||||
touchmove: 'handleTouchmove',
|
||||
touchend: 'handleTouchend',
|
||||
};
|
||||
private _mouseListeners: Record<'mousedown' | 'mousemove' | 'mouseup' | 'wheel', MouseHandler> = {
|
||||
mousedown: 'handleMousedown',
|
||||
mousemove: 'handleMousemove',
|
||||
mouseup: 'handleMouseup',
|
||||
wheel: 'handleWheel',
|
||||
};
|
||||
private _otherListeners: Record<'resize', OtherHandler> = {
|
||||
resize: 'handleResize',
|
||||
};
|
||||
|
||||
private get touchListeners(): TouchListeners {
|
||||
return this.properties.touchListeners ? this.properties.touchListeners : this._touchListeners;
|
||||
}
|
||||
|
||||
private get mouseListeners(): MouseListeners {
|
||||
return this.properties.mouseListeners ? this.properties.mouseListeners : this._mouseListeners;
|
||||
}
|
||||
|
||||
private get otherListeners(): OtherListeners {
|
||||
return this.properties.otherListeners ? this.properties.otherListeners : this._otherListeners;
|
||||
}
|
||||
|
||||
constructor(properties: TouchProperties) {
|
||||
this.properties = properties;
|
||||
this.element = this.properties.element;
|
||||
this.elementPosition = this.getElementPosition();
|
||||
|
||||
this.toggleEventListeners('addEventListener');
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.toggleEventListeners('removeEventListener');
|
||||
}
|
||||
|
||||
private toggleEventListeners(action: 'addEventListener' | 'removeEventListener'): void {
|
||||
let listeners: TouchListeners | MouseListeners | OtherListeners;
|
||||
|
||||
if (this.properties.listeners === 'mouse and touch') {
|
||||
listeners = Object.assign(this.touchListeners, this.mouseListeners);
|
||||
} else {
|
||||
listeners = this.detectTouchScreen() ? this.touchListeners : this.mouseListeners;
|
||||
}
|
||||
|
||||
if (this.properties.resize) {
|
||||
listeners = Object.assign(listeners, this.otherListeners);
|
||||
}
|
||||
|
||||
for (const listener in listeners) {
|
||||
const handler = listeners[listener as keyof typeof listeners];
|
||||
if (!handler) continue;
|
||||
|
||||
const method = this[handler as keyof this];
|
||||
if (typeof method !== 'function') continue;
|
||||
|
||||
let boundMethod: EventListener;
|
||||
|
||||
if (action === 'addEventListener') {
|
||||
// Create and store the bound method
|
||||
boundMethod = method.bind(this) as EventListener;
|
||||
this.boundHandlers.set(listener, boundMethod);
|
||||
} else {
|
||||
// Retrieve the stored bound method
|
||||
const storedHandler = this.boundHandlers.get(listener);
|
||||
if (!storedHandler) continue; // Skip if no handler was stored
|
||||
boundMethod = storedHandler;
|
||||
}
|
||||
|
||||
// Determine target element (Window, Document, or this.element)
|
||||
const target = listener === 'resize' ? window : listener === 'mouseup' || listener === 'mousemove' ? document : this.element;
|
||||
|
||||
// Add or remove the listener
|
||||
if (action === 'addEventListener') {
|
||||
target.addEventListener(listener, boundMethod, false);
|
||||
} else {
|
||||
target.removeEventListener(listener, boundMethod, false);
|
||||
this.boundHandlers.delete(listener); // Clean up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addEventListeners(listener: string): void {
|
||||
const handler = this._mouseListeners[listener as keyof typeof this._mouseListeners];
|
||||
if (!handler) return;
|
||||
|
||||
const method = this[handler as keyof this];
|
||||
if (typeof method === 'function') {
|
||||
// Create a unique key for dynamically added listeners
|
||||
const key = `dynamic-${listener}`;
|
||||
const boundMethod = method.bind(this) as EventListener;
|
||||
this.boundHandlers.set(key, boundMethod);
|
||||
window.addEventListener(listener, boundMethod, false);
|
||||
}
|
||||
}
|
||||
|
||||
public removeEventListeners(listener: string): void {
|
||||
// Try to retrieve the dynamically added listener
|
||||
const key = `dynamic-${listener}`;
|
||||
const boundMethod = this.boundHandlers.get(key);
|
||||
|
||||
if (boundMethod) {
|
||||
window.removeEventListener(listener, boundMethod, false);
|
||||
this.boundHandlers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Listeners
|
||||
*/
|
||||
|
||||
/* Touchstart */
|
||||
|
||||
private handleTouchstart = (event: TouchEvent): void => {
|
||||
this.elementPosition = this.getElementPosition();
|
||||
this.touchstartTime = new Date().getTime();
|
||||
|
||||
if (this.eventType === undefined) {
|
||||
this.getTouchstartPosition(event);
|
||||
}
|
||||
|
||||
this.runHandler('touchstart', event);
|
||||
};
|
||||
|
||||
/* Touchmove */
|
||||
|
||||
private handleTouchmove = (event: TouchEvent): void => {
|
||||
const touches = event.touches;
|
||||
|
||||
// Pan
|
||||
if (this.detectPan(touches)) {
|
||||
this.runHandler('pan', event);
|
||||
}
|
||||
|
||||
// Pinch
|
||||
if (this.detectPinch(event)) {
|
||||
this.runHandler('pinch', event);
|
||||
}
|
||||
};
|
||||
|
||||
private handleLinearSwipe(event: any): void {
|
||||
//event.preventDefault();
|
||||
|
||||
this.i++;
|
||||
|
||||
if (this.i > 3) {
|
||||
this.eventType = this.getLinearSwipeType(event);
|
||||
}
|
||||
|
||||
if (this.eventType === 'horizontal-swipe') {
|
||||
this.runHandler('horizontal-swipe', event);
|
||||
}
|
||||
|
||||
if (this.eventType === 'vertical-swipe') {
|
||||
this.runHandler('vertical-swipe', event);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touchend */
|
||||
|
||||
private handleTouchend = (event: TouchEvent): void => {
|
||||
const touches = event.touches;
|
||||
|
||||
// Double Tap
|
||||
if (this.detectDoubleTap()) {
|
||||
this.runHandler('double-tap', event);
|
||||
}
|
||||
|
||||
// Tap
|
||||
this.detectTap();
|
||||
|
||||
this.runHandler('touchend', event);
|
||||
this.eventType = 'touchend';
|
||||
|
||||
if (touches && touches.length === 0) {
|
||||
this.eventType = undefined;
|
||||
this.i = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/* Mousedown */
|
||||
|
||||
private handleMousedown = (event: MouseEvent): void => {
|
||||
this.isMousedown = true;
|
||||
this.elementPosition = this.getElementPosition();
|
||||
this.touchstartTime = new Date().getTime();
|
||||
|
||||
if (this.eventType === undefined) {
|
||||
this.getMousedownPosition(event);
|
||||
}
|
||||
|
||||
this.runHandler('mousedown', event);
|
||||
};
|
||||
|
||||
/* Mousemove */
|
||||
|
||||
private handleMousemove = (event: MouseEvent): void => {
|
||||
//event.preventDefault();
|
||||
|
||||
if (!this.isMousedown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pan
|
||||
this.runHandler('pan', event);
|
||||
|
||||
// Linear swipe
|
||||
switch (this.detectLinearSwipe(event)) {
|
||||
case 'horizontal-swipe':
|
||||
// FIXME: looks like an error
|
||||
// @ts-ignore
|
||||
event.swipeType = 'horizontal-swipe';
|
||||
this.runHandler('horizontal-swipe', event);
|
||||
break;
|
||||
case 'vertical-swipe':
|
||||
// FIXME: looks like an error
|
||||
// @ts-ignore
|
||||
event.swipeType = 'vertical-swipe';
|
||||
this.runHandler('vertical-swipe', event);
|
||||
break;
|
||||
}
|
||||
|
||||
// Linear swipe
|
||||
if (this.detectLinearSwipe(event) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
this.handleLinearSwipe(event);
|
||||
}
|
||||
};
|
||||
|
||||
/* Mouseup */
|
||||
|
||||
private handleMouseup = (event: MouseEvent): void => {
|
||||
// Tap
|
||||
this.detectTap();
|
||||
|
||||
this.isMousedown = false;
|
||||
this.runHandler('mouseup', event);
|
||||
this.eventType = undefined;
|
||||
this.i = 0;
|
||||
};
|
||||
|
||||
/* Wheel */
|
||||
|
||||
private handleWheel = (event: WheelEvent): void => {
|
||||
this.runHandler('wheel', event);
|
||||
};
|
||||
|
||||
/* Resize */
|
||||
|
||||
private handleResize = (event: Event): void => {
|
||||
this.runHandler('resize', event);
|
||||
};
|
||||
|
||||
private runHandler(eventName: EventType, event: unknown): void {
|
||||
if (eventName && (this.handlers as any)[eventName]) {
|
||||
const handler = (this.handlers as any)[eventName];
|
||||
if (handler) {
|
||||
handler(event as Event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Detection
|
||||
*/
|
||||
|
||||
private detectPan(touches: TouchList): boolean {
|
||||
return (touches.length === 1 && !this.eventType) || this.eventType === 'pan';
|
||||
}
|
||||
|
||||
private detectDoubleTap(): boolean {
|
||||
if (this.eventType != undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - this.lastTap;
|
||||
|
||||
window.clearTimeout(this.doubleTapTimeout);
|
||||
|
||||
if (tapLength < this.doubleTapMinTimeout && tapLength > 0) {
|
||||
return true;
|
||||
} else {
|
||||
this.doubleTapTimeout = window.setTimeout(() => {
|
||||
window.clearTimeout(this.doubleTapTimeout);
|
||||
}, this.doubleTapMinTimeout);
|
||||
}
|
||||
this.lastTap = currentTime;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private detectTap(): void {
|
||||
if (this.eventType != undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - this.touchstartTime;
|
||||
|
||||
if (tapLength > 0) {
|
||||
if (tapLength < this.tapMinTimeout) {
|
||||
this.runHandler('tap', {});
|
||||
} else {
|
||||
this.runHandler('longtap', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private detectPinch(event: TouchEvent): boolean {
|
||||
const touches = event.touches;
|
||||
return (touches.length === 2 && this.eventType === undefined) || this.eventType === 'pinch';
|
||||
}
|
||||
|
||||
private detectLinearSwipe(event: MouseEvent | TouchEvent): 'vertical-swipe' | 'horizontal-swipe' | undefined {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
if ((touches.length === 1 && !this.eventType) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
return this.getLinearSwipeType(event);
|
||||
}
|
||||
} else {
|
||||
if (!this.eventType || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
return this.getLinearSwipeType(event);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getLinearSwipeType(event: TouchEvent | MouseEvent): 'vertical-swipe' | 'horizontal-swipe' {
|
||||
if (this.eventType !== 'horizontal-swipe' && this.eventType !== 'vertical-swipe') {
|
||||
const movementX = Math.abs(this.moveLeft(0, event) - this.startX);
|
||||
const movementY = Math.abs(this.moveTop(0, event) - this.startY);
|
||||
|
||||
if (movementY * 3 > movementX) {
|
||||
return 'vertical-swipe';
|
||||
} else {
|
||||
return 'horizontal-swipe';
|
||||
}
|
||||
} else {
|
||||
return this.eventType;
|
||||
}
|
||||
}
|
||||
|
||||
private getElementPosition(): DOMRect {
|
||||
return this.element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
private getTouchstartPosition(event: TouchEvent): void {
|
||||
this.startX = event.touches[0].clientX - this.elementPosition.left;
|
||||
this.startY = event.touches[0].clientY - this.elementPosition.top;
|
||||
}
|
||||
|
||||
private getMousedownPosition(event: MouseEvent): void {
|
||||
this.startX = event.clientX - this.elementPosition.left;
|
||||
this.startY = event.clientY - this.elementPosition.top;
|
||||
}
|
||||
|
||||
private moveLeft(index: number, event: TouchEvent | MouseEvent): number {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
return touches[index].clientX - this.elementPosition.left;
|
||||
} else {
|
||||
return (event as MouseEvent).clientX - this.elementPosition.left;
|
||||
}
|
||||
}
|
||||
|
||||
private moveTop(index: number, event: TouchEvent | MouseEvent): number {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
return touches[index].clientY - this.elementPosition.top;
|
||||
} else {
|
||||
return (event as MouseEvent).clientY - this.elementPosition.top;
|
||||
}
|
||||
}
|
||||
|
||||
private detectTouchScreen(): boolean {
|
||||
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ');
|
||||
const mq = (query: string): boolean => {
|
||||
return window.matchMedia(query).matches;
|
||||
};
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
|
||||
// https://git.io/vznFH
|
||||
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('');
|
||||
return mq(query);
|
||||
}
|
||||
|
||||
/* Public properties and methods */
|
||||
public on(event: EventType, handler: (event: Event) => void): void {
|
||||
if (event) {
|
||||
(this.handlers as any)[event] = handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,457 +1,9 @@
|
|||
export interface Properties {
|
||||
element: HTMLElement;
|
||||
listeners?: 'auto' | 'mouse and touch';
|
||||
touchListeners?: TouchListeners;
|
||||
mouseListeners?: MouseListeners;
|
||||
otherListeners?: OtherListeners;
|
||||
resize?: boolean;
|
||||
}
|
||||
|
||||
export type EventType =
|
||||
| undefined
|
||||
| 'touchstart'
|
||||
| 'touchend'
|
||||
| 'touchmove'
|
||||
| 'mousedown'
|
||||
| 'mouseup'
|
||||
| 'mousemove'
|
||||
| 'pan'
|
||||
| 'pinch'
|
||||
| 'horizontal-swipe'
|
||||
| 'vertical-swipe'
|
||||
| 'tap'
|
||||
| 'longtap'
|
||||
| 'wheel'
|
||||
| 'double-tap'
|
||||
| 'resize';
|
||||
export type TouchHandler = 'handleTouchstart' | 'handleTouchmove' | 'handleTouchend';
|
||||
export type MouseHandler = 'handleMousedown' | 'handleMousemove' | 'handleMouseup' | 'handleWheel';
|
||||
export type OtherHandler = 'handleResize';
|
||||
|
||||
export type TouchListeners = Partial<Record<'touchstart' | 'touchmove' | 'touchend', TouchHandler>>;
|
||||
export type MouseListeners = Partial<Record<'mousedown' | 'mousemove' | 'mouseup' | 'wheel', MouseHandler>>;
|
||||
export type OtherListeners = Partial<Record<'resize', OtherHandler>>;
|
||||
|
||||
export class Touches {
|
||||
private properties: Properties;
|
||||
private element: HTMLElement;
|
||||
private elementPosition: DOMRect;
|
||||
private eventType: EventType = undefined;
|
||||
private handlers: TouchListeners | MouseListeners | OtherListeners = {};
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private lastTap = 0;
|
||||
private doubleTapTimeout: number = 0;
|
||||
private doubleTapMinTimeout = 300;
|
||||
private tapMinTimeout = 200;
|
||||
private touchstartTime = 0;
|
||||
private i: number = 0;
|
||||
private isMousedown = false;
|
||||
|
||||
// Store bound event handlers to prevent memory leaks
|
||||
private boundHandlers = new Map<string, EventListener>();
|
||||
|
||||
private _touchListeners: Record<'touchstart' | 'touchmove' | 'touchend', TouchHandler> = {
|
||||
touchstart: 'handleTouchstart',
|
||||
touchmove: 'handleTouchmove',
|
||||
touchend: 'handleTouchend',
|
||||
};
|
||||
private _mouseListeners: Record<'mousedown' | 'mousemove' | 'mouseup' | 'wheel', MouseHandler> = {
|
||||
mousedown: 'handleMousedown',
|
||||
mousemove: 'handleMousemove',
|
||||
mouseup: 'handleMouseup',
|
||||
wheel: 'handleWheel',
|
||||
};
|
||||
private _otherListeners: Record<'resize', OtherHandler> = {
|
||||
resize: 'handleResize',
|
||||
};
|
||||
|
||||
private get touchListeners(): TouchListeners {
|
||||
return this.properties.touchListeners ? this.properties.touchListeners : this._touchListeners;
|
||||
}
|
||||
|
||||
private get mouseListeners(): MouseListeners {
|
||||
return this.properties.mouseListeners ? this.properties.mouseListeners : this._mouseListeners;
|
||||
}
|
||||
|
||||
private get otherListeners(): OtherListeners {
|
||||
return this.properties.otherListeners ? this.properties.otherListeners : this._otherListeners;
|
||||
}
|
||||
|
||||
constructor(properties: Properties) {
|
||||
this.properties = properties;
|
||||
this.element = this.properties.element;
|
||||
this.elementPosition = this.getElementPosition();
|
||||
|
||||
this.toggleEventListeners('addEventListener');
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.toggleEventListeners('removeEventListener');
|
||||
}
|
||||
|
||||
private toggleEventListeners(action: 'addEventListener' | 'removeEventListener'): void {
|
||||
let listeners: TouchListeners | MouseListeners | OtherListeners;
|
||||
|
||||
if (this.properties.listeners === 'mouse and touch') {
|
||||
listeners = Object.assign(this.touchListeners, this.mouseListeners);
|
||||
} else {
|
||||
listeners = this.detectTouchScreen() ? this.touchListeners : this.mouseListeners;
|
||||
}
|
||||
|
||||
if (this.properties.resize) {
|
||||
listeners = Object.assign(listeners, this.otherListeners);
|
||||
}
|
||||
|
||||
for (const listener in listeners) {
|
||||
const handler = listeners[listener as keyof typeof listeners];
|
||||
if (!handler) continue;
|
||||
|
||||
const method = this[handler as keyof this];
|
||||
if (typeof method !== 'function') continue;
|
||||
|
||||
let boundMethod: EventListener;
|
||||
|
||||
if (action === 'addEventListener') {
|
||||
// Create and store the bound method
|
||||
boundMethod = method.bind(this) as EventListener;
|
||||
this.boundHandlers.set(listener, boundMethod);
|
||||
} else {
|
||||
// Retrieve the stored bound method
|
||||
const storedHandler = this.boundHandlers.get(listener);
|
||||
if (!storedHandler) continue; // Skip if no handler was stored
|
||||
boundMethod = storedHandler;
|
||||
}
|
||||
|
||||
// Determine target element (Window, Document, or this.element)
|
||||
const target = listener === 'resize' ? window : listener === 'mouseup' || listener === 'mousemove' ? document : this.element;
|
||||
|
||||
// Add or remove the listener
|
||||
if (action === 'addEventListener') {
|
||||
target.addEventListener(listener, boundMethod, false);
|
||||
} else {
|
||||
target.removeEventListener(listener, boundMethod, false);
|
||||
this.boundHandlers.delete(listener); // Clean up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addEventListeners(listener: string): void {
|
||||
const handler = this._mouseListeners[listener as keyof typeof this._mouseListeners];
|
||||
if (!handler) return;
|
||||
|
||||
const method = this[handler as keyof this];
|
||||
if (typeof method === 'function') {
|
||||
// Create a unique key for dynamically added listeners
|
||||
const key = `dynamic-${listener}`;
|
||||
const boundMethod = method.bind(this) as EventListener;
|
||||
this.boundHandlers.set(key, boundMethod);
|
||||
window.addEventListener(listener, boundMethod, false);
|
||||
}
|
||||
}
|
||||
|
||||
public removeEventListeners(listener: string): void {
|
||||
// Try to retrieve the dynamically added listener
|
||||
const key = `dynamic-${listener}`;
|
||||
const boundMethod = this.boundHandlers.get(key);
|
||||
|
||||
if (boundMethod) {
|
||||
window.removeEventListener(listener, boundMethod, false);
|
||||
this.boundHandlers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Listeners
|
||||
*/
|
||||
|
||||
/* Touchstart */
|
||||
|
||||
private handleTouchstart = (event: TouchEvent): void => {
|
||||
this.elementPosition = this.getElementPosition();
|
||||
this.touchstartTime = new Date().getTime();
|
||||
|
||||
if (this.eventType === undefined) {
|
||||
this.getTouchstartPosition(event);
|
||||
}
|
||||
|
||||
this.runHandler('touchstart', event);
|
||||
};
|
||||
|
||||
/* Touchmove */
|
||||
|
||||
private handleTouchmove = (event: TouchEvent): void => {
|
||||
const touches = event.touches;
|
||||
|
||||
// Pan
|
||||
if (this.detectPan(touches)) {
|
||||
this.runHandler('pan', event);
|
||||
}
|
||||
|
||||
// Pinch
|
||||
if (this.detectPinch(event)) {
|
||||
this.runHandler('pinch', event);
|
||||
}
|
||||
};
|
||||
|
||||
private handleLinearSwipe(event: any): void {
|
||||
//event.preventDefault();
|
||||
|
||||
this.i++;
|
||||
|
||||
if (this.i > 3) {
|
||||
this.eventType = this.getLinearSwipeType(event);
|
||||
}
|
||||
|
||||
if (this.eventType === 'horizontal-swipe') {
|
||||
this.runHandler('horizontal-swipe', event);
|
||||
}
|
||||
|
||||
if (this.eventType === 'vertical-swipe') {
|
||||
this.runHandler('vertical-swipe', event);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touchend */
|
||||
|
||||
private handleTouchend = (event: TouchEvent): void => {
|
||||
const touches = event.touches;
|
||||
|
||||
// Double Tap
|
||||
if (this.detectDoubleTap()) {
|
||||
this.runHandler('double-tap', event);
|
||||
}
|
||||
|
||||
// Tap
|
||||
this.detectTap();
|
||||
|
||||
this.runHandler('touchend', event);
|
||||
this.eventType = 'touchend';
|
||||
|
||||
if (touches && touches.length === 0) {
|
||||
this.eventType = undefined;
|
||||
this.i = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/* Mousedown */
|
||||
|
||||
private handleMousedown = (event: MouseEvent): void => {
|
||||
this.isMousedown = true;
|
||||
this.elementPosition = this.getElementPosition();
|
||||
this.touchstartTime = new Date().getTime();
|
||||
|
||||
if (this.eventType === undefined) {
|
||||
this.getMousedownPosition(event);
|
||||
}
|
||||
|
||||
this.runHandler('mousedown', event);
|
||||
};
|
||||
|
||||
/* Mousemove */
|
||||
|
||||
private handleMousemove = (event: MouseEvent): void => {
|
||||
//event.preventDefault();
|
||||
|
||||
if (!this.isMousedown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pan
|
||||
this.runHandler('pan', event);
|
||||
|
||||
// Linear swipe
|
||||
switch (this.detectLinearSwipe(event)) {
|
||||
case 'horizontal-swipe':
|
||||
// FIXME: looks like an error
|
||||
// @ts-ignore
|
||||
event.swipeType = 'horizontal-swipe';
|
||||
this.runHandler('horizontal-swipe', event);
|
||||
break;
|
||||
case 'vertical-swipe':
|
||||
// FIXME: looks like an error
|
||||
// @ts-ignore
|
||||
event.swipeType = 'vertical-swipe';
|
||||
this.runHandler('vertical-swipe', event);
|
||||
break;
|
||||
}
|
||||
|
||||
// Linear swipe
|
||||
if (this.detectLinearSwipe(event) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
this.handleLinearSwipe(event);
|
||||
}
|
||||
};
|
||||
|
||||
/* Mouseup */
|
||||
|
||||
private handleMouseup = (event: MouseEvent): void => {
|
||||
// Tap
|
||||
this.detectTap();
|
||||
|
||||
this.isMousedown = false;
|
||||
this.runHandler('mouseup', event);
|
||||
this.eventType = undefined;
|
||||
this.i = 0;
|
||||
};
|
||||
|
||||
/* Wheel */
|
||||
|
||||
private handleWheel = (event: WheelEvent): void => {
|
||||
this.runHandler('wheel', event);
|
||||
};
|
||||
|
||||
/* Resize */
|
||||
|
||||
private handleResize = (event: Event): void => {
|
||||
this.runHandler('resize', event);
|
||||
};
|
||||
|
||||
private runHandler(eventName: EventType, event: unknown): void {
|
||||
if (eventName && (this.handlers as any)[eventName]) {
|
||||
const handler = (this.handlers as any)[eventName];
|
||||
if (handler) {
|
||||
handler(event as Event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Detection
|
||||
*/
|
||||
|
||||
private detectPan(touches: TouchList): boolean {
|
||||
return (touches.length === 1 && !this.eventType) || this.eventType === 'pan';
|
||||
}
|
||||
|
||||
private detectDoubleTap(): boolean {
|
||||
if (this.eventType != undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - this.lastTap;
|
||||
|
||||
window.clearTimeout(this.doubleTapTimeout);
|
||||
|
||||
if (tapLength < this.doubleTapMinTimeout && tapLength > 0) {
|
||||
return true;
|
||||
} else {
|
||||
this.doubleTapTimeout = window.setTimeout(() => {
|
||||
window.clearTimeout(this.doubleTapTimeout);
|
||||
}, this.doubleTapMinTimeout);
|
||||
}
|
||||
this.lastTap = currentTime;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private detectTap(): void {
|
||||
if (this.eventType != undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - this.touchstartTime;
|
||||
|
||||
if (tapLength > 0) {
|
||||
if (tapLength < this.tapMinTimeout) {
|
||||
this.runHandler('tap', {});
|
||||
} else {
|
||||
this.runHandler('longtap', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private detectPinch(event: TouchEvent): boolean {
|
||||
const touches = event.touches;
|
||||
return (touches.length === 2 && this.eventType === undefined) || this.eventType === 'pinch';
|
||||
}
|
||||
|
||||
private detectLinearSwipe(event: MouseEvent | TouchEvent): 'vertical-swipe' | 'horizontal-swipe' | undefined {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
if ((touches.length === 1 && !this.eventType) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
return this.getLinearSwipeType(event);
|
||||
}
|
||||
} else {
|
||||
if (!this.eventType || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') {
|
||||
return this.getLinearSwipeType(event);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getLinearSwipeType(event: TouchEvent | MouseEvent): 'vertical-swipe' | 'horizontal-swipe' {
|
||||
if (this.eventType !== 'horizontal-swipe' && this.eventType !== 'vertical-swipe') {
|
||||
const movementX = Math.abs(this.moveLeft(0, event) - this.startX);
|
||||
const movementY = Math.abs(this.moveTop(0, event) - this.startY);
|
||||
|
||||
if (movementY * 3 > movementX) {
|
||||
return 'vertical-swipe';
|
||||
} else {
|
||||
return 'horizontal-swipe';
|
||||
}
|
||||
} else {
|
||||
return this.eventType;
|
||||
}
|
||||
}
|
||||
|
||||
private getElementPosition(): DOMRect {
|
||||
return this.element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
private getTouchstartPosition(event: TouchEvent): void {
|
||||
this.startX = event.touches[0].clientX - this.elementPosition.left;
|
||||
this.startY = event.touches[0].clientY - this.elementPosition.top;
|
||||
}
|
||||
|
||||
private getMousedownPosition(event: MouseEvent): void {
|
||||
this.startX = event.clientX - this.elementPosition.left;
|
||||
this.startY = event.clientY - this.elementPosition.top;
|
||||
}
|
||||
|
||||
private moveLeft(index: number, event: TouchEvent | MouseEvent): number {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
return touches[index].clientX - this.elementPosition.left;
|
||||
} else {
|
||||
return (event as MouseEvent).clientX - this.elementPosition.left;
|
||||
}
|
||||
}
|
||||
|
||||
private moveTop(index: number, event: TouchEvent | MouseEvent): number {
|
||||
const touches = (event as TouchEvent).touches;
|
||||
|
||||
if (touches) {
|
||||
return touches[index].clientY - this.elementPosition.top;
|
||||
} else {
|
||||
return (event as MouseEvent).clientY - this.elementPosition.top;
|
||||
}
|
||||
}
|
||||
|
||||
private detectTouchScreen(): boolean {
|
||||
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ');
|
||||
const mq = (query: string): boolean => {
|
||||
return window.matchMedia(query).matches;
|
||||
};
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
|
||||
// https://git.io/vznFH
|
||||
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('');
|
||||
return mq(query);
|
||||
}
|
||||
|
||||
/* Public properties and methods */
|
||||
public on(event: EventType, handler: (event: Event) => void): void {
|
||||
if (event) {
|
||||
(this.handlers as any)[event] = handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @deprecated This file has been moved to services/touches.service.ts
|
||||
* This re-export maintains backward compatibility.
|
||||
* Please update your imports to use:
|
||||
* import { Touches, EventType } from '@meddv/ngx-pinch-zoom';
|
||||
* or
|
||||
* import { Touches, EventType } from 'ngx-pinch-zoom/lib/services';
|
||||
*/
|
||||
export * from './services/touches.service';
|
||||
|
|
|
|||
|
|
@ -2,15 +2,11 @@
|
|||
* Public API Surface of ngx-pinch-zoom
|
||||
*/
|
||||
|
||||
// Legacy interfaces (for backward compatibility)
|
||||
export * from './lib/interfaces';
|
||||
|
||||
// Models
|
||||
// Models (includes legacy interfaces for backward compatibility)
|
||||
export * from './lib/models';
|
||||
|
||||
// Services
|
||||
export * from './lib/services';
|
||||
|
||||
// Components
|
||||
export * from './lib/pinch-zoom.component';
|
||||
export * from './lib/components';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue