
Создание компонента
Наш пример сайта был построен на платформе Drupal, но общий подход будет аналогичным в любой системе управления контентом. Сначала нам нужно создать компонент, который будет содержать все необходимые поля, которые мы хотим отобразить в нашем параллаксе. В этом примере мы будем использовать типы параграфов и создадим два вида слайдов: один с изображением и другой без изображения.
Слайд параллакса с изображением
Этот слайд позволит нам добавить изображение, заголовок и текстовую информацию, которую мы хотим передать на этом конкретном слайде, а также задать выравнивание текста (слева, по центру или справа) и возможность скрыть или показать кредит изображения.

Слайд параллакса без изображения
Этот слайд похож на предыдущий, но имеет несколько ключевых отличий. Он не включает изображение и все, что с ним связано, и позволяет использовать более обширное форматирование текста на слайдах без изображения. Это означает, что текст на таком слайде может занимать гораздо больше пространства без необходимости беспокоиться о проблемах контраста цветов с использованием продвинутого форматирования текста.

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

Подключение компонента к пользовательской теме
Когда наш компонент готов, следующим шагом будет его интеграция в пользовательскую тему. В этом примере мы используем тему Emulsify.
Сначала мы создадим файл paragraph--parallax-slideshow.html.twig, который будет включать шаблон parallax-slideshow.twig, содержащий JavaScript-библиотеку parallax-slideshow, отвечающую за всю логику работы эффекта параллакса, а также необходимые стили.
{{ attach_library('your_theme/parallax-slideshow') }}
{% set drupal = true %}
{% include "@organisms/parallax-slideshow/parallax-slideshow.twig"
with {
'slideshow_id': paragraph.id.0.value,
'slides': content.field_slide_items|render
}
%}
Вот как выглядит шаблон parallax-slideshow.twig. Обратите внимание на пустой <div class="parallax-slideshow__image-wrapper"></div>
. Это место, где будут отображаться изображения слайдов и где будет происходить эффект плавного перехода между ними.
{%
set classes = [
paragraph.bundle|clean_class,
"parallax-slideshow",
]
%}
<div{{ attributes.addClass(classes) }} data-id="{{ slideshow_id }}">
<div class="parallax-slideshow__wrapper">
<div class="parallax-slideshow__image-wrapper"></div>
{{ slides }}
</div>
</div>
Затем мы создадим файлы paragraph--parallax-image-slide.html.twig и paragraph--parallax-blank-slide.html.twig. Оба файла включают шаблон parallax-slide.twig, который является молекулой в системе дизайна, организующей содержимое каждого слайда и добавляющей все необходимые стили. Они почти идентичны, за исключением того, что слайд без изображения не передает изображение в шаблон parallax-slide.twig.
{% include "@molecules/parallax-slide/parallax-slide.twig" with {
'slide_id': paragraph.id.0.value,
'slide_img': content.field_image|render,
'slide_title': paragraph.field_component_title.0.value,
'slide_caption': content.field_caption|render,
'slide_caption_alignment': paragraph.field_caption_alignment.0.value,
'slide_hide_credit': paragraph.field_hide_credit.0.value,
'slide_type': paragraph.type.0.value.target_id,
} %}
Вот как выглядит шиблон parallax-slide.twig:
{%
set classes = [
'parallax-slide',
slide_caption_alignment ? 'parallax-slide--caption-' ~ slide_caption_alignment|lower : '',
slide_type ? 'parallax-slide--' ~ slide_type|replace({'_': '-'}) : '',
]
%}
<div {{ attributes.addClass(classes) }} slide-data-id="{{ slide_id }}">
<div class="parallax-slide__info-wrapper">
<div class="parallax-slide__info-inner-wrapper full-width">
{% if slide_title %}
<div class="parallax-slide__title-wrapper">
<h1 class="parallax-slide__title">{{ slide_title }}</h1>
</div>
{% endif %}
{% if slide_caption %}
<div class="parallax-slide__caption">{{ slide_caption }}</div>
{% endif %}
</div>
</div>
</div>
Предварительная загрузка данных параллакса
Чтобы избежать заметной задержки между слайдами, компоненту необходимо предварительно загрузить первые два изображения при загрузке страницы. По мере того как пользователь начинает прокручивать страницу, дополнительные изображения загружаются динамически в фоновом режиме. Это обеспечивает плавный переход между слайдами без заметной задержки и улучшает общий пользовательский опыт.
Нам нужно передать структурированные данные из бэкенда в JavaScript. Ниже приведена функция, которая загружает данные и прикрепляет их к drupalSettings
для использования в теме.
function your_theme_preprocess_paragraph_parallax_slideshow(&$variables) {
$paragraph = $variables['paragraph'];
$pid = $paragraph->id();
$lazy_load_data[$pid] = [];
if ($paragraph->hasField('field_slide_items')) {
$slide_items_ref = $paragraph->get('field_slide_items');
$slide_items = $slide_items_ref->referencedEntities();
foreach ($slide_items as $slide_id => $slide) {
// Начальная настройка массива.
$lazy_load_data[$pid][$slide_id] = [
'id' => NULL,
'image' => NULL,
];
// ID.
if ($slide->hasField('id') && !$slide->get('id')->isEmpty()) {
$lazy_load_data[$pid][$slide_id]['id'] = $slide->get('id')->first()->getValue();
}
// Отрендеренное изображение.
if ($slide->hasField('field_image') && !$slide->get('field_image')->isEmpty()) {
$lazy_load_data[$pid][$slide_id]['image'] = _your_theme_get_rendered_slide_image($slide);
}
}
}
// Прикрепляем к объекту JSON для чтения в теме.
$variables['#attached']['drupalSettings']['yourTheme']['parallaxSlideshowData'] = $lazy_load_data;
$variables['#attached']['library'][] = 'your_theme/parallax-slideshow';
}
function your_theme_get_rendered_slide_image($slide) {
if ($slide->hasField('field_image') && !$slide->get('field_image')->isEmpty()) {
$image_view = $slide->field_image->view('default');
$rendered_image = \Drupal::service('renderer')->render($image_view);
return $rendered_image;
}
return NULL;
}
После того как данные прикреплены к drupalSettings
в нашем файле JavaScript, мы можем получить доступ к parallaxSlideshowData
для динамической загрузки изображений и управления эффектом параллакса.
Реализация параллакса с помощью JavaScript
Ниже приведен разбор того, как файл JavaScript оживляет параллакса слайд-шоу.
Drupal.behaviors.parallaxSlideshow = {
attach: function (context) {
const parallaxSlideshowData =
drupalSettings.yourTheme.parallaxSlideshowData;
if (!parallaxSlideshowData) return;
const slideshows = once('parallax-slideshow', '.parallax-slideshow', context);
slideshows.forEach((slideshow) => {
const loadedSlideIds = new Set();
const loadedImages = new Set();
initializeParallaxSlideshow(slideshow, parallaxSlideshowData, loadedSlideIds, loadedImages);
});
},
};
Давайте начнем с получения данных слайд-шоу из drupalSettings
и убедимся, что скрипт выполняется только один раз для каждого элемента слайд-шоу. Функция initializeParallaxSlideshow
отвечает за настройку и управление поведением параллакса слайд-шоу, инициализируя каждое слайд-шоу. Отслеживая, какие слайды уже загружены, мы предотвращаем повторную загрузку:
const slideshowDataID = slideshow.getAttribute('data-id');
const slideshowData = parallaxSlideshowData[slideshowDataID];
if (!slideshowData) return;
Затем вызывается функция preloadSlides
, которая предварительно загружает изображения или другие ресурсы для первых двух слайдов, чтобы избежать видимой задержки между слайдами.
function preloadSlides(slideshowData, slideshow, loadedSlideIds, loadedImages){
slideshowData.slice(0, 2).forEach((slideData, index) => {
// Проверяем, был ли слайд уже добавлен
if (loadedSlideIds.has(slideData.id)) return;
// Отмечаем слайд как загруженный
loadedSlideIds.add(slideData.id);
if (slideData.image !== null) {
createImageDiv(slideData.id, slideData.image, slideshow, loadedImages, index === 0);
}
});
}
Далее вызывается вспомогательная функция createImageDiv
, которая отвечает за создание и управление элементом изображения в параллаксе слайд-шоу.
function createImageDiv(slideID, slideImage, slideshow, loadedImages, firstImage = false) {
const imgDiv = document.createElement('div');
imgDiv.className = 'parallax-slideshow__image';
imgDiv.innerHTML = slideImage;
if (firstImage) {
const image = imgDiv.querySelector('img');
image.addEventListener('load', () => {
const slideshowOverlay = slideshow.querySelector(
'.parallax-slideshow__overlay',
);
const slideshowWrapper = slideshow.querySelector(
'.parallax-slideshow__wrapper',
);
if (slideshowOverlay) {
slideshowOverlay.classList.add('fade-out');
setTimeout(() => {
document.body.style.overflow = '';
slideshowWrapper.removeChild(slideshowOverlay);
}, 1000);
}
});
}
// Добавляем пользовательский атрибут для идентификатора слайда
imgDiv.setAttribute('data-slide-image-id', slideID);
loadedImages.add({
id: slideID,
image: imgDiv,
});
}
Причина, по которой мы проверяем, является ли изображение первым, заключается в том, что мы хотим, чтобы начальный слайд плавно появлялся из черного фона при полной загрузке. После загрузки изображения оно находит оверлей и обертку слайд-шоу, затем скрывает оверлей, удаляет его и восстанавливает прокрутку.
Давайте вернемся к функции initializeParallaxSlideshow
. После функции preloadSlides
идет обработчик события прокрутки для эффекта параллакса, который слушает события прокрутки для динамического обновления позиции изображения слайд-шоу.
Идея заключается в том, чтобы позволить обертке изображения занимать всю высоту видимой области, но поскольку могут быть компоненты до или после параллакса слайд-шоу, в какой-то момент необходимо изменить положение обертки изображения, чтобы позволить пользователю прокручивать и взаимодействовать с другими компонентами.
window.addEventListener('scroll', () => {
const windowHeight = window.innerHeight;
const top = slideshow.getBoundingClientRect().top;
const bottom = slideshow.getBoundingClientRect().bottom;
const slideshowImageWrapper = slideshow.querySelector(
'.parallax-slideshow__image-wrapper',
);
if (top < 0 && bottom > windowHeight) {
slideshowImageWrapper.style.position = 'fixed';
slideshowImageWrapper.style.top = 0;
} else {
slideshowImageWrapper.style.position = 'absolute';
if (windowHeight > bottom) {
slideshowImageWrapper.style.top = 'unset';
slideshowImageWrapper.style.bottom = 0;
}
if (windowHeight < top) {
slideshowImageWrapper.style.top = 0;
slideshowImageWrapper.style.bottom = 'unset';
}
}
});
Следующий фрагмент кода проверяет, является ли параллакс слайд-шоу первым компонентом страницы и является ли первый слайд изображением.
// Проверяем, находится ли слайд-шоу внутри родительского элемента .content-top
const isContentTopParent = slideshow.closest('.content-top') !== null;
// Получаем первый слайд и проверяем, содержит ли он класс parallax-slide--parallax-image-slide
const firstSlide = slideshow.querySelector('.parallax-slide');
const isFirstSlideParallaxImageSlide = firstSlide && firstSlide.classList.contains('parallax-slide--parallax-image-slide');
// Блокируем прокрутку, если .content-top присутствует и первый слайд является изображением
if (isContentTopParent && isFirstSlideParallaxImageSlide) {
const overlay = document.createElement('div');
overlay.className = 'parallax-slideshow__overlay';
slideshow
.querySelector('.parallax-slideshow__wrapper')
.appendChild(overlay);
document.body.style.overflow = 'hidden';
}
Затем идет фрагмент кода, который перебирает все слайды в слайд-шоу и вызывает функцию initializeSlideObserver()
для каждого слайда.
const slides = slideshow.querySelectorAll('.parallax-slide');
slides.forEach((slide, index) => {
const infoInnerWrapper = slide.querySelector(
'.parallax-slide__info-inner-wrapper',
);
initializeSlideObserver(slideshow, infoInnerWrapper, slide, slideshowData, loadedSlideIds, loadedImages);
// Добавляем классы, если первое изображение является слайдом
if (index === 0) {
slide.classList.add('initial-slide');
if (isFirstSlideParallaxImageSlide) {
slide.classList.add('initial-slide-image');
}
}
});
Теперь давайте рассмотрим функцию initializeSlideObserver()
— она отвечает за настройку Intersection Observer для отслеживания момента, когда слайд входит в видимую область, и динамически обновляет отображаемое изображение слайд-шоу в соответствии с этим. Она гарантирует, что слайд-шоу загружает следующее изображение только при необходимости, предотвращая ненужное рендеринг и улучшая производительность.
// Инициализация Intersection Observer для слайдов
function initializeSlideObserver(slideshow, infoInnerWrapper, slide, slideshowData, loadedSlideIds, loadedImages) {
// Отслеживает, когда infoInnerWrapper входит или выходит из видимой области,
// и вызывает обратный вызов при изменении видимости
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const slideshowWrapper = slideshow.querySelector(
'.parallax-slideshow__wrapper',
);
const slideshowImageWrapper = slideshowWrapper.querySelector(
'.parallax-slideshow__image-wrapper',
);
const slideID = slide.getAttribute('slide-data-id');
const slideImage = Array.from(loadedImages).find(
(loadedImage) => loadedImage.id === slideID,
);
const { isIntersecting } = entry;
// Проверяет, пересекает ли слайд корневой элемент
if (isIntersecting) {
const parent = slide.parentNode;
const slides = Array.from(
parent.querySelectorAll('.parallax-slide'),
);
const index = slides.indexOf(slide);
if (index !== 0) {
// Если это не первый слайд, вызываем функцию для предварительной загрузки следующего слайда.
loadNextSlide(slideshowData, index, loadedSlideIds, loadedImages);
}
// Проверяем, существует ли изображение
const previousImage = slideshowImageWrapper.querySelector(
'.parallax-slideshow__image',
);
if (slideImage) {
slideImage.image.classList.add('fade-in');
slideshowImageWrapper.appendChild(slideImage.image);
// Если найдено предыдущее изображение, удаляем класс fade-in и удаляем его после задержки
if (previousImage) {
const previosImageID = previousImage.getAttribute(
'data-slide-image-id',
);
if (previosImageID !== slideID) {
setTimeout(() => {
previousImage.classList.add('fade-out'); // Добавляем класс fade-out
previousImage.classList.remove('fade-in'); // Удаляем класс fade-in
previousImage.classList.remove('fade-out'); // Удаляем класс fade-out
slideshowImageWrapper.removeChild(previousImage);
}, 500);
}
}
} else {
if (previousImage) {
const previosImageID = previousImage.getAttribute(
'data-slide-image-id',
);
if (previosImageID !== slideID) {
previousImage.classList.add('fade-out'); // Добавляем класс fade-out
setTimeout(() => {
previousImage.classList.remove('fade-out');
slideshowImageWrapper.removeChild(previousImage);
}, 500);
}
}
}
}
});
},
{
// Обратный вызов срабатывает, когда хотя бы 5% infoInnerWrapper видно.
threshold: 0.05,
},
);
observer.observe(infoInnerWrapper);
}
Наконец, идет функция loadNextSlide
, которая отвечает за предварительную загрузку следующего изображения слайда, чтобы обеспечить плавный переход при прокрутке пользователем. Это предотвращает ненужную повторную загрузку уже загруженных изображений. Эта функция очень похожа на функцию preloadSlides
.
function loadNextSlide(slideshowData, currentIndex, loadedSlideIds, loadedImages) {
if (currentIndex + 1 < slideshowData.length) {
const nextSlideData = slideshowData[currentIndex + 1];
// Проверяем, был ли слайд уже добавлен
if (loadedSlideIds.has(nextSlideData.id)) return;
// Отмечаем слайд как загруженный
loadedSlideIds.add(nextSlideData.id);
if (nextSlideData.image !== null) {
createImageDiv(nextSlideData.id, nextSlideData.image, null, loadedImages);
}
}
}
Данные функции — обработка создания изображений, наблюдение за слайдами и предварительная загрузка — позволяют реализовать динамичное и эффективное параллакс слайд-шоу, которое плавно переходит между изображениями при прокрутке пользователем. Используя API Intersection Observer, логику предварительной загрузки и плавные эффекты перехода, слайд-шоу обеспечивает визуально привлекательный опыт без ненужной нагрузки на производительность.
После добавления необходимых стилей для управления позиционированием, анимациями и переходами ваше параллакс слайд-шоу должно быть полностью функциональным. Этот подход не только усиливает повествовательный аспект вашего контента, но и делает взаимодействие плавным и легким.
Теперь все, что осталось, — это доработать визуальные элементы, чтобы они соответствовали вашему дизайну, и вы готовы создать захватывающий опыт прокрутки!