v1.2.5: 갤러리 드롭 위치 표시 및 파일명 캡션 토글 정리
미리보기 갤러리 드래그 시 드롭 대상 셀을 시각적으로 표시하고, 파일명 토글을 캡션(figcaption) 표시로 맞춤. 미리보기 클릭→작성 모드 전환은 제거. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,8 @@ const activeLightboxImages = ref([])
|
||||
const activeLightboxIndex = ref(0)
|
||||
/** @type {import('vue').Ref<{ blockId: string, imageIndex: number }|null>} */
|
||||
const galleryDragState = ref(null)
|
||||
/** @type {import('vue').Ref<{ blockId: string, targetIndex: number }|null>} */
|
||||
const galleryDropTarget = ref(null)
|
||||
|
||||
/**
|
||||
* 마크다운 블록을 생성
|
||||
@@ -35,6 +37,23 @@ const galleryDragState = ref(null)
|
||||
* @param {Object} options - 추가 블록 옵션
|
||||
* @returns {Object} 블록
|
||||
*/
|
||||
/**
|
||||
* 블록에 원본 마크다운 줄 범위를 붙인다.
|
||||
* @param {Object} block - 블록
|
||||
* @param {number} startLine - 시작 줄(0-based)
|
||||
* @param {number} endLine - 끝 줄(0-based)
|
||||
* @returns {Object} 블록
|
||||
*/
|
||||
const attachSourceRange = (block, startLine, endLine) => {
|
||||
block.meta = {
|
||||
...(block.meta && typeof block.meta === 'object' ? block.meta : {}),
|
||||
startLine,
|
||||
endLine: endLine ?? startLine
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
const createBlock = (type = 'paragraph', text = '', level = null, id = '', options = {}) => ({
|
||||
id,
|
||||
type,
|
||||
@@ -276,18 +295,21 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
if (trimmedLine === BLANK_PARAGRAPH_MARKER) {
|
||||
blocks.push(createBlock('spacer', '', null, `block-${blocks.length}`, { meta: { legacy: true } }))
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(createBlock('spacer', '', null, `block-${blocks.length}`, { meta: { legacy: true } }), startLine, startLine))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (!trimmedLine) {
|
||||
blocks.push(createBlock('spacer', '', null, `block-${blocks.length}`))
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(createBlock('spacer', '', null, `block-${blocks.length}`), startLine, startLine))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === '>>>') {
|
||||
const startLine = index
|
||||
const contentLines = []
|
||||
index += 1
|
||||
|
||||
@@ -296,17 +318,26 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('quote', contentLines.join('\n').trim(), null, `block-${blocks.length}`, { variant: 'alt' }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('quote', contentLines.join('\n').trim(), null, `block-${blocks.length}`, { variant: 'alt' }),
|
||||
startLine,
|
||||
index - 1
|
||||
))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::bookmark') {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const bookmarkMeta = parseBookmarkMeta(contentLines.join('\n'))
|
||||
|
||||
if (bookmarkMeta.url) {
|
||||
blocks.push(createBlock('bookmark', '', null, `block-${blocks.length}`, { meta: bookmarkMeta }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('bookmark', '', null, `block-${blocks.length}`, { meta: bookmarkMeta }),
|
||||
startLine,
|
||||
nextIndex
|
||||
))
|
||||
}
|
||||
|
||||
index = nextIndex
|
||||
@@ -314,14 +345,20 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::signup') {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const signupMeta = parseSignupMeta(contentLines.join('\n'))
|
||||
blocks.push(createBlock('signup', '', null, `block-${blocks.length}`, { meta: signupMeta }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('signup', '', null, `block-${blocks.length}`, { meta: signupMeta }),
|
||||
startLine,
|
||||
nextIndex
|
||||
))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::gallery') {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const images = []
|
||||
|
||||
@@ -332,35 +369,46 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
}
|
||||
})
|
||||
|
||||
blocks.push(createBlock('gallery', '', null, `block-${blocks.length}`, {
|
||||
images,
|
||||
meta: {
|
||||
startLine: index,
|
||||
endLine: nextIndex
|
||||
}
|
||||
}))
|
||||
blocks.push(attachSourceRange(createBlock('gallery', '', null, `block-${blocks.length}`, {
|
||||
images
|
||||
}), startLine, nextIndex))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith(':::callout')) {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
blocks.push(createBlock('callout', contentLines.join('\n'), null, `block-${blocks.length}`, parseCalloutOptions(trimmedLine)))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('callout', contentLines.join('\n'), null, `block-${blocks.length}`, parseCalloutOptions(trimmedLine)),
|
||||
startLine,
|
||||
nextIndex
|
||||
))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith(':::toggle')) {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const title = trimmedLine.replace(/^:::toggle\s*/, '').trim()
|
||||
blocks.push(createBlock('toggle', contentLines.join('\n'), null, `block-${blocks.length}`, { title }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('toggle', contentLines.join('\n'), null, `block-${blocks.length}`, { title }),
|
||||
startLine,
|
||||
nextIndex
|
||||
))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::embed') {
|
||||
const startLine = index
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
blocks.push(createBlock('embed', '', null, `block-${blocks.length}`, { url: contentLines.join('\n').trim() }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('embed', '', null, `block-${blocks.length}`, { url: contentLines.join('\n').trim() }),
|
||||
startLine,
|
||||
nextIndex
|
||||
))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
@@ -368,12 +416,14 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
const image = parseImageLine(trimmedLine)
|
||||
|
||||
if (image) {
|
||||
blocks.push(createBlock('image', '', null, `block-${blocks.length}`, image))
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(createBlock('image', '', null, `block-${blocks.length}`, image), startLine, startLine))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('```')) {
|
||||
const startLine = index
|
||||
const codeLines = []
|
||||
index += 1
|
||||
|
||||
@@ -382,13 +432,18 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('code', codeLines.join('\n'), null, `block-${blocks.length}`))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('code', codeLines.join('\n'), null, `block-${blocks.length}`),
|
||||
startLine,
|
||||
index
|
||||
))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === '---') {
|
||||
blocks.push(createBlock('divider', '', null, `block-${blocks.length}`))
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(createBlock('divider', '', null, `block-${blocks.length}`), startLine, startLine))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
@@ -396,12 +451,18 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/)
|
||||
|
||||
if (headingMatch) {
|
||||
blocks.push(createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`))
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`),
|
||||
startLine,
|
||||
startLine
|
||||
))
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('> ')) {
|
||||
const startLine = index
|
||||
const quoteLines = []
|
||||
|
||||
while (index < lines.length && lines[index].trim().startsWith('>')) {
|
||||
@@ -409,11 +470,16 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('quote', quoteLines.join('\n').trim(), null, `block-${blocks.length}`))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('quote', quoteLines.join('\n').trim(), null, `block-${blocks.length}`),
|
||||
startLine,
|
||||
index - 1
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^- /.test(trimmedLine)) {
|
||||
const startLine = index
|
||||
const items = []
|
||||
|
||||
while (index < lines.length && /^- /.test(lines[index].trim())) {
|
||||
@@ -421,11 +487,12 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('list', items, null, `block-${blocks.length}`))
|
||||
blocks.push(attachSourceRange(createBlock('list', items, null, `block-${blocks.length}`), startLine, index - 1))
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^\d+\.\s+/.test(trimmedLine)) {
|
||||
const startLine = index
|
||||
const items = []
|
||||
|
||||
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
|
||||
@@ -433,10 +500,15 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('list', items, null, `block-${blocks.length}`, { ordered: true }))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('list', items, null, `block-${blocks.length}`, { ordered: true }),
|
||||
startLine,
|
||||
index - 1
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraphStartLine = index
|
||||
const paragraphLines = [cleanParagraphLine(line)]
|
||||
let shouldJoinNextLine = hasMarkdownHardBreak(line)
|
||||
index += 1
|
||||
@@ -454,7 +526,11 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
index += 1
|
||||
}
|
||||
|
||||
blocks.push(createBlock('paragraph', paragraphLines.join('\n'), null, `block-${blocks.length}`))
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('paragraph', paragraphLines.join('\n'), null, `block-${blocks.length}`),
|
||||
paragraphStartLine,
|
||||
index - 1
|
||||
))
|
||||
}
|
||||
|
||||
return blocks
|
||||
@@ -590,6 +666,51 @@ const onGalleryDragStart = (event, blockId, imageIndex) => {
|
||||
*/
|
||||
const onGalleryDragEnd = () => {
|
||||
galleryDragState.value = null
|
||||
galleryDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 갤러리 드래그 중 드롭 대상 셀 표시
|
||||
* @param {DragEvent} event - 드래그 이벤트
|
||||
* @param {Object} block - 갤러리 블록
|
||||
* @param {number} imageIndex - 호버 중인 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGalleryDragOverItem = (event, block, imageIndex) => {
|
||||
if (!props.interactive || !galleryDragState.value) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (galleryDragState.value.blockId !== block.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (galleryDragState.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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -624,6 +745,7 @@ const onGalleryDrop = (event, block, targetIndex) => {
|
||||
})
|
||||
|
||||
galleryDragState.value = null
|
||||
galleryDropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -758,16 +880,21 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
v-else-if="block.type === 'gallery'"
|
||||
class="content-markdown-renderer__gallery my-8 grid grid-cols-2 gap-2 md:grid-cols-3"
|
||||
@dragleave="onGalleryDragLeaveGallery($event, block)"
|
||||
>
|
||||
<figure
|
||||
v-for="(image, imageIndex) in block.images"
|
||||
:key="`${block.id}-${imageIndex}-${image.url}`"
|
||||
class="content-markdown-renderer__gallery-item min-w-0"
|
||||
:class="interactive ? 'content-markdown-renderer__gallery-item--interactive' : ''"
|
||||
class="content-markdown-renderer__gallery-item relative min-w-0"
|
||||
:class="{
|
||||
'content-markdown-renderer__gallery-item--interactive': interactive,
|
||||
'content-markdown-renderer__gallery-item--dragging': interactive && galleryDragState?.blockId === block.id && galleryDragState?.imageIndex === imageIndex,
|
||||
'content-markdown-renderer__gallery-item--drop-target': interactive && galleryDropTarget?.blockId === block.id && galleryDropTarget?.targetIndex === imageIndex
|
||||
}"
|
||||
:draggable="interactive"
|
||||
@dragstart="onGalleryDragStart($event, block.id, imageIndex)"
|
||||
@dragend="onGalleryDragEnd"
|
||||
@dragover.prevent
|
||||
@dragover="onGalleryDragOverItem($event, block, imageIndex)"
|
||||
@drop="onGalleryDrop($event, block, imageIndex)"
|
||||
>
|
||||
<button
|
||||
@@ -853,4 +980,31 @@ onBeforeUnmount(() => {
|
||||
.content-markdown-renderer__gallery-item--interactive:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.content-markdown-renderer__gallery-item--dragging {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.content-markdown-renderer__gallery-item--drop-target {
|
||||
outline: 3px solid #ff7a00;
|
||||
outline-offset: 3px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.content-markdown-renderer__gallery-item--drop-target::after {
|
||||
content: '여기로 이동';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 122, 0, 0.22);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user