Доброго времени суток, друзья!
Обзор
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) }) }
Результат:
Изначально фон контейнера белый.
При пересечении с областью просмотра наполовину, фон изображения меняется на небесно-голубой.
Замена изображения
Задача: менять изображение-заполнитель на оригинальное при прокрутке страницы пользователем.
Код:
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)) }
Результат:
Первое изображение загружено, поскольку находится в области просмотра. Второе — заполнитель.
При дальнейшей прокрутке заполнитель заменяется исходным изображением.
Изменение фона контейнера
Задача: менять фон контейнера при прокрутке страницы пользователем туда и обратно.
Код:
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 } })
Результат:
Фон контейнера меняется от светло-синего…
через синий…
до светло-красного.
Работа с видео
Задача: ставить запущенное видео на паузу и запускать его снова в зависимости от попадания видео в область просмотра.
Код:
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% видео, его воспроизведение возобновляется.
Прогресс просмотра страницы
Задача: показывать прогресс просмотра страницы по мере прокрутки страницы пользователем.
Код:
// страница состоит из нескольких контейнеров и параграфа для вывода прогресса 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.
Бесконечная прокрутка
Задача: реализовать бесконечный список.
Код:
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 элементов списка. Последний элемент выходит за пределы области просмотра.
При попытке добраться до последнего элемента создается новый (последний) элемент, скрытый от пользователя. И так до бесконечности.
Изменение размеров дочернего элемента при изменении размеров родительского элемента
Задача: установить зависимость размеров одного элемента от другого.
Код:
// у нас есть два контейнера - родитель и ребенок // и параграф для вывода ширины ребенка 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 («почти» обусловлено реализацией обратного механизма).
Работа с анимацией
Задача: анимировать объект при его видимости.
Код:
// у нас есть Барт Симпсон и две анимации // одна анимация перемещает Барта влево // другая - вправо 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% Барта за пределы области просмотра, он возвращается в начальное положение.
Благодарю за внимание.
ссылка на оригинал статьи https://habr.com/ru/post/494670/
Добавить комментарий