From 398877fd92b6fb45d568c9f2ad42b765d3c074da Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 7 May 2026 11:06:36 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=EC=97=90=EB=94=94?= =?UTF-8?q?=ED=84=B0=20=ED=95=9C=EA=B8=80=20=EC=9E=85=EB=A0=A5=EA=B3=BC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B8=94=EB=A1=9D=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 | 157 +++++++++++++++++++++++--- docs/history.md | 8 ++ docs/map.md | 2 +- docs/spec.md | 8 +- docs/todo.md | 2 + docs/update.md | 11 ++ package-lock.json | 4 +- package.json | 2 +- 8 files changed, 171 insertions(+), 23 deletions(-) diff --git a/components/admin/AdminBlockEditor.vue b/components/admin/AdminBlockEditor.vue index 50527c8..fc08249 100644 --- a/components/admin/AdminBlockEditor.vue +++ b/components/admin/AdminBlockEditor.vue @@ -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) -})