/* Shared finger-following track for skin choices and report detail panels. */ window.SwipeTrack = (function () { 'use strict'; function create(viewport, track, options) { var target = options.gestureTarget || viewport, index = 0; var drag = { id: null, horizontal: false, vertical: false }, suppressUntil = 0; function release() { var id = drag.id; drag.id = null; drag.horizontal = drag.vertical = false; viewport.classList.remove('dragging'); track.classList.remove('dragging'); try { if (id != null && target.hasPointerCapture(id)) target.releasePointerCapture(id); } catch (_) {} } function go(next, instant) { index = Math.max(0, Math.min(options.count - 1, next)); release(); if (instant) track.style.transition = 'none'; track.style.transform = 'translateX(-' + index * 100 + '%)'; if (instant) { void track.offsetWidth; track.style.transition = ''; } } function cancel() { go(index, true); if (options.onCancel) options.onCancel(index); } function lockDirection(dx, dy) { if (drag.vertical) return false; if (drag.horizontal) return true; var threshold = options.startThreshold || 8, ratio = options.directionRatio || 1; if (Math.max(Math.abs(dx), Math.abs(dy)) <= threshold) return false; // Touch and pointer events must share the same decision, regardless of // which event stream the browser dispatches first. if (Math.abs(dy) * ratio >= Math.abs(dx)) { drag.vertical = true; return false; } drag.horizontal = true; if (options.onStart) options.onStart(index); viewport.classList.add('dragging'); track.classList.add('dragging'); try { target.setPointerCapture(drag.id); } catch (_) {} return true; } target.addEventListener('pointerdown', function (e) { if (drag.id != null || e.isPrimary === false || e.button !== 0 || !viewport.clientWidth) return; suppressUntil = 0; // A fresh tap is intentional, not the preceding drag's click. if (options.gestureSelector && (!e.target.closest || !e.target.closest(options.gestureSelector))) return; drag.id = e.pointerId; drag.startX = e.clientX; drag.startY = e.clientY; drag.offset = track.getBoundingClientRect().left - viewport.getBoundingClientRect().left; drag.width = viewport.clientWidth; drag.lastX = e.clientX; drag.lastAt = e.timeStamp; drag.velocity = 0; drag.horizontal = drag.vertical = false; // Capture only after direction lock: an ordinary image tap must still zoom. }); target.addEventListener('pointermove', function (e) { if (e.pointerId !== drag.id || drag.vertical) return; var dx = e.clientX - drag.startX, dy = e.clientY - drag.startY; if (!lockDirection(dx, dy)) return; if (e.cancelable) e.preventDefault(); var elapsed = e.timeStamp - drag.lastAt; if (elapsed > 0) drag.velocity = (e.clientX - drag.lastX) / elapsed; drag.lastX = e.clientX; drag.lastAt = e.timeStamp; var offset = drag.offset + dx, min = -(options.count - 1) * drag.width; var edgeLimit = options.maxOverscroll == null ? Infinity : options.maxOverscroll; if (offset > 0) offset = Math.min(edgeLimit, offset * .25); else if (offset < min) offset = min - Math.min(edgeLimit, (min - offset) * .25); track.style.transform = 'translate3d(' + offset + 'px,0,0)'; if (options.onMove) options.onMove(Math.max(0, Math.min(options.count - 1, -offset / drag.width))); }); if (options.preventNativeHorizontalPan) { // preventDefault on pointermove does not stop native viewport panning. // Cancel only the single-touch horizontal gesture before the browser // starts scrolling and sends pointercancel. Vertical/text/multi-touch // gestures keep their native behavior; taps still open image zoom. target.addEventListener('touchmove', function (e) { if (drag.id == null || e.touches.length !== 1) return; var touch = e.touches[0]; if (lockDirection(touch.clientX - drag.startX, touch.clientY - drag.startY) && e.cancelable) e.preventDefault(); }, { passive: false }); } window.addEventListener('pointerup', function (e) { if (e.pointerId !== drag.id) return; var horizontal = drag.horizontal, dx = e.clientX - drag.startX, next = index; if (horizontal) { var start = Math.round(-drag.offset / drag.width); next = start; var speed = e.timeStamp - drag.lastAt > 100 ? 0 : drag.velocity; if (Math.abs(dx) >= Math.min(50, drag.width * .2) || (Math.abs(dx) > 12 && Math.abs(speed) > .4)) { next = Math.round(-(drag.offset + dx) / drag.width); if (next === start) next += (Math.abs(speed) > .4 ? speed : dx) < 0 ? 1 : -1; } suppressUntil = Date.now() + 350; } go(next); if (horizontal && options.onSelect) options.onSelect(index); }); window.addEventListener('pointercancel', function (e) { if (e.pointerId === drag.id) cancel(); }); target.addEventListener('lostpointercapture', function (e) { // Touch starts with implicit capture on an image/button. Its bubbled loss // when we take horizontal capture is not a cancellation of this track. if (e.target === target && e.pointerId === drag.id) cancel(); }); target.addEventListener('dragstart', function (e) { e.preventDefault(); }); target.addEventListener('click', function (e) { if (Date.now() < suppressUntil) { e.preventDefault(); e.stopPropagation(); } }, true); window.addEventListener('resize', cancel); go(0, true); return { go: go, cancel: cancel, isDragging: function () { return drag.horizontal; } }; } return { create: create }; })();