글쓰기 스크롤과 블록 드롭 피드백 보정

This commit is contained in:
2026-05-07 15:22:50 +09:00
parent 38f8abb1ff
commit 4e5ccb2726
8 changed files with 116 additions and 12 deletions

View File

@@ -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)"
>
<button
@@ -1574,6 +1622,24 @@ defineExpose({
transform 160ms ease;
}
.admin-block-editor__row::after {
position: absolute;
left: 0;
right: 0;
z-index: 20;
height: 3px;
border-radius: 999px;
background: #2eb6ea;
box-shadow: 0 0 0 1px rgba(46, 182, 234, 0.18);
content: "";
opacity: 0;
pointer-events: none;
transform: scaleX(0.98);
transition:
opacity 120ms ease,
transform 120ms ease;
}
.admin-block-editor__row:hover::before,
.admin-block-editor__row--selected::before {
background: #eff1f2;
@@ -1581,6 +1647,18 @@ defineExpose({
transform: scaleX(1);
}
.admin-block-editor__row--drop-before::after {
top: -18px;
opacity: 1;
transform: scaleX(1);
}
.admin-block-editor__row--drop-after::after {
bottom: -18px;
opacity: 1;
transform: scaleX(1);
}
.admin-block-editor__handle {
min-height: 32px;
}