/** * AKSRT Mini Touch — General Direction Gesture Engine * * Full direction gesture (top/bottom/left/right) via atan2 angle filtering, * real-time translate3d follow + cursor, onStart/onIng/onEnd callbacks, * compatible with swiper/scroll-x containers. * * Supports: * - [data-aksrt-swipe] left/right swipe triggers * - [data-aksrt-pull-dismiss] pull-to-dismiss with opacity tracking * - [data-mini-touch="mobile-nav"] mobile menu right-swipe close * - aksrt.minitouch(el, opts) programmatic API (from aksrt-runtime.js) * * @package AKSRT */ (function () { 'use strict'; var MIN_SWIPE_DISTANCE = 40; var MAX_SWIPE_TIME = 300; function getDirection(angle) { if (angle >= -45 && angle < 45) return 'right'; if (angle >= 45 && angle < 135) return 'bottom'; if (angle >= -135 && angle < -45) return 'top'; return 'left'; } // ── Declarative swipe: [data-aksrt-swipe] ── function bindSwipe(container) { if (container.dataset.aksrtSwipeBound) return; container.dataset.aksrtSwipeBound = '1'; var startX = 0, startY = 0, startTime = 0; var moving = false; container.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; startX = e.touches[0].clientX; startY = e.touches[0].clientY; startTime = Date.now(); moving = true; }, { passive: true }); container.addEventListener('touchmove', function (e) { if (!moving) return; var dx = e.touches[0].clientX - startX; var dy = e.touches[0].clientY - startY; if (Math.abs(dy) > Math.abs(dx) * 1.5) moving = false; }, { passive: true }); container.addEventListener('touchend', function (e) { if (!moving) return; moving = false; var ct = e.changedTouches ? e.changedTouches[0] : e; var dx = ct.clientX - startX; var dt = Date.now() - startTime; if (dt > MAX_SWIPE_TIME || Math.abs(dx) < MIN_SWIPE_DISTANCE) return; if (Math.abs(ct.clientY - startY) > Math.abs(dx)) return; var direction = dx > 0 ? 'right' : 'left'; var selector = container.getAttribute('data-aksrt-swipe-' + direction); if (selector) { var target = container.querySelector(selector) || document.querySelector(selector); if (target) { target.click(); return; } } container.dispatchEvent(new CustomEvent('aksrt:swipe', { detail: { direction: direction, distance: Math.abs(dx) }, bubbles: true })); }, { passive: true }); } // ── Pull to dismiss with real-time opacity tracking ── function bindPullDismiss(el) { if (el.dataset.aksrtPullBound) return; el.dataset.aksrtPullBound = '1'; var startY = 0, currentY = 0, pulling = false; el.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; // Disable pull when zoomed (scale > 1) var img = el.querySelector('img'); if (img) { var transform = getComputedStyle(img).transform; if (transform && transform !== 'none') { var parts = transform.match(/matrix\(([^)]+)\)/); if (parts) { var vals = parts[1].split(',').map(Number); var scaleX = Math.sqrt(vals[0] * vals[0] + vals[1] * vals[1]); if (scaleX > 1.05) return; } } } if (el.scrollTop > 0) return; startY = e.touches[0].clientY; currentY = startY; pulling = true; }, { passive: true }); el.addEventListener('touchmove', function (e) { if (!pulling) return; currentY = e.touches[0].clientY; var dy = currentY - startY; if (dy > 0) { var opacity = Math.max(0.2, (150 - dy) / 80); el.style.transform = 'translate3d(0, ' + (dy * 0.5) + 'px, 0)'; el.style.opacity = String(Math.min(1, opacity)); } }, { passive: true }); el.addEventListener('touchend', function () { if (!pulling) return; pulling = false; var dy = currentY - startY; if (dy > 120) { el.style.transform = 'translate3d(0, ' + (dy + 100) + 'px, 0)'; el.style.opacity = '0'; el.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out'; setTimeout(function () { el.dispatchEvent(new CustomEvent('aksrt:pull-dismiss', { bubbles: true })); el.style.transform = ''; el.style.opacity = ''; el.style.transition = ''; }, 200); } else { el.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out'; el.style.transform = ''; el.style.opacity = ''; setTimeout(function () { el.style.transition = ''; }, 200); } }, { passive: true }); } // ── [data-mini-touch="..."] declarative gesture ── function bindMiniTouch(el) { if (el.dataset.miniTouchBound) return; el.dataset.miniTouchBound = '1'; var type = el.getAttribute('data-mini-touch'); if (type === 'mobile-nav') { bindMobileNavSwipe(el); } else if (type === 'bottom-close') { bindBottomClose(el); } } function bindMobileNavSwipe(el) { var startX = 0, currentX = 0, active = false; el.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; startX = e.touches[0].clientX; currentX = startX; active = true; }, { passive: true }); el.addEventListener('touchmove', function (e) { if (!active) return; currentX = e.touches[0].clientX; var dx = currentX - startX; if (dx > 0) { el.style.transform = 'translateX(' + dx + 'px)'; el.style.opacity = String(Math.max(0.3, 1 - dx / 300)); } }, { passive: true }); el.addEventListener('touchend', function () { if (!active) return; active = false; var dx = currentX - startX; if (dx > 80) { el.style.transform = 'translateX(100%)'; el.style.opacity = '0'; setTimeout(function () { el.classList.remove('is-open'); el.style.transform = ''; el.style.opacity = ''; document.body.classList.remove('nav-open'); }, 200); } else { el.style.transition = 'transform 0.2s, opacity 0.2s'; el.style.transform = ''; el.style.opacity = ''; setTimeout(function () { el.style.transition = ''; }, 200); } }, { passive: true }); } function bindBottomClose(el) { var startY = 0, currentY = 0, active = false; el.addEventListener('touchstart', function (e) { if (e.touches.length !== 1) return; startY = e.touches[0].clientY; currentY = startY; active = true; }, { passive: true }); el.addEventListener('touchmove', function (e) { if (!active) return; currentY = e.touches[0].clientY; var dy = currentY - startY; if (dy > 0) { el.style.transform = 'translateY(' + dy + 'px)'; } }, { passive: true }); el.addEventListener('touchend', function () { if (!active) return; active = false; var dy = currentY - startY; if (dy > 80) { el.dispatchEvent(new CustomEvent('aksrt:pull-dismiss', { bubbles: true })); } el.style.transform = ''; }, { passive: true }); } // ── Auto-init ── function init() { document.querySelectorAll('[data-aksrt-swipe]').forEach(bindSwipe); document.querySelectorAll('[data-aksrt-pull-dismiss]').forEach(bindPullDismiss); document.querySelectorAll('[data-mini-touch]').forEach(bindMiniTouch); document.addEventListener('aksrt:lightbox-open', function (e) { var lightbox = e.target; if (lightbox) bindPullDismiss(lightbox); }); } // Register for rehydration if (window.AksrtFrontend) { AksrtFrontend.register('aksrt-mini-touch', function (root) { (root || document).querySelectorAll('[data-aksrt-swipe]').forEach(bindSwipe); (root || document).querySelectorAll('[data-aksrt-pull-dismiss]').forEach(bindPullDismiss); (root || document).querySelectorAll('[data-mini-touch]').forEach(bindMiniTouch); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();