- Операторы
- Управляющие инструкции
- JS Объекты
- браузер BOM
- HTML DOM
- Свойства узлов
- «Семья» элементов
- Выборка элементов DOM
- Добавление и удаление узлов
- Размеры и прокрутка
- Intersection Observer API
- Атрибуты и стили
- CSSStyleSheet
- Выделение
- Хранение данных
- События
- HTML Объекты
- Промисы, async/await
- Сетевые запросы
- Бинарные данные и файлы
- Модули
- Классы
- Разное
Intersection Observer API
Intersection Observer API позволяет веб-приложениям асинхронно следить за изменением пересечения элемента с его родителем или областью видимости документа viewport
.
Исторически обнаружение видимости отдельного элемента или видимости двух элементов по отношению друг к другу было непростой задачей. Варианты решения этой задачи были ненадёжными и замедляли работу браузера. К несчастью, по мере того как веб "взрослел", потребность в решении этой проблемы только росла по многим причинам, таким как:
- Отложенная загрузка изображений или другого контента по мере прокрутки страницы.
- Реализация веб-сайтов с "бесконечным скроллом", где контент подгружается по мере того как страница прокручивается вниз, и пользователю не нужно переключаться между страницами.
- Отчёт о видимости рекламы с целью посчитать доходы от неё.
- Принятие решения, запускать ли какой-то процесс или анимацию в зависимости от того, увидит пользователь результат или нет.
В прошлом реализация обнаружения пересечения элементов подразумевала использование обработчиков событий и циклов, вызывающих методы типа Element.getBoundingClientRect(), чтобы собрать необходимую информацию о каждом затронутом элементе. Поскольку весь этот код работает в основном потоке, возникают проблемы с производительностью.
Конструктор IntersectionObserver()
Синтаксис
new IntersectionObserver(callback[, options])
Параметры
- options
- Необязательный объект, в котором можно указать любую комбинацию следующих опций:
- root - Объект, который является областью наблюдения. По умолчанию это viewport (видимая часть страницы сайта в окне браузера), но это может быть какой-то определённый HTML-элемент на странице (Например:
root:document.getElementById("main")
. - rootMargin - В этот параметр передаются значения, которыми можно увеличить или уменьшить область root-элемента. Значения передаются точно в таком же формате, в котором они передаются при работе с обычным CSS-свойством margin. То есть вы можете передать какое-то одно значение, которое будет применяться со всех сторон, например
rootMargin:'25px'
, либо передать несколько значений в одну строку, для каждой стороныrootMargin:'10px 11% -10px 25px'
(в таком порядке – сверху справа внизу слева). По умолчанию:rootMargin:'0px'
- threshold - Либо одно число, либо массив чисел от 0,0 до 1,0, определяющий отношение площади пересечения к общей площади ограничивающей рамки для наблюдаемой цели. Например:
threshold:0
- По умолчанию. В этом случае возвратная функция сработает два раза – как только первый пиксель элемента попадёт в область наблюдения, и как только последний пиксель покинет область наблюдения.
threshold:0.5
- В данном случае возвратная функция сработает, когда центр (50%) элемента будет пересекать область наблюдения в любом направлении.
threshold:[0, 0.2, 0.5, 1]
Можно передать массив значений. И для каждого из них будет срабатывать возвратная функция:
1) первый пиксель элемента попадает в область наблюдения, либо последний пиксель выходит из области наблюдения
2) 20% элемента внутри области наблюдения (направление не имеет значения)
3) то же самое для 50%
4) и когда элемент полностью в области наблюдения.
- root - Объект, который является областью наблюдения. По умолчанию это viewport (видимая часть страницы сайта в окне браузера), но это может быть какой-то определённый HTML-элемент на странице (Например:
- callback
- Функция, которая вызывается, когда видимый процент целевого элемента пересекает пороговое значение. Обратный вызов получает на вход два параметра: entry (массив объектов, каждый из которых представляет один порог, который был преодолен, становясь либо более, либо менее заметным, чем процент, указанный этим порогом) и observer (ntersectionObserver, для которого вызывается
callback
).
Перебор каждой записи аentry
можно выполнить с помощью циклаfor...of
или любого из методов объектаArray
, напримерArray.prototype.forEach()
.// какие-либо параметры const options = { // root: document.querySelector( '#viewport' ), rootMargin: '0px', threshold: [ 0, 0.5 ] }; const observer = new IntersectionObserver( trueCallback, options ); const target = document.querySelector( '#target' ); observer.observe( target ); // запускаем "слежку" за элементом(ами) в константе target // callback-функция (возвратная функция) const trueCallback = function(entries, observer) { entries.forEach((entry) => { // делаем что-либо для каждого переданного элемента (в нашем случае он один) console.log( 'сработало' ); // например можно добавить какой-либо CSS-класс элементу entry.target.classList.add( 'some-class' ); }); }
Возвращаемое значение
Новый IntersectionObserver, который можно использовать для отслеживания видимости целевого элемента в пределах указанного корневого пересечения любого из указанных порогов видимости. Вызовите его метод Observe(), чтобы начать наблюдать за изменениями видимости заданной цели.
IntersectionObserverEntry
Интерфейс IntersectionObserverEntry описывает пересечение между целевым элементом и его корневым контейнером в определенный момент перехода.
Свойства
Для простоты понимания, свойства распаковывать таким образом:
const { time, rootBounds, boundingClientRect, intersectionRect, target, isIntersecting, isVisible, intersectionRatio } = entry;
- time
- Время, когда видимость изменилась, является меткой времени высокой точности в миллисекундах
- rootBounds
- Информация о прямоугольной области корневого элемента. Границы вычисляются, как описано в документации для Element.getBoundingClientRect()
- boundingClientRect
Информация о прямоугольной области целевого элемента.- intersectionRect
Информация о пересечении целевого элемента и области просмотра (или корневого элемента).- target
- Наблюдаемый целевой элемент является объектом узла DOM. Благодаря этому свойству мы получаем доступ к отслеживаемому элементу и можем производить с ним какие-либо действия, например добавить или удалить CSS-класс.
- isIntersecting
Это свойство возвращает логическое значение, указывающее, пересекается ли элемент с окном просмотра:
true - элемент переходит из непересекающегося в пересекающийся; false - элемент переходит из пересекающегося в непересекающийся.- intersectionRatio
Это свойство обычно используется в качестве дополнительного условия внутри callback-функции.
Суть этого свойства в том, что оно содержит информацию о том, на сколько процентов (от 0 до 1) наблюдаемый элемент пересекает наблюдаемую область (находится в ней).Скриншот ниже поможет вам понять этот параметр.
Но есть некоторые ситуации, когда условия со свойством
intersectionRatio
никогда не сработают.
Например, если вы установили параметрthreshold
равным 1, и пытаетесь добавить условиеintersectionRatio == 0.5
, то оно не выполнится никогда (или только при загрузке страницы с нахождением элемента в целевой области), потому что обратная функция не будет срабатывать на моменте, когда элемент находится наполовину в наблюдаемой области. Если вы установилиthreshold
в 0.5, то шанс его выполнения тоже близок к нулю, потому чтоintersectionRatio
будет близким к 0.5, типо 0.5073958039283752, а в таком случае условие ведь тоже не выполняется!! Конечно можно попробовать округлить, но не проще ли использовать знаки<
или>
для таких условий. Но ещё один момент, если у васthreshold
равен 1 и вы делаете проверкуintersectionRatio == 1
, то она будет выполняться, когда элемент входит в область, а когда выходит – не будет, потому что максимальное значение свойства 1, а в обратную сторону будет что-то типо 0.9973958039283752! Вот такое огромное количество разных моментиков, про которые лучше бы помнить.
Методы
observe()
Метод Observe() добавляет элемент в набор целевых элементов, за которыми наблюдает IntersectionObserver. Один наблюдатель имеет один набор порогов и один корень, но может наблюдать за несколькими целевыми элементами на предмет изменений видимости в соответствии с ними.
Чтобы прекратить наблюдение за элементом, вызовите unobserve()
.
Синтаксис
observe(targetElement)
Параметры
- targetElement
- Элемент, видимость которого необходимо отслеживать в корне. Этот элемент должен быть потомком корневого элемента (или содержаться в текущем документе, если корнем является область просмотра документа).
Возвращаемое значение
НетПример
const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.intersectionRatio > 0) { // Добавьте класс 'active', если цель наблюдения находится внутри области просмотра. entry.target.classList.add("active"); } else { // В противном случае удалить класс'active' entry.target.classList.remove("active"); } }); }); const boxElList = document.querySelectorAll(".box"); boxElList.forEach((el) => { io.observe(el); });
disconnect()
Метод disconnect() отключает объект IntersectionObserver
от наблюдения любой цели.
Синтаксис
disconnect()
Параметры
НетВозвращаемое значение
НетtakeRecords()
Метод TakeRecords() возвращает массив объектов IntersectionObserverEntry, по одному для каждого целевого элемента, в котором произошло изменение пересечения с момента последней проверки пересечений, либо явно посредством вызова этого метода, либо неявно посредством автоматического вызова объекта наблюдателя.
..Примечание. Если вы используете обратный вызов для отслеживания этих изменений, вам не нужно вызывать этот метод. Вызов этого метода очищает список ожидающих пересечений, поэтому обратный вызов не будет запущен.
.Синтаксис
takeRecords()
Параметры
НетВозвращаемое значение
Массив объектов IntersectionObserverEntry, по одному для каждого целевого элемента, пересечение которого с корнем изменилось с момента последней проверки пересечений.
unobserve()
Метод сообщает объекту IntersectionObserver прекратить наблюдение за конкретным целевым элементом.
Синтаксис
unobserve(target)
Параметры
- target
- Элемент прекращения наблюдения. Если указанный элемент не наблюдается, этот метод ничего не делает и исключение не создается.
Возвращаемое значение
НетПример
const observer = new IntersectionObserver(callback); observer.observe(document.getElementById("elementToObserve")); // … observer.unobserve(document.getElementById("elementToObserve"));
Примеры
Примеры позаимствованы с сайта misha.agency/javascript/intersection-observer-api
1. Квадраты
/* css 1 */ .target { display: flex; align-items: center; justify-content: center; min-height: 100vh; } .target--intro { flex-direction: column; } .box { display: flex; align-items: center; justify-content: center; width: 150px; height: 150px; color: #fff; text-transform: uppercase; text-align: center; background-color: #1eeacb; transition: transform 1s ease-in; } .box--spin.box--visible { transform: rotate(1080deg); } .box--grow.box--visible { transform: scale(1.5); } .box--move-right.box--visible { transform: translateX(50px); }
/* js 1 */ // функция всего лишь добавляет CSS-класс, который и осуществляет анимацию const scrollImations = (entries, observer) => { entries.forEach(entry => { // анимируем, если элемент целиком попадает в отслеживаемую область if (entry.isIntersecting && entry.intersectionRatio == 1) { entry.target.classList.add('box--visible'); } else { entry.target.classList.remove('box--visible'); } }); }; // создаём обсервер с параметрами const options = { threshold: 1.0 }; const observer = new IntersectionObserver(scrollImations, options); const boxes = document.querySelectorAll('.box'); boxes.forEach(box => { observer.observe(box); });
<div class="target target--intro"> <h1>Пример с квадратами</h1> <p>(скролльте вниз)</p> </div> <div class="target"> <div class="box box--spin"><span>Вращение</span></div> </div> <div class="target"> <div class="box box--grow"><span>Увеличение</span></div> </div> <div class="target"> <div class="box box--move-right"><span>Сдвиг вправо</span></div> </div>
В этом примере, если вы скроллите вниз, появляются квадраты, которые выполняют различные действия. По сути всё, что делает IntersectionObserver
в этом примере, это добавляет CSS-класс к квадрату, когда он попадает в область отслеживания (когда становится полностью видимым на экране). И именно CSS-класс и осуществляет анимацию квадрата. И несмотря на то, что добавляем CSS-класс один и тот же для каждого квадрата, анимации различаются, потому что сами квадраты имеют разные CSS-классы!
Также обратите внимание, что самый первый и второй квадрат (тот, который крутится и тот, который увеличивается) немного багуют на определённой позиции скролла. Это потому что когда квадрат поворачивается, у него увеличивается область элемента и IntersectionObserver
пытается пересчитать всё по-новой. Увеличивающийся квадрат и вовсе начинает пульсировать на определённой позиции скролла.
Ещё мы использовали параметр threshold
равным 1.0, и условие со свойством intersectionRatio
entry.intersectionRatio == 1
, это нужно именно для того, чтобы анимация квадрата срабатывала только тогда, когда квадрат целиком попадает в область отслеживания.
2. Навигация-содержание с выделением текущего элемента в ней.
/* css 2 */ nav { position: fixed; width: 100%; top: 0; left: 0; right: 0; background-color: #fff; } nav ul { list-style-type: none; display: flex; align-items: center; justify-content: space-around; width: 100%; max-width: 800px; height: 50px; margin: 0 auto; padding: 0; } nav li { display: inline-block; padding: 5px; } nav a { display: block; height: 40px; padding: 0 20px; line-height: 40px; text-decoration: none; text-transform: uppercase; color: #323232; font-weight: bold; border-radius: 4px; transition: background-color 0.3s ease-in; } nav a:hover, nav a:active, nav a:focus { background-color: rgba(184,214,168,0.5); } nav a.active { background-color: rgba(184,214,168,0.5); } section { display: flex; align-items: center; justify-content: center; min-height: 100vh; } p { text-align: center; color: #fff; font-size: 3.5em; font-weight: bold; text-transform: uppercase; } #one { background-color: #6ca392; } #two { background-color: #ffa58c; } #three { background-color: #ff4f30; } #four { background-color: #576b51; } #five { background-color: #392a1b; }
/* js 2 */ const changeNav = (entries, observer) => { entries.forEach(entry => { // чекаем, то элемент пересекает наблюдаемую область более, чем на 55% if (entry.isIntersecting && entry.intersectionRatio >= 0.55) { // удаляем активный класс у элемента меню document.querySelector('.active').classList.remove('active'); // получаем ID секции, которая текущая let id = entry.target.getAttribute('id'); // обращаемся к ссылке меню, у которой href равен ID секции let newLink = document.querySelector(`[href="#${id}"]`).classList.add('active'); } }); }; // обратите внимание на значение опции threshold const options = { threshold: 0.55 }; const observer = new IntersectionObserver(changeNav, options); // передаём все секции в обсервер const sections = document.querySelectorAll('section'); sections.forEach(section => { observer.observe(section); });
<title>CodePen - Intersection Observer #2 - Onpage Navigation</title> <nav> <ul> <li><a class="active" href="#one">Раз</a></li> <li><a href="#two">Два</a></li> <li><a href="#three">Три</a></li> <li><a href="#four">Четыре</a></li> <li><a href="#five">Пять</a></li> </ul> </nav> <section id="one"> <p>Секция раз</p> </section> <section id="two"> <p>Секция два</p> </section> <section id="three"> <p>Секция три</p> </section> <section id="four"> <p>Секция четыре</p> </section> <section id="five"> <p>Секция пять</p> </section>
В этом примере, если вы скроллите вниз, появляются квадраты, которые выполняют различные действия. По сути всё, что делает IntersectionObserver
в этом примере, это добавляет CSS-класс к квадрату, когда он попадает в область отслеживания (когда становится полностью видимым на экране). И именно CSS-класс и осуществляет анимацию квадрата. И несмотря на то, что добавляем CSS-класс один и тот же для каждого квадрата, анимации различаются, потому что сами квадраты имеют разные CSS-классы!
Также обратите внимание, что самый первый и второй квадрат (тот, который крутится и тот, который увеличивается) немного багуют на определённой позиции скролла. Это потому что когда квадрат поворачивается, у него увеличивается область элемента и IntersectionObserver
пытается пересчитать всё по-новой. Увеличивающийся квадрат и вовсе начинает пульсировать на определённой позиции скролла.
3. Навигация-содержание № 2
/* css 3 */ body { font-family: "Open Sans", sans-serif; line-height: 1.7; } h1, h2, h3 { font-family: "PT Serif", Georgia, Times New Roman, serif; font-weight: 400; } .container { display: grid; padding: 10px; grid-template-columns: 150px auto; grid-gap: 30px; } .toc { position: sticky; top: 0; align-self: start; } .toc > ul { padding: 0; list-style: none; } .toc a { display: block; color: black; padding: 3px; transition: background ease 0.2s, color ease 0.2s; } .is-active { background-color: #b0e3e6; color: black; text-decoration: none; }
/* js 3 */ function ready(fn) { document.addEventListener('DOMContentLoaded', fn, false); } ready(() => { const TableOfContents = { container: document.querySelector('.js-toc'), links: null, headings: null, intersectionOptions: { rootMargin: '0px', threshold: 1 }, previousSection: null, observer: null, init() { this.handleObserver = this.handleObserver.bind(this); this.setUpObserver(); this.findLinksAndHeadings(); this.observeSections(); }, handleObserver(entries, observer) { entries.forEach(entry => { let href = `#${entry.target.getAttribute('id')}`, link = this.links.find(l => l.getAttribute('href') === href); if (entry.isIntersecting && entry.intersectionRatio >= 1) { link.classList.add('is-visible'); this.previousSection = entry.target.getAttribute('id'); } else { link.classList.remove('is-visible'); } this.highlightFirstActive(); }); }, highlightFirstActive() { let firstVisibleLink = this.container.querySelector('.is-visible'); this.links.forEach(link => { link.classList.remove('is-active'); }); if (firstVisibleLink) { firstVisibleLink.classList.add('is-active'); } if (!firstVisibleLink && this.previousSection) { this.container.querySelector( `a[href="#${this.previousSection}"]`). classList.add('is-active'); } }, observeSections() { this.headings.forEach(heading => { this.observer.observe(heading); }); }, setUpObserver() { this.observer = new IntersectionObserver( this.handleObserver, this.intersectionOptions); }, findLinksAndHeadings() { this.links = [...this.container.querySelectorAll('a')]; this.headings = this.links.map(link => { let id = link.getAttribute('href'); return document.querySelector(id); }); } }; TableOfContents.init(); });
<div class="container"> <div class="toc js-toc"> <p>Содержание</p> <ul class="js-toc-list"> <li> <a href="#wordpress">WordPress</a> </li> <li> <a href="#woocommerce">WooCommerce</a> <ul> <li> <a href="#plugins">плагины</a> </li> <li> <a href="#themes">темы</a> </li> </ul> </li> <li> <a href="#gutenberg">Gutenberg</a> <ul> <li> <a href="#blocks">блоки</a> </li> </ul> </li> </ul> </div> <div> <h1>Какой-то длинный артикл с главами и подглавами</h1> <p style="height:140px;background:#ccc">Здесь расположен текст высотой 140px</p> <h2 id="wordpress">WordPress</h2> <p style="height:540px;background:#ccdddd">Здесь расположен текст высотой 540px</p> <h2 id="woocommerce">WooCommerce</h2> <p style="height:100px;background:#ccddee">Здесь расположен текст высотой 100px</p> <h3 id="plugins">плагины</h3> <p style="height:640px;background:#dffddd">Здесь расположен текст высотой 640px</p> <h3 id="themes">темы</h3> <p style="height:630px;background:#ccaaaa">Здесь расположен текст высотой 630px</p> <h2 id="gutenberg">Gutenberg</h2> <p style="height:600px;background:#ccbbff">Здесь расположен текст высотой 600px</p> <h3 id="blocks">блоки</h3> <p style="height:540px;background:#eeeecc">Здесь расположен текст высотой 540px</p> </div> </div>
В этом примере для удобства создан объект TableOfContents
, со своими свойствами и методами.
В качестве отслеживаемых элементов используем заголовки с атрибутами id
, которые по сути являются якорями.
Прежде всего нам нужно учитывать, что одновременно на странице могут находиться два и более заголовков! Поэтому на все соответствующие заголовкам пункты меню мы добавляем класс is-visible
, но только на первый из них мы добавляем класс is-active
, которые и подсвечивает ссылку в меню.
Также важно помнить, что контент какого-либо из заголовков может быть супер-длинным, благодаря чему вообще на странице может не быть заголовков. Именно поэтому последний активный заголовок мы сохраняем в переменную и затем проверяем, если на странице нет ни одного заголовка, то используем заголовок из переменной!