공개 본문 이미지 드래그 상태 분리
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
import { getGalleryRows } from '../../lib/content-gallery-images.js'
|
||||
import { useContentMarkdownLightbox } from '../../composables/useContentMarkdownLightbox.js'
|
||||
import { useContentMarkdownBlocks } from '../../composables/useContentMarkdownBlocks.js'
|
||||
import { useContentMarkdownImageDrag } from '../../composables/useContentMarkdownImageDrag.js'
|
||||
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
|
||||
import ProseCodeBlock from './ProseCodeBlock.vue'
|
||||
import ProseHtml from './ProseHtml.vue'
|
||||
@@ -95,20 +96,10 @@ const emit = defineEmits([
|
||||
'content-replace'
|
||||
])
|
||||
|
||||
/** @type {string} 라이브 이미지·갤러리 드래그 payload MIME */
|
||||
const LIVE_IMAGE_DRAG_MIME = 'application/x-sori-live-image'
|
||||
/** @type {string} 본문 리스트 마커 색상 */
|
||||
const CONTENT_LIST_MARKER_COLOR = '#2eb6ea'
|
||||
/** @type {import('vue').Ref<Record<string, number>>} 갤러리 이미지 자연 비율 */
|
||||
const galleryImageAspectRatios = ref({})
|
||||
/** @type {import('vue').Ref<{ kind: 'image'|'gallery', sourceLine?: number, startLine?: number, endLine?: number, blockId: string, imageIndex?: number, image?: Object }|null>} */
|
||||
const liveImageDragState = ref(null)
|
||||
/** @type {import('vue').Ref<{ blockId: string, targetIndex: number }|null>} */
|
||||
const galleryDropTarget = ref(null)
|
||||
/** @type {import('vue').Ref<{ blockId: string }|null>} */
|
||||
const imageBlockDropTarget = ref(null)
|
||||
/** @type {import('vue').Ref<{ insertBeforeLine: number }|null>} */
|
||||
const blockInsertDropTarget = ref(null)
|
||||
/** @type {import('vue').Ref<number|null>} */
|
||||
const pendingFocusLine = ref(null)
|
||||
/** @type {import('vue').Ref<'start'|'end'|'auto'>} 포커스 후 커서 위치 */
|
||||
@@ -143,6 +134,36 @@ const {
|
||||
inArticleAdCode: computed(() => props.inArticleAdCode)
|
||||
})
|
||||
|
||||
const {
|
||||
liveImageDragState,
|
||||
galleryDropTarget,
|
||||
imageBlockDropTarget,
|
||||
blockInsertDropTarget,
|
||||
getBlockInsertBeforeLine,
|
||||
onImageBlockDragStart,
|
||||
onImageBlockDragOver,
|
||||
onImageBlockDragLeave,
|
||||
onImageBlockDrop,
|
||||
onImageBlockDragEnd,
|
||||
onBlockInsertDragOver,
|
||||
onBlockInsertDragLeave,
|
||||
onBlockInsertDrop,
|
||||
onTailInsertDragOver,
|
||||
onTailInsertDrop,
|
||||
onGalleryDragStart,
|
||||
onGalleryDragEnd,
|
||||
onGalleryDragOverItem,
|
||||
onGalleryDragLeaveGallery,
|
||||
onGalleryDrop
|
||||
} = useContentMarkdownImageDrag({
|
||||
interactive: computed(() => props.interactive),
|
||||
tailInsertBeforeLine,
|
||||
onMergeImagesToGallery: (payload) => emit('merge-images-to-gallery', payload),
|
||||
onInsertImageToGallery: (payload) => emit('insert-image-to-gallery', payload),
|
||||
onExtractGalleryImage: (payload) => emit('extract-gallery-image', payload),
|
||||
onGalleryReorder: (payload) => emit('gallery-reorder', payload)
|
||||
})
|
||||
|
||||
/**
|
||||
* 빈 줄 공백 블록 높이를 반환한다.
|
||||
* @param {Object} block - 렌더링 블록
|
||||
@@ -1582,433 +1603,6 @@ const openContentLightbox = (lightboxKey) => {
|
||||
openContentLightboxFromImages(contentLightboxImages.value, lightboxKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 앞 삽입 위치(0-based 줄)를 반환한다.
|
||||
* @param {Object} block - 콘텐츠 블록
|
||||
* @returns {number|null} 삽입 줄 번호
|
||||
*/
|
||||
const getBlockInsertBeforeLine = (block) => {
|
||||
const line = block?.meta?.startLine
|
||||
|
||||
return typeof line === 'number' ? line : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 블록을 드래그 payload용 객체로 변환한다.
|
||||
* @param {Object} block - 이미지 블록
|
||||
* @returns {Object} 이미지 데이터
|
||||
*/
|
||||
const toImageDragPayload = (block) => ({
|
||||
url: block.url,
|
||||
caption: block.caption,
|
||||
width: block.width,
|
||||
useAlt: block.useAlt,
|
||||
legacyBracketLabel: block.legacyBracketLabel
|
||||
})
|
||||
|
||||
/**
|
||||
* 라이브 이미지 드래그 payload를 dataTransfer에 기록한다.
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} payload - 드래그 payload
|
||||
* @returns {void}
|
||||
*/
|
||||
const writeLiveImageDragData = (event, payload) => {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData(LIVE_IMAGE_DRAG_MIME, JSON.stringify(payload))
|
||||
}
|
||||
|
||||
/**
|
||||
* 드래그 payload를 읽는다.
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @returns {Object|null} payload
|
||||
*/
|
||||
const readLiveImageDragData = (event) => {
|
||||
try {
|
||||
const raw = event.dataTransfer.getData(LIVE_IMAGE_DRAG_MIME)
|
||||
|
||||
return raw ? JSON.parse(raw) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 이미지·갤러리 드래그 상태를 초기화한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const clearLiveImageDragUi = () => {
|
||||
liveImageDragState.value = null
|
||||
galleryDropTarget.value = null
|
||||
imageBlockDropTarget.value = null
|
||||
blockInsertDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 단일 이미지 블록 드래그 시작
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 이미지 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onImageBlockDragStart = (event, block) => {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.target instanceof Element && event.target.closest('button')) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
kind: 'image',
|
||||
sourceLine: block.meta.startLine,
|
||||
blockId: block.id,
|
||||
image: toImageDragPayload(block)
|
||||
}
|
||||
|
||||
liveImageDragState.value = payload
|
||||
writeLiveImageDragData(event, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 이미지 블록 위 드래그 오버(갤러리 병합 대상)
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 대상 이미지 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onImageBlockDragOver = (event, block) => {
|
||||
if (!props.interactive || liveImageDragState.value?.kind !== 'image') {
|
||||
return
|
||||
}
|
||||
|
||||
if (liveImageDragState.value.sourceLine === block.meta.startLine) {
|
||||
imageBlockDropTarget.value = null
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
imageBlockDropTarget.value = { blockId: block.id }
|
||||
blockInsertDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 이미지 블록 드래그 이탈
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 이미지 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onImageBlockDragLeave = (event, block) => {
|
||||
const related = event.relatedTarget
|
||||
|
||||
if (related && event.currentTarget.contains(related)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (imageBlockDropTarget.value?.blockId === block.id) {
|
||||
imageBlockDropTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 이미지 두 블록 병합 드롭
|
||||
* @param {DragEvent} event - 드롭 이벤트
|
||||
* @param {Object} block - 대상 이미지 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onImageBlockDrop = (event, block) => {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const payload = readLiveImageDragData(event) || liveImageDragState.value
|
||||
|
||||
if (payload?.kind !== 'image' || typeof payload.sourceLine !== 'number') {
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
if (payload.sourceLine === block.meta.startLine) {
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
emit('merge-images-to-gallery', {
|
||||
sourceLine: payload.sourceLine,
|
||||
targetLine: block.meta.startLine
|
||||
})
|
||||
clearLiveImageDragUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 이미지 블록 드래그 종료
|
||||
* @returns {void}
|
||||
*/
|
||||
const onImageBlockDragEnd = () => {
|
||||
imageBlockDropTarget.value = null
|
||||
|
||||
if (liveImageDragState.value?.kind === 'image') {
|
||||
liveImageDragState.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 사이 삽입선 드래그 오버(갤러리 이미지 분리)
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 기준 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockInsertDragOver = (event, block) => {
|
||||
const insertBeforeLine = getBlockInsertBeforeLine(block)
|
||||
|
||||
if (!props.interactive || insertBeforeLine === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const dragKind = liveImageDragState.value?.kind || readLiveImageDragData(event)?.kind
|
||||
|
||||
if (dragKind !== 'gallery') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
blockInsertDropTarget.value = { insertBeforeLine }
|
||||
galleryDropTarget.value = null
|
||||
imageBlockDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 사이 삽입선 드래그 이탈
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockInsertDragLeave = (event) => {
|
||||
const related = event.relatedTarget
|
||||
|
||||
if (related && event.currentTarget.contains(related)) {
|
||||
return
|
||||
}
|
||||
|
||||
blockInsertDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 사이에 갤러리 이미지를 단일 블록으로 분리해 삽입
|
||||
* @param {DragEvent} event - 드롭 이벤트
|
||||
* @param {Object} block - 기준 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockInsertDrop = (event, block) => {
|
||||
const insertBeforeLine = getBlockInsertBeforeLine(block)
|
||||
|
||||
if (!props.interactive || insertBeforeLine === null) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const payload = readLiveImageDragData(event) || liveImageDragState.value
|
||||
|
||||
if (payload?.kind !== 'gallery'
|
||||
|| typeof payload.startLine !== 'number'
|
||||
|| typeof payload.endLine !== 'number'
|
||||
|| typeof payload.imageIndex !== 'number') {
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
emit('extract-gallery-image', {
|
||||
startLine: payload.startLine,
|
||||
endLine: payload.endLine,
|
||||
imageIndex: payload.imageIndex,
|
||||
insertBeforeLine,
|
||||
image: payload.image
|
||||
})
|
||||
clearLiveImageDragUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 맨 아래 삽입선 드래그 오버
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onTailInsertDragOver = (event) => {
|
||||
if (!props.interactive || liveImageDragState.value?.kind !== 'gallery') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
blockInsertDropTarget.value = { insertBeforeLine: tailInsertBeforeLine.value }
|
||||
galleryDropTarget.value = null
|
||||
imageBlockDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 맨 아래에 갤러리 이미지 분리 삽입
|
||||
* @param {DragEvent} event - 드롭 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onTailInsertDrop = (event) => {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const payload = readLiveImageDragData(event) || liveImageDragState.value
|
||||
const insertBeforeLine = tailInsertBeforeLine.value
|
||||
|
||||
if (payload?.kind !== 'gallery'
|
||||
|| typeof payload.startLine !== 'number'
|
||||
|| typeof payload.endLine !== 'number'
|
||||
|| typeof payload.imageIndex !== 'number') {
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
emit('extract-gallery-image', {
|
||||
startLine: payload.startLine,
|
||||
endLine: payload.endLine,
|
||||
imageIndex: payload.imageIndex,
|
||||
insertBeforeLine,
|
||||
image: payload.image
|
||||
})
|
||||
clearLiveImageDragUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 시작
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragStart = (event, block, imageIndex) => {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
const image = block.images[imageIndex]
|
||||
const payload = {
|
||||
kind: 'gallery',
|
||||
startLine: block.meta.startLine,
|
||||
endLine: block.meta.endLine,
|
||||
blockId: block.id,
|
||||
imageIndex,
|
||||
image
|
||||
}
|
||||
|
||||
liveImageDragState.value = payload
|
||||
writeLiveImageDragData(event, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 종료
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragEnd = () => {
|
||||
clearLiveImageDragUi()
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 중 드롭 대상 셀 표시
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @param {number} imageIndex - 호버 중인 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragOverItem = (event, block, imageIndex) => {
|
||||
if (!props.interactive || !['image', 'gallery'].includes(liveImageDragState.value?.kind)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
blockInsertDropTarget.value = null
|
||||
|
||||
if (liveImageDragState.value.kind === 'image') {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
galleryDropTarget.value = { blockId: block.id, targetIndex: imageIndex }
|
||||
imageBlockDropTarget.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (liveImageDragState.value.blockId !== block.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (liveImageDragState.value.imageIndex === imageIndex) {
|
||||
galleryDropTarget.value = null
|
||||
return
|
||||
}
|
||||
|
||||
galleryDropTarget.value = { blockId: block.id, targetIndex: imageIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 영역을 벗어나면 드롭 대상 표시를 해제한다.
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragLeaveGallery = (event, block) => {
|
||||
const related = event.relatedTarget
|
||||
|
||||
if (related && event.currentTarget.contains(related)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (galleryDropTarget.value?.blockId === block.id) {
|
||||
galleryDropTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드롭으로 순서 변경
|
||||
* @param {DragEvent} event - 드롭 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @param {number} targetIndex - 대상 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDrop = (event, block, targetIndex) => {
|
||||
if (!props.interactive || !['image', 'gallery'].includes(liveImageDragState.value?.kind)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (liveImageDragState.value.kind === 'image') {
|
||||
emit('insert-image-to-gallery', {
|
||||
sourceLine: liveImageDragState.value.sourceLine,
|
||||
startLine: block.meta?.startLine,
|
||||
endLine: block.meta?.endLine,
|
||||
targetIndex: targetIndex + 1,
|
||||
image: liveImageDragState.value.image
|
||||
})
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
const { blockId, imageIndex: fromIndex } = liveImageDragState.value
|
||||
|
||||
if (blockId !== block.id || fromIndex === targetIndex) {
|
||||
clearLiveImageDragUi()
|
||||
return
|
||||
}
|
||||
|
||||
const images = [...block.images]
|
||||
const [moved] = images.splice(fromIndex, 1)
|
||||
images.splice(targetIndex, 0, moved)
|
||||
|
||||
emit('gallery-reorder', {
|
||||
startLine: block.meta?.startLine,
|
||||
endLine: block.meta?.endLine,
|
||||
images
|
||||
})
|
||||
|
||||
clearLiveImageDragUi()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user