Skip to content

Commit

Permalink
chore: set default scroll position, add key controls, refactor #1542
Browse files Browse the repository at this point in the history
  • Loading branch information
marek-mihok committed Oct 5, 2022
1 parent d795ed1 commit 7eac925
Showing 1 changed file with 94 additions and 84 deletions.
178 changes: 94 additions & 84 deletions ui/src/parts/lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { clas } from '../theme'

const
iconStyles: Fluent.IButtonStyles = {
icon: { color: '#ffffff', lineHeight: 22, height: 'unset', padding: 4 },
iconHovered: { color: '#ffffff' }, // TODO:
icon: { color: '#fff', lineHeight: 22, height: 'unset', padding: 4 },
iconHovered: { color: '#fff' },
iconPressed: { color: 'rgba(255, 255, 255, 0.7)' },
flexContainer: { justifyContent: 'center' },
root: { margin: '4px 4px', width: 38, height: 38, backgroundColor: 'rgba(0, 0, 0, 0.3)' },
Expand All @@ -34,17 +34,11 @@ const
width: '100%',
height: '100%',
zIndex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.9)', // TODO:
touchAction: 'pinch-zoom' // TODO:
backgroundColor: 'rgba(0, 0, 0, 0.9)',
},
img: {
// flexGrow: 1,
maxWidth: '100%', // TODO: 3000x2000 vertical scrollbar
// alignSelf: 'center',
maxWidth: '100%',
objectFit: 'scale-down',
// transformOrigin: 'top left',
// transition: 'transform 0.25s ease',
// cursor: 'pointer',
},
header: {
width: '100%',
Expand All @@ -54,13 +48,12 @@ const
content: {
display: 'flex',
position: 'relative',
maxWidth: '100%',
overflow: 'auto',
justifyContent: 'center',
},
footer: {
textAlign: 'center',
color: '#ffffff', // TODO:
color: '#fff',
width: '100%'
},
imageNav: {
Expand All @@ -71,8 +64,8 @@ const
},
navImg: {
boxSizing: 'border-box',
height: '120px',
objectFit: 'cover',
height: '120px',
width: '120px',
margin: '0px 2px',
filter: 'brightness(30%)',
Expand All @@ -91,139 +84,157 @@ const
width: "auto",
padding: "16px",
marginTop: "-50px",
color: "white",
fontWeight: "bold",
fontSize: "20px",
transition: "0.6s ease",
borderRadius: "0 3px 3px 0",
color: "#fff",
userSelect: "none",
},
imgCaptions: {
whiteSpace: 'nowrap',
padding: '10px 40px',
height: '20px'
},
text: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
text: { textOverflow: 'ellipsis', overflow: 'hidden' },
title: { fontWeight: 500 },
description: { color: '#bbbbbb' }, // TODO: theme colors
description: { color: '#bbb' },
navImgPlaceholder: { display: 'inline-block', width: '124px' }
})

interface Props {
visible: B,
onDismiss: () => void,
images: {
title: S,
description?: S,
type?: S,
image?: S,
path?: S,
}[],
images: { title: S, description?: S, type?: S, image?: S, path?: S, }[],
defaultImageIdx?: U
}

const lazyImageObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const lazyImage = entry.target as HTMLImageElement
lazyImage.src = lazyImage.dataset.src!
lazyImage.classList.remove("lazy")
lazyImageObserver.unobserve(lazyImage)
}
const
keys = { esc: 27, left: 37, right: 39 } as const,
lazyImageObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const lazyImage = entry.target as HTMLImageElement
lazyImage.src = lazyImage.dataset.src!
lazyImage.classList.remove("lazy")
lazyImageObserver.unobserve(lazyImage)
}
})
})
})

export const Lightbox = ({ visible, onDismiss, images, defaultImageIdx }: Props) => {
const
[activeImageIdx, setActiveImageIdx] = React.useState(defaultImageIdx || 0),
imageNavRef = React.useRef<HTMLDivElement | undefined>(),
handleClickLeft = () => {
const nextImgIdx = (activeImageIdx === 0) ? (images.length - 1) : activeImageIdx - 1
setActiveImageIdx(nextImgIdx)
if (imageNavRef.current) {
const isLeft = activeImageIdx <= Math.floor(imageNavRef.current.scrollLeft / 124)
if (nextImgIdx === images.length - 1) imageNavRef.current.scrollLeft = imageNavRef.current.scrollWidth - imageNavRef.current?.clientWidth
else if (isLeft) imageNavRef.current.scrollBy({ left: (activeImageIdx * 124) - 124 - imageNavRef.current.scrollLeft, top: 0, behavior: 'smooth' })
}
},
handleClickRight = () => {
const nextImgIdx = (activeImageIdx === images.length - 1) ? 0 : activeImageIdx + 1
setActiveImageIdx(nextImgIdx)
if (imageNavRef.current) {
const pageImageCount = Math.floor(imageNavRef.current?.clientWidth / 124)
const isRight = activeImageIdx >= Math.floor(imageNavRef.current.scrollLeft / 124) + (pageImageCount - 1)
if (nextImgIdx === 0) imageNavRef.current.scrollLeft = 0
else if (isRight) imageNavRef.current.scrollBy({ left: ((activeImageIdx + 1 - pageImageCount) * 124) + 124 - imageNavRef.current.scrollLeft, top: 0, behavior: 'smooth' })
}
[activeImageIdx, _setActiveImageIdx] = React.useState(defaultImageIdx || 0),
activeImageRef = React.useRef(defaultImageIdx || 0),
setActiveImageIdx = (idx: U) => {
activeImageRef.current = idx
_setActiveImageIdx(idx)
},
onClose = () => {
onDismiss()
setActiveImageIdx(defaultImageIdx || 0)
if (imageNavRef.current) imageNavRef.current.scrollLeft = 0
imageNavRef = React.useRef<HTMLDivElement | undefined>()

// handle image navigation scroll
React.useEffect(() => {
const navRef = imageNavRef.current
if (navRef) {
const
viewportImageCount = Math.floor(navRef?.clientWidth / 124),
isActiveImageRight = activeImageIdx >= Math.floor(navRef.scrollLeft / 124) + (viewportImageCount - 1),
isActiveImageLeft = activeImageIdx <= Math.floor(navRef.scrollLeft / 124)

if (activeImageIdx === 0) navRef.scrollLeft = 0
else if (activeImageIdx === images.length - 1) navRef.scrollLeft = navRef.scrollWidth - navRef?.clientWidth
else if (isActiveImageLeft) navRef.scrollBy({ left: (activeImageIdx - 1) * 124 - navRef.scrollLeft, behavior: 'smooth' })
else if (isActiveImageRight) navRef.scrollBy({ left: (activeImageIdx + 2 - viewportImageCount) * 124 - navRef.scrollLeft, behavior: 'smooth' })
}
}, [activeImageIdx, images.length])

// initialize intersection observer for lazy images
React.useLayoutEffect(() => {
const lazyImages = [].slice.call(document.querySelectorAll(".lazy"))
lazyImages.forEach(lazyImage => lazyImageObserver.observe(lazyImage))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

if (images.length === 0 || (activeImageIdx >= images.length)) return <>{'Error'}</> // TODO:
// set initial scroll position when defaultImageIdx is specified
React.useEffect(() => {
if (defaultImageIdx && imageNavRef.current) {
imageNavRef.current.scrollLeft = (activeImageIdx * 124) - 124 - imageNavRef.current.scrollLeft
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible])

// Add keyboard events listener
React.useEffect(() => {
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

if (images.length === 0) throw new Error('No images passed to image lightbox component.')
if (activeImageIdx >= images.length) throw new Error(`Image with defaultImageIdx:${activeImageIdx} does not exist.`)

const
{ type, image, path, title, description } = images[activeImageIdx],
src = path
? path
: (image && type)
? `data:image/${type};base64,${image}`
: ''
: '',
isGallery = images.length > 1,
onClose = () => {
onDismiss()
setActiveImageIdx(defaultImageIdx || 0)
if (imageNavRef.current) imageNavRef.current.scrollLeft = 0
},
handleKeyDown = React.useCallback((ev: KeyboardEvent) => {
switch (ev.keyCode) {
case keys.right:
setActiveImageIdx(activeImageRef.current === images.length - 1 ? 0 : activeImageRef.current + 1)
break
case keys.left:
setActiveImageIdx(activeImageRef.current === 0 ? images.length - 1 : activeImageRef.current - 1)
break
case keys.esc:
onClose()
break
default:
break
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeImageIdx, images.length])

return (
<div aria-hidden={true} className={css.body} style={visible ? undefined : { display: 'none' }} >
<div aria-hidden={true} className={css.body} style={visible ? undefined : { display: 'none' }}>
<div className={css.header}>
<Fluent.ActionButton
styles={iconStyles}
onClick={onClose}
iconProps={{ iconName: 'Cancel', style: { fontSize: '22px' }, }}
/>
</div>
<div className={css.content} style={{ height: `calc(100% - ${images.length > 1 ? '262px' : '100px'})` }}>
<img
className={css.img}
alt={title}
src={src}
style={{
// maxHeight: 'inherit',
// objectFit: 'none'
}}
/>
{images.length > 1 &&
<div className={css.content} style={{ height: `calc(100% - ${isGallery ? '262px' : '100px'})` }}>
<img className={css.img} alt={title} src={src} />
{isGallery &&
<>
<div className={css.arrow} style={{ left: 0 }}>
<Fluent.ActionButton
styles={iconStyles}
onClick={handleClickLeft}
onClick={() => setActiveImageIdx(activeImageIdx === 0 ? images.length - 1 : activeImageIdx - 1)}
iconProps={{ iconName: 'ChevronLeft', style: { fontSize: '22px' } }}
/>
</div>
<div className={css.arrow} style={{ right: 0 }}>
<Fluent.ActionButton
styles={iconStyles}
onClick={handleClickRight}
onClick={() => setActiveImageIdx(activeImageIdx === images.length - 1 ? 0 : activeImageIdx + 1)}
iconProps={{ iconName: 'ChevronRight', style: { fontSize: '22px' } }}
/>
</div>
</>
}
</div>
<div className={css.footer} style={{ height: images.length > 1 ? '260px' : '60px' }}>
<div className={css.footer} style={{ height: isGallery ? '260px' : '60px' }}>
<div className={css.imgCaptions}>
<div title={title} className={clas(css.text, css.title)}>{title}</div>
<div title={description} className={clas(css.text, css.description)}>{description ? description : ''}</div>
<div title={description} className={clas(css.text, css.description)}>{description || ''}</div>
</div>
{images.length > 1 && <div className={css.imageNav} ref={imageNavRef}>
{isGallery && <div className={css.imageNav} ref={imageNavRef}>
{images.map(({ type, image, path, title }, idx) => {
const src = path
? path
Expand All @@ -232,12 +243,11 @@ export const Lightbox = ({ visible, onDismiss, images, defaultImageIdx }: Props)
: ''
return <div key={idx} className={css.navImgPlaceholder}>
<img
key={'img' + idx}
className={clas(css.img, css.navImg, 'lazy')}
style={activeImageIdx === idx ? { filter: 'unset', border: '2px solid red' } : undefined}
alt={title}
data-src={src}
onClick={() => { setActiveImageIdx(idx) }}
onClick={() => setActiveImageIdx(idx)}
/>
</div>
})}
Expand Down

0 comments on commit 7eac925

Please sign in to comment.