UNPKG

hy-masonry

Version:

Animated Masonry Layout as Web Component - Living organisms that morph and breathe

248 lines (212 loc) 7.07 kB
import { MasonryItem } from '../types'; export class InteractionHandler { private longPressTimers: Map<string, number> = new Map(); private touchStartPositions: Map<string, { x: number; y: number }> = new Map(); private longPressDelay: number = 500; private touchEnabled: boolean = true; constructor(config: { longPressDelay?: number; touchEnabled?: boolean } = {}) { this.longPressDelay = config.longPressDelay || 500; this.touchEnabled = config.touchEnabled !== false; } /** * Setup event listeners for an item */ setupItemListeners(item: MasonryItem, element: HTMLElement): void { // Mouse events if (item.hoverable !== false) { element.addEventListener('mouseenter', (e) => this.handleMouseEnter(e, item)); element.addEventListener('mouseleave', (e) => this.handleMouseLeave(e, item)); } // Click events if (item.clickable !== false) { element.addEventListener('click', (e) => this.handleClick(e, item)); } // Touch events if (this.touchEnabled) { element.addEventListener('touchstart', (e) => this.handleTouchStart(e, item)); element.addEventListener('touchend', (e) => this.handleTouchEnd(e, item)); element.addEventListener('touchmove', (e) => this.handleTouchMove(e, item)); } // Keyboard events element.addEventListener('keydown', (e) => this.handleKeyDown(e, item)); element.setAttribute('tabindex', '0'); } /** * Handle mouse enter */ private handleMouseEnter(event: Event, item: MasonryItem): void { const element = event.target as HTMLElement; element.classList.add('hover'); if (item.morphOnHover && item.morphSize) { this.dispatchMorphEvent(element, item.morphSize, item.morphDuration || 300); } if (item.onHover) { item.onHover(event, item); } this.dispatchEvent('item:hover', { item, element }); } /** * Handle mouse leave */ private handleMouseLeave(event: Event, item: MasonryItem): void { const element = event.target as HTMLElement; element.classList.remove('hover'); // Reset to original size if morphed if (item.morphOnHover && item.morphSize) { const originalSize = element.getAttribute('data-original-size') || item.size; this.dispatchMorphEvent(element, originalSize, item.morphDuration || 300); } this.dispatchEvent('item:hover-end', { item, element }); } /** * Handle click */ private handleClick(event: Event, item: MasonryItem): void { if (item.onClick) { item.onClick(event, item); } this.dispatchEvent('item:click', { item, event }); } /** * Handle touch start */ private handleTouchStart(event: TouchEvent, item: MasonryItem): void { const touch = event.touches[0]; const element = event.target as HTMLElement; const itemId = element.getAttribute('data-item-id'); if (itemId) { this.touchStartPositions.set(itemId, { x: touch.clientX, y: touch.clientY }); // Start long press timer const timer = window.setTimeout(() => { this.handleLongPress(event, item); }, this.longPressDelay); this.longPressTimers.set(itemId, timer); } } /** * Handle touch end */ private handleTouchEnd(event: TouchEvent, _item: MasonryItem): void { const element = event.target as HTMLElement; const itemId = element.getAttribute('data-item-id'); if (itemId) { // Clear long press timer const timer = this.longPressTimers.get(itemId); if (timer) { clearTimeout(timer); this.longPressTimers.delete(itemId); } this.touchStartPositions.delete(itemId); } } /** * Handle touch move */ private handleTouchMove(event: TouchEvent, _item: MasonryItem): void { const element = event.target as HTMLElement; const itemId = element.getAttribute('data-item-id'); if (itemId) { const startPos = this.touchStartPositions.get(itemId); if (startPos) { const touch = event.touches[0]; const deltaX = Math.abs(touch.clientX - startPos.x); const deltaY = Math.abs(touch.clientY - startPos.y); // If moved significantly, cancel long press if (deltaX > 10 || deltaY > 10) { const timer = this.longPressTimers.get(itemId); if (timer) { clearTimeout(timer); this.longPressTimers.delete(itemId); } } } } } /** * Handle long press */ private handleLongPress(event: Event, item: MasonryItem): void { const element = event.target as HTMLElement; if (item.morphOnLongPress && item.morphSize) { this.dispatchMorphEvent(element, item.morphSize, item.morphDuration || 500); } if (item.onLongPress) { item.onLongPress(event, item); } this.dispatchEvent('item:longpress', { item, element }); } /** * Handle key down */ private handleKeyDown(event: KeyboardEvent, item: MasonryItem): void { const element = event.target as HTMLElement; switch (event.key) { case 'Enter': case ' ': event.preventDefault(); this.handleClick(event, item); break; case 'Escape': // Reset morphing if active if (item.morphOnHover && item.morphSize) { const originalSize = element.getAttribute('data-original-size') || item.size; this.dispatchMorphEvent(element, originalSize, item.morphDuration || 300); } break; } } /** * Dispatch morph event */ private dispatchMorphEvent(element: HTMLElement, targetSize: string, duration: number): void { const event = new CustomEvent('item:morph-request', { detail: { element, targetSize, duration }, bubbles: true, cancelable: true }); element.dispatchEvent(event); } /** * Dispatch custom event */ private dispatchEvent(eventName: string, detail: any): void { const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true }); document.dispatchEvent(event); } /** * Clean up event listeners */ cleanupItemListeners(element: HTMLElement): void { const itemId = element.getAttribute('data-item-id'); if (itemId) { const timer = this.longPressTimers.get(itemId); if (timer) { clearTimeout(timer); this.longPressTimers.delete(itemId); } this.touchStartPositions.delete(itemId); } } /** * Get active timers count */ getActiveTimersCount(): number { return this.longPressTimers.size; } /** * Clear all timers */ clearAllTimers(): void { this.longPressTimers.forEach(timer => clearTimeout(timer)); this.longPressTimers.clear(); this.touchStartPositions.clear(); } }