Files
sori.studio/composables/useContentMarkdownLightbox.js

192 lines
4.8 KiB
JavaScript

import { computed, getCurrentInstance, onBeforeUnmount, ref } from 'vue'
/**
* 공개 Markdown 렌더러의 이미지 라이트박스 상태를 관리한다.
* @param {Object} [options] - 설정
* @param {() => (Window|null)} [options.getKeyboardTarget] - 키보드 이벤트 대상 반환 함수
* @returns {{
* activeLightboxImages: import('vue').Ref<Array<Object>>,
* activeLightboxIndex: import('vue').Ref<number>,
* activeLightboxImage: import('vue').ComputedRef<Object|undefined>,
* openLightbox: (images: Array<Object>, index: number) => void,
* openContentLightbox: (images: Array<Object>, lightboxKey: string) => void,
* closeLightbox: () => void,
* showPreviousImage: () => void,
* showNextImage: () => void,
* handleLightboxKeydown: (event: KeyboardEvent) => void
* }} 라이트박스 상태와 액션
*/
export const useContentMarkdownLightbox = (options = {}) => {
const getKeyboardTarget = options.getKeyboardTarget || (() => {
if (!import.meta.client) {
return null
}
return window
})
const activeLightboxImages = ref([])
const activeLightboxIndex = ref(0)
const activeLightboxImage = computed(() => activeLightboxImages.value[activeLightboxIndex.value])
let keyboardListenerAttached = false
/**
* 전역 키보드 이벤트를 연결한다.
* @returns {void}
*/
const attachKeyboardListener = () => {
if (keyboardListenerAttached) {
return
}
const target = getKeyboardTarget()
if (!target) {
return
}
target.addEventListener('keydown', handleLightboxKeydown)
keyboardListenerAttached = true
}
/**
* 전역 키보드 이벤트를 해제한다.
* @returns {void}
*/
const detachKeyboardListener = () => {
if (!keyboardListenerAttached) {
return
}
const target = getKeyboardTarget()
if (target) {
target.removeEventListener('keydown', handleLightboxKeydown)
}
keyboardListenerAttached = false
}
/**
* 라이트박스를 연다.
* @param {Array<Object>} images - 이미지 목록
* @param {number} index - 시작 인덱스
* @returns {void}
*/
const openLightbox = (images, index) => {
const nextImages = Array.isArray(images) ? images : []
if (!nextImages.length) {
activeLightboxImages.value = []
activeLightboxIndex.value = 0
detachKeyboardListener()
return
}
activeLightboxImages.value = nextImages
activeLightboxIndex.value = Math.max(0, Math.min(Number(index) || 0, nextImages.length - 1))
attachKeyboardListener()
}
/**
* 본문 전체 이미지 목록에서 지정 이미지를 라이트박스로 연다.
* @param {Array<Object>} images - 본문 라이트박스 이미지 목록
* @param {string} lightboxKey - 이미지 식별 키
* @returns {void}
*/
const openContentLightbox = (images, lightboxKey) => {
const targetImages = Array.isArray(images) ? images : []
const index = targetImages.findIndex((image) => image.lightboxKey === lightboxKey)
if (index < 0) {
return
}
openLightbox(targetImages, index)
}
/**
* 라이트박스를 닫는다.
* @returns {void}
*/
const closeLightbox = () => {
activeLightboxImages.value = []
activeLightboxIndex.value = 0
detachKeyboardListener()
}
/**
* 라이트박스 이전 이미지로 이동한다.
* @returns {void}
*/
const showPreviousImage = () => {
if (activeLightboxImages.value.length <= 1) {
return
}
activeLightboxIndex.value = activeLightboxIndex.value === 0
? activeLightboxImages.value.length - 1
: activeLightboxIndex.value - 1
}
/**
* 라이트박스 다음 이미지로 이동한다.
* @returns {void}
*/
const showNextImage = () => {
if (activeLightboxImages.value.length <= 1) {
return
}
activeLightboxIndex.value = (activeLightboxIndex.value + 1) % activeLightboxImages.value.length
}
/**
* 라이트박스 키보드 조작을 처리한다.
* @param {KeyboardEvent} event - 키보드 이벤트
* @returns {void}
*/
function handleLightboxKeydown (event) {
if (!activeLightboxImage.value) {
return
}
if (event.key === 'Escape') {
event.preventDefault()
closeLightbox()
return
}
if (activeLightboxImages.value.length <= 1) {
return
}
if (event.key === 'ArrowLeft') {
event.preventDefault()
showPreviousImage()
return
}
if (event.key === 'ArrowRight') {
event.preventDefault()
showNextImage()
}
}
if (getCurrentInstance()) {
onBeforeUnmount(detachKeyboardListener)
}
return {
activeLightboxImages,
activeLightboxIndex,
activeLightboxImage,
openLightbox,
openContentLightbox,
closeLightbox,
showPreviousImage,
showNextImage,
handleLightboxKeydown
}
}