From 4e5ccb27262c2a05c53315e8cba6f25385f10d82 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 7 May 2026 15:22:50 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=80=EC=93=B0=EA=B8=B0=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EA=B3=BC=20=EB=B8=94=EB=A1=9D=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminBlockEditor.vue | 84 ++++++++++++++++++++++++++- docs/history.md | 8 +++ docs/map.md | 4 +- docs/spec.md | 5 +- docs/update.md | 9 +++ layouts/admin.vue | 12 +++- package-lock.json | 4 +- package.json | 2 +- 8 files changed, 116 insertions(+), 12 deletions(-) diff --git a/components/admin/AdminBlockEditor.vue b/components/admin/AdminBlockEditor.vue index 50504db..90dc844 100644 --- a/components/admin/AdminBlockEditor.vue +++ b/components/admin/AdminBlockEditor.vue @@ -13,6 +13,8 @@ const blockRefs = ref([]) const activeBlockId = ref('') const selectedBlockId = ref('') const draggingBlockId = ref('') +const dragTargetIndex = ref(-1) +const dragTargetPosition = ref('') const slashQuery = ref('') const slashMenuDirection = ref('down') const highlightedCommandIndex = ref(0) @@ -24,6 +26,7 @@ const isMediaPickerOpen = ref(false) const isLoadingMedia = ref(false) const isComposingText = ref(false) const isNormalizingTrailingBlock = ref(false) +const pendingSoftLineBreakIndex = ref(-1) let blockIdSeed = 0 const imageWidthOptions = [ @@ -684,10 +687,19 @@ const finishTextComposition = (event, index) => { const block = syncTextBlockFromDom(index) if (!block) { + pendingSoftLineBreakIndex.value = -1 return } applyMarkdownShortcut(block, index) + + if (pendingSoftLineBreakIndex.value === index && isTextBlock(block) && block.type !== 'code') { + pendingSoftLineBreakIndex.value = -1 + insertSoftLineBreak(index) + return + } + + pendingSoftLineBreakIndex.value = -1 emitContent() }, 0) }) @@ -1057,6 +1069,11 @@ const handleEnter = (event, index) => { if (isComposingText.value || event.isComposing || event.keyCode === 229) { event.preventDefault() + + if (event.shiftKey && isTextBlock(currentBlock) && currentBlock.type !== 'code') { + pendingSoftLineBreakIndex.value = index + } + return } @@ -1195,10 +1212,30 @@ const deleteSelectedBlock = (event, index) => { const startBlockDrag = (event, block) => { draggingBlockId.value = block.id selectedBlockId.value = block.id + dragTargetIndex.value = -1 + dragTargetPosition.value = '' event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', block.id) } +/** + * 블록 드래그 중 드롭 위치 표시 갱신 + * @param {DragEvent} event - 드래그 이벤트 + * @param {number} targetIndex - 드래그 중인 대상 인덱스 + * @returns {void} + */ +const updateBlockDropTarget = (event, targetIndex) => { + if (!draggingBlockId.value) { + return + } + + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + const rect = event.currentTarget.getBoundingClientRect() + dragTargetIndex.value = targetIndex + dragTargetPosition.value = event.clientY < rect.top + rect.height / 2 ? 'before' : 'after' +} + /** * 블록 드롭 이동 처리 * @param {DragEvent} event - 드롭 이벤트 @@ -1206,18 +1243,25 @@ const startBlockDrag = (event, block) => { * @returns {void} */ const dropBlock = (event, targetIndex) => { + event.preventDefault() const draggedId = event.dataTransfer.getData('text/plain') || draggingBlockId.value const sourceIndex = getBlockIndex(draggedId) + const targetPosition = dragTargetIndex.value === targetIndex ? dragTargetPosition.value : 'after' + const insertionIndex = targetPosition === 'after' ? targetIndex + 1 : targetIndex - if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) { + if (sourceIndex < 0 || targetIndex < 0) { draggingBlockId.value = '' + dragTargetIndex.value = -1 + dragTargetPosition.value = '' return } const [draggedBlock] = editorBlocks.value.splice(sourceIndex, 1) - const nextTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex + const nextTargetIndex = sourceIndex < insertionIndex ? insertionIndex - 1 : insertionIndex editorBlocks.value.splice(nextTargetIndex, 0, draggedBlock) draggingBlockId.value = '' + dragTargetIndex.value = -1 + dragTargetPosition.value = '' selectedBlockId.value = draggedBlock.id normalizeTrailingTextBlock() emitContent() @@ -1229,6 +1273,8 @@ const dropBlock = (event, targetIndex) => { */ const finishBlockDrag = () => { draggingBlockId.value = '' + dragTargetIndex.value = -1 + dragTargetPosition.value = '' } /** @@ -1302,11 +1348,13 @@ defineExpose({ :class="{ 'admin-block-editor__row--selected': selectedBlockId === block.id, 'admin-block-editor__row--dragging opacity-50': draggingBlockId === block.id, + 'admin-block-editor__row--drop-before': dragTargetIndex === index && dragTargetPosition === 'before', + 'admin-block-editor__row--drop-after': dragTargetIndex === index && dragTargetPosition === 'after', 'admin-block-editor__row--text': isTextBlock(block), 'admin-block-editor__row--structure': !isTextBlock(block) }" :data-editor-block-id="block.id" - @dragover.prevent + @dragover="updateBlockDropTarget($event, index)" @drop="dropBlock($event, index)" >