블록 에디터 한글 입력과 코드 블록 보정

This commit is contained in:
2026-05-07 11:06:36 +09:00
parent 60c5c3d5c9
commit 398877fd92
8 changed files with 171 additions and 23 deletions

View File

@@ -11,6 +11,8 @@ const emit = defineEmits(['update:modelValue'])
const editorBlocks = ref([])
const blockRefs = ref([])
const activeBlockId = ref('')
const selectedBlockId = ref('')
const draggingBlockId = ref('')
const slashQuery = ref('')
const slashMenuDirection = ref('down')
const highlightedCommandIndex = ref(0)
@@ -21,9 +23,7 @@ const mediaPickerTarget = ref(null)
const isMediaPickerOpen = ref(false)
const isLoadingMedia = ref(false)
const isComposingText = ref(false)
const isCompositionEnterGuardActive = ref(false)
const isNormalizingTrailingBlock = ref(false)
let compositionEnterGuardTimer = null
let blockIdSeed = 0
const imageWidthOptions = [
@@ -594,9 +594,7 @@ const updateBlockText = (event, index) => {
* @returns {void}
*/
const startTextComposition = () => {
window.clearTimeout(compositionEnterGuardTimer)
isComposingText.value = true
isCompositionEnterGuardActive.value = true
}
/**
@@ -607,11 +605,12 @@ const startTextComposition = () => {
*/
const finishTextComposition = (event, index) => {
isComposingText.value = false
updateBlockText(event, index)
window.clearTimeout(compositionEnterGuardTimer)
compositionEnterGuardTimer = window.setTimeout(() => {
isCompositionEnterGuardActive.value = false
}, 120)
nextTick(() => {
window.setTimeout(() => {
updateBlockText(event, index)
}, 0)
})
}
/**
@@ -627,10 +626,13 @@ const applyMarkdownShortcut = (block, index) => {
{ marker: '### ', type: 'heading', level: 3 },
{ marker: '> ', type: 'quote' },
{ marker: '- ', type: 'list' },
{ marker: '```', type: 'code', exact: true },
{ marker: '``` ', type: 'code' }
].sort((a, b) => b.marker.length - a.marker.length)
const shortcut = shortcutMap.find((item) => block.text.startsWith(item.marker))
const shortcut = shortcutMap.find((item) => item.exact
? block.text === item.marker
: block.text.startsWith(item.marker))
if (!shortcut) {
return
@@ -969,7 +971,7 @@ const highlightPreviousCommand = (event) => {
const handleEnter = (event, index) => {
const currentBlock = editorBlocks.value[index]
if (isComposingText.value || isCompositionEnterGuardActive.value || event.isComposing || event.keyCode === 229) {
if (isComposingText.value || event.isComposing || event.keyCode === 229) {
event.preventDefault()
return
}
@@ -1034,6 +1036,106 @@ const handleBackspace = (event, index) => {
focusBlock(Math.max(index - 1, 0))
}
/**
* 블록 인덱스 반환
* @param {string} blockId - 블록 ID
* @returns {number} 블록 인덱스
*/
const getBlockIndex = (blockId) => editorBlocks.value.findIndex((block) => block.id === blockId)
/**
* 블록 선택 상태 적용
* @param {Object} block - 선택할 블록
* @returns {void}
*/
const selectBlock = (block) => {
selectedBlockId.value = block.id
activeBlockId.value = block.id
slashQuery.value = ''
}
/**
* 지정 블록 삭제
* @param {number} index - 삭제할 블록 인덱스
* @returns {void}
*/
const deleteBlock = (index) => {
if (index < 0) {
return
}
if (editorBlocks.value.length <= 1) {
editorBlocks.value.splice(0, 1, createEditorBlock())
selectedBlockId.value = ''
activeBlockId.value = editorBlocks.value[0].id
emitContent()
focusBlock(0)
return
}
editorBlocks.value.splice(index, 1)
selectedBlockId.value = ''
normalizeTrailingTextBlock()
emitContent()
focusBlock(Math.min(index, editorBlocks.value.length - 1))
}
/**
* 선택한 블록 삭제 키 처리
* @param {KeyboardEvent} event - 키보드 이벤트
* @param {number} index - 삭제할 블록 인덱스
* @returns {void}
*/
const deleteSelectedBlock = (event, index) => {
event.preventDefault()
deleteBlock(index)
}
/**
* 블록 드래그 시작 처리
* @param {DragEvent} event - 드래그 이벤트
* @param {Object} block - 드래그할 블록
* @returns {void}
*/
const startBlockDrag = (event, block) => {
draggingBlockId.value = block.id
selectedBlockId.value = block.id
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', block.id)
}
/**
* 블록 드롭 이동 처리
* @param {DragEvent} event - 드롭 이벤트
* @param {number} targetIndex - 이동 대상 인덱스
* @returns {void}
*/
const dropBlock = (event, targetIndex) => {
const draggedId = event.dataTransfer.getData('text/plain') || draggingBlockId.value
const sourceIndex = getBlockIndex(draggedId)
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) {
draggingBlockId.value = ''
return
}
const [draggedBlock] = editorBlocks.value.splice(sourceIndex, 1)
const nextTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
editorBlocks.value.splice(nextTargetIndex, 0, draggedBlock)
draggingBlockId.value = ''
selectedBlockId.value = draggedBlock.id
normalizeTrailingTextBlock()
emitContent()
}
/**
* 블록 드래그 종료 처리
* @returns {void}
*/
const finishBlockDrag = () => {
draggingBlockId.value = ''
}
/**
* 현재 블록 활성화
* @param {Object} block - 에디터 블록
@@ -1042,6 +1144,7 @@ const handleBackspace = (event, index) => {
const activateBlock = (block) => {
const index = editorBlocks.value.findIndex((item) => item.id === block.id)
activeBlockId.value = block.id
selectedBlockId.value = ''
updateSlashQuery(block)
updateSlashMenuDirection(index)
}
@@ -1092,10 +1195,6 @@ watch(editorBlocks, () => {
defineExpose({
focusFirstBlock: () => focusBlock(0)
})
onBeforeUnmount(() => {
window.clearTimeout(compositionEnterGuardTimer)
})
</script>
<template>
@@ -1104,9 +1203,29 @@ onBeforeUnmount(() => {
<div
v-for="(block, index) in editorBlocks"
:key="block.id"
class="admin-block-editor__row relative"
class="admin-block-editor__row group/block relative rounded transition-colors"
:class="{
'admin-block-editor__row--selected bg-[#eff1f2] ring-1 ring-[#d8dde1]': selectedBlockId === block.id,
'admin-block-editor__row--dragging opacity-50': draggingBlockId === block.id
}"
:data-editor-block-id="block.id"
@dragover.prevent
@drop="dropBlock($event, index)"
>
<button
class="admin-block-editor__handle absolute -left-10 top-1 z-10 grid size-7 cursor-grab place-items-center rounded text-sm font-semibold text-[#8e9cac] opacity-0 transition-colors hover:bg-[#eff1f2] hover:text-[#394047] group-hover/block:opacity-100 focus:opacity-100 active:cursor-grabbing"
type="button"
draggable="true"
aria-label="블록 이동 선택"
@click.stop="selectBlock(block)"
@keydown.delete="deleteSelectedBlock($event, index)"
@keydown.backspace="deleteSelectedBlock($event, index)"
@dragstart="startBlockDrag($event, block)"
@dragend="finishBlockDrag"
>
<span aria-hidden="true"></span>
</button>
<hr v-if="block.type === 'divider'" class="admin-block-editor__divider my-6 border-line">
<figure
@@ -1340,4 +1459,10 @@ onBeforeUnmount(() => {
.admin-block-editor__block {
color: #1f2328;
}
.admin-block-editor__code {
background: #15171a;
color: #f8fafc;
caret-color: #f8fafc;
}
</style>