/* 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); } 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. 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 (!drag.horizontal) { if (Math.abs(dy) > 8 && Math.abs(dy) >= Math.abs(dx)) { drag.vertical = true; return; } if (Math.abs(dx) <= 8 || Math.abs(dx) <= Math.abs(dy)) return; drag.horizontal = true; viewport.classList.add('dragging'); track.classList.add('dragging'); try { target.setPointerCapture(e.pointerId); } catch (_) {} } 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; if (offset > 0) offset *= .25; else if (offset < min) offset = min + (offset - min) * .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))); }); 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 }; })();