Использование событий привязки к прокрутке
Модуль CSS привязки к прокрутке определяет два события привязки к прокрутке: scrollsnapchanging и scrollsnapchange. Они позволяют выполнять JavaScript-код в ответ на определение браузером, что новые цели привязки к прокрутке находятся в очереди и соответственно выбраны.
Это руководство предоставляет обзор этих событий, а также полные примеры.
Обзор событий
События привязки к прокрутке устанавливаются на контейнере прокрутки, который содержит потенциальные цели привязки к прокрутке:
-
Событие
scrollsnapchangingсрабатывает, когда браузер определяет, что при завершении текущего жеста прокрутки будет выбрана новая цель привязки к прокрутке. Это ожидаемая цель привязки к прокрутке. Конкретно, это событие срабатывает во время жеста прокрутки каждый раз, когда пользователь перемещает указатель по потенциальным новым целям привязки. Хотя событиеscrollsnapchangingможет срабатывать несколько раз для каждого жеста прокрутки, оно не срабатывает для всех потенциальных целей привязки к прокрутке при жесте прокрутки, который проходит над несколькими целями. Вместо этого оно срабатывает только для последней цели, на которой прокрутка потенциально остановится. -
Событие
scrollsnapchangeсрабатывает в конце операции прокрутки, когда выбрана новая цель привязки к прокрутке. Конкретно, это событие срабатывает, когда жест прокрутки завершен, но только если выбрана новая цель привязки. Это событие срабатывает непосредственно перед срабатыванием событияscrollend.
Давайте рассмотрим пример, демонстрирующий эти два события в действии (вы увидите, как это реализовано позже в статье):
Попробуйте прокрутить список блоков вверх и вниз:
- Попробуйте медленно прокручивать контейнер вверх и вниз, не отпуская жест прокрутки. Например, перетащите палец(и) по области прокрутки на сенсорном устройстве или трекпаде, или удерживайте нажатой кнопку мыши на полосе прокрутки и перемещайте мышь. Блоки, над которыми вы проезжаете, должны становиться более темными серыми, а затем возвращаться к нормальному виду, когда вы от них уезжаете. Это событие
scrollsnapchangingв действии. - Теперь попробуйте отпустить жест прокрутки; ближайший к позиции прокрутки блок должен анимироваться в фиолетовый цвет с белым текстом. Анимация происходит, когда срабатывает событие
scrollsnapchange. - Наконец, попробуйте прокрутить быстро. Например, сильно хлопните пальцем по экрану, чтобы прокрутить мимо нескольких потенциальных целей, прежде чем начать останавливаться рядом с целью, расположенной ниже в контейнере прокрутки. Вы увидите срабатывание только одного события
scrollsnapchangingпо мере замедления прокрутки, прежде чем событиеscrollsnapchangeсработает и выбранная цель привязки станет фиолетовой.
Объект события SnapEvent
Оба вышеперечисленных события используют объект события SnapEvent. У него есть две ключевые свойства, определяющие работу событий привязки к прокрутке:
-
snapTargetBlockвозвращает ссылку на элемент, к которому произведена привязка в направлении блока при срабатывании события, илиnullесли привязка к прокрутке происходит только по направлению по горизонтали, поэтому к элементу не привязана в направлении блока. -
snapTargetInlineвозвращает ссылку на элемент, к которому произведена привязка в направлении строки при срабатывании события, илиnullесли привязка к прокрутке происходит только по направлению блока, поэтому к элементу не привязана по направлению строки.
Эти свойства позволяют функциям обработчиков событий указывать элемент, к которому произведена привязка (в случае scrollsnapchange) или элемент, к которому была бы произведена привязка, если бы жест прокрутки был завершен сейчас (в случае scrollsnapchanging) — в одномерных и двумерных случаях. Затем вы можете манипулировать этими элементами любым способом, например, непосредственно изменяя стили через их свойства style, устанавливая классы, для которых определены стили в таблице стилей и т. д.
Связь со свойством CSS scroll-snap-type
Доступные значения свойств SnapEvent напрямую соответствуют значению свойства CSS scroll-snap-type, заданного для контейнера прокрутки:
- Если ось привязки задана как
block(или физическое значение оси, эквивалентноеblockв текущем режиме написания), толькоsnapTargetBlockвозвращает ссылку на элемент. - Если ось привязки задана как
inline(или физическое значение оси, эквивалентноеinlineв текущем режиме написания), толькоsnapTargetInlineвозвращает ссылку на элемент. - Если ось привязки задана как
both,snapTargetBlockиsnapTargetInlineвозвращают ссылку на элемент.
Обработка одномерных прокруток
Если вы работаете с горизонтальной прокруткой, только свойство объекта события snapTargetInline будет изменяться при изменении элемента привязки, если содержимое имеет горизонтальное направление writing-mode, или свойство snapTargetBlock если содержимое имеет вертикальное направление writing-mode.
И наоборот, если вы работаете с вертикальной прокруткой, только свойство snapTargetBlock будет изменяться при изменении элемента привязки, если содержимое имеет горизонтальное направление writing-mode, или свойство snapTargetInline если содержимое имеет вертикальное направление writing-mode.
В обоих случаях не изменяемое свойство из двух возвращает null.
Давайте рассмотрим фрагмент примера, чтобы показать типичную функцию обработчика события привязки к одномерной прокрутке:
scrollingElem.addEventListener("scrollsnapchange", (event) => {
event.snapTargetBlock.className = "select-section";
});
В этом фрагменте функция обработчика scrollsnapchange устанавливается на элемент контейнера прокрутки в направлении блока, внутри которого находятся цели привязки. При срабатывании события мы устанавливаем класс select-section на элементе snapTargetBlock, что может использоваться для стилизации недавно выбранной цели привязки, чтобы она выглядела выбранной (например, с анимацией).
Обработка двумерных прокруток
Если вы работаете с горизонтальной и вертикальной прокруткой, код становится более сложным. Это связано с тем, что свойства snapTargetBlock и snapTargetInline возвращают ссылку на элемент (ни одно из них не возвращает null), и одно или другое значение будет меняться в зависимости от направления прокрутки и writing-mode содержимого:
- Если прокрутка выполняется по горизонтали, свойство
snapTargetInlineбудет изменяться при изменении привязанного элемента, если содержимое имеет горизонтальное направлениеwriting-mode, или свойствоsnapTargetBlockесли содержимое имеет вертикальное направлениеwriting-mode. - Если прокрутка выполняется по вертикали, свойство
snapTargetBlockбудет изменяться при изменении привязанного элемента, если содержимое имеет горизонтальное направлениеwriting-mode, или свойствоsnapTargetInlineесли содержимое имеет вертикальное направлениеwriting-mode.
Для обработки этого вам, вероятно, потребуется отслеживать, какой элемент snapTargetBlock или snapTargetInline изменился. Давайте рассмотрим пример:
const prevState = {
snapTargetInline: "s1",
snapTargetBlock: "s1",
};
scrollingElem.addEventListener("scrollsnapchange", (event) => {
if (!(prevState.snapTargetBlock === event.snapTargetBlock.id)) {
console.log(
`The container was scrolled in the block direction to element ${event.snapTargetBlock.id}`,
);
}
if (!(prevState.snapTargetInline === event.snapTargetInline.id)) {
console.log(
`The container was scrolled in the block direction to element ${event.snapTargetBlock.id}`,
);
}
prevState.snapTargetBlock = event.snapTargetBlock.id;
prevState.snapTargetInline = event.snapTargetInline.id;
});
В этом фрагменте мы сначала определяем объект (prevState), который хранит идентификатор предыдущих элементов snapTargetBlock и snapTargetInline.
В функции обработчика события мы используем операторы if для проверки:
- Совпадает ли ID
prevState.snapTargetBlockс ID текущего элементаevent.snapTargetBlock. - Совпадает ли ID
prevState.snapTargetInlineс ID текущего элементаevent.snapTargetInline.
Если значения различаются, это означает, что прокрутка выполнена в этом направлении (блок или строка), и мы выводим сообщение в консоль, чтобы указать на это. В реальном примере, скорее всего, вы каким-то образом будете стилизовать элемент привязки, чтобы показать, что он привязан.
Затем мы обновляем значения prevState.snapTargetBlock и prevState.snapTargetInline в готовности к следующему запуску обработчика события.
В оставшейся части статьи мы рассмотрим несколько полных примеров событий привязки к прокрутке, с которыми вы можете поиграть в живых визуализациях в конце каждого раздела.
Пример одномерной прокрутки
В этом примере представлен вертикально прокручиваемый <main> элемент, содержащий несколько светло-серых <section> элементов, которые являются целями привязки к прокрутке. Когда ожидается новая цель привязки, она станет темнее серой. Когда новая цель привязки выбрана, она плавно анимируется в фиолетовый цвет с белым текстом. Если ранее была выбрана другая цель привязки, она плавно анимируется обратно в серый цвет с черным текстом.
HTML
HTML-код примера состоит из одного элемента <main>. Элементы <section> будут добавлены динамически с помощью JavaScript позже, чтобы уменьшить размер страницы.
<main></main>
CSS
В CSS мы начинаем с применения к элементу <main> толстого черного border, фиксированной width и height. Мы устанавливаем его значение overflow в scroll, чтобы скрывать выходящий за пределы контент и обеспечивать возможность прокрутки, и устанавливаем scroll-snap-type в block mandatory, чтобы привязка к целевым точкам в направлении блока всегда происходила.
main {
border: 3px solid black;
width: 250px;
height: 450px;
overflow: scroll;
scroll-snap-type: block mandatory;
}
Каждому элементу <section> задаётся margin в 50px, чтобы отделить элементы <section> и сделать поведение привязки по прокрутке более заметным. Затем мы устанавливаем scroll-snap-align в center, чтобы указать, что мы хотим привязываться к центру каждой целевой точки.
section {
margin: 50px auto;
scroll-snap-align: center;
transition: 0.5s ease;
}
Изменения стилей, упомянутые выше, будут применяться к элементам <section> с помощью JavaScript. Класс select-section будет применяться для обозначения выбора — это задаёт фиолетовый фон и белый цвет текста. Класс pending будет применяться для обозначения ожидаемого выбора целевой точки — это задаёт более тёмно-серый цвет фона целевого выбора.
.pending {
background-color: #ccc;
}
.select-section {
background: purple;
color: white;
}
JavaScript
В JavaScript мы начинаем с получения ссылки на элемент <main> и определения количества генерируемых элементов <section> (в данном случае 21) и переменной для начала отсчёта. Затем мы используем цикл while для генерации элементов <section>, придавая каждому из них дочерний элемент h2 с текстом Section плюс текущее значение n.
const mainElem = document.querySelector("main");
const sectionCount = 21;
let n = 1;
while (n <= sectionCount) {
mainElem.innerHTML += `
<section>
<h2>Section ${n}</h2>
</section>
`;
n++;
}
Теперь перейдём к обработчику события scrollsnapchanging. Когда дочерний элемент элемента <main> (т. е. любой элемент <section>) становится ожидаемой целевой точкой привязки, мы:
- Проверяем, был ли ранее у элемента применён класс
pending, и, если да, удаляем его. Это делается для того, чтобы только текущая ожидаемая целевая точка получила классpendingи была окрашена в более тёмно-серый цвет. Мы не хотим, чтобы ранее ожидаемые целевые точки, которые больше не являются таковыми, сохраняли стили. - Придаём элементу, на который ссылается свойство
snapTargetBlock(который будет одним из элементов<section>) классpending, чтобы он стал более тёмно-серым.
mainElem.addEventListener("scrollsnapchanging", (event) => {
const previousPending = document.querySelector(".pending");
if (previousPending) {
previousPending.classList.remove("pending");
}
event.snapTargetBlock.classList.add("pending");
});
Примечание: Для этой демонстрации нам не нужно беспокоиться о свойстве объекта события snapTargetInline — мы осуществляем только вертикальную прокрутку, а демонстрация использует горизонтальный режим записи, поэтому изменяется только значение snapTargetBlock. В этом случае snapTargetInline всегда вернёт null.
Когда завершается жест прокрутки и элемент <section> фактически выбирается в качестве целевой точки привязки, срабатывает обработчик события scrollsnapchange. Это:
- Проверяет, была ли ранее выбрана целевая точка привязки — т. е. если класс
select-sectionбыл ранее применён к элементу. Если да, мы удаляем его. - Применяем класс
select-sectionк элементу<section>, на который ссылается свойствоsnapTargetBlock, чтобы целевая точка привязки, которая только что была выбрана, имела применённую к ней анимацию выбора.
mainElem.addEventListener("scrollsnapchange", (event) => {
const currentlySnapped = document.querySelector(".select-section");
if (currentlySnapped) {
currentlySnapped.classList.remove("select-section");
}
event.snapTargetBlock.classList.add("select-section");
});
Результат
Попробуйте прокручивать контейнер прокрутки вверх и вниз, наблюдая за описанным выше поведением:
Пример двухмерного прокручиваемого элемента
Этот пример аналогичен предыдущему, за исключением того, что он содержит горизонтально и вертикально прокручиваемый элемент <main>, содержащий несколько светло-серых элементов <section>, которые являются целевыми точками привязки.
HTML-код примера такой же, как и в предыдущем примере — один элемент <main>.
CSS
CSS для этого примера аналогичен CSS в предыдущем примере. Наиболее существенные отличия следующие.
Сначала рассмотрим стили элемента <main>. Мы хотим, чтобы элементы <section> были расположены в виде сетки, поэтому мы используем CSS-сетку для указания, что мы хотим отображать их в семи столбцах, используя значение grid-template-columns равное repeat(7, 1fr). Мы также указываем отступы вокруг элементов <section> путём установки значений padding и gap на элементе <main>, а не margin на элементах <section>.
Наконец, поскольку в этом примере прокрутка происходит в обоих направлениях, мы устанавливаем scroll-snap-type в both mandatory, чтобы целевые точки привязки в направлении блока и направления строки всегда привязывались.
main {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 100px;
gap: 50px;
overflow: scroll;
border: 3px solid black;
width: 350px;
height: 350px;
scroll-snap-type: both mandatory;
}
Далее, в этом примере мы будем использовать CSS-анимации вместо переходов. Это приводит к более сложному коду, но позволяет более точно контролировать применяемые анимации.
Сначала мы определяем классы, которые будут применяться для обозначения того, что выбор целевой точки привязки был сделан или ожидается. Классы select-section и deselect-section будут применять анимации ключевых кадров для обозначения выбора или отмены выбора. Класс pending будет применяться для обозначения ожидаемого выбора целевой точки привязки (он применяет более тёмно-серый цвет фона к выбору, как и в предыдущем примере).
Анимация с использованием @keyframes происходит от серого фона и чёрного (стандартного) цвета текста к фиолетовому фону и белому цвету текста и обратно соответственно. Последняя анимация несколько отличается от первой — она также использует opacity для создания эффекта плавного затухания/возвращения.
.select-section {
animation: select 0.8s ease forwards;
}
.deselect-section {
animation: deselect 0.8s ease forwards;
}
.pending {
background-color: #ccc;
}
@keyframes select {
from {
background: #eee;
color: black;
}
to {
background: purple;
color: white;
}
}
@keyframes deselect {
0% {
background: purple;
color: white;
opacity: 1;
}
80% {
background: #eee;
color: black;
opacity: 0.1;
}
100% {
background: #eee;
color: black;
opacity: 1;
}
}
JavaScript
В JavaScript мы начинаем так же, как и в предыдущем примере, за исключением того, что на этот раз мы генерируем 49 <section> элементов и присваиваем каждому ID s плюс текущее значение n, чтобы отслеживать их позже. С помощью заданной выше CSS-сетки у нас семь столбцов по семь <section> элементов.
const mainElem = document.querySelector("main");
const sectionCount = 49;
let n = 1;
while (n <= sectionCount) {
mainElem.innerHTML += `
<section id="s${n}">
<h2>Section ${n}</h2>
</section>
`;
n++;
}
Далее мы определяем объект под названием prevState, который позволяет отслеживать ранее выбранную точку фиксации в любой момент — его свойства хранят ID предыдущих точек фиксации по горизонтали и по вертикали. Это важно для определения, нужно ли стилизовать новую точку фиксации по горизонтали или по вертикали каждый раз при срабатывании обработчика события.
const prevState = {
snapTargetInline: "s1",
snapTargetBlock: "s1",
};
Например, предположим, что контейнер прокрутки прокручен так, что ID нового SnapEvent.snapTargetBlock элемента изменился (он не равен ID, сохранённому в prevState.snapTargetBlock), но ID нового SnapEvent.snapTargetInline элемента по-прежнему совпадает с ID, сохранённым в prevState.snapTargetInline. Это означает, что мы переместились к новой точке фиксации по вертикали, поэтому мы должны стилизовать SnapEvent.snapTargetBlock, но мы не переместились к новой точке фиксации по горизонтали, поэтому не нужно стилизовать SnapEvent.snapTargetInline.
На этот раз мы сначала объясним функцию обработчика события scrollsnapchange. В этой функции мы:
- Начинаем с проверки, имеет ли ранее выбранный
<section>элемент точки фиксации (как указано наличием классаselect-section) применён классdeselect-section, чтобы отобразить анимацию отмены выбора. Если ранее не была выбрана ни одна точка фиксации, мы применяем классselect-sectionк первому<section>в DOM, чтобы он отображался как выбранный при первой загрузке страницы. - Сравниваем ID ранее выбранной точки фиксации с ID только что выбранной точки фиксации, как для выбора по вертикали, так и для выбора по горизонтали. Если они различаются, это означает, что выбор изменился, поэтому мы применяем класс
select-sectionк соответствующей точке фиксации, чтобы визуально это показать. - Обновляем
prevState.snapTargetBlockиprevState.snapTargetInlineтак, чтобы они равнялись ID точек фиксации прокрутки, которые только что были выбраны, чтобы при следующем срабатывании события они были предыдущими выбранными.
mainElem.addEventListener("scrollsnapchange", (event) => {
if (document.querySelector(".select-section")) {
document.querySelector(".select-section").className = "deselect-section";
} else {
document.querySelector("section").className = "select-section";
}
if (!(prevState.snapTargetBlock === event.snapTargetBlock.id)) {
event.snapTargetBlock.className = "select-section";
}
if (!(prevState.snapTargetInline === event.snapTargetInline.id)) {
event.snapTargetInline.className = "select-section";
}
prevState.snapTargetBlock = event.snapTargetBlock.id;
prevState.snapTargetInline = event.snapTargetInline.id;
});
Когда срабатывает функция обработчика события scrollsnapchanging, мы:
- Удаляем класс
pendingиз элемента, к которому он ранее был применён, чтобы только текущая потенциальная цель получила классpendingи была окрашена в более тёмно-серый цвет. - Придаём текущему потенциальному элементу класс
pending, чтобы он стал более тёмно-серым, но только если у него ещё нет применённого классаselect-section— мы хотим, чтобы ранее выбранная цель сохраняла фиолетовый стиль выделения, пока не будет выбрана новая цель. Мы также добавляем дополнительную проверку в операторыif, чтобы стилизовать только потенциальную точку фиксации по горизонтали или вертикали, в зависимости от того, какая из них изменилась. Опять же, мы сравниваем предыдущую точку фиксации с текущей точкой фиксации в каждом случае.
mainElem.addEventListener("scrollsnapchanging", (event) => {
const previousPending = document.querySelector(".pending");
if (previousPending) {
previousPending.className = "";
}
if (
!(event.snapTargetBlock.className === "select-section") &&
!(prevState.snapTargetBlock === event.snapTargetBlock.id)
) {
event.snapTargetBlock.className = "pending";
}
if (
!(event.snapTargetInline.className === "select-section") &&
!(prevState.snapTargetInline === event.snapTargetInline.id)
) {
event.snapTargetInline.className = "pending";
}
});
Результат
Попробуйте прокрутить горизонтально и вертикально контейнер прокрутки, наблюдая за описанным выше поведением:
События фиксации прокрутки на Document и Window
В этой статье мы рассмотрели события фиксации прокрутки, которые срабатывают на интерфейсе Element, но те же события также срабатывают на объектах Document и Window. Смотрите:
-
Ссылки на события
scrollsnapchangeиscrollsnapchanging. -
Ссылки на события
scrollsnapchangeиscrollsnapchanging.
Они работают примерно так же, как и Element версии, за исключением того, что весь HTML-документ должен быть задан как контейнер прокрутки (т.е., scroll-snap-type устанавливается на элементе <html>).
Например, если мы возьмём похожий пример, как те, которые мы рассматривали выше, где у нас есть элемент <main> содержащий значительное содержимое:
<main> <!-- Significant content --> </main>
Элемент <main> можно преобразовать в контейнер прокрутки, используя комбинацию CSS-свойств, например:
main {
width: 250px;
height: 450px;
overflow: scroll;
}
Затем вы можете реализовать поведение фиксации прокрутки на содержимом прокрутки, указав свойство scroll-snap-type на элементе <html>:
html {
scroll-snap-type: block mandatory;
}
Следующий фрагмент JavaScript вызовет событие scrollsnapchange на HTML-документе, когда дочерний элемент элемента <main> станет новой выбранной точкой фиксации. В обработчике функции мы устанавливаем класс selected на дочерний элемент, на который указывает SnapEvent.snapTargetBlock, который можно использовать для его стилизации так, чтобы он выглядел выбранным (например, с анимацией) при срабатывании события.
document.addEventListener("scrollsnapchange", (event) => {
event.snapTargetBlock.classList.add("selected");
});
Мы можем вызвать событие на Window вместо этого, чтобы достичь той же функциональности:
window.addEventListener("scrollsnapchange", (event) => {
event.snapTargetBlock.classList.add("selected");
});
См. также
-
scrollsnapchangingсобытие -
scrollsnapchangeсобытие SnapEvent- Модуль CSS scroll snap
- События фиксации прокрутки на developer.chrome.com (2024)
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap/Using_scroll_snap_events