This commit is contained in:
@@ -52,9 +52,34 @@ interface ImagePreviewAcknowledgement {
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
interface ImagePreviewView {
|
||||
centerX: number
|
||||
centerY: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
interface ImagePreviewCommandBase {
|
||||
id: string
|
||||
requestId: string
|
||||
sentAt: number
|
||||
viewerId: string
|
||||
}
|
||||
|
||||
interface ImagePreviewCloseCommand extends ImagePreviewCommandBase {
|
||||
type: 'close'
|
||||
}
|
||||
|
||||
interface ImagePreviewViewCommand extends ImagePreviewCommandBase {
|
||||
type: 'view'
|
||||
view: ImagePreviewView
|
||||
}
|
||||
|
||||
type ImagePreviewCommand = ImagePreviewCloseCommand | ImagePreviewViewCommand
|
||||
|
||||
interface EasySlidesSharedState {
|
||||
easySlidesImagePreview?: {
|
||||
acknowledgement?: ImagePreviewAcknowledgement
|
||||
command?: ImagePreviewCommand
|
||||
request?: ImagePreviewRequest
|
||||
}
|
||||
}
|
||||
@@ -67,6 +92,15 @@ interface PendingImagePreview {
|
||||
requestId: string
|
||||
}
|
||||
|
||||
interface ImagePointer {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const IMAGE_MIN_SCALE = 0.1
|
||||
const IMAGE_MAX_SCALE = 4
|
||||
const IMAGE_SCALE_STEP = 0.1
|
||||
|
||||
const nav = useNav()
|
||||
const easySharedState = sharedState as typeof sharedState & EasySlidesSharedState
|
||||
const clientId = createClientId()
|
||||
@@ -78,16 +112,35 @@ const imageInteractions = new Map<HTMLImageElement, ImageInteraction>()
|
||||
let observer: MutationObserver | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let channel: BroadcastChannel | undefined
|
||||
let imageChannel: BroadcastChannel | undefined
|
||||
let lightbox: HTMLElement | undefined
|
||||
let lightboxViewport: HTMLElement | undefined
|
||||
let lightboxImage: HTMLImageElement | undefined
|
||||
let lightboxClose: HTMLButtonElement | undefined
|
||||
let lightboxDecrease: HTMLButtonElement | undefined
|
||||
let lightboxIncrease: HTMLButtonElement | undefined
|
||||
let lightboxScaleOutput: HTMLOutputElement | undefined
|
||||
let activeImage: HTMLImageElement | undefined
|
||||
let activePreviewRequestId: string | undefined
|
||||
let pendingImagePreview: PendingImagePreview | undefined
|
||||
let stopAcknowledgementWatch: (() => void) | undefined
|
||||
let stopCommandWatch: (() => void) | undefined
|
||||
let stopRequestWatch: (() => void) | undefined
|
||||
let fitFrame: number | undefined
|
||||
let imageViewFrame: number | undefined
|
||||
let pendingSharedView: ImagePreviewView | undefined
|
||||
let pendingRemoteView: ImagePreviewView | undefined
|
||||
let lastHandledRequestId = easySharedState.easySlidesImagePreview?.request?.id ?? ''
|
||||
let lastHandledCommandId = easySharedState.easySlidesImagePreview?.command?.id ?? ''
|
||||
let bodyOverflow = ''
|
||||
let imageScale = 1
|
||||
let imageCenterX = 0.5
|
||||
let imageCenterY = 0.5
|
||||
let imageViewTouched = false
|
||||
let lightboxPointerMoved = false
|
||||
let pinchDistance = 0
|
||||
let pinchMidpoint: ImagePointer | undefined
|
||||
const imagePointers = new Map<number, ImagePointer>()
|
||||
|
||||
function createClientId() {
|
||||
if (typeof crypto.randomUUID === 'function')
|
||||
@@ -227,6 +280,260 @@ function applyScale(key: string, kind: ScalableKind, scale: number) {
|
||||
})
|
||||
}
|
||||
|
||||
function clampImageScale(scale: number) {
|
||||
return Math.min(IMAGE_MAX_SCALE, Math.max(IMAGE_MIN_SCALE, Math.round(scale * 10_000) / 10_000))
|
||||
}
|
||||
|
||||
function imageGeometry(scale = imageScale) {
|
||||
if (!lightboxViewport || !lightboxImage || !lightboxImage.naturalWidth || !lightboxImage.naturalHeight)
|
||||
return undefined
|
||||
return {
|
||||
imageHeight: lightboxImage.naturalHeight * scale,
|
||||
imageWidth: lightboxImage.naturalWidth * scale,
|
||||
viewportHeight: lightboxViewport.clientHeight,
|
||||
viewportWidth: lightboxViewport.clientWidth,
|
||||
}
|
||||
}
|
||||
|
||||
function clampImageCenter(center: number, imageSize: number, viewportSize: number) {
|
||||
if (imageSize <= viewportSize || imageSize <= 0)
|
||||
return 0.5
|
||||
const edge = viewportSize / (2 * imageSize)
|
||||
return Math.min(1 - edge, Math.max(edge, center))
|
||||
}
|
||||
|
||||
function currentImageView(): ImagePreviewView {
|
||||
return {
|
||||
centerX: imageCenterX,
|
||||
centerY: imageCenterY,
|
||||
scale: imageScale,
|
||||
}
|
||||
}
|
||||
|
||||
function renderImageView() {
|
||||
if (!lightboxImage || !lightboxViewport)
|
||||
return
|
||||
const geometry = imageGeometry()
|
||||
if (!geometry)
|
||||
return
|
||||
imageCenterX = clampImageCenter(imageCenterX, geometry.imageWidth, geometry.viewportWidth)
|
||||
imageCenterY = clampImageCenter(imageCenterY, geometry.imageHeight, geometry.viewportHeight)
|
||||
lightboxImage.style.width = `${geometry.imageWidth}px`
|
||||
lightboxImage.style.height = `${geometry.imageHeight}px`
|
||||
lightboxImage.style.transform = `translate(${-imageCenterX * 100}%, ${-imageCenterY * 100}%)`
|
||||
lightboxImage.dataset.easyImageScale = String(imageScale)
|
||||
lightboxImage.dataset.easyImageCenterX = String(imageCenterX)
|
||||
lightboxImage.dataset.easyImageCenterY = String(imageCenterY)
|
||||
lightboxViewport.classList.toggle(
|
||||
'easy-image-lightbox-pannable',
|
||||
geometry.imageWidth > geometry.viewportWidth + 1 || geometry.imageHeight > geometry.viewportHeight + 1,
|
||||
)
|
||||
const percentage = `${Math.round(imageScale * 100)}%`
|
||||
if (lightboxScaleOutput) {
|
||||
lightboxScaleOutput.value = percentage
|
||||
lightboxScaleOutput.textContent = percentage
|
||||
lightboxScaleOutput.title = `当前图片缩放 ${percentage}`
|
||||
}
|
||||
if (lightboxDecrease)
|
||||
lightboxDecrease.disabled = imageScale <= IMAGE_MIN_SCALE
|
||||
if (lightboxIncrease)
|
||||
lightboxIncrease.disabled = imageScale >= IMAGE_MAX_SCALE
|
||||
}
|
||||
|
||||
function broadcastImageView() {
|
||||
if (!activePreviewRequestId)
|
||||
return
|
||||
pendingSharedView = currentImageView()
|
||||
if (imageViewFrame !== undefined)
|
||||
return
|
||||
imageViewFrame = requestAnimationFrame(() => {
|
||||
imageViewFrame = undefined
|
||||
const view = pendingSharedView
|
||||
pendingSharedView = undefined
|
||||
if (!activePreviewRequestId || !view)
|
||||
return
|
||||
const command: ImagePreviewViewCommand = {
|
||||
id: createClientId(),
|
||||
requestId: activePreviewRequestId,
|
||||
sentAt: Date.now(),
|
||||
type: 'view',
|
||||
view,
|
||||
viewerId: clientId,
|
||||
}
|
||||
lastHandledCommandId = command.id
|
||||
imageChannel?.postMessage(command)
|
||||
updateImagePreviewState({ command })
|
||||
})
|
||||
}
|
||||
|
||||
function applyImageView(view: ImagePreviewView, shouldBroadcast = true, touched = true) {
|
||||
if (!Number.isFinite(view.scale) || !Number.isFinite(view.centerX) || !Number.isFinite(view.centerY))
|
||||
return
|
||||
imageScale = clampImageScale(view.scale)
|
||||
imageCenterX = Math.min(1, Math.max(0, view.centerX))
|
||||
imageCenterY = Math.min(1, Math.max(0, view.centerY))
|
||||
imageViewTouched ||= touched
|
||||
renderImageView()
|
||||
if (shouldBroadcast)
|
||||
broadcastImageView()
|
||||
}
|
||||
|
||||
function resetImageView() {
|
||||
const geometry = imageGeometry(1)
|
||||
if (!geometry)
|
||||
return
|
||||
const scale = clampImageScale(Math.min(
|
||||
1,
|
||||
geometry.viewportWidth / geometry.imageWidth,
|
||||
geometry.viewportHeight / geometry.imageHeight,
|
||||
))
|
||||
imageViewTouched = false
|
||||
applyImageView({ centerX: 0.5, centerY: 0.5, scale }, false, false)
|
||||
}
|
||||
|
||||
function zoomImageAt(requestedScale: number, clientX?: number, clientY?: number, shouldBroadcast = true) {
|
||||
const oldGeometry = imageGeometry()
|
||||
if (!oldGeometry || !lightboxViewport)
|
||||
return
|
||||
const scale = clampImageScale(requestedScale)
|
||||
let centerX = imageCenterX
|
||||
let centerY = imageCenterY
|
||||
if (clientX !== undefined && clientY !== undefined && scale !== imageScale) {
|
||||
const viewportRect = lightboxViewport.getBoundingClientRect()
|
||||
const pointerX = clientX - viewportRect.left
|
||||
const pointerY = clientY - viewportRect.top
|
||||
const imageLeft = oldGeometry.viewportWidth / 2 - imageCenterX * oldGeometry.imageWidth
|
||||
const imageTop = oldGeometry.viewportHeight / 2 - imageCenterY * oldGeometry.imageHeight
|
||||
const anchorX = (pointerX - imageLeft) / oldGeometry.imageWidth
|
||||
const anchorY = (pointerY - imageTop) / oldGeometry.imageHeight
|
||||
const nextWidth = lightboxImage!.naturalWidth * scale
|
||||
const nextHeight = lightboxImage!.naturalHeight * scale
|
||||
centerX = (oldGeometry.viewportWidth / 2 - pointerX + anchorX * nextWidth) / nextWidth
|
||||
centerY = (oldGeometry.viewportHeight / 2 - pointerY + anchorY * nextHeight) / nextHeight
|
||||
}
|
||||
applyImageView({ centerX, centerY, scale }, shouldBroadcast)
|
||||
}
|
||||
|
||||
function panImageBy(deltaX: number, deltaY: number, shouldBroadcast = true) {
|
||||
const geometry = imageGeometry()
|
||||
if (!geometry)
|
||||
return
|
||||
applyImageView({
|
||||
centerX: imageCenterX - deltaX / geometry.imageWidth,
|
||||
centerY: imageCenterY - deltaY / geometry.imageHeight,
|
||||
scale: imageScale,
|
||||
}, shouldBroadcast)
|
||||
}
|
||||
|
||||
function pinchMetrics() {
|
||||
const pointers = Array.from(imagePointers.values()).slice(0, 2)
|
||||
if (pointers.length < 2)
|
||||
return undefined
|
||||
return {
|
||||
distance: Math.hypot(pointers[1].x - pointers[0].x, pointers[1].y - pointers[0].y),
|
||||
midpoint: {
|
||||
x: (pointers[0].x + pointers[1].x) / 2,
|
||||
y: (pointers[0].y + pointers[1].y) / 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function handleImagePointerDown(event: PointerEvent) {
|
||||
if (!lightboxViewport || (event.pointerType === 'mouse' && event.button !== 0))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (imagePointers.size === 0)
|
||||
lightboxPointerMoved = false
|
||||
imagePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
|
||||
try {
|
||||
lightboxViewport.setPointerCapture(event.pointerId)
|
||||
}
|
||||
catch {
|
||||
// Synthetic pointer events used by tests do not participate in native pointer capture.
|
||||
}
|
||||
lightboxViewport.classList.add('easy-image-lightbox-dragging')
|
||||
const metrics = pinchMetrics()
|
||||
pinchDistance = metrics?.distance ?? 0
|
||||
pinchMidpoint = metrics?.midpoint
|
||||
}
|
||||
|
||||
function handleImagePointerMove(event: PointerEvent) {
|
||||
const previous = imagePointers.get(event.pointerId)
|
||||
if (!previous)
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const current = { x: event.clientX, y: event.clientY }
|
||||
imagePointers.set(event.pointerId, current)
|
||||
if (Math.hypot(current.x - previous.x, current.y - previous.y) > 1)
|
||||
lightboxPointerMoved = true
|
||||
|
||||
if (imagePointers.size === 1) {
|
||||
panImageBy(current.x - previous.x, current.y - previous.y)
|
||||
return
|
||||
}
|
||||
|
||||
const metrics = pinchMetrics()
|
||||
if (!metrics)
|
||||
return
|
||||
if (pinchDistance > 0)
|
||||
zoomImageAt(imageScale * metrics.distance / pinchDistance, metrics.midpoint.x, metrics.midpoint.y, false)
|
||||
if (pinchMidpoint)
|
||||
panImageBy(metrics.midpoint.x - pinchMidpoint.x, metrics.midpoint.y - pinchMidpoint.y)
|
||||
pinchDistance = metrics.distance
|
||||
pinchMidpoint = metrics.midpoint
|
||||
}
|
||||
|
||||
function handleImagePointerEnd(event: PointerEvent) {
|
||||
if (!imagePointers.has(event.pointerId))
|
||||
return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
imagePointers.delete(event.pointerId)
|
||||
if (lightboxViewport?.hasPointerCapture(event.pointerId))
|
||||
lightboxViewport.releasePointerCapture(event.pointerId)
|
||||
const metrics = pinchMetrics()
|
||||
pinchDistance = metrics?.distance ?? 0
|
||||
pinchMidpoint = metrics?.midpoint
|
||||
if (imagePointers.size === 0)
|
||||
lightboxViewport?.classList.remove('easy-image-lightbox-dragging')
|
||||
}
|
||||
|
||||
function handleImageWheel(event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const sensitivity = event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? 0.002 : 0.05
|
||||
zoomImageAt(imageScale * Math.exp(-event.deltaY * sensitivity), event.clientX, event.clientY)
|
||||
}
|
||||
|
||||
function handleLightboxImageLoad() {
|
||||
if (!lightbox || lightbox.hidden)
|
||||
return
|
||||
resetImageView()
|
||||
if (pendingRemoteView) {
|
||||
const view = pendingRemoteView
|
||||
pendingRemoteView = undefined
|
||||
applyImageView(view, false)
|
||||
}
|
||||
}
|
||||
|
||||
function createImageScaleButton(label: string, text: string, onClick: () => void) {
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.className = 'easy-image-scale-button'
|
||||
button.setAttribute('aria-label', label)
|
||||
button.title = label
|
||||
button.textContent = text
|
||||
button.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onClick()
|
||||
})
|
||||
return button
|
||||
}
|
||||
|
||||
function createLightbox() {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'easy-image-lightbox'
|
||||
@@ -235,9 +542,20 @@ function createLightbox() {
|
||||
overlay.setAttribute('aria-modal', 'true')
|
||||
overlay.setAttribute('aria-label', '图片全屏预览')
|
||||
|
||||
const viewport = document.createElement('div')
|
||||
viewport.className = 'easy-image-lightbox-viewport'
|
||||
|
||||
const preview = document.createElement('img')
|
||||
preview.className = 'easy-image-lightbox-preview'
|
||||
preview.alt = ''
|
||||
preview.draggable = false
|
||||
preview.addEventListener('load', handleLightboxImageLoad)
|
||||
viewport.addEventListener('pointerdown', handleImagePointerDown)
|
||||
viewport.addEventListener('pointermove', handleImagePointerMove)
|
||||
viewport.addEventListener('pointerup', handleImagePointerEnd)
|
||||
viewport.addEventListener('pointercancel', handleImagePointerEnd)
|
||||
viewport.addEventListener('wheel', handleImageWheel, { passive: false })
|
||||
viewport.append(preview)
|
||||
|
||||
const close = document.createElement('button')
|
||||
close.type = 'button'
|
||||
@@ -245,38 +563,66 @@ function createLightbox() {
|
||||
close.setAttribute('aria-label', '关闭图片全屏预览')
|
||||
close.title = '关闭(Esc)'
|
||||
close.textContent = '×'
|
||||
close.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
close.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
closeLightbox()
|
||||
})
|
||||
|
||||
const toolbar = document.createElement('nav')
|
||||
toolbar.className = 'easy-image-scale-toolbar'
|
||||
toolbar.setAttribute('aria-label', '图片缩放')
|
||||
toolbar.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
const decrease = createImageScaleButton('缩小图片', '−', () => zoomImageAt(imageScale - IMAGE_SCALE_STEP))
|
||||
const scaleOutput = document.createElement('output')
|
||||
scaleOutput.className = 'easy-image-scale-value'
|
||||
scaleOutput.setAttribute('aria-live', 'polite')
|
||||
scaleOutput.value = '100%'
|
||||
scaleOutput.textContent = '100%'
|
||||
const increase = createImageScaleButton('放大图片', '+', () => zoomImageAt(imageScale + IMAGE_SCALE_STEP))
|
||||
toolbar.append(decrease, scaleOutput, increase)
|
||||
|
||||
overlay.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
overlay.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.target === overlay)
|
||||
const clickedBackdrop = event.target === overlay || event.target === viewport
|
||||
if (clickedBackdrop && !lightboxPointerMoved)
|
||||
closeLightbox()
|
||||
lightboxPointerMoved = false
|
||||
})
|
||||
overlay.append(preview, close)
|
||||
overlay.append(viewport, close, toolbar)
|
||||
document.body.append(overlay)
|
||||
lightbox = overlay
|
||||
lightboxViewport = viewport
|
||||
lightboxImage = preview
|
||||
lightboxClose = close
|
||||
lightboxDecrease = decrease
|
||||
lightboxIncrease = increase
|
||||
lightboxScaleOutput = scaleOutput
|
||||
}
|
||||
|
||||
function showLightbox(src: string, alt: string, image?: HTMLImageElement) {
|
||||
function showLightbox(src: string, alt: string, image?: HTMLImageElement, requestId?: string) {
|
||||
if (!lightbox || !lightboxImage || !lightboxClose)
|
||||
return
|
||||
const wasHidden = lightbox.hidden
|
||||
activeImage = image
|
||||
lightboxImage.src = src
|
||||
activePreviewRequestId = requestId
|
||||
pendingRemoteView = undefined
|
||||
imageViewTouched = false
|
||||
imageScale = 1
|
||||
imageCenterX = 0.5
|
||||
imageCenterY = 0.5
|
||||
lightboxImage.alt = alt
|
||||
lightbox.hidden = false
|
||||
lightboxImage.src = src
|
||||
if (wasHidden)
|
||||
bodyOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.addEventListener('keydown', handleLightboxKeydown, true)
|
||||
if (lightboxImage.complete && lightboxImage.naturalWidth)
|
||||
requestAnimationFrame(handleLightboxImageLoad)
|
||||
lightboxClose.focus()
|
||||
}
|
||||
|
||||
@@ -284,17 +630,54 @@ function openLightbox(image: HTMLImageElement) {
|
||||
showLightbox(image.currentSrc || image.src, image.alt, image)
|
||||
}
|
||||
|
||||
function closeLightbox(restoreFocus = true) {
|
||||
function broadcastImageClose() {
|
||||
if (!activePreviewRequestId)
|
||||
return
|
||||
const command: ImagePreviewCloseCommand = {
|
||||
id: createClientId(),
|
||||
requestId: activePreviewRequestId,
|
||||
sentAt: Date.now(),
|
||||
type: 'close',
|
||||
viewerId: clientId,
|
||||
}
|
||||
lastHandledCommandId = command.id
|
||||
imageChannel?.postMessage(command)
|
||||
updateImagePreviewState({ command })
|
||||
}
|
||||
|
||||
function closeLightbox(restoreFocus = true, shouldBroadcast = true) {
|
||||
if (!lightbox || lightbox.hidden)
|
||||
return
|
||||
if (shouldBroadcast)
|
||||
broadcastImageClose()
|
||||
document.removeEventListener('keydown', handleLightboxKeydown, true)
|
||||
lightbox.hidden = true
|
||||
if (lightboxImage)
|
||||
if (imageViewFrame !== undefined) {
|
||||
cancelAnimationFrame(imageViewFrame)
|
||||
imageViewFrame = undefined
|
||||
}
|
||||
pendingSharedView = undefined
|
||||
pendingRemoteView = undefined
|
||||
imagePointers.clear()
|
||||
lightboxViewport?.classList.remove('easy-image-lightbox-dragging', 'easy-image-lightbox-pannable')
|
||||
if (lightboxImage) {
|
||||
lightboxImage.removeAttribute('src')
|
||||
lightboxImage.style.removeProperty('width')
|
||||
lightboxImage.style.removeProperty('height')
|
||||
lightboxImage.style.removeProperty('transform')
|
||||
delete lightboxImage.dataset.easyImageScale
|
||||
delete lightboxImage.dataset.easyImageCenterX
|
||||
delete lightboxImage.dataset.easyImageCenterY
|
||||
}
|
||||
document.body.style.overflow = bodyOverflow
|
||||
if (restoreFocus)
|
||||
activeImage?.focus()
|
||||
activeImage = undefined
|
||||
activePreviewRequestId = undefined
|
||||
imageScale = 1
|
||||
imageCenterX = 0.5
|
||||
imageCenterY = 0.5
|
||||
imageViewTouched = false
|
||||
}
|
||||
|
||||
function handleLightboxKeydown(event: KeyboardEvent) {
|
||||
@@ -304,8 +687,18 @@ function handleLightboxKeydown(event: KeyboardEvent) {
|
||||
closeLightbox()
|
||||
}
|
||||
else if (event.key === 'Tab') {
|
||||
const focusable = [lightboxClose, lightboxDecrease, lightboxIncrease].filter(
|
||||
(element): element is HTMLButtonElement => Boolean(element && !element.disabled),
|
||||
)
|
||||
if (!focusable.length)
|
||||
return
|
||||
event.preventDefault()
|
||||
lightboxClose?.focus()
|
||||
const currentIndex = focusable.indexOf(document.activeElement as HTMLButtonElement)
|
||||
const direction = event.shiftKey ? -1 : 1
|
||||
const nextIndex = currentIndex < 0
|
||||
? 0
|
||||
: (currentIndex + direction + focusable.length) % focusable.length
|
||||
focusable[nextIndex].focus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,13 +761,43 @@ function handleImagePreviewAcknowledgement(acknowledgement: ImagePreviewAcknowle
|
||||
closeLightbox(false)
|
||||
}
|
||||
|
||||
function isImagePreviewCommand(command: ImagePreviewCommand | undefined): command is ImagePreviewCommand {
|
||||
if (!command || !command.id || !command.requestId || !command.viewerId || !Number.isFinite(command.sentAt))
|
||||
return false
|
||||
if (command.type === 'close')
|
||||
return true
|
||||
return command.type === 'view'
|
||||
&& Number.isFinite(command.view?.scale)
|
||||
&& Number.isFinite(command.view?.centerX)
|
||||
&& Number.isFinite(command.view?.centerY)
|
||||
}
|
||||
|
||||
function handleImagePreviewCommand(command: ImagePreviewCommand | undefined) {
|
||||
if (!isImagePreviewCommand(command) || !isProjectionRoute() || command.id === lastHandledCommandId)
|
||||
return
|
||||
if (command.requestId !== activePreviewRequestId)
|
||||
return
|
||||
if (command.sentAt < mountedAt - 100 || Date.now() - command.sentAt > 10_000)
|
||||
return
|
||||
lastHandledCommandId = command.id
|
||||
if (command.type === 'close') {
|
||||
closeLightbox(false, false)
|
||||
return
|
||||
}
|
||||
if (!lightboxImage?.naturalWidth) {
|
||||
pendingRemoteView = command.view
|
||||
return
|
||||
}
|
||||
applyImageView(command.view, false)
|
||||
}
|
||||
|
||||
function handleImagePreviewRequest(request: ImagePreviewRequest | undefined) {
|
||||
if (!request || !isProjectionRoute() || request.id === lastHandledRequestId)
|
||||
return
|
||||
if (request.sentAt < mountedAt - 100 || Date.now() - request.sentAt > 10_000)
|
||||
return
|
||||
lastHandledRequestId = request.id
|
||||
showLightbox(request.src, request.alt)
|
||||
showLightbox(request.src, request.alt, undefined, request.id)
|
||||
updateImagePreviewState({
|
||||
acknowledgement: {
|
||||
requestId: request.id,
|
||||
@@ -574,6 +997,16 @@ function scheduleCodeHeightFit() {
|
||||
})
|
||||
}
|
||||
|
||||
function handleViewportResize() {
|
||||
scheduleCodeHeightFit()
|
||||
if (!lightbox || lightbox.hidden || !lightboxImage?.naturalWidth)
|
||||
return
|
||||
if (imageViewTouched)
|
||||
renderImageView()
|
||||
else
|
||||
resetImageView()
|
||||
}
|
||||
|
||||
function cleanupImage(image: HTMLImageElement, interaction: ImageInteraction) {
|
||||
image.removeEventListener('click', interaction.click)
|
||||
image.removeEventListener('keydown', interaction.keydown)
|
||||
@@ -628,13 +1061,17 @@ function cleanupCodeHosts() {
|
||||
|
||||
function cleanupContent() {
|
||||
clearPendingImagePreview()
|
||||
closeLightbox()
|
||||
closeLightbox(false, false)
|
||||
imageInteractions.forEach((interaction, image) => cleanupImage(image, interaction))
|
||||
imageInteractions.clear()
|
||||
lightbox?.remove()
|
||||
lightbox = undefined
|
||||
lightboxViewport = undefined
|
||||
lightboxImage = undefined
|
||||
lightboxClose = undefined
|
||||
lightboxDecrease = undefined
|
||||
lightboxIncrease = undefined
|
||||
lightboxScaleOutput = undefined
|
||||
cleanupTableHosts()
|
||||
cleanupCodeHosts()
|
||||
}
|
||||
@@ -659,11 +1096,17 @@ onMounted(() => {
|
||||
if (isScaleMessage(event.data))
|
||||
updateScale(event.data.key, event.data.kind, event.data.scale, false)
|
||||
})
|
||||
imageChannel = new BroadcastChannel(`easy-slides:image-preview:${import.meta.env.BASE_URL}`)
|
||||
imageChannel.addEventListener('message', event => handleImagePreviewCommand(event.data as ImagePreviewCommand))
|
||||
}
|
||||
stopAcknowledgementWatch = watch(
|
||||
() => easySharedState.easySlidesImagePreview?.acknowledgement,
|
||||
handleImagePreviewAcknowledgement,
|
||||
)
|
||||
stopCommandWatch = watch(
|
||||
() => easySharedState.easySlidesImagePreview?.command,
|
||||
handleImagePreviewCommand,
|
||||
)
|
||||
stopRequestWatch = watch(
|
||||
() => easySharedState.easySlidesImagePreview?.request,
|
||||
handleImagePreviewRequest,
|
||||
@@ -676,7 +1119,7 @@ onMounted(() => {
|
||||
scheduleCodeHeightFit()
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
window.addEventListener('resize', scheduleCodeHeightFit)
|
||||
window.addEventListener('resize', handleViewportResize)
|
||||
void document.fonts?.ready.then(scheduleCodeHeightFit)
|
||||
scheduleCodeHeightFit()
|
||||
})
|
||||
@@ -685,9 +1128,11 @@ onBeforeUnmount(() => {
|
||||
observer?.disconnect()
|
||||
resizeObserver?.disconnect()
|
||||
channel?.close()
|
||||
imageChannel?.close()
|
||||
stopAcknowledgementWatch?.()
|
||||
stopCommandWatch?.()
|
||||
stopRequestWatch?.()
|
||||
window.removeEventListener('resize', scheduleCodeHeightFit)
|
||||
window.removeEventListener('resize', handleViewportResize)
|
||||
if (fitFrame !== undefined)
|
||||
cancelAnimationFrame(fitFrame)
|
||||
cleanupContent()
|
||||
|
||||
Reference in New Issue
Block a user