نظرة عامة
يتوقع المستخدمون الحصول على ملاحظات مرئية فورية عند التفاعل مع عناصر واجهة المستخدم، مثل لوحات العرض الدوّارة أو معارض الصور. لا تقدّم ميزة "محاذاة مواضع التمرير" التقليدية ملاحظات إلا بعد اكتمال إيماءة التمرير واستقرار العنصر. باستخدام أحداث محاذاة مواضع التمرير، وتحديدًا scrollsnapchanging، يمكنك تقديم الملاحظات الفورية أثناء إيماءة التمرير، مع تمييز هدف المحاذاة المعلّق قبل أن يرفع المستخدم إصبعه أو الماوس.
التنفيذ
1. يرصد الميكروفون scrollsnapchanging
أضِف متتبِّع أحداث لـ scrollsnapchanging إلى حاوية الانتقال. يتم تنشيط هذا الحدث عندما يحدّد المتصفّح أنّه من المرجّح اختيار هدف محاذاة جديد.
const container = document.querySelector('#gallery');
const thumbnails = document.querySelectorAll('.thumbnail');
const items = document.querySelectorAll('.gallery-item');
container.addEventListener('scrollsnapchanging', (event) => {
// Highlight pending snap target during scroll for real-time feedback.
const pendingTarget = event.snapTargetInline;
const index = [...items].indexOf(pendingTarget);
if (index === -1 || !thumbnails[index]) return;
// Use lightweight class toggle to avoid layout thrashing during rapid events.
// Note: aria-current is NOT toggled here. It tracks the settled "current"
// item, which is updated in the scrollsnapchange handler below.
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[index].classList.add('pending');
});
يستخدم هذا المثال snapTargetInline لأنّ المعرض يتم تمريره أفقيًا. إذا كانت حاوية الانتقال تتيح الانتقال عموديًا، استخدِم snapTargetBlock بدلاً من ذلك.
2. يرصد الميكروفون scrollsnapchange
لإنهاء الحالة عند اكتمال إيماءة الانتقال واستقرار العنصر في مكانه، استمِع إلى الحدث scrollsnapchange. هذا الإجراء مطلوب لتحديد الحالة النشطة النهائية.
container.addEventListener('scrollsnapchange', (event) => {
// Promote pending state to active on scroll completion.
const snappedTarget = event.snapTargetInline;
const index = [...items].indexOf(snappedTarget);
if (index === -1 || !thumbnails[index]) return;
// Establish final active state and clean up pending.
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[index].classList.add('active');
thumbnails[index].setAttribute('aria-current', 'true');
});
3- مزامنة الحالة الأولية
عند تحميل الصفحة، قد يستعيد المتصفّح موضع الانتقال (مثلاً، من خلال التنقّل في السجلّ أو رابط إلى موضع ثابت). لن يتم تشغيل أحداث scrollsnapchange أو scroll تلقائيًا. يمكنك إجراء فحص هندسي لمرة واحدة لمزامنة واجهة المستخدم مع موضع الانتقال الأولي.
// Note: For item.offsetLeft to be relative to the container,
// the container MUST be the offsetParent (e.g., `position: relative`).
const findClosestItemIndex = () => {
// Center-distance assumes scroll-snap-align: center on items.
// For start-aligned snap, compare scrollLeft to item.offsetLeft directly.
const containerCenter = container.scrollLeft + container.clientWidth / 2;
let closestIndex = 0;
let minDistance = Infinity;
items.forEach((item, index) => {
const itemCenter = item.offsetLeft + item.offsetWidth / 2;
const distance = Math.abs(containerCenter - itemCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
});
return closestIndex;
};
const initActiveItem = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
if (document.readyState === 'complete') {
initActiveItem();
} else {
window.addEventListener('load', initActiveItem, { once: true });
}
استراتيجيات احتياطية
بالنسبة إلى المتصفّحات التي لا تتيح استخدام scrollsnapchanging، لن توفّر واجهة المستخدم ملاحظات فورية أثناء إيماءة الانتقال تلقائيًا، وستتم إلغاء مزامنة واجهة المستخدم المرتبطة مع المحتوى.
MANDATORY: Provide a fallback for browsers without support, or the linked UI will desynchronize from the content.
يُفضَّل محاكاة الطبيعة الفورية لـ scrollsnapchanging باستخدام متتبِّع الأحداث scroll مع requestAnimationFrame وحسابات المسافة الهندسية لتحديد أقرب هدف محاذاة أثناء تصفُّح المستخدم بشكل نشط.
if ('onscrollsnapchanging' in Element.prototype) {
// Use native scroll snap events
} else {
// Fallback: use scroll + requestAnimationFrame for eager feedback
// (assumes the same container, thumbnails, items defined in step 1)
let scrollTimeout;
let rafId = null;
const promotePendingToActive = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
container.addEventListener('scroll', () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
// DO NOT forget to clean up stale pending classes
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[closestIndex].classList.add('pending');
});
// Debounce fallback for browsers that don't support scrollend
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(promotePendingToActive, 100);
}, { passive: true });
// Fallback: use Baseline `scrollend` event to promote pending to active cleanly where supported
container.addEventListener('scrollend', () => {
clearTimeout(scrollTimeout);
promotePendingToActive();
});
}
يحاكي الإجراء الاحتياطي الهندسي scroll + requestAnimationFrame سلوك التوقّع الأصلي للانتقال السريع، بما في ذلك التعامل مع الانتقال الآلي بشكل صحيح. بما أنّ الدوال مثل scrollIntoView تُطلق أحداث scroll بشكل طبيعي أثناء تنفيذها، ستظل واجهة المستخدم متزامنة بسلاسة طوال حركة الانتقال بدون الحاجة إلى منطق مخصّص إضافي.