v1.2.4: 이미지 캡션 표시 수정 및 미리보기 갤러리 드래그 정렬
파일명 alt와 캡션을 분리해 공개·미리보기에 캡션이 보이도록 하고, 관리자 미리보기에서 갤러리 순서를 드래그로 바꿀 수 있게 했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
|
||||
import { resolveActiveBlockContext } from '../../lib/markdown-block-context.js'
|
||||
import { serializeImageMarkdown } from '../../lib/markdown-image.js'
|
||||
import { getImageDefaultAltLabel, serializeImageMarkdown } from '../../lib/markdown-image.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -629,7 +629,41 @@ const updateActiveMediaImage = (imageIndex, patch) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const setActiveMediaUseAlt = (imageIndex, enabled) => {
|
||||
updateActiveMediaImage(imageIndex, { useAlt: enabled })
|
||||
const image = activeMediaBlock.value?.images[imageIndex]
|
||||
|
||||
if (!image) {
|
||||
return
|
||||
}
|
||||
|
||||
const patch = { useAlt: enabled }
|
||||
|
||||
if (enabled && !String(image.caption || '').trim()) {
|
||||
const legacy = String(image.legacyBracketLabel || '').trim()
|
||||
const filename = getImageDefaultAltLabel(image.url)
|
||||
|
||||
if (legacy && legacy !== filename) {
|
||||
patch.caption = legacy
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveMediaImage(imageIndex, patch)
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리보기에서 갤러리 순서 변경
|
||||
* @param {{ startLine: number, endLine: number, images: Array<Object> }} payload - 갤러리 줄 범위·이미지 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const onPreviewGalleryReorder = ({ startLine, endLine, images }) => {
|
||||
if (typeof startLine !== 'number' || typeof endLine !== 'number' || !Array.isArray(images)) {
|
||||
return
|
||||
}
|
||||
|
||||
replaceLineRange(startLine, endLine, [
|
||||
':::gallery',
|
||||
...images.map(createImageMarkdown),
|
||||
':::'
|
||||
], false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1318,7 +1352,12 @@ const handleKeydown = (event) => {
|
||||
style="--site-text: #15171a; --site-muted: #6b7280; --site-panel: #f6f7f8; --site-line: #e3e6e8; --site-accent: #2eb6ea;"
|
||||
tabindex="0"
|
||||
>
|
||||
<ContentMarkdownRenderer v-if="markdownValue.trim()" :content="markdownValue" />
|
||||
<ContentMarkdownRenderer
|
||||
v-if="markdownValue.trim()"
|
||||
:content="markdownValue"
|
||||
interactive
|
||||
@gallery-reorder="onPreviewGalleryReorder"
|
||||
/>
|
||||
<p v-else class="admin-markdown-editor__preview-empty text-sm text-[#8e9cac]">
|
||||
미리보기할 본문이 없습니다.
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import {
|
||||
getImageAltAttribute,
|
||||
getImageCaption,
|
||||
getImageDisplayCaption,
|
||||
parseImageMarkdownLine
|
||||
} from '../../lib/markdown-image.js'
|
||||
|
||||
@@ -9,13 +9,22 @@ const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 관리자 미리보기: 갤러리 드래그 정렬 등 */
|
||||
interactive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['gallery-reorder'])
|
||||
|
||||
const BLANK_PARAGRAPH_MARKER = '<!--sori:blank-paragraph-->'
|
||||
|
||||
const activeLightboxImages = ref([])
|
||||
const activeLightboxIndex = ref(0)
|
||||
/** @type {import('vue').Ref<{ blockId: string, imageIndex: number }|null>} */
|
||||
const galleryDragState = ref(null)
|
||||
|
||||
/**
|
||||
* 마크다운 블록을 생성
|
||||
@@ -323,7 +332,13 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
}
|
||||
})
|
||||
|
||||
blocks.push(createBlock('gallery', '', null, `block-${blocks.length}`, { images }))
|
||||
blocks.push(createBlock('gallery', '', null, `block-${blocks.length}`, {
|
||||
images,
|
||||
meta: {
|
||||
startLine: index,
|
||||
endLine: nextIndex
|
||||
}
|
||||
}))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
@@ -552,6 +567,65 @@ const showNextImage = () => {
|
||||
activeLightboxIndex.value = (activeLightboxIndex.value + 1) % activeLightboxImages.value.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 시작
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {string} blockId - 블록 ID
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragStart = (event, blockId, imageIndex) => {
|
||||
if (!props.interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
galleryDragState.value = { blockId, imageIndex }
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', String(imageIndex))
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 종료
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragEnd = () => {
|
||||
galleryDragState.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드롭으로 순서 변경
|
||||
* @param {DragEvent} event - 드롭 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @param {number} targetIndex - 대상 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDrop = (event, block, targetIndex) => {
|
||||
if (!props.interactive || !galleryDragState.value) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const { blockId, imageIndex: fromIndex } = galleryDragState.value
|
||||
|
||||
if (blockId !== block.id || fromIndex === targetIndex) {
|
||||
galleryDragState.value = null
|
||||
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
|
||||
})
|
||||
|
||||
galleryDragState.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이트박스 키보드 조작(Esc 닫기, 좌우 이전·다음)
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
@@ -640,12 +714,9 @@ onBeforeUnmount(() => {
|
||||
v-else-if="block.type === 'image'"
|
||||
:src="block.url"
|
||||
:alt="getImageAltAttribute(block)"
|
||||
:caption="getImageDisplayCaption(block)"
|
||||
:variant="block.width"
|
||||
>
|
||||
<template v-if="getImageCaption(block)">
|
||||
{{ getImageCaption(block) }}
|
||||
</template>
|
||||
</ProseImage>
|
||||
/>
|
||||
<ProseCallout
|
||||
v-else-if="block.type === 'callout'"
|
||||
:emoji-enabled="block.calloutEmojiEnabled"
|
||||
@@ -684,16 +755,39 @@ onBeforeUnmount(() => {
|
||||
:placeholder="block.meta.placeholder"
|
||||
/>
|
||||
<ProseEmbed v-else-if="block.type === 'embed'" :url="block.url" />
|
||||
<div v-else-if="block.type === 'gallery'" class="content-markdown-renderer__gallery my-8 grid grid-cols-2 gap-2 md:grid-cols-3">
|
||||
<button
|
||||
<div
|
||||
v-else-if="block.type === 'gallery'"
|
||||
class="content-markdown-renderer__gallery my-8 grid grid-cols-2 gap-2 md:grid-cols-3"
|
||||
>
|
||||
<figure
|
||||
v-for="(image, imageIndex) in block.images"
|
||||
:key="`${block.id}-${image.url}`"
|
||||
class="content-markdown-renderer__gallery-button overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]"
|
||||
type="button"
|
||||
@click="openLightbox(block.images, imageIndex)"
|
||||
:key="`${block.id}-${imageIndex}-${image.url}`"
|
||||
class="content-markdown-renderer__gallery-item min-w-0"
|
||||
:class="interactive ? 'content-markdown-renderer__gallery-item--interactive' : ''"
|
||||
:draggable="interactive"
|
||||
@dragstart="onGalleryDragStart($event, block.id, imageIndex)"
|
||||
@dragend="onGalleryDragEnd"
|
||||
@dragover.prevent
|
||||
@drop="onGalleryDrop($event, block, imageIndex)"
|
||||
>
|
||||
<img class="content-markdown-renderer__gallery-image aspect-[4/3] w-full object-cover transition-transform hover:scale-[1.02]" :src="image.url" :alt="getImageAltAttribute(image)">
|
||||
</button>
|
||||
<button
|
||||
class="content-markdown-renderer__gallery-button w-full overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]"
|
||||
type="button"
|
||||
@click="openLightbox(block.images, imageIndex)"
|
||||
>
|
||||
<img
|
||||
class="content-markdown-renderer__gallery-image aspect-[4/3] w-full object-cover transition-transform hover:scale-[1.02]"
|
||||
:src="image.url"
|
||||
:alt="getImageAltAttribute(image)"
|
||||
>
|
||||
</button>
|
||||
<figcaption
|
||||
v-if="getImageDisplayCaption(image)"
|
||||
class="content-markdown-renderer__gallery-caption mt-1.5 text-center text-xs text-[var(--site-muted)]"
|
||||
>
|
||||
{{ getImageDisplayCaption(image) }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<pre
|
||||
v-else-if="block.type === 'code'"
|
||||
@@ -750,3 +844,13 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content-markdown-renderer__gallery-item--interactive {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.content-markdown-renderer__gallery-item--interactive:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,11 @@ defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 이미지 아래 표시용 캡션 */
|
||||
caption: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'regular'
|
||||
@@ -26,8 +31,11 @@ defineProps({
|
||||
<div class="overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||
<img class="prose-image__media w-full object-cover" :src="src" :alt="alt">
|
||||
</div>
|
||||
<figcaption v-if="$slots.default" class="prose-image__caption mt-1.5 text-center text-sm text-[var(--site-muted)]">
|
||||
<slot />
|
||||
<figcaption
|
||||
v-if="caption"
|
||||
class="prose-image__caption mt-1.5 text-center text-sm text-[var(--site-muted)]"
|
||||
>
|
||||
{{ caption }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user