mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:31:09 -05:00
Add park and ride card components with advanced search functionality
- Implemented park card component with image, status badge, favorite button, and quick stats overlay. - Developed ride card component featuring thrill level badge, status badge, favorite button, and detailed stats. - Created advanced search page with filters for parks and rides, including location, type, status, and thrill level. - Added dynamic quick search functionality with results display. - Enhanced user experience with JavaScript for filter toggling, range slider updates, and view switching. - Included custom CSS for improved styling of checkboxes and search results layout.
This commit is contained in:
881
static/js/backup/mobile-touch.js
Normal file
881
static/js/backup/mobile-touch.js
Normal file
@@ -0,0 +1,881 @@
|
||||
/**
|
||||
* ThrillWiki Mobile Touch Support - Enhanced Mobile and Touch Experience
|
||||
*
|
||||
* This module provides mobile-optimized interactions, touch-friendly controls,
|
||||
* responsive map sizing, and battery-conscious features for mobile devices
|
||||
*/
|
||||
|
||||
class MobileTouchSupport {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
enableTouchOptimizations: true,
|
||||
enableSwipeGestures: true,
|
||||
enablePinchZoom: true,
|
||||
enableResponsiveResize: true,
|
||||
enableBatteryOptimization: true,
|
||||
touchDebounceDelay: 150,
|
||||
swipeThreshold: 50,
|
||||
swipeVelocityThreshold: 0.3,
|
||||
maxTouchPoints: 2,
|
||||
orientationChangeDelay: 300,
|
||||
...options
|
||||
};
|
||||
|
||||
this.isMobile = this.detectMobileDevice();
|
||||
this.isTouch = this.detectTouchSupport();
|
||||
this.orientation = this.getOrientation();
|
||||
this.mapInstances = new Set();
|
||||
this.touchHandlers = new Map();
|
||||
this.gestureState = {
|
||||
isActive: false,
|
||||
startDistance: 0,
|
||||
startCenter: null,
|
||||
lastTouchTime: 0
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mobile touch support
|
||||
*/
|
||||
init() {
|
||||
if (!this.isTouch && !this.isMobile) {
|
||||
console.log('Mobile touch support not needed for this device');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupTouchOptimizations();
|
||||
this.setupSwipeGestures();
|
||||
this.setupResponsiveHandling();
|
||||
this.setupBatteryOptimization();
|
||||
this.setupAccessibilityEnhancements();
|
||||
this.bindEventHandlers();
|
||||
|
||||
console.log('Mobile touch support initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if device is mobile
|
||||
*/
|
||||
detectMobileDevice() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone'];
|
||||
|
||||
return mobileKeywords.some(keyword => userAgent.includes(keyword)) ||
|
||||
window.innerWidth <= 768 ||
|
||||
(typeof window.orientation !== 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect touch support
|
||||
*/
|
||||
detectTouchSupport() {
|
||||
return 'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current orientation
|
||||
*/
|
||||
getOrientation() {
|
||||
if (screen.orientation) {
|
||||
return screen.orientation.angle;
|
||||
} else if (window.orientation !== undefined) {
|
||||
return window.orientation;
|
||||
}
|
||||
return window.innerWidth > window.innerHeight ? 90 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup touch optimizations
|
||||
*/
|
||||
setupTouchOptimizations() {
|
||||
if (!this.options.enableTouchOptimizations) return;
|
||||
|
||||
// Add touch-optimized styles
|
||||
this.addTouchStyles();
|
||||
|
||||
// Enhance touch targets
|
||||
this.enhanceTouchTargets();
|
||||
|
||||
// Optimize scroll behavior
|
||||
this.optimizeScrollBehavior();
|
||||
|
||||
// Setup touch feedback
|
||||
this.setupTouchFeedback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add touch-optimized CSS styles
|
||||
*/
|
||||
addTouchStyles() {
|
||||
if (document.getElementById('mobile-touch-styles')) return;
|
||||
|
||||
const styles = `
|
||||
<style id="mobile-touch-styles">
|
||||
@media (max-width: 768px) {
|
||||
/* Touch-friendly button sizes */
|
||||
.btn, button, .filter-chip, .filter-pill {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Larger touch targets for map controls */
|
||||
.leaflet-control-zoom a {
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* Mobile-optimized map containers */
|
||||
.map-container {
|
||||
height: 60vh !important;
|
||||
min-height: 300px !important;
|
||||
}
|
||||
|
||||
/* Touch-friendly popup styling */
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 16px 20px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Improved form controls */
|
||||
input, select, textarea {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Touch-friendly filter panels */
|
||||
.filter-panel {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Mobile navigation improvements */
|
||||
.roadtrip-planner {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.parks-list .park-item {
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Swipe indicators */
|
||||
.swipe-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 40px;
|
||||
background: rgba(59, 130, 246, 0.5);
|
||||
border-radius: 2px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.swipe-indicator.left {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.swipe-indicator.right {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Extra small screens */
|
||||
.map-container {
|
||||
height: 50vh !important;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch feedback */
|
||||
.touch-feedback {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.touch-feedback::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.touch-feedback.active::after {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* Prevent text selection on mobile */
|
||||
.no-select {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Optimize touch scrolling */
|
||||
.touch-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
document.head.insertAdjacentHTML('beforeend', styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance touch targets for better accessibility
|
||||
*/
|
||||
enhanceTouchTargets() {
|
||||
const smallTargets = document.querySelectorAll('button, .btn, a, input[type="checkbox"], input[type="radio"]');
|
||||
|
||||
smallTargets.forEach(target => {
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
// If target is smaller than 44px (Apple's recommended minimum), enhance it
|
||||
if (rect.width < 44 || rect.height < 44) {
|
||||
target.classList.add('touch-enhanced');
|
||||
target.style.minWidth = '44px';
|
||||
target.style.minHeight = '44px';
|
||||
target.style.display = 'inline-flex';
|
||||
target.style.alignItems = 'center';
|
||||
target.style.justifyContent = 'center';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize scroll behavior for mobile
|
||||
*/
|
||||
optimizeScrollBehavior() {
|
||||
// Add momentum scrolling to scrollable elements
|
||||
const scrollableElements = document.querySelectorAll('.scrollable, .overflow-auto, .overflow-y-auto');
|
||||
|
||||
scrollableElements.forEach(element => {
|
||||
element.classList.add('touch-scroll');
|
||||
element.style.webkitOverflowScrolling = 'touch';
|
||||
});
|
||||
|
||||
// Prevent body scroll when interacting with maps
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
if (e.target.closest('.leaflet-container')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup touch feedback for interactive elements
|
||||
*/
|
||||
setupTouchFeedback() {
|
||||
const interactiveElements = document.querySelectorAll('button, .btn, .filter-chip, .filter-pill, .park-item');
|
||||
|
||||
interactiveElements.forEach(element => {
|
||||
element.classList.add('touch-feedback');
|
||||
|
||||
element.addEventListener('touchstart', (e) => {
|
||||
element.classList.add('active');
|
||||
|
||||
setTimeout(() => {
|
||||
element.classList.remove('active');
|
||||
}, 300);
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup swipe gesture support
|
||||
*/
|
||||
setupSwipeGestures() {
|
||||
if (!this.options.enableSwipeGestures) return;
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchStartTime = 0;
|
||||
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
if (e.touches.length === 1) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
touchStartTime = Date.now();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', (e) => {
|
||||
if (e.changedTouches.length === 1) {
|
||||
const touchEndX = e.changedTouches[0].clientX;
|
||||
const touchEndY = e.changedTouches[0].clientY;
|
||||
const touchEndTime = Date.now();
|
||||
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
const deltaY = touchEndY - touchStartY;
|
||||
const deltaTime = touchEndTime - touchStartTime;
|
||||
const velocity = Math.abs(deltaX) / deltaTime;
|
||||
|
||||
// Check if this is a swipe gesture
|
||||
if (Math.abs(deltaX) > this.options.swipeThreshold &&
|
||||
Math.abs(deltaY) < Math.abs(deltaX) &&
|
||||
velocity > this.options.swipeVelocityThreshold) {
|
||||
|
||||
const direction = deltaX > 0 ? 'right' : 'left';
|
||||
this.handleSwipeGesture(direction, e.target);
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe gestures
|
||||
*/
|
||||
handleSwipeGesture(direction, target) {
|
||||
// Handle swipe on filter panels
|
||||
if (target.closest('.filter-panel')) {
|
||||
if (direction === 'down' || direction === 'up') {
|
||||
this.toggleFilterPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle swipe on road trip list
|
||||
if (target.closest('.parks-list')) {
|
||||
if (direction === 'left') {
|
||||
this.showParkActions(target);
|
||||
} else if (direction === 'right') {
|
||||
this.hideParkActions(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit custom swipe event
|
||||
const swipeEvent = new CustomEvent('swipe', {
|
||||
detail: { direction, target }
|
||||
});
|
||||
document.dispatchEvent(swipeEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup responsive handling for orientation changes
|
||||
*/
|
||||
setupResponsiveHandling() {
|
||||
if (!this.options.enableResponsiveResize) return;
|
||||
|
||||
// Handle orientation changes
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
this.handleOrientationChange();
|
||||
}, this.options.orientationChangeDelay);
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
this.handleWindowResize();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Handle viewport changes (for mobile browsers with dynamic toolbars)
|
||||
this.setupViewportHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle orientation change
|
||||
*/
|
||||
handleOrientationChange() {
|
||||
const newOrientation = this.getOrientation();
|
||||
|
||||
if (newOrientation !== this.orientation) {
|
||||
this.orientation = newOrientation;
|
||||
|
||||
// Resize all map instances
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.invalidateSize) {
|
||||
mapInstance.invalidateSize();
|
||||
}
|
||||
});
|
||||
|
||||
// Emit orientation change event
|
||||
const orientationEvent = new CustomEvent('orientationChanged', {
|
||||
detail: { orientation: this.orientation }
|
||||
});
|
||||
document.dispatchEvent(orientationEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize
|
||||
*/
|
||||
handleWindowResize() {
|
||||
// Update mobile detection
|
||||
this.isMobile = this.detectMobileDevice();
|
||||
|
||||
// Resize map instances
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.invalidateSize) {
|
||||
mapInstance.invalidateSize();
|
||||
}
|
||||
});
|
||||
|
||||
// Update touch targets
|
||||
this.enhanceTouchTargets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup viewport handler for dynamic mobile toolbars
|
||||
*/
|
||||
setupViewportHandler() {
|
||||
// Use visual viewport API if available
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', () => {
|
||||
this.handleViewportChange();
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for older browsers
|
||||
let lastHeight = window.innerHeight;
|
||||
|
||||
const checkViewportChange = () => {
|
||||
if (Math.abs(window.innerHeight - lastHeight) > 100) {
|
||||
lastHeight = window.innerHeight;
|
||||
this.handleViewportChange();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', checkViewportChange);
|
||||
document.addEventListener('focusin', checkViewportChange);
|
||||
document.addEventListener('focusout', checkViewportChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle viewport changes
|
||||
*/
|
||||
handleViewportChange() {
|
||||
// Adjust map container heights
|
||||
const mapContainers = document.querySelectorAll('.map-container');
|
||||
mapContainers.forEach(container => {
|
||||
const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
||||
|
||||
if (viewportHeight < 500) {
|
||||
container.style.height = '40vh';
|
||||
} else {
|
||||
container.style.height = ''; // Reset to CSS default
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup battery optimization
|
||||
*/
|
||||
setupBatteryOptimization() {
|
||||
if (!this.options.enableBatteryOptimization) return;
|
||||
|
||||
// Reduce update frequency when battery is low
|
||||
if ('getBattery' in navigator) {
|
||||
navigator.getBattery().then(battery => {
|
||||
const optimizeBattery = () => {
|
||||
if (battery.level < 0.2) { // Battery below 20%
|
||||
this.enableBatterySaveMode();
|
||||
} else {
|
||||
this.disableBatterySaveMode();
|
||||
}
|
||||
};
|
||||
|
||||
battery.addEventListener('levelchange', optimizeBattery);
|
||||
optimizeBattery();
|
||||
});
|
||||
}
|
||||
|
||||
// Reduce activity when page is not visible
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.pauseNonEssentialFeatures();
|
||||
} else {
|
||||
this.resumeNonEssentialFeatures();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable battery save mode
|
||||
*/
|
||||
enableBatterySaveMode() {
|
||||
console.log('Enabling battery save mode');
|
||||
|
||||
// Reduce map update frequency
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.options) {
|
||||
mapInstance.options.updateInterval = 5000; // Increase to 5 seconds
|
||||
}
|
||||
});
|
||||
|
||||
// Disable animations
|
||||
document.body.classList.add('battery-save-mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable battery save mode
|
||||
*/
|
||||
disableBatterySaveMode() {
|
||||
console.log('Disabling battery save mode');
|
||||
|
||||
// Restore normal update frequency
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.options) {
|
||||
mapInstance.options.updateInterval = 1000; // Restore to 1 second
|
||||
}
|
||||
});
|
||||
|
||||
// Re-enable animations
|
||||
document.body.classList.remove('battery-save-mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause non-essential features
|
||||
*/
|
||||
pauseNonEssentialFeatures() {
|
||||
// Pause location watching
|
||||
if (window.userLocation && window.userLocation.stopWatching) {
|
||||
window.userLocation.stopWatching();
|
||||
}
|
||||
|
||||
// Reduce map updates
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.pauseUpdates) {
|
||||
mapInstance.pauseUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume non-essential features
|
||||
*/
|
||||
resumeNonEssentialFeatures() {
|
||||
// Resume location watching if it was active
|
||||
if (window.userLocation && window.userLocation.options.watchPosition) {
|
||||
window.userLocation.startWatching();
|
||||
}
|
||||
|
||||
// Resume map updates
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.resumeUpdates) {
|
||||
mapInstance.resumeUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup accessibility enhancements for mobile
|
||||
*/
|
||||
setupAccessibilityEnhancements() {
|
||||
// Add focus indicators for touch navigation
|
||||
const focusableElements = document.querySelectorAll('button, a, input, select, textarea, [tabindex]');
|
||||
|
||||
focusableElements.forEach(element => {
|
||||
element.addEventListener('focus', () => {
|
||||
element.classList.add('touch-focused');
|
||||
});
|
||||
|
||||
element.addEventListener('blur', () => {
|
||||
element.classList.remove('touch-focused');
|
||||
});
|
||||
});
|
||||
|
||||
// Enhance keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
document.body.classList.add('keyboard-navigation');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', () => {
|
||||
document.body.classList.remove('keyboard-navigation');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind event handlers
|
||||
*/
|
||||
bindEventHandlers() {
|
||||
// Handle double-tap to zoom
|
||||
this.setupDoubleTapZoom();
|
||||
|
||||
// Handle long press
|
||||
this.setupLongPress();
|
||||
|
||||
// Handle pinch gestures
|
||||
if (this.options.enablePinchZoom) {
|
||||
this.setupPinchZoom();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup double-tap to zoom
|
||||
*/
|
||||
setupDoubleTapZoom() {
|
||||
let lastTapTime = 0;
|
||||
|
||||
document.addEventListener('touchend', (e) => {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - lastTapTime < 300) {
|
||||
// Double tap detected
|
||||
const target = e.target;
|
||||
if (target.closest('.leaflet-container')) {
|
||||
this.handleDoubleTapZoom(e);
|
||||
}
|
||||
}
|
||||
|
||||
lastTapTime = currentTime;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle double-tap zoom
|
||||
*/
|
||||
handleDoubleTapZoom(e) {
|
||||
const mapContainer = e.target.closest('.leaflet-container');
|
||||
if (!mapContainer) return;
|
||||
|
||||
// Find associated map instance
|
||||
this.mapInstances.forEach(mapInstance => {
|
||||
if (mapInstance.getContainer() === mapContainer) {
|
||||
const currentZoom = mapInstance.getZoom();
|
||||
const newZoom = currentZoom < mapInstance.getMaxZoom() ? currentZoom + 2 : mapInstance.getMinZoom();
|
||||
|
||||
mapInstance.setZoom(newZoom, {
|
||||
animate: true,
|
||||
duration: 0.3
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup long press detection
|
||||
*/
|
||||
setupLongPress() {
|
||||
let pressTimer;
|
||||
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
pressTimer = setTimeout(() => {
|
||||
this.handleLongPress(e);
|
||||
}, 750); // 750ms for long press
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', () => {
|
||||
clearTimeout(pressTimer);
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', () => {
|
||||
clearTimeout(pressTimer);
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle long press
|
||||
*/
|
||||
handleLongPress(e) {
|
||||
const target = e.target;
|
||||
|
||||
// Emit long press event
|
||||
const longPressEvent = new CustomEvent('longPress', {
|
||||
detail: { target, touches: e.touches }
|
||||
});
|
||||
target.dispatchEvent(longPressEvent);
|
||||
|
||||
// Provide haptic feedback if available
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup pinch zoom for maps
|
||||
*/
|
||||
setupPinchZoom() {
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
if (e.touches.length === 2) {
|
||||
this.gestureState.isActive = true;
|
||||
this.gestureState.startDistance = this.getDistance(e.touches[0], e.touches[1]);
|
||||
this.gestureState.startCenter = this.getCenter(e.touches[0], e.touches[1]);
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (this.gestureState.isActive && e.touches.length === 2) {
|
||||
this.handlePinchZoom(e);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
document.addEventListener('touchend', () => {
|
||||
this.gestureState.isActive = false;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pinch zoom gesture
|
||||
*/
|
||||
handlePinchZoom(e) {
|
||||
if (!e.target.closest('.leaflet-container')) return;
|
||||
|
||||
const currentDistance = this.getDistance(e.touches[0], e.touches[1]);
|
||||
const scale = currentDistance / this.gestureState.startDistance;
|
||||
|
||||
// Emit pinch event
|
||||
const pinchEvent = new CustomEvent('pinch', {
|
||||
detail: { scale, center: this.gestureState.startCenter }
|
||||
});
|
||||
e.target.dispatchEvent(pinchEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distance between two touch points
|
||||
*/
|
||||
getDistance(touch1, touch2) {
|
||||
const dx = touch1.clientX - touch2.clientX;
|
||||
const dy = touch1.clientY - touch2.clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get center point between two touches
|
||||
*/
|
||||
getCenter(touch1, touch2) {
|
||||
return {
|
||||
x: (touch1.clientX + touch2.clientX) / 2,
|
||||
y: (touch1.clientY + touch2.clientY) / 2
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register map instance for mobile optimizations
|
||||
*/
|
||||
registerMapInstance(mapInstance) {
|
||||
this.mapInstances.add(mapInstance);
|
||||
|
||||
// Apply mobile-specific map options
|
||||
if (this.isMobile && mapInstance.options) {
|
||||
mapInstance.options.zoomControl = false; // Use custom larger controls
|
||||
mapInstance.options.attributionControl = false; // Save space
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister map instance
|
||||
*/
|
||||
unregisterMapInstance(mapInstance) {
|
||||
this.mapInstances.delete(mapInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filter panel for mobile
|
||||
*/
|
||||
toggleFilterPanel() {
|
||||
const filterPanel = document.querySelector('.filter-panel');
|
||||
if (filterPanel) {
|
||||
filterPanel.classList.toggle('mobile-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show park actions on swipe
|
||||
*/
|
||||
showParkActions(target) {
|
||||
const parkItem = target.closest('.park-item');
|
||||
if (parkItem) {
|
||||
parkItem.classList.add('actions-visible');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide park actions
|
||||
*/
|
||||
hideParkActions(target) {
|
||||
const parkItem = target.closest('.park-item');
|
||||
if (parkItem) {
|
||||
parkItem.classList.remove('actions-visible');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is mobile
|
||||
*/
|
||||
isMobileDevice() {
|
||||
return this.isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device supports touch
|
||||
*/
|
||||
isTouchDevice() {
|
||||
return this.isTouch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device info
|
||||
*/
|
||||
getDeviceInfo() {
|
||||
return {
|
||||
isMobile: this.isMobile,
|
||||
isTouch: this.isTouch,
|
||||
orientation: this.orientation,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
pixelRatio: window.devicePixelRatio || 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize mobile touch support
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.mobileTouchSupport = new MobileTouchSupport();
|
||||
|
||||
// Register existing map instances
|
||||
if (window.thrillwikiMap) {
|
||||
window.mobileTouchSupport.registerMapInstance(window.thrillwikiMap);
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = MobileTouchSupport;
|
||||
} else {
|
||||
window.MobileTouchSupport = MobileTouchSupport;
|
||||
}
|
||||
Reference in New Issue
Block a user