aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js37
1 files changed, 25 insertions, 12 deletions
diff --git a/src/app.js b/src/app.js
index 9d5c86e..6945c10 100644
--- a/src/app.js
+++ b/src/app.js
@@ -4570,6 +4570,16 @@ function balanceCard() {
// a card mid-drag is browsing, not choosing; release is the choice.
const settle = (el) => {
if (el._dragging) return;
+ // Direct scrollLeft writes during a mouse drag can queue scrollend.
+ // It may arrive AFTER release, before the smooth glide's first frame.
+ // Only the chosen destination can end that glide; an intermediate
+ // position must not switch the pane back or restore snap early.
+ if (el._mouseTarget != null) {
+ if (Math.abs(el.scrollLeft - el._mouseTarget) > 2) return;
+ el._mouseTarget = null;
+ clearTimeout(el._snapT);
+ el.style.scrollSnapType = '';
+ }
endCarouselDrag(); // motion has stopped — unfreeze the strip and sync it
const mid = el.scrollLeft + el.clientWidth / 2;
let best = 0, bestD = Infinity;
@@ -4642,6 +4652,8 @@ function balanceCard() {
// where it started. Kill its timer too so they don't pile up.
el._dragGen = (el._dragGen || 0) + 1;
clearTimeout(el._snapT);
+ clearTimeout(el._settle);
+ el._mouseTarget = null;
const x0 = e.clientX, s0 = el.scrollLeft;
const kids0 = [...el.children];
// the card the drag started on — a flick throws relative to it
@@ -4674,9 +4686,6 @@ function balanceCard() {
window.removeEventListener('pointerup', up);
el.classList.remove('grabbing');
el._dragging = false;
- // safety: if the release glide produces no scroll events, settle
- // never fires to unfreeze the strip — do it after a beat
- setTimeout(() => { if (!el._dragging && (el._skipMorph || _renderDeferred)) endCarouselDrag(); }, 700);
if (!moved) { endCarouselDrag(); return; }
el._dragged = Date.now(); // the release's click must not double as a card tap
// Commit to a neighbor on a small, deliberate move — not only when
@@ -4699,17 +4708,21 @@ function balanceCard() {
else if (Math.abs(travelled) > 0.15) dir = travelled > 0 ? 1 : -1; // pulled ~a sixth of a card
const best = Math.max(0, Math.min(kids.length - 1, fromIdx + dir));
const k = kids[best];
- if (k) el.scrollTo({ left: k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2, behavior: 'smooth' });
- // Restore mandatory snap only once THIS glide has come to rest, and
- // only if no newer drag has taken over — a fixed timer used to fire
- // mid-glide and snap the card back to its origin. scrollend marks
- // the true end; the timer is just a fallback for when the target is
- // already reached (no scroll, no scrollend).
+ // Clamp to the real scroll range: the end cards cannot always be
+ // perfectly centered within the strip's padding.
+ el._mouseTarget = k ? Math.max(0, Math.min(el.scrollWidth - el.clientWidth,
+ k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2)) : el.scrollLeft;
+ el.scrollTo({ left: el._mouseTarget, behavior: 'smooth' });
+ // The shared settle handler restores snap only at this destination.
+ // If the browser omits scrollend, finish there after a bounded wait.
+ // A newer grab invalidates this fallback, including its unfreeze.
const gen = el._dragGen;
- const restore = () => { if (el._dragGen === gen && !el._dragging) el.style.scrollSnapType = ''; };
- el.addEventListener('scrollend', restore, { once: true });
clearTimeout(el._snapT);
- el._snapT = setTimeout(restore, 700);
+ el._snapT = setTimeout(() => {
+ if (el._dragGen !== gen || el._dragging || el._mouseTarget == null) return;
+ el.scrollTo({ left: el._mouseTarget, behavior: 'instant' });
+ settle(el);
+ }, 700);
// The chosen card is known RIGHT HERE — commit it now so the
// history below loads under the glide. The hold lifts (renders may
// run again) but the strip stays morph-frozen until settle; settle