Intersection Observer API: примеры использования

от автора

Доброго времени суток, друзья!

Обзор

Intersection Observer API (IOA) позволяет приложению асинхронно наблюдать за пересечением элемента (target) с его родителем (root) или областью просмотра (viewport). Другими словами, этот API обеспечивает вызов определенной функции каждый раз при пересечении целевого элемента с root или viewport.

Примеры использования:

  • «ленивая» или отложенная загрузка изображений
  • бесконечная прокрутка страницы
  • получение информации о видимости рекламы для целей расчета стоимости показов
  • запуск процесса или анимации, находящихся в поле зрения пользователя


Для начала работы с IOA необходимо с помощью конструктора создать объект-наблюдатель с двумя параметрами — функцией обратного вызова и настройками:

// настройки let options = {     root: document.querySelector('.scroll-list'),     rootMargin: '5px',     threshold: 0.5 }  // функция обратного вызова let callback = function(entries, observer){     ... }  // наблюдатель let observer = new IntersectionObserver(callback, options) 

Настройки:

  • root — элемент, который выступает в роли области просмотра для target (предок целевого элемента или null для viewport)
  • rootMargin — отступы вокруг root (margin в CSS, по умолчанию все отступы равны 0)
  • threshold — число или массив чисел, указывающий допустимый процент пересечения target и root

Далее создается целевой элемент, за которым наблюдает observer:

let target = document.querySelector('.list-item') observer.observe(target) 

Вызов callback возвращает объект, содержащий записи об изменениях, произошедших с целевым элементом:

let callback = (entries, observer) => {     entries.forEach(entry => {         // entry (запись) - изменение         //   entry.boundingClientRect         //   entry.intersectionRatio         //   entry.intersectionRect         //   entry.isIntersecting         //   entry.rootBounds         //   entry.target         //   entry.time     }) } 

В сети полно информации по теории, но довольно мало материалов по практике использования IOA. Я решил немного восполнить этот пробел.

Примеры

«Ленивая» (отложенная) загрузка изображений

Задача: загружать (показывать) изображения по мере прокрутки страницы пользователем.

Код:

// ждем полной загрузки страницы window.onload = () => {     // устанавливаем настройки     const options = {         // родитель целевого элемента - область просмотра         root: null,         // без отступов         rootMargin: '0px',         // процент пересечения - половина изображения         threshold: 0.5     }      // создаем наблюдатель     const observer = new IntersectionObserver((entries, observer) => {         // для каждой записи-целевого элемента         entries.forEach(entry => {             // если элемент является наблюдаемым             if (entry.isIntersecting) {                 const lazyImg = entry.target                 // выводим информацию в консоль - проверка работоспособности наблюдателя                 console.log(lazyImg)                 // меняем фон контейнера                 lazyImg.style.background = 'deepskyblue'                 // прекращаем наблюдение                 observer.unobserve(lazyImg)             }         })     }, options)      // с помощью цикла следим за всеми img на странице     const arr = document.querySelectorAll('img')     arr.forEach(i => {         observer.observe(i)     }) } 

Результат:

Изначально фон контейнера белый.

При пересечении с областью просмотра наполовину, фон изображения меняется на небесно-голубой.

Codepen

Github

Замена изображения

Задача: менять изображение-заполнитель на оригинальное при прокрутке страницы пользователем.

Код:

window.onload = () => {     const observer = new IntersectionObserver((entries, observer) => {         entries.forEach(entry => {             if (entry.isIntersecting) {                 console.log(entry)                 // ссылка на оригинальное изображение хранится в атрибуте "data-src"                 entry.target.src = entry.target.dataset.src                 observer.unobserve(entry.target)             }         })     }, { threshold: 0.5 })      document.querySelectorAll('img').forEach(img => observer.observe(img)) } 

Результат:

Первое изображение загружено, поскольку находится в области просмотра. Второе — заполнитель.

При дальнейшей прокрутке заполнитель заменяется исходным изображением.

Codepen

Github

Изменение фона контейнера

Задача: менять фон контейнера при прокрутке страницы пользователем туда и обратно.

Код:

window.addEventListener('load', event => {     let box = document.querySelector('div')     // ratio - процент видимости элемента     let prevRatio = 0.0      let observer = new IntersectionObserver((entries, observer) => {         entries.forEach(entry => {             let curRatio = entry.intersectionRatio                          // при прокрутке цвет меняется от светло-синего до светло-красного             // мы хотим наблюдать эффект при прокрутке страницы в обе стороны (вниз и вверх)            // поэтому наблюдение за элементом не прекращается             curRatio > prevRatio ? entry.target.style.background = `rgba(40,40,190,${curRatio})` : entry.target.style.background = `rgba(190,40,40,${curRatio})`              prevRatio = curRatio         })     }, {         threshold: buildThresholdList()     })      observer.observe(box)          // функция построения шкалы пересечения     // шкала представляет собой массив из 20 элементов, определяющих цвет контейнера     function buildThresholdList() {         let thresholds = []         let steps = 20          for (let i = 1.0; i <= steps; i++) {             let ratio = i / steps             thresholds.push(ratio)         }         return thresholds     } }) 

Результат:

Фон контейнера меняется от светло-синего…

через синий…

до светло-красного.

Codepen

Github

Работа с видео

Задача: ставить запущенное видео на паузу и запускать его снова в зависимости от попадания видео в область просмотра.

Код:

window.onload = () => {     let video = document.querySelector('video')      let observer = new IntersectionObserver(() => {         // если видео запущено         if (!video.paused) {             // приостанавливаем проигрывание             video.pause()         // если видео было запущено ранее (текущее время проигрывания > 0)         } else if(video.currentTime != 0) {             // продолжаем проигрывание             video.play()         }     }, { threshold: 0.4 })      observer.observe(video) } 

Результат:

Пока видео находится в области просмотра, оно проигрывается.

Как только видео выходит за пределы области просмотра больше чем на 40%, его воспроизведение приостанавливается. При попадании в область просмотра > 40% видео, его воспроизведение возобновляется.

Codepen

Github

Прогресс просмотра страницы

Задача: показывать прогресс просмотра страницы по мере прокрутки страницы пользователем.

Код:

// страница состоит из нескольких контейнеров и параграфа для вывода прогресса let p = document.querySelector('p') // n - количество просмотренных контейнеров let n = 0  let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {         if(entry.isIntersecting){             // observer наблюдает за div             // и сообщает об увеличении количества просмотренных контейнеров             // выводим эту информацию в параграф             p.textContent = `${n++} div viewed`             observer.unobserve(entry.target)         }     }) }, {threshold: 0.9})  document.querySelectorAll('div').forEach(div => observer.observe(div)) 

Результат:

Страница только что загрузилась, поэтому мы еще не просмотрели ни одного контейнера.

При достижении конца страницы в параграф выводится информация о просмотре 4 div.

Codepen

Github

Бесконечная прокрутка

Задача: реализовать бесконечный список.

Код:

let ul = document.querySelector('ul') let n = 1  // функция создания элемента списка function createLi(){     li = document.createElement('li')     li.innerHTML = `${++n} item`     ul.append(li) }  // для того, чтобы все время наблюдать за последним элементом списка // мы используем нечто вроде замыкания // прекращаем наблюдать за целевым элементом после создания очередного li // и начинаем наблюдать за этим новым (последним) элементом let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {         if (entry.isIntersecting) {             createLi()         }         observer.unobserve(entry.target)         observer.observe(document.querySelector('li:last-child'))     }) }, {     threshold: 1 })  observer.observe(document.querySelector('li')) 

Результат:

Имеем 12 элементов списка. Последний элемент выходит за пределы области просмотра.

При попытке добраться до последнего элемента создается новый (последний) элемент, скрытый от пользователя. И так до бесконечности.

Codepen

Github

Изменение размеров дочернего элемента при изменении размеров родительского элемента

Задача: установить зависимость размеров одного элемента от другого.

Код:

// у нас есть два контейнера - родитель и ребенок // и параграф для вывода ширины ребенка let info = document.querySelector('.info') let parent = document.querySelector('.parent') let child = document.querySelector('.child') // отнимаем от ширины ребенка 50px для наглядности child.style.width = parent.offsetWidth - 50 + 'px' // выводим ширину ребенка в параграф info.textContent = `child width: ${child.offsetWidth}px`  let options = {     // областью просмотра для ребенка является родитель     root: parent,     threshold: 1 }  let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {         // если расстояние между родителем и ребенком составляет меньше 50px         if ((entry.target.parentElement.offsetWidth - entry.target.offsetWidth) < 50) {             // уменьшаем ширину ребенка на 50px             entry.target.style.width = entry.target.offsetWidth - 50 + 'px'         }     }) }, options)  observer.observe(child)  // к сожалению, не додумался, как реализовать обратный эффект с помощью IOA // поэтому реализовал его с помощью обработки resize window.addEventListener('resize', () => {     info.textContent = `child width: ${child.offsetWidth}px`     if ((parent.offsetWidth - child.offsetWidth) > 51) {         child.style.width = child.offsetWidth + 50 + 'px'     } }) 

Результат:

Исходное состояние.

При уменьшении ширины родительского элемента, уменьшается ширина дочернего элемента. При этом расстояние между ними почти всегда равняется 50px («почти» обусловлено реализацией обратного механизма).

Codepen

Github

Работа с анимацией

Задача: анимировать объект при его видимости.

Код:

// у нас есть Барт Симпсон и две анимации // одна анимация перемещает Барта влево // другая - вправо let observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {         entry.isIntersecting ? entry.target.classList.replace('to-left', 'to-right') : entry.target.classList.replace('to-right', 'to-left')     }) }, {     threshold: .5 })  observer.observe(document.querySelector('img')) 

Результат:

Мы видим часть головы Барта. Барт прижался к левой стороне области просмотра.

При попадании более 50% Барта в область просмотра, он перемещается на середину. При выходе более 50% Барта за пределы области просмотра, он возвращается в начальное положение.

Codepen

Github

Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/494670/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *