/** @jsx h */
import { h, Component } from 'preact';
export default class Gallery extends Component {
constructor(props) {
super(props);
// Set initial state
this.state = {
showLightbox: false,
};
}
// Handle Keydown function with event parameter
handleKeyDown = (event) => {
const { showLightbox } = this.state;
// If the lightbox is showing
if (showLightbox) {
// Define buttons and keycodes
const firstArrow = document.querySelector('.lightbox .arrows .arrows__left');
const lastArrow = document.querySelector('.lightbox .arrows .arrows__right');
const closeIcon = document.querySelector('.lightbox .close-button');
const TAB_KEY = 9;
const ESCAPE_KEY = 27;
const LEFT_ARROW = 37;
const RIGHT_ARROW = 39;
// If esc is clicked, call the close function
if (event.keyCode === ESCAPE_KEY) this.onClose();
// If left arrow is clicked, call the changeImage function
if (event.keyCode === LEFT_ARROW) this.changeImage(event, -1);
// If left arrow is clicked, call the changeImage function
if (event.keyCode === RIGHT_ARROW) this.changeImage(event, 1);
// If tab is clicked, keep focus on the arrows
if (event.keyCode === TAB_KEY && !event.shiftKey) {
if (document.activeElement === firstArrow) {
event.preventDefault();
lastArrow.focus();
} else if (document.activeElement === lastArrow) {
event.preventDefault();
closeIcon.focus();
} else {
event.preventDefault();
firstArrow.focus();
}
}
if (event.keyCode === TAB_KEY && event.shiftKey) {
if (document.activeElement === firstArrow) {
event.preventDefault();
closeIcon.focus();
} else if (document.activeElement === lastArrow) {
event.preventDefault();
firstArrow.focus();
} else {
event.preventDefault();
lastArrow.focus();
}
}
}
}
// onClick function
onClick = (e, key) => {
// Prevent default action (href="#")
e.preventDefault();
/*
Set state:
activeImage = the image's index in the array of images
showLightbox = true
Callback:
- Get left arrow button and focus on it
- Add no scroll class to body
- Call scrollToThumb function
*/
this.setState({
activeImage: key,
showLightbox: true,
}, () => {
document.querySelector('.lightbox .arrows .arrows__left').focus();
document.body.classList.add('no-scroll');
this.scrollToThumb();
});
}
// onClose function
onClose = () => {
/*
Set state:
showLightbox = false
Callback:
- Remove no scroll class from body
*/
this.setState({
showLightbox: false,
}, () => document.body.classList.remove('no-scroll'));
}
// / changeImage function
changeImage = (e, calc) => {
const { activeImage } = this.state;
const { images } = this.props;
let newCalc = calc;
// If first image is active and parameter is -1
if (activeImage === 0 && calc === -1) {
// set parameter to the length of the array to go right to the last image
newCalc = images.length - 1;
} else if (activeImage === (images.length - 1) && calc === 1) {
// If last image is active and parameter is 1
// set parameter to the (negative)length of the array to go right to the first image
newCalc = -(images.length - 1);
}
/*
Set state:
activeImage = selected image + or - calc amount
Callback:
- Call scrollToThumb function
*/
this.setState(state => ({
activeImage: state.activeImage + newCalc,
}), () => this.scrollToThumb());
}
// scrollToThumb function
scrollToThumb = () => {
/* Define variables for:
- Lightbox div
- Thumbs div
- First thumbnail div
- Active thumbnail div
- The offsetTop of the clicked thumbnail on mobile devices
- X-axis offset of first div
*/
const lightbox = document.querySelector('.lightbox');
const thumbs = document.querySelector('.thumbs');
const firstThumb = document.querySelectorAll('.thumb')[0];
const activeThumb = document.querySelector('.thumb--active');
const activeTop = document.querySelector('.thumb--active').offsetTop;
const firstOffset = firstThumb.offsetLeft;
// Set the scroll position to show the selected thumb with some space to the left (200px)
thumbs.scrollLeft = activeThumb.offsetLeft - firstOffset - 200;
// Set the scroll top to scroll to pressed thumbnail image for mobile devices
lightbox.scrollTop = activeTop - 30;
}
/*
renderOverlay function
Parameters:
- maxImages = based on the layout prop, how many images are the maximum that will show on page
- i = the current image number
*/
renderOverlay = (maxImages, i) => {
const { images } = this.props;
// Set overflow images to the amount of EXTRA images not showing on page
const overflowImages = images.length - maxImages;
// plural Or No is set to "s" if there is more than one and blank if there is just one
const pluralOrNo = overflowImages > 1 ? 's' : '';
// If there are more images than the max amount showing AND it is the last image
if (images.length > maxImages && i === maxImages) {
// Return an overlay with an extra class and content showing the amount of images left
return (
<div className="gallery-image__overlay gallery-image__overlay--last">
{`+${overflowImages} more image${pluralOrNo}`}
</div>
);
}
// Otherwise...
// Return the blank overlay
return <div className="gallery-image__overlay" />;
}
/*
galleryImage function
Parameters:
- cols = Chassis columns defined based on the selected style and which image it is
- path = image.path
- alt = image.alt
- i = image number
*/
galleryImage = (cols, path, alt, maxImages, i) => (
<div className={cols}>
<a
onClick={e => this.onClick(e, i)}
href="#lightbox"
>
<div className="gallery-image">
<img
src={path}
alt={alt}
className="ch-img--responsive ch-hand gallery-image__image"
/>
{this.renderOverlay(maxImages, (i + 1))}
</div>
</a>
</div>
)
// renderImages function
renderImages = () => {
let cols;
let maxImages;
const { layout, images } = this.props;
if (layout === '4/3') {
maxImages = 7;
} else if (layout === '4') {
maxImages = 4;
} else if (layout === '6') {
maxImages = 6;
} else {
maxImages = layout === '4/3' ? 7 : 8;
}
// Cleaned images array is the first 7 images
const cleanedImages = images.slice(0, maxImages);
// Amount is the length of that array (I've done this incase we change 7 to a different number)
const amount = cleanedImages.length;
// Map the images
const returnImages = cleanedImages.map((image, i) => {
// If the defined style is four by 3...
if (layout === '4/3') {
// Layout for the second and third-last image
if ((amount - 1) === i + 1 || (amount - 2) === i + 1) cols = 'xs:ch-col--6 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
// Layout for the last image
else if (amount === i + 1) cols = 'xs:ch-col--12 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
// Otherwise, layout is just a simple grid
else cols = 'xs:ch-col--6 sm:ch-col--3 ch-mb--2 sm:ch-mb--4';
} else if (layout === '6') {
// If the defined style is four by 3...
// Layout is just a simple grid
cols = 'xs:ch-col--6 sm:ch-col--4 ch-mb--2 sm:ch-mb--4';
} else cols = 'xs:ch-col--6 sm:ch-col--3 ch-mb--2 sm:ch-mb--4';
// Return an image from the galleryImage function based on the parameters from above
return (
this.galleryImage(cols, image.path, image.alt, maxImages, i)
);
});
// Return images
return returnImages;
}
// renderLightbox function
renderLightbox = () => {
const showLightbox = this.state;
// Listen for keydown event and call function
document.addEventListener('keydown', this.handleKeyDown);
// Render lightbox
const lightbox = (
<div
className={`lightbox ${showLightbox ? 'lightbox--visible' : ''}`}
>
{this.renderImage()}
{this.renderCounter()}
<div className="thumbs ch-mh--auto">
{this.renderThumbnails()}
</div>
<button
className="ch-pull--right close-button ch-ma--3"
onClick={e => this.onClose(e)}
type="button"
/>
</div>
);
return lightbox;
}
// renderImage function to show featuredImage
renderImage = () => {
const { images } = this.props;
const { activeImage } = this.state;
return (
<div className="ch-display--none md:ch-display--flex imageContainer">
<figure>
<div className="overlays ch-mh--auto md:ch-mt--8 ch-hand">
<div
className="overlay"
onClick={e => this.changeImage(e, -1)}
/>
<div
className="overlay"
onClick={e => this.changeImage(e, 1)}
/>
</div>
<img
src={images[activeImage].path}
alt={images[activeImage].alt}
className="ch-img--responsive featuredImage ch-mh--auto md:ch-mt--8 ch-hand"
onClick={e => this.changeImage(e, 1)}
/>
<figcaption className="caption ch-mt--1 ch-mh--auto ch-mb--4 ch-text--center">{images[activeImage].caption}</figcaption>
</figure>
{this.renderNavigation()}
</div>
);
}
// renderCounter function to show which image the user is on
renderCounter = () => {
const { images } = this.props;
const { activeImage } = this.state;
return (
<p className="counter ch-display--none md:ch-display--block ch-text--center ch-mb--0">
{`Image ${activeImage + 1}/${images.length}`}
</p>
);
}
// renderNavigation function to show arrows
renderNavigation = () => (
<div className="arrows ch-display--none md:ch-display--block">
<button
className="arrow arrows__left ch-absolute"
onClick={e => this.changeImage(e, -1)}
type="button"
/>
<button
className="arrow arrows__right ch-absolute"
onClick={e => this.changeImage(e, 1)}
type="button"
/>
</div>
)
// renderThumbnails function to show list of thumbnails (On mobile these will be used)
renderThumbnails = () => {
const { images } = this.props;
const { activeImage } = this.state;
const thumbs = images.map((image, i) => (
<div
className={`thumb md:ch-display--inline-block ch-mt--4 md:ch-mt--2 ch-mr--2${i === activeImage ? ' thumb--active md:ch-ba--2 md:ch-bc--white' : ''}`}
onClick={e => this.onClick(e, i)}
>
<figure>
<img
src={images[i].path}
alt={images[i].alt}
className="ch-img--responsive ch-mh--auto ch-mt--4 md:ch-mt--0"
/>
<figcaption className="caption ch-mt--1 ch-mh--auto ch-mb--4 md:ch-mb--8 md:ch-display--none">{images[i].caption}</figcaption>
</figure>
</div>
));
return thumbs;
}
// Final render function
render() {
const { showLightbox } = this.state;
return (
<div>
{this.renderImages()}
{showLightbox ? this.renderLightbox() : null}
</div>
);
}
}