<div class="detail-hero">
<div class="detail-hero__inner">
<div class="detail-hero__item">
<div class="detail-hero__item-inner">
<figure class="image image--fluid detail-hero__image">
<img loading="lazy" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201400%20700%22%3E%3C%2Fsvg%3E" data-srcset="//via.placeholder.com/1400x700 1400w" data-sizes="auto" alt="" class="image__img lazyload">
</figure>
</div>
</div>
<div class="detail-hero__item">
<div class="detail-hero__item-inner">
<figure class="image image--fluid detail-hero__image">
<img loading="lazy" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201400%20700%22%3E%3C%2Fsvg%3E" data-srcset="//via.placeholder.com/1400x700 1400w" data-sizes="auto" alt="" class="image__img lazyload">
</figure>
</div>
</div>
<div class="detail-hero__item">
<div class="detail-hero__item-inner">
<figure class="image image--fluid detail-hero__image">
<img loading="lazy" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201400%20700%22%3E%3C%2Fsvg%3E" data-srcset="//via.placeholder.com/1400x700 1400w" data-sizes="auto" alt="" class="image__img lazyload">
</figure>
</div>
</div>
<div class="detail-hero__item">
<div class="detail-hero__item-inner">
<figure class="image image--fluid detail-hero__image">
<img loading="lazy" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201400%20700%22%3E%3C%2Fsvg%3E" data-srcset="//via.placeholder.com/1400x700 1400w" data-sizes="auto" alt="" class="image__img lazyload">
</figure>
</div>
</div>
</div>
<div class="detail-hero__left-button"></div>
<div class="detail-hero__right-button"></div>
<div class="detail-hero__bubble-list">
<div class="detail-hero__bubble-item" data-slide=0>
<div class="detail-hero__bubble"></div>
</div>
<div class="detail-hero__bubble-item" data-slide=1>
<div class="detail-hero__bubble"></div>
</div>
<div class="detail-hero__bubble-item" data-slide=2>
<div class="detail-hero__bubble"></div>
</div>
<div class="detail-hero__bubble-item" data-slide=3>
<div class="detail-hero__bubble"></div>
</div>
</div>
</div>
<div class="detail-hero">
<div class="detail-hero__inner">
{% for item in data.items %}
<div class="detail-hero__item">
<div class="detail-hero__item-inner">
{% include '@image' with {class: "detail-hero__image", modifier: "image--fluid", data: item.image|srcset('1400x700')} %}
</div>
</div>
{% endfor %}
</div>
{% if data.items|length > 1 %}
<div class="detail-hero__left-button"></div>
<div class="detail-hero__right-button"></div>
<div class="detail-hero__bubble-list">
{% for item in data.items %}
<div class="detail-hero__bubble-item" data-slide={{loop.index - 1}}>
<div class="detail-hero__bubble"></div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{
"language": "en-US",
"data": {
"items": [
{
"image": true,
"link": "#"
},
{
"image": true,
"link": "#"
},
{
"image": true,
"link": "#"
},
{
"image": true,
"link": "#"
}
]
}
}
@keyframes detail-hero__item--appear {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes detail-hero__item--disappear {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes detail-hero__item-inner--lighten {
from {
filter: brightness(80%); /* stylelint-disable-line plugin/no-unsupported-browser-features */
}
to {
filter: brightness(100%); /* stylelint-disable-line plugin/no-unsupported-browser-features */
}
}
@keyframes detail-hero__item-inner--darken {
from {
filter: brightness(100%); /* stylelint-disable-line plugin/no-unsupported-browser-features */
}
to {
filter: brightness(80%); /* stylelint-disable-line plugin/no-unsupported-browser-features */
}
}
@keyframes detail-hero__image--appear {
from {
transform: translateX(-80%);
}
to {
transform: translateX(0);
}
}
@keyframes detail-hero__image--disappear {
from {
transform: translateX(0);
}
to {
transform: translateX(-80%);
}
}
.detail-hero {
width: 100%;
transition: border-bottom-color $transition-duration $transition-easing;
@include aspect-ratio(100, 50, detail-hero__inner);
}
.detail-hero__inner {
overflow: hidden;
position: relative;
}
.detail-hero__left-button {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 30%;
z-index: map_get($zindex, detailHeroGradient);
opacity: 0;
transition: opacity $transition-duration $transition-easing;
background: linear-gradient(.25turn, var(--color-compliment), transparent); /* stylelint-disable-line plugin/no-unsupported-browser-features */
cursor: pointer;
&:hover {
@media (hover: hover) {
opacity: .5;
}
}
}
.detail-hero__right-button {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 30%;
z-index: map_get($zindex, detailHeroGradient);
opacity: 0;
transition: opacity $transition-duration $transition-easing;
background: linear-gradient(.25turn, transparent, var(--color-compliment)); /* stylelint-disable-line plugin/no-unsupported-browser-features */
cursor: pointer;
&:hover {
@media (hover: hover) {
opacity: .5;
}
}
}
.detail-hero__item {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
&.is-hidden {
z-index: map_get($zindex, detailHeroBelow);
&.is-reversed {
animation: detail-hero__item--disappear 1s $transition-easing forwards;
z-index: map_get($zindex, detailHeroAbove);
}
}
&.is-active {
animation: detail-hero__item--appear 1s $transition-easing forwards;
z-index: map_get($zindex, detailHeroAbove);
&.is-reversed {
animation: none;
z-index: map_get($zindex, detailHeroBelow);
}
}
}
.detail-hero__item-inner {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.is-hidden & {
animation: detail-hero__item-inner--darken 1s $transition-easing forwards;
}
.is-active & {
animation: none;
}
.is-hidden.is-reversed & {
animation: none;
}
.is-active.is-reversed & {
animation: detail-hero__item-inner--lighten 1s $transition-easing forwards;
}
}
.detail-hero__image {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
.is-active & {
animation: detail-hero__image--appear 1s $transition-easing forwards;
}
.is-active.is-reversed & {
animation: none;
}
.is-hidden.is-reversed & {
animation: detail-hero__image--disappear 1s $transition-easing forwards;
}
}
.image__img {
.detail-hero & {
width: 100%;
display: block;
}
}
.detail-hero__bubble-list {
position: absolute;
justify-content: center;
right: 16px;
bottom: 8px;
display: flex;
z-index: map_get($zindex, detailHeroBubbles);
@include bp(sm-min) {
right: 32px;
bottom: 7px;
}
}
.detail-hero__bubble-item {
width: 10px;
height: 10px;
cursor: pointer;
@include bp(sm-min) {
width: 24px;
height: 24px;
}
}
.detail-hero__bubble {
border-radius: 50%;
margin: 2px;
width: 6px;
height: 6px;
transition: $transition-duration $transition-easing;
transition-property: transform, border, background-color;
border: 1px solid var(--color-brand);
@include bp(sm-min) {
border: 8px solid var(--color-brand);
transform: scale(.25);
margin: 3px;
width: 18px;
height: 18px;
}
&.is-active {
background-color: var(--color-brand);
@include bp(sm-min) {
background-color: transparent;
border: 2px solid var(--color-brand);
transform: scale(1);
}
}
}
import './detail-hero.scss';
import Component from '@component';
import ClickEvent = JQuery.ClickEvent;
import TouchStartEvent = JQuery.TouchStartEvent;
import TouchMoveEvent = JQuery.TouchMoveEvent;
interface IDetailHeroSettings {
leftButtonClass: string;
rightButtonClass: string;
bubbleItemClass: string;
bubbleClass: string;
imageClass: string;
isHiddenClass: string;
isActiveClass: string;
isReversedClass: string;
isHoveringLeftClass: string;
isHoveringRightClass: string;
clickThreshold: number;
swipeThreshold: number;
}
export default class DetailHero extends Component {
static initSelector: string = '.detail-hero';
images: JQuery;
bubbles: JQuery;
bubbleItems: JQuery;
activeImage: number;
settings: IDetailHeroSettings;
readyForInteraction: boolean;
touchStartX: number;
leftButton: JQuery;
rightButton: JQuery;
constructor(element: HTMLElement) {
super(element);
this.settings = {
bubbleClass: 'detail-hero__bubble',
bubbleItemClass: 'detail-hero__bubble-item',
clickThreshold: .3,
imageClass: 'detail-hero__item',
isActiveClass: 'is-active',
isHiddenClass: 'is-hidden',
isHoveringLeftClass: 'is-hovering-left',
isHoveringRightClass: 'is-hovering-right',
isReversedClass: 'is-reversed',
leftButtonClass: 'detail-hero__left-button',
rightButtonClass: 'detail-hero__right-button',
swipeThreshold: 150,
};
this.images = this.element.find('.' + this.settings.imageClass);
if (this.images.length > 1) {
this.bubbles = this.element.find('.' + this.settings.bubbleClass);
this.bubbleItems = this.element.find('.' + this.settings.bubbleItemClass);
this.activeImage = 0;
this.bubbles[this.activeImage].classList.add(this.settings.isActiveClass);
this.readyForInteraction = true;
this.leftButton = this.element.find('.' + this.settings.leftButtonClass);
this.rightButton = this.element.find('.' + this.settings.rightButtonClass);
this.images.css('display', 'none');
this.images[this.activeImage].style.display = 'block';
this.init();
}
}
init(): void {
this.bubbleItems.on('click', this.bubbleClickHandler.bind(this));
this.leftButton.on('click', () => this.setActiveImage(this.activeImage - 1, true));
this.rightButton.on('click', () => this.setActiveImage(this.activeImage + 1, false));
this.swipeEvents();
}
bubbleClickHandler(event: ClickEvent): void {
this.setActiveImage(event.currentTarget.attributes['data-slide'].value, false);
}
swipeEvents(): void {
this.element.on('touchstart', this.touchStartHandler.bind(this));
this.element.on('touchmove', this.touchMoveHandler.bind(this));
}
touchStartHandler(event: TouchStartEvent): void {
this.touchStartX = event.touches[0].screenX;
}
touchMoveHandler(event: TouchMoveEvent): void {
const currentTouchX: number = event.touches[0].screenX;
if (this.touchStartX && Math.abs(currentTouchX - this.touchStartX) > this.settings.swipeThreshold) {
const reverse: boolean = currentTouchX > this.touchStartX;
this.setActiveImage(this.activeImage + (reverse ? -1 : 1), reverse);
this.touchStartX = undefined;
}
}
setActiveImage(index: number, reverse: boolean): void {
if (this.readyForInteraction) {
const animationEndListener: () => void = (): void => {
this.readyForInteraction = true;
this.images[newActive].removeEventListener('animationend', animationEndListener);
};
const previousActive: number = this.activeImage;
const newActive: number = index === -1 ? this.images.length-1 : index % this.images.length;
this.images.css('display', '');
this.activeImage = newActive;
this.readyForInteraction = false;
this.images[newActive].addEventListener('animationend', animationEndListener);
if (reverse) {
this.images.addClass(this.settings.isReversedClass);
} else {
this.images.removeClass(this.settings.isReversedClass);
}
if (this.images.length > 1) {
this.images.removeClass(this.settings.isHiddenClass);
this.bubbles[previousActive].classList.remove(this.settings.isActiveClass);
this.images[previousActive].classList.remove(this.settings.isActiveClass);
this.images[previousActive].classList.add(this.settings.isHiddenClass);
this.images[newActive].classList.add(this.settings.isActiveClass);
this.bubbles[newActive].classList.add(this.settings.isActiveClass);
}
}
}
}