블록 에디터 한글 입력과 코드 블록 보정
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user