Синхронизация видимости элемента с Intersection Observer API

Этот перевод не завершён. Пожалуйста, помогите перевести эту статью с английского

Intersection Observer API позволяет в асинхронном режиме уведомлять приложение о том, что какой-то интересующий нас элемент в той или иной степени перекрыл родительский или другой элемент, в том числе Document. В этой статье мы построим пример блога, в котором в DOM динамически встраиваются рекламные блоки. Затем, с помощью Intersection Observer API, мы выясним, сколько времени показывается каждая отдельная реклама пользователю. Когда такая реклама показывается дольше, чем одну минуту, мы заменяем её на новую.

Хотя многие элементы в нашем примере слабо связаны с реальным миром, этого будет достаточно для понимания API. В реальном мире статьи чаще всего отличаются между собой и хранятся не в клиенте, а загружаются из базы данных; да и реклама не состоит из одной только строчки текста.

Есть важная причина, почему мы используем отслеживание видимости рекламы. Вышло так, что наиболее частое употребление Flash или скриптов в Web рекламе нужно для того, чтобы оценивать эффективность рекламы, а значит, её стоимость. Без Intersection Observer API эта задача свелась бы к повсеместному применению setTimeout и setInterval для каждой отдельной рекламы. Такие техники могут драматически ухудшить производительность страницы. Использование API в этом случае может позволит браузеру взять на себя обработку сложной логики и не только ускорит приложение, но и спасёт вас от ошибок, которые обязательно появятся при использовании setTimeout / setInterval.

Начнём!

Структура приложения: HTML

Структура Web-приложений не очень сложна. Мы будем использовать CSS Grid для стилизации и макетирования, так что всё достаточно очевидно:

<div class="wrapper">
  <header>
    <h1>A Fake Blog</h1>
    <h2>Showing Intersection Observer in action!</h2>
  </header>

  <aside>
    <nav>
      <ul>
        <li><a href="#link1">A link</a></li>
        <li><a href="#link2">Another link</a></li>
        <li><a href="#link3">One more link</a></li>
      </ul>
    </nav>
  </aside>

  <main>
  </main>
</div>

Это заготовка для приложения. В верхней части приложения находится блок <header>. Ниже - боковая панель <aside>, заполненная ссылками. В самом конце структуры - основное тело приложения. Приложение стартует с пустым элементом <main> -  он будет заполнен позже с помощью JavaScript.

Стилизация приложения с помощью CSS

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

Основа

Мы создаем стили для <body> и <main> так, чтобы определить фоновый цвет и сеточную систему.

body {
  font-family: "Open Sans", "Arial", "Helvetica", sans-serif;
  background-color: aliceblue;
}

.wrapper {
  display: grid;
  grid-template-columns: auto minmax(min-content, 1fr);
  grid-template-rows: auto minmax(min-content, 1fr);
  max-width: 700px;
  margin: 0 auto;
  background-color: aliceblue;
}

Элемент приложения <body> сконфигурирован так, чтобы использовать общеупотребимый шрифт из семейства sans-serif и цвет "aliceblue" в качестве фона. Класс "wrapper" оборачивает всё приложение, включая header, sidebar и body content.

Обёртка определяет также CSS Grid сетку, как сетку из двух колонок и двух строк. Первая колонка (размер вычисляется автоматически на основе контента) используется для боковой панели, а вторая колонка (будет использоваться для основного содержимого) имеет ширину, достаточную, чтобы вместить содержимое и занять всю доступную площадь.

Первая строка будет использована для заголовка сайта. Размеры строк определяются схожим образом - размер первой вычисляется на основе контента, а второй - занимает всё доступной пространство, но не меньше размера, необходимого для показа содержимого.

Ширина обёртки зафиксирована - 700px, так что её размер будет удобен для представления приложения в MDN.

The wrapper's width is fixed at 700px so that it will fit in the available space when presented inline on MDN below.

Заголовок

Заголовок достаточно прост, так как в нашем примере он содержит небольшой текст.

header {
  grid-column: 1 / -1;
  grid-row: 1;
  background-color: aliceblue;
}

Значение grid-row равно 1, так как мы хотим поместить заголовок в верхнюю строку сеточной системы. Более интересно использование grid-column; Мы указываем здесь, что блок занимает пространство с первой колонки до первой с конца (то есть последней).

Боковая панель

Боковая панель используется для показа ссылок на другие страницы приложения. Ни одна из них не будет работать в нашем примере. Вся боковая панель нужна, чтобы просто приблизить внешний вид приложения к реальному блогу. Боковая панель создается с помощью элемента <aside>.

aside {
  grid-column: 1;
  grid-row: 2;
  background-color: cornsilk;
  padding: 5px 10px;
}

aside ul {
  padding-left: 0;
}

aside ul li {
  list-style: none;
}

aside ul li a {
  text-decoration: none;
}

Важно отметить, что значение grid-column здесь установлено в 1 для того, чтобы поместить панель в левую часть экрана. Если вы поменяете это значение на "-1", то панель переместится вправо, однако, в этом случае, вам понадобится немного изменить стили внутренних элементов. Значение grid-row равно 2, чтобы боковая панель заняла область вдоль области контента.

Область контента

Контент будет содержаться в элементе <main>.

main {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  margin-left: 16px;
  font-size: 16px;
}

Главная особенность здесь - контент занимает вторую колонку и вторую строку.

Статьи

Каждая статья состоит из элемента <article>:

article {
  background-color: white;
  padding: 6px;
}

article:not(:last-child) {
  margin-bottom: 8px;
}

article h2 {
  margin-top: 0;
}

Эти стили создают область с белым фоном с небольшими отступами как внутри области, так и между областями.

Рекламные блоки

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

.ad {
  height: 96px;
  padding: 6px;
  border-color: #555;
  border-style: solid;
  border-width: 1px;
}

.ad:not(:last-child) {
  margin-bottom: 8px;
}

.ad h2 {
  margin-top: 0;
}

.ad div {
  position: relative;
  float: right;
  padding: 0 4px;
  height: 20px;
  width: 120px;
  font-size: 14px;
  bottom: 30px;
  border: 1px solid black;
  background-color: rgba(255, 255, 255, 0.5);
}

Здесь нет никакой магии. Простой CSS.

Совмещаем с JavaScript

Перейдём к JavaScript коду, который всё оживит. Начнем с глобальных переменных:

let contentBox;

let nextArticleID = 1;
let visibleAds = new Set();
let previouslyVisibleAds = null;

let adObserver;
let refreshIntervalID = 0;

Вот что здесь используется:

contentBox
Ссылка на элемент <main>. Это место, куда мы будем вставлять статьи и рекламу.
nextArticleID
Каждая статья получает уникальный цифровой ID. Эта переменная позволяет понять, какой следующий ID использовать.
visibleAds
Set используется для отслеживания текущих видимых на экране рекламных блоков.
previouslyVisibleAds
Используется для временного хранения списка рекламных блоков в то время, как документ невидим (например, если пользователь переключился на другой таб)
adObserver
Содержит экземпляр IntersectionObserver, используемый для вычисления наложения рекламных блоков и границ элемента <main>.
refreshIntervalID
Переменная для хранения ID интервала, который возвращается функцией setInterval(). Этот интервал будет использоваться для запуска переодических обновлений рекламных блоков.

Установка

Для первичного запуска приложения мы вызовем функцию startup():

window.addEventListener("load", startup, false);

function startup() {
  contentBox = document.querySelector("main");

  document.addEventListener("visibilitychange", handleVisibilityChange, false);

  let observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: [0.0, 0.75]
  };

  adObserver = new IntersectionObserver(intersectionCallback,
                    observerOptions);

  buildContents();
  refreshIntervalID = window.setInterval(handleRefreshInterval, 1000);
}

Вначале мы получаем элемент <main>, в который можем вставлять содержимое. Затем мы устанавливаем обработчик на событие visibilitychange. Это событие срабатывает, когда документ меняет состояние между видим/невидим, например, когда пользователь переключается между табами. Intersection Observer API не должен засчитывать пересечение с элементом Main, если пользователь не будет в это время смотреть на вкладку. Таким образом, мы должны останавливать наши таймеры каждый раз, когда пользователь уходит со страницы. С помощью этого обработчика.

Затем мы устанавливаем параметры для IntersectionObserver. Параметры определяют, что IntersectionObserver должен отслеживать перекрытия с областью видимости документа (параметр root в значении null). У нас нет отступов для модификации корневой области; мы хотим отслеживать совпадение границ элементов и видимого документа именно для целей обнаружения перекрытий.

Параметр "порог" (threshold) содержит массив со значениями 0.0 и 0.75; Это заставит обработчик вызываться каждый раз, когда целевой элемент становится полностью обёрнут или только начинает выходить из зоны перекрытия (коэффициент перекрытия 0.0) или проходит порог в 75% видимости в обоих направлениях (коэффициент перекрытия 0.75).

Наблюдатель adObserver создается с помощью конструктора IntersectionObserver. В аргументы конструктора мы передаём функцию обратного вызова (intersectionCallback) и ранее определенный объект параметров.

После этого мы вызываем функцию buildContents(). Её мы напишем чуть позже. Функция генерирует и вставляет в контейнер статьи и рекламные блоки.

В конце мы устанавливаем интервал, который каждую секунду запускает проверку - нужно ли что-то обновить. Нам необходимо второе обновление, так как в каждом отдельном рекламном блоке мы показываем таймер. В реальном приложении это не понадобится.

Обработка изменения видимости документа

Давайте рассмотрим обработчик события visibilitychange. Это событие срабатывает, когда документ становится видим или невидим. Как правило, это случается, когда пользователь переключается между табами. Так как Intersection Observer отслеживает только перекрытия элемента с корневым элементом, нам необходимо отдельно позаботиться о детекции видимости документа. Для этого мы используем Page Visibility API.

function handleVisibilityChange() {
  if (document.hidden) {
    if (!previouslyVisibleAds) {
      previouslyVisibleAds = visibleAds;
      visibleAds = [];
      previouslyVisibleAds.forEach(function(adBox) {
        updateAdTimer(adBox);
        adBox.dataset.lastViewStarted = 0;
      });
    }
  } else {
    previouslyVisibleAds.forEach(function(adBox) {
      adBox.dataset.lastViewStarted = performance.now();
    });
    visibleAds = previouslyVisibleAds;
    previouslyVisibleAds = null;
  }
}

Так как событие само по себе не указывает, стал ли документ видимым или, наоборот, невидимым, мы должны вручную проверить свойство document.hidden. В теории, это событие может сработать несколько раз, поэтому нам нужно обрабатывать только те рекламные блоки, учёт которых ещё не был приостановлен.

Для остановки таймеров нам нужно удалить ссылки на рекламные блоки из коллекции visibleAds и пометить их как неактивные. Чтобы это сделать, мы начинаем с сохранения ссылок на текущие видимые элементы в переменную previouslyVisibleAds. Это нужно, чтобы в дальнейшем можно было восстановить счётчики для этих блоков. Так мы указываем приложению, что эту рекламу не надо считать активной. Затем, если пользователь вернулся в документ, мы вызываем функцию  updateAdTimer() для каждого отложенного элемента. Эта функция обновляет общее время видимости элемента. После этого мы присваиваем переменной dataset.lastViewStarted значение 0, что означает, что таймер не запущен.

Если документ стал видимым, мы выполняем обратный процесс: сначала мы проходим через коллекцию previouslyVisibleAds. Для каждого элемента мы присваиваем  dataset.lastViewStarted значение, соответствующее текущему времени документа (в миллисекундах с момента создания документа). Это время можно узнать с помощью  метода performance.now(). Затем мы присваиваем переменной  visibleAds закешированное ранее значение previouslyVisibleAds, с обнулением последней переменной. Теперь рекламные блоки перезапущены и настроены, так что время простоя не будет учиваться.

Обработчик изменений наложения

При каждой итерации в браузерном event loop, каждый наблюдатель  IntersectionObserver проверяет, не прошел ли какой-либо из элементов-целей через пороговые значения наблюдателя.  Для каждого наблюдателя список таких целей собирается в один список и отправляется в функцию обратного вызова наблюдателя. Каждый элемент списка - это IntersectionObserverEntry объект. В нашем приложении intersectionCallback() выглядит так:

function intersectionCallback(entries) {
  entries.forEach(function(entry) {
    let adBox = entry.target;

    if (entry.isIntersecting) {
      if (entry.intersectionRatio >= 0.75) {
        adBox.dataset.lastViewStarted = entry.time;
        visibleAds.add(adBox);
      }
    } else {
      visibleAds.delete(adBox);
      if ((entry.intersectionRatio === 0.0) && (adBox.dataset.totalViewTime >= 60000)) {
        replaceAd(adBox);
      }
    }
  });
}

Как мы упоминали ранее, функция обратного вызова IntersectionObserver  получает на вход массив элементов, которые активировали наблюдателя. В нашей функции мы итерируемся по этому массиву. Если элемент пересекается с корневым элементом, мы знаем, что он стал видимым. Если он становится видимым более, чем на 75%, мы считаем, что реклама видима и мы запускаем таймер, выставляя значение  dataset.lastViewStarted равным времени изменения параметра перекрытия entry.time. Затем мы добавляем рекламный блок в набор visibleAds.

Если рекламный блок уходит из зоны видимости, мы удаляем его из набор видимых элементов. Затем, в зависимости от значения entry.ratio, мы либо меняем рекламу, либо ставим на паузу. Так, если значение равно 0.0 и реклама уже была видна минимум минуту, мы вызываем функцию replaceAd() . В этом случае пользоватль видит разные рекламные блоки, но сама реклама меняется незаметно для пользователя.

Обработка периодический событий

Каждую секунду у нас срабатывает интервал handleRefreshInterval(), который мы задали в функции startup(). Главная задача этого интервала - обновлять таймеры каждую секунду и перерисовывать значение таймеров.

function handleRefreshInterval() {
  let redrawList = [];

  visibleAds.forEach(function(adBox) {
    let previousTime = adBox.dataset.totalViewTime;
    updateAdTimer(adBox);

    if (previousTime != adBox.dataset.totalViewTime) {
      redrawList.push(adBox);
    }
  });

  if (redrawList.length) {
    window.requestAnimationFrame(function(time) {
      redrawList.forEach(function(adBox) {
        drawAdTimer(adBox);
      });
    });
  }
}

Массив redrawList используется для хранения списка рекламных блоков, которые должны быть перерисованы в следующем цикле перерисовки. Это нужно, так как таймеры текущих рекламных блоков не всегда совпадают с реальными таймингами из-за прочих системных процессов. Или из-за того, что вы указали в качестве интервала промежуток не в 1000мс.

Затем, для каждого видимого рекламного блока, мы сохраняем значение dataset.totalViewTime (количество миллисекунд, которое текущая реклама была видима с момента последнего обновления этого значения). После этого вызываем функцию updateAdTimer() для обновления времени. Если оно изменилось, мы вставляем рекламный блок в список redrawList. Таким образом, при обработке следующего кадра приложение знает, что нужно перерисовать.

И, наконец, если существует хоть один элемент, который нужно перерисовать, мы будем используем requestAnimationFrame(), чтобы отложить отрисовку каждого элемента на тот момент, когда будет формироваться следующий кадр.

Обновление таймера видимости рекламы

Ранее мы уже видели, что если нам нужно обновить общее время видимости рекламы - мы вызываем функцию updateAdTimer(). Эта функция принимает в качестве аргумента объект HTMLDivElement.

function updateAdTimer(adBox) {
  let lastStarted = adBox.dataset.lastViewStarted;
  let currentTime = performance.now();

  if (lastStarted) {
    let diff = currentTime - lastStarted;

    adBox.dataset.totalViewTime = parseFloat(adBox.dataset.totalViewTime) + diff;
  }

  adBox.dataset.lastViewStarted = currentTime;
}

Для отслеживания времени видимости элемента мы используем два data-атрибута на каждом рекламном блоке:

lastViewStarted
Время в миллисекундах относительно первоначальной загрузки страницы до момента, когда счётчик рекламного блока был обновлён или блок стал невидим. Если значение равно нулю - блок не был видим в последний раз, когда проверялся.
totalViewTime
Общее время видимости рекламного блока.

Значение этих атрибутов можно получить с помощью HTMLElement.dataset. Значения - строки, но вы можете конвертировать их в числа. Фактически, JavaScript делает это автоматически, но нам всё равно придется в одном месте сделать это вручную.

Функция начинается с выяснения времени, когда происходила последняя проверка видимости рекламы (adBox.dataset.lastViewStarted). Мы также получаем текущее время с момента создания документа с помощью performance.now() currentTime.

Если время последней проверки lastStarted не равно нулю - это значит, что таймер сейчас уже запущен. В этом случае мы вычисляем разницу между текущим временем и временем старта проверки. Это значение покажет, сколько реклама была видима с момента последнего старта детекции. Затем это значение прибавляем к уже имееющемуся totalViewTime. Обратите внимание не вызов parseFloat(): так как все значения из Dataset - строки, JavaScript пытается соединить строки вместо того, чтобы просуммировать числа.

В конце мы присваеваем lastViewStarted текущее значение. Это делается вне зависимости от того, был ли элемент видим во время вызова функции или нет - это позволяет таймеру рекламных блоков срабатывать всегда, когда эта функция вызывается. Это имеет смысл, потому что вызов может сработать ровно в тот момент, когда реклама только появилась.

Показываем таймер рекламы

Внутри каждого рекламного блока мы отображаем текущее значение общего времени видимости в формате мин:сек. Для этого мы передаем в функцию drawAdTimer контейнер:

function drawAdTimer(adBox) {
  let timerBox = adBox.querySelector(".timer");
  let totalSeconds = adBox.dataset.totalViewTime / 1000;
  let sec = Math.floor(totalSeconds % 60);
  let min = Math.floor(totalSeconds / 60);

  timerBox.innerText = min + ":" + sec.toString().padStart(2, "0");
}

Функция находит внутри переданного контейнера блок с классом timer. Затем забирает данные о текущем общем времени видимости блока. С помощью деления на 1000, 60 и 60 мы преобразуем результат в нужный формат (миллисекунды -> секунды -> минуты / секунды)

Метод String.padStart() используется для того, чтобы убедиться, что число секунд всегда состоят из двух цифр.

Строим содержимое страницы

Функция buildContents() вызывается при старте приложения. Она формирует тело статьи и добавляет рекламные блоки:

let loremIpsum = "<p>Lorem ipsum dolor sit amet, consectetur adipiscing" +
  " elit. Cras at sem diam. Vestibulum venenatis massa in tincidunt" +
  " egestas. Morbi eu lorem vel est sodales auctor hendrerit placerat" +
  " risus. Etiam rutrum faucibus sem, vitae mattis ipsum ullamcorper" +
  " eu. Donec nec imperdiet nibh, nec vehicula libero. Phasellus vel" +
  " malesuada nulla. Aliquam sed magna aliquam, vestibulum nisi at," +
  " cursus nunc.</p>";

function buildContents() {
  for (let i=0; i<5; i++) {
    contentBox.appendChild(createArticle(loremIpsum));

    if (!(i % 2)) {
      loadRandomAd();
    }
  }
}

Переменная loremIpsum содержит текст, который мы используем как тело статьи. Разумеется, в реальном мире вы будете забирать статьи из какой-то базы данных. Но это тема другой статьи, поэтому мы пошли простым путём.

buildContents() создаёт страницу с пятью статьями. Каждая нечётная статья содержит рекламные блоки.  Статьи будут вставлены в блок контента <main>. после того, как будет вызван метод createArticle(), который мы разберем позже.

Рекламные блоки создаются с помощью функции loadRandomAd(). Эта функция создает и вставляет блоки одновременно. Как мы увидим позже, эта же функция может и заменить уже существующую рекламу. Но пока что просто добавим рекламу в существующий текст.

Создаем статью

Для создания элемента статьи <article> и её содержимого мы используем функцию createArticle(), которая в качестве входных данных принимает строку-текст статьи.

function createArticle(contents) {
  let articleElem = document.createElement("article");
  articleElem.id = nextArticleID;

  let titleElem = document.createElement("h2");
  titleElem.id = nextArticleID;
  titleElem.innerText = "Article " + nextArticleID + " title";
  articleElem.appendChild(titleElem);

  articleElem.innerHTML += contents;
  nextArticleID +=1 ;

  return articleElem;
}

Сперва, элемент <article> создаётся и ему присваивается уникальный ID nextArticleID (это просто счётчик от нуля до бесконечности). Затем мы создаем и добавляем элемент <h2> для заголовка и применяем HTML из переменной contents. Наконец, мы увеличиваем значение nextArticleID (таким образом, следующий элемент получит уникальный ID) и возвращаем элемент статьи обратно.

Создание рекламного блока

Функция loadRandomAd() имитирует загрузку рекламы и её добавление на страницу. Если вы не указываете значение для replaceBox, создается и применяется новый контейнер для рекламы. Если вы указали replaceBox, этот контейнер рассматривается, как уже существующий элемент. Вместо создания нового, существующий элемент изменяется, чтобы содержать актуальные данные. Это помогает избежать риска неэффективной перерисовки элементов, если вы сначала будете удалять элемент из DOM, а затем вставлять новый.

function loadRandomAd(replaceBox) {
  let ads = [
    {
      bgcolor: "#cec",
      title: "Eat Green Beans",
      body: "Make your mother proud—they're good for you!"
    },
    {
      bgcolor: "aquamarine",
      title: "MillionsOfFreeBooks.whatever",
      body: "Read classic literature online free!"
    },
    {
      bgcolor: "lightgrey",
      title: "3.14 Shades of Gray: A novel",
      body: "Love really does make the world go round..."
    },
    {
      bgcolor: "#fee",
      title: "Flexbox Florist",
      body: "When life's layout gets complicated, send flowers."
    }
  ];
  let adBox, title, body, timerElem;

  let ad = ads[Math.floor(Math.random()*ads.length)];

  if (replaceBox) {
    adObserver.unobserve(replaceBox);
    adBox = replaceBox;
    title = replaceBox.querySelector(".title");
    body = replaceBox.querySelector(".body");
    timerElem = replaceBox.querySelector(".timer");
  } else {
    adBox = document.createElement("div");
    adBox.className = "ad";
    title = document.createElement("h2");
    body = document.createElement("p");
    timerElem = document.createElement("div");
    adBox.appendChild(title);
    adBox.appendChild(body);
    adBox.appendChild(timerElem);
  }

  adBox.style.backgroundColor = ad.bgcolor;

  title.className = "title";
  body.className = "body";
  title.innerText = ad.title;
  body.innerHTML = ad.body;

  adBox.dataset.totalViewTime = 0;
  adBox.dataset.lastViewStarted = 0;

  timerElem.className="timer";
  timerElem.innerText = "0:00";

  if (!replaceBox) {
    contentBox.appendChild(adBox);
  }

  adObserver.observe(adBox);
}

Вначале мы определяем массив ads. Этот массив содержит данные, необходимые для создания рекламных блоков. В реальном приложении, конечно, мы будем загружать эти данные из базы или, что более вероятно, из рекламного сервиса, который будет использовать какой-то API. Тем не менее, наша простая задача решается: каждый рекламный блок представлен тремя свойствами: фоновым цветом (bgcolor), заголовком (title) и текстовым содержимым (body).

Затем мы определяем несколько переменных:

adBox
Определяет контейнер, содержащий рекламу. Вновь добавленные рекламные блоки будут добавлены к странице с помощьюDocument.createElement(). Когда замещается существующая реклама, в этой переменной указан элемент (replaceBox).
title
Содержит ссылку на элемент <h2>.
body
Содержит ссылку на элемент <p>.
timerElem
Содержит ссылку на элемент таймера <div>.

Случайный рекламный блок вычисляется с помощью Math.floor(Math.random() * ads.length). Результат этой функции - целое число между 0 и максимальным количеством рекламных блоков. Соответствующий рекламный блок теперь доступен нам из переменной adBox.

Если replaceBox содержит какое-то значение, мы рассматриваем его как элемент рекламного блока. Мы завершаем наблюдение за элементом с помощью IntersectionObserver.unobserve(). Затем собираем в локальные переменные данные из каждого свойства элемента: заголовок, тело и таймер.

Если никакое значение не указано для replaceBox, мы создаем новый элемент. Создаётся новый контейнер <div>. Его CSS-параметры задаются с помощью класса "ad". Затем создаются заголовок рекламного блока, его текст и таймер.  Соотстветвенно, это <h2>, <p> и <div>. Эти элементы применяются к контейнеру adBox.

После этого разветвления наш код вновь возвращается к единому. Фоновый цвет рекламных блоков присваевается соответственно записям. Элементам присваиваются классы и содержимое.

Наступаем время присвоить data-параметры, чтобы отслеживать видимость рекламных блоков с помощью установки adBox.dataset.totalViewTime и adBox.dataset.lastViewStarted равными нулю.

Наконец, мы устанавливаем CSS-класс контейнеру таймера. С помощью этого класса приложение сможет с лёгкостью собирать данные и обновлять их для каждого таймера. По умолчанию, текст этого контейнера - "0:00".

Если мы создаём новую рекламу, мы должны применить элемент к страницы с помощью Document.appendChild(). Если мы лишь заменяем рекламный блок - он уже представлен в DOM и всё, что нам нужно сделать - это обновить его. Затем мы вызываем функцию observe(). adObserver начинает отслеживать изменения перекрытия элементов в видимой области приложения. С этого момента любой рекламный блок, который становится на 100% скрыт или хотя бы на один пиксель видим или преодолевает порог в 75% видимости в любом направлении, запускает вычисление таймингов и обновление содержимого таймеров.

Замена существующей рекламы

Наша функция обработки перекрытия отслеживает рекламные блоки. Когда они становятся на 100% и общее время их видимости достаточное для того, рекламный блок заменяется на новый. Когда это происходит, вызывается функция replaceAd().

function replaceAd(adBox) {
  let visibleTime;

  updateAdTimer(adBox);

  visibleTime = adBox.dataset.totalViewTime
  console.log("  Replacing ad: " + adBox.querySelector("h2").innerText + " - visible for " + visibleTime)

  loadRandomAd(adBox);
}

replaceAd() начинается с вызова updateAdTimer() для существующего рекламного блока, чтобы убедиться, что таймер обновлён. С помощью этого вызова мы убеждаемся, что totalViewTime, который мы используем для обработки, действительно совпадает с тем, что видел пользователь. Мы логгируем это значение и загружаем в рекламный блок новые данные. Помните, что в реальном мире вы не должны логгировать подобные вещи, а скорее использовать API для сбор логов.

Результат

Вы можете увидеть результат в окне ниже. Попробуйте экспериментировать с прокрутой и понаблюдайте за тем, как изменение видимости затрагивает каждый таймер. Кроме того, обратите внимание, что каждый рекламный блок обновляется только в том случае, если он уже был видим в течение минуты.

См. также: