Replace hybrid site-nav/drawer shell with zibll navbar-top layouts 1/2/3, body.mobile-navbar-show drawer, wired NavWalker + aksrt_menu_options meta, nav_folding/chevron, VIP switches, and SPA/PHP schema parity. No dual-class compat paths.
257 lines
8.1 KiB
JavaScript
257 lines
8.1 KiB
JavaScript
/**
|
|
* 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.style.transform = '';
|
|
el.style.opacity = '';
|
|
// Zibll contract: drawer open state lives on body.
|
|
document.body.classList.remove('mobile-navbar-show');
|
|
}, 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();
|
|
}
|
|
})();
|