englishпо-русски
© 2022 — Владимир Лысов, Челябинск, Россия гитхаб dubaua@gmail.com

Зачем нужен иммёрсер?

Иногда дизайнеры создают сложную логику и фиксируют части интерфейса. А еще они красят разделы страницы в контрастные цвета. Как с этим справиться?

Вам поможет иммёрсер — джаваскрипт библиотека для замены фиксированных элементов при прокрутке страницы.

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

Иммёрсер написан на чистом джаваскрипте. Всего 5.39Кб в сжатии gzip.

Термины

Корневой элемент иммёрсера — это родительский контейнер для ваших фиксированных блоков. Фактически они позиционированы абсолютно внутри фиксированного корневого элемента. Слои — это разделы страницы, окрашенные в разные цвета. Еще вы наверняка захотите добавить навигацию по разделам, выделяющую активный раздел.

Установка

Через npm:

Через yarn:

Или если вы хотите использовать иммёрсер в браузере как глобальную переменную:

npm install immerser

yarn add immerser

<script src="https://unpkg.com/immerser@3.1.1/dist/immerser.min.js"></script>

Подготовьте разметку

Сначала настройте свой фиксированный контейнер как корневой элемент иммёрсера, добавив атрибут data-immerser

Затем расположите в нем абсолютно позиционированные дочерние элементы и добавьте каждому атрибут data-immerser-solid="solid-id" с идентификатором блока.

Добавьте каждому слою атрибут data-immerser-layer. Передайте конфигурацию в виде JSON в каждый слой с помощью атрибута data-immerser-layer-config='{"solid-id": "classname-modifier"}'. Также вы можете передать конфигурацию всех слоев массивом в параметре solidClassnameArray настроек. Конфигурация должна содержать описание классов для блоков, когда они находятся поверх слоя.

Так же вы можете добавить элемент с атрибутом data-immerser-pager для создания навигации.

<div class="fixed" data-immerser>
  <div class="fixed__pager pager" data-immerser-pager data-immerser-solid="pager"></div>
  <a href="#reasoning" class="fixed__logo logo" data-immerser-solid="logo">иммёрсер</a>
  <div class="fixed__menu menu" data-immerser-solid="menu">
    <a href="#reasoning" class="menu__link">Зачем нужен иммёрсер</a>
    <a href="#how-to-use" class="menu__link">Как пользоваться</a>
    <a href="#how-it-works" class="menu__link">Принцип работы</a>
    <a href="#options" class="menu__link">Настройки</a>
    <a href="#recipes" class="menu__link">Рецепты</a>
  </div>
  <div class="fixed__language language" data-immerser-solid="language">
    <a href="/" class="language__link">english</a>
    <a href="/ru.html" class="language__link">по-русски</a>
  </div>
  <div class="fixed__about about" data-immerser-solid="about">
    © 2022 — Владимир Лысов, Челябинск, Россия
    <a href="https://github.com/dubaua/immerser">гитхаб</a>
    <a href="mailto:dubaua@gmail.com">dubaua@gmail.com</a>
  </div>
</div>

<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="reasoning"></div>
<div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="how-to-use"></div>
<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="how-it-works"></div>
<div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="options"></div>
<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="recipes"></div>

Примените стили

Добавьте стили цвета текста и фона на ваши блоки и слои с помощью классов, переданных в дата-атрибут или настройки. В примере я использую методологию БЭМ.

.fixed {
  position: fixed;
  top: 2em;
  bottom: 3em;
  left: 3em;
  right: 3em;
  z-index: 1;
}
.fixed__pager {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(0, -50%);
}
.fixed__logo {
  position: absolute;
  top: 0;
  left: 0;
}
.fixed__menu {
  position: absolute;
  top: 0;
  right: 0;
}
.fixed__language {
  position: absolute;
  bottom: 0;
  left: 0;
}
.fixed__about {
  position: absolute;
  bottom: 0;
  right: 0;
}
.pager,
.logo,
.menu,
.language,
.about {
  color: black;
}
.pager--contrast,
.logo--contrast,
.menu--contrast,
.language--contrast,
.about--contrast {
  color: white;
}

Инициализируйте иммёрсер

Добавьте иммёрсер в код и создайте экземпляр с настройками.

// Вам не нужно импортировать иммёрсер,
// если вы используете его в браузере как глобальную переменную
import Immerser from 'immerser';

const immerserInstance = new Immerser({
  // будет переопределена настройками,
  // переданными в атрибут data-immerser-layer-config каждого слоя
  solidClassnameArray: [
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    {
      pager: 'pager--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    {
      logo: 'logo--contrast-only-md',
      pager: 'pager--contrast-only-md',
      language: 'language--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
  ],
  hasToUpdateHash: true,
  fromViewportWidth: 1024,
  pagerLinkActiveClassname: 'pager__link--active',
  scrollAdjustThreshold: 50,
  scrollAdjustDelay: 600,
  onInit(immerser) {
    // колбек после инициализации
  },
  onBind(immerser) {
    // колбек после привязки к документу
  },
  onUnbind(immerser) {
    // колбек после отвязки от документа
  },
  onDestroy(immerser) {
    // колбек после уничтожения
  },
  onActiveLayerChange(activeIndex, immerser) {
    // колбек после смены активного слоя
  },
});

Принцип работы

Сначала иммёрсер собирает информацию о слоях, блоках, окне и документе. Затем скрипт создает карту состояний для каждого слоя. Карта содержит размеры слоя, блоков и позиции их пересечений при скролле.

После сбора информации скрипт копирует все блоки в маскирующий контейнер и применяет к каждому классы, переданные в настройках. Если вы добавили навигацию, то иммёрсер создаст ссылки на каждый слой.

Затем иммёрсер подписывается на события скролла документа и изменения размеров окна.

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

Настройки

Вы можете передать настройки параметром функции конструктора или дата-атрибутом в документе. Дата-аттрибут обрабатывается последним, поэтому он переопределит настройки, переданные в конструктор.

параметртипзначение по умолчаниюописание
solidClassnameArrayarray[]Массив настроек слоев. Конфигурация, переданная в data-immerser-layer-config перезапишет эту настройку для соответствующего слоя. Пример конфигурации показан выше
fromViewportWidthnumber0Минимальная ширина окна для инициализации иммёрсера
pagerThresholdnumber0.5Насколько должен следующий слой быть видим в окне, чтобы он стал активен в навигации
hasToUpdateHashbooleanfalseФлаг, контролирующий обновление хеша страницы
scrollAdjustThresholdnumber0Дистанция до верха или низа окна браузера в пикселях. Если текущая дистанция меньше переданного значения, то скрипт подстроит положение скролла
scrollAdjustDelaynumber600Сколько ждать бездействия пользователя, чтобы начать подстройку скролла
pagerLinkActiveClassnamestringpager-link-activeПрименяется, к каждой ссылке пейджера, ссылающуюся на активный слой
onInitfunctionnullКолбек после инициализации. Принимает один параметр — экземпляр иммёрсера
onBindfunctionnullКолбек после привязки к документу. Принимает один параметр — экземпляр иммёрсера
onUnbindfunctionnullКолбек после отвязки от документа. Принимает один параметр — экземпляр иммёрсера
onDestroyfunctionnullКолбек после уничтожения. Принимает один параметр — экземпляр иммёрсера
onActiveLayerChangefunctionnullКолбек после смены активного слоя. Принимает два параметра: индекс следующего слоя и экземпляр иммёрсера

Клонирование подписчиков событий

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

<div class="fixed" data-immerser>
  <div data-immerser-mask>
    <div data-immerser-mask-inner>
      <!-- ваша разметка -->
    </div>
  </div>
  <div data-immerser-mask>
    <div data-immerser-mask-inner>
      <!-- ваша разметка -->
    </div>
  </div>
</div>

Обработка наведения на границах слоев

Если вы наведете мышь на элемент, находящийся на границе слоев, то псевдоселектор :hover сработает только на одну часть. Чтобы наведение сработало на все клоны элемента, задайте идентификатор наведения в атрибуте data-immerser-synchro-hover="hoverId". При наведении мыши на такой элемент, ко всем его клонам добавится класс _hover. Стилизуйте по этому селектору вместе с псевдоселектором :hover, чтобы добиться нужного эффекта.

a:hover,
a._hover {
  color: magenta;
}

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

Иммёрсер не отслеживает изменения документа, если вы динамически добавляете или удаляете ноды. Если вы меняете высоту документа, и хотите, чтобы иммёрсер пересчитал и перерисовал блоки, вызовите метод onDOMChange у экземпляра иммёрсера.

// adding or removing node, that changes DOM height
document.appendChild(someNode);
document.removeChild(anotherNode);

// then explicitly redraw immerser
immerserInstance.onDOMChange();