2023-11-13 08:05:05 +00:00
|
|
|
import { clsx } from 'clsx'
|
2024-01-21 08:05:36 +00:00
|
|
|
import { Show, createEffect, on, createMemo, createSignal, onCleanup } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2024-01-21 08:05:36 +00:00
|
|
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
2023-11-13 16:55:32 +00:00
|
|
|
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { Icon } from '../Icon'
|
|
|
|
|
|
|
|
import styles from './Lightbox.module.scss'
|
2023-11-13 08:05:05 +00:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
class?: string
|
2023-12-21 13:56:19 +00:00
|
|
|
imageAlt?: string
|
2023-11-13 08:05:05 +00:00
|
|
|
image: string
|
|
|
|
onClose: () => void
|
|
|
|
}
|
|
|
|
|
|
|
|
const ZOOM_STEP = 1.08
|
2023-12-21 13:56:19 +00:00
|
|
|
const TRANSITION_SPEED = 300
|
|
|
|
|
2023-11-13 08:05:05 +00:00
|
|
|
export const Lightbox = (props: Props) => {
|
|
|
|
const [zoomLevel, setZoomLevel] = createSignal(1)
|
2024-01-21 08:05:36 +00:00
|
|
|
const [pictureScalePercentage, setPictureScalePercentage] = createSignal<number | null>(null)
|
2023-12-21 13:56:19 +00:00
|
|
|
const [translateX, setTranslateX] = createSignal(0)
|
|
|
|
const [translateY, setTranslateY] = createSignal(0)
|
|
|
|
const [transitionEnabled, setTransitionEnabled] = createSignal(false)
|
|
|
|
|
|
|
|
const lightboxRef: {
|
|
|
|
current: HTMLElement
|
|
|
|
} = {
|
|
|
|
current: null,
|
|
|
|
}
|
2023-11-13 08:05:05 +00:00
|
|
|
|
2024-01-22 10:44:56 +00:00
|
|
|
const handleSmoothAction = (action: () => void) => {
|
|
|
|
setTransitionEnabled(true)
|
|
|
|
action()
|
|
|
|
setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED)
|
|
|
|
}
|
|
|
|
|
2023-11-13 08:05:05 +00:00
|
|
|
const closeLightbox = () => {
|
2023-12-21 13:56:19 +00:00
|
|
|
lightboxRef.current?.classList.add(styles.fadeOut)
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
props.onClose()
|
2024-01-21 08:05:36 +00:00
|
|
|
}, 200)
|
2023-11-13 08:05:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const zoomIn = (event) => {
|
|
|
|
event.stopPropagation()
|
2024-01-22 10:44:56 +00:00
|
|
|
|
|
|
|
handleSmoothAction(() => {
|
|
|
|
setZoomLevel(zoomLevel() * ZOOM_STEP)
|
|
|
|
})
|
2023-11-13 08:05:05 +00:00
|
|
|
}
|
2023-12-21 13:56:19 +00:00
|
|
|
|
2023-11-13 08:05:05 +00:00
|
|
|
const zoomOut = (event) => {
|
|
|
|
event.stopPropagation()
|
2024-01-22 10:44:56 +00:00
|
|
|
|
|
|
|
handleSmoothAction(() => {
|
|
|
|
setZoomLevel(zoomLevel() / ZOOM_STEP)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const positionReset = () => {
|
|
|
|
setTranslateX(0)
|
|
|
|
setTranslateY(0)
|
2023-11-13 08:05:05 +00:00
|
|
|
}
|
2023-12-21 13:56:19 +00:00
|
|
|
|
2023-11-22 21:31:51 +00:00
|
|
|
const zoomReset = (event) => {
|
|
|
|
event.stopPropagation()
|
2024-01-22 10:44:56 +00:00
|
|
|
|
|
|
|
handleSmoothAction(() => {
|
|
|
|
setZoomLevel(1)
|
|
|
|
positionReset()
|
|
|
|
})
|
2023-11-22 21:31:51 +00:00
|
|
|
}
|
2023-11-13 08:05:05 +00:00
|
|
|
|
2024-01-22 10:44:56 +00:00
|
|
|
const handleMouseWheelZoom = (event) => {
|
2023-12-21 13:56:19 +00:00
|
|
|
event.preventDefault()
|
2024-01-22 10:44:56 +00:00
|
|
|
event.stopPropagation()
|
2023-12-21 13:56:19 +00:00
|
|
|
|
2024-01-22 10:44:56 +00:00
|
|
|
let scale = zoomLevel()
|
2023-12-21 13:56:19 +00:00
|
|
|
scale += event.deltaY * -0.01
|
|
|
|
scale = Math.min(Math.max(0.125, scale), 4)
|
|
|
|
|
2024-01-22 10:44:56 +00:00
|
|
|
handleSmoothAction(() => {
|
|
|
|
setZoomLevel(scale * ZOOM_STEP)
|
|
|
|
})
|
2023-12-21 13:56:19 +00:00
|
|
|
}
|
2023-11-13 08:05:05 +00:00
|
|
|
|
2023-11-13 16:55:32 +00:00
|
|
|
useEscKeyDownHandler(closeLightbox)
|
|
|
|
|
2023-12-21 13:56:19 +00:00
|
|
|
let startX: number = 0
|
|
|
|
let startY: number = 0
|
|
|
|
let isDragging: boolean = false
|
|
|
|
|
|
|
|
const onMouseDown: (event: MouseEvent) => void = (event) => {
|
|
|
|
startX = event.clientX - translateX()
|
|
|
|
startY = event.clientY - translateY()
|
|
|
|
isDragging = true
|
|
|
|
event.preventDefault()
|
|
|
|
}
|
|
|
|
|
|
|
|
const onMouseMove: (event: MouseEvent) => void = (event) => {
|
|
|
|
if (isDragging) {
|
|
|
|
setTranslateX(event.clientX - startX)
|
|
|
|
setTranslateY(event.clientY - startY)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const onMouseUp: () => void = () => {
|
|
|
|
isDragging = false
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', onMouseMove)
|
|
|
|
document.addEventListener('mouseup', onMouseUp)
|
|
|
|
|
|
|
|
onCleanup(() => {
|
|
|
|
document.removeEventListener('mousemove', onMouseMove)
|
|
|
|
document.removeEventListener('mouseup', onMouseUp)
|
|
|
|
})
|
|
|
|
|
|
|
|
const lightboxStyle = createMemo(() => ({
|
|
|
|
transform: `translate(${translateX()}px, ${translateY()}px) scale(${zoomLevel()})`,
|
|
|
|
transition: transitionEnabled() ? `transform ${TRANSITION_SPEED}ms ease-in-out` : '',
|
|
|
|
cursor: 'grab',
|
|
|
|
}))
|
|
|
|
|
2024-01-21 08:05:36 +00:00
|
|
|
let fadeTimer
|
|
|
|
|
|
|
|
createEffect(
|
|
|
|
on(
|
|
|
|
() => zoomLevel(),
|
|
|
|
() => {
|
|
|
|
clearTimeout(fadeTimer)
|
|
|
|
|
|
|
|
fadeTimer = setTimeout(() => {
|
|
|
|
setPictureScalePercentage(null)
|
|
|
|
}, 2200)
|
|
|
|
|
|
|
|
setPictureScalePercentage(Math.round(zoomLevel() * 100))
|
|
|
|
},
|
|
|
|
{ defer: true },
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2023-11-13 08:05:05 +00:00
|
|
|
return (
|
2023-12-21 13:56:19 +00:00
|
|
|
<div
|
|
|
|
class={clsx(styles.Lightbox, props.class)}
|
|
|
|
onClick={closeLightbox}
|
2024-01-22 10:44:56 +00:00
|
|
|
onWheel={(e) => e.preventDefault()}
|
2023-12-21 13:56:19 +00:00
|
|
|
ref={(el) => (lightboxRef.current = el)}
|
|
|
|
>
|
2024-01-21 08:05:36 +00:00
|
|
|
<Show when={pictureScalePercentage()}>
|
|
|
|
<div class={styles.scalePercentage}>{`${pictureScalePercentage()}%`}</div>
|
|
|
|
</Show>
|
2023-11-13 08:05:05 +00:00
|
|
|
<span class={styles.close} onClick={closeLightbox}>
|
2023-11-22 21:31:51 +00:00
|
|
|
<Icon name="close-white" class={styles.icon} />
|
2023-11-13 08:05:05 +00:00
|
|
|
</span>
|
|
|
|
<div class={styles.zoomControls}>
|
|
|
|
<button class={styles.control} onClick={(event) => zoomOut(event)}>
|
2023-11-22 21:31:51 +00:00
|
|
|
−
|
2023-11-13 08:05:05 +00:00
|
|
|
</button>
|
2023-11-22 21:31:51 +00:00
|
|
|
<button class={clsx(styles.control, styles.controlDefault)} onClick={(event) => zoomReset(event)}>
|
|
|
|
1:1
|
2023-11-13 16:55:32 +00:00
|
|
|
</button>
|
2023-11-13 08:05:05 +00:00
|
|
|
<button class={styles.control} onClick={(event) => zoomIn(event)}>
|
2023-11-22 21:31:51 +00:00
|
|
|
+
|
2023-11-13 08:05:05 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<img
|
|
|
|
class={styles.image}
|
2024-01-21 08:05:36 +00:00
|
|
|
src={getImageUrl(props.image, { noSizeUrlPart: true })}
|
2023-12-21 13:56:19 +00:00
|
|
|
alt={props.imageAlt || ''}
|
2023-11-13 08:05:05 +00:00
|
|
|
onClick={(event) => event.stopPropagation()}
|
2024-01-22 10:44:56 +00:00
|
|
|
onWheel={handleMouseWheelZoom}
|
2023-12-21 13:56:19 +00:00
|
|
|
style={lightboxStyle()}
|
|
|
|
onMouseDown={onMouseDown}
|
2023-11-13 08:05:05 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|