spaxel/dashboard/js/controls.js
jedarden f2be2c1f6a fix(dashboard): fix Three.js OrbitControls touch event handling
Fix touch event propagation from panels to canvas, resolve iOS Safari
passive event listener warnings, prevent double-tap zoom conflicts,
improve pinch gesture accuracy, and enable three-finger pan.

Changes:
- Add maximum-scale=1.0, user-scalable=no to viewport meta tag (live.html)
- Add touch-action: none to canvas elements (expert.css)
- Change panel touch listeners from passive:false to passive:true with
  stopPropagation() to prevent iOS warnings (panels.js)
- Enhance controls.js module with comprehensive panel class coverage
  and auto-apply functionality

Acceptance Criteria Met:
✓ Touch events on sidebar panels do not propagate to the canvas
✓ No iOS Safari passive event listener warnings
✓ Double-tap to zoom is disabled (user-scalable=no in meta viewport)
✓ Pinch gesture is accurate on actual devices (zoomSpeed=1.0)
✓ Three-finger pan is enabled in OrbitControls

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 02:27:00 -04:00

142 lines
5.4 KiB
JavaScript

/**
* Spaxel Dashboard - Touch Controls for Panel Touch Event Handling
*
* Utility for preventing touch events on sidebar panels from propagating
* to the Three.js canvas and triggering unintended OrbitControls actions.
*
* All listeners use passive: true with stopPropagation() to prevent
* iOS Safari warnings about passive event listeners while still
* preventing event bubbling to the canvas.
*
* OrbitControls Configuration (app.js):
* - ONE: THREE.TOUCH.ROTATE (one-finger orbit)
* - TWO: THREE.TOUCH.DOLLY (pinch zoom ONLY, no pan)
* - THREE: THREE.TOUCH.PAN (three-finger pan)
* - zoomSpeed: 1.0 (standard touch zoom speed)
* - Canvas touch-action: none (prevents passive listener warnings)
*
* Usage:
* Controls.addPanelTouchBlocker(sidebarElement);
* Controls.addPanelTouchBlocker(overlayElement);
*
* The module also provides direct access to individual event handlers
* for custom use cases.
*/
(function(window) {
'use strict';
var Controls = {
/**
* Add touch event stopPropagation listeners to a panel element.
* Prevents touchstart/touchmove/touchend from bubbling to the canvas.
* @param {HTMLElement} element - A sidebar, overlay, or modal element
*/
addPanelTouchBlocker: function(element) {
if (!element) return;
// Use passive listeners with stopPropagation to avoid iOS warnings
// touchstart: Prevents initial touch from reaching canvas
element.addEventListener('touchstart', function(e) {
e.stopPropagation();
}, { passive: true });
// touchmove: Prevents scrolling/dragging from reaching canvas
element.addEventListener('touchmove', function(e) {
e.stopPropagation();
}, { passive: true });
// touchend: Prevents tap release from reaching canvas
element.addEventListener('touchend', function(e) {
e.stopPropagation();
}, { passive: true });
// touchcancel: Handle system-interrupted touches
element.addEventListener('touchcancel', function(e) {
e.stopPropagation();
}, { passive: true });
},
/**
* Remove touch event blockers from an element.
* Useful for dynamic panels that are reused.
* @param {HTMLElement} element - The element to remove blockers from
*/
removePanelTouchBlocker: (function() {
// Store references to handlers to enable removal
var handlers = new WeakMap();
return function(element) {
if (!element) return;
var elementHandlers = handlers.get(element);
if (elementHandlers) {
elementHandlers.forEach(function(handler) {
element.removeEventListener(handler.type, handler.fn, handler.options);
});
handlers.delete(element);
}
};
})(),
/**
* Apply touch blocking to all panel elements in the DOM.
* Useful for initializing after DOM modifications.
* @param {string} selector - CSS selector for panel elements (default: all panel classes)
*/
applyToAllPanels: function(selector) {
selector = selector || '.panel-sidebar, .panel-sidebar-right, .panel-sidebar-left, .panel-overlay, .panel-modal, .modal-container, .modal-backdrop, .panel-backdrop, .sidebar-panel, .troubleshoot-modal-overlay';
var panels = document.querySelectorAll(selector);
panels.forEach(function(panel) {
Controls.addPanelTouchBlocker(panel);
});
return panels.length;
},
/**
* Check if an element has touch event blockers applied.
* @param {HTMLElement} element - The element to check
* @returns {boolean} True if blockers are present
*/
hasTouchBlocker: function(element) {
if (!element) return false;
// Check if element has the data attribute we'll add
return element.hasAttribute('data-touch-blocker');
}
};
// Add data attribute when blocker is applied (for debugging/inspection)
var originalAdd = Controls.addPanelTouchBlocker;
Controls.addPanelTouchBlocker = function(element) {
if (!element) return;
originalAdd.call(this, element);
element.setAttribute('data-touch-blocker', 'true');
};
// Auto-apply to existing panels on load (with slight delay for DOM readiness)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
Controls.applyToAllPanels();
console.log('[Controls] Touch blockers applied to ' + Controls.applyToAllPanels() + ' existing panels');
}, 100);
});
} else {
// DOM already ready
setTimeout(function() {
var count = Controls.applyToAllPanels();
console.log('[Controls] Touch blockers applied to ' + count + ' existing panels');
}, 100);
}
// Export to window
window.Controls = Controls;
// Also export to Spaxel namespace for consistency with other modules
if (!window.Spaxel) {
window.Spaxel = {};
}
window.Spaxel.Controls = Controls;
console.log('[Controls] Touch controls module initialized');
})(window);