블록 에디터 줄바꿈과 핸들 표시 보정

This commit is contained in:
2026-05-07 15:14:32 +09:00
parent 5bda4d5472
commit 38f8abb1ff
7 changed files with 140 additions and 21 deletions

View File

@@ -439,9 +439,9 @@ const setBlockRef = (element, index) => {
if (
!isComposingText.value
&& isTextBlock(editorBlocks.value[index])
&& element.innerText !== editorBlocks.value[index]?.text
&& element.textContent !== editorBlocks.value[index]?.text
) {
element.innerText = editorBlocks.value[index]?.text || ''
element.textContent = editorBlocks.value[index]?.text || ''
}
}
}
@@ -516,7 +516,49 @@ const updateSlashMenuDirection = (index) => {
const getTextBlockDomText = (index) => {
const element = blockRefs.value[index]
return element?.innerText.replace(/\n$/, '') || ''
return element?.textContent || ''
}
/**
* 현재 선택 영역에 문단 내부 줄바꿈을 삽입
* @param {number} index - 블록 인덱스
* @returns {void}
*/
const insertSoftLineBreak = (index) => {
const block = editorBlocks.value[index]
const element = blockRefs.value[index]
if (!block || !element) {
return
}
element.focus()
const selection = window.getSelection()
if (!selection) {
return
}
const range = selection?.rangeCount ? selection.getRangeAt(0) : document.createRange()
if (!selection.rangeCount || !element.contains(range.commonAncestorContainer)) {
range.selectNodeContents(element)
range.collapse(false)
}
range.deleteContents()
const lineBreak = document.createTextNode('\n')
range.insertNode(lineBreak)
range.setStartAfter(lineBreak)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
block.text = getTextBlockDomText(index)
activeBlockId.value = block.id
slashQuery.value = ''
updateSlashMenuDirection(index)
emitContent()
}
/**
@@ -685,7 +727,7 @@ const applyMarkdownShortcut = (block, index) => {
const element = blockRefs.value[index]
if (element) {
element.innerText = block.text
element.textContent = block.text
focusBlock(index)
}
})
@@ -750,7 +792,7 @@ const applyCommand = (command) => {
const element = blockRefs.value[index]
if (element) {
element.innerText = ''
element.textContent = ''
}
slashQuery.value = ''
@@ -1018,7 +1060,13 @@ const handleEnter = (event, index) => {
return
}
if (event.shiftKey && isTextBlock(currentBlock)) {
if (currentBlock.type === 'code') {
return
}
if (event.shiftKey && isTextBlock(currentBlock) && currentBlock.type !== 'code') {
event.preventDefault()
insertSoftLineBreak(index)
return
}
@@ -1033,10 +1081,6 @@ const handleEnter = (event, index) => {
return
}
if (currentBlock.type === 'code' && !event.shiftKey) {
return
}
event.preventDefault()
if (['divider', 'image', 'gallery', 'toggle', 'embed'].includes(currentBlock.type)) {
@@ -1254,9 +1298,9 @@ defineExpose({
<div
v-for="(block, index) in editorBlocks"
:key="block.id"
class="admin-block-editor__row group/block relative rounded transition-colors"
class="admin-block-editor__row group/block relative isolate rounded"
:class="{
'admin-block-editor__row--selected bg-[#eff1f2] ring-1 ring-[#d8dde1]': selectedBlockId === block.id,
'admin-block-editor__row--selected': selectedBlockId === block.id,
'admin-block-editor__row--dragging opacity-50': draggingBlockId === block.id,
'admin-block-editor__row--text': isTextBlock(block),
'admin-block-editor__row--structure': !isTextBlock(block)
@@ -1266,7 +1310,7 @@ defineExpose({
@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"
class="admin-block-editor__handle absolute -left-9 bottom-0 top-0 z-10 flex w-5 cursor-grab items-stretch justify-center rounded opacity-0 outline-none transition-opacity duration-150 group-hover/block:opacity-100 focus:opacity-100 active:cursor-grabbing"
type="button"
draggable="true"
aria-label="블록 이동 선택"
@@ -1276,7 +1320,9 @@ defineExpose({
@dragstart="startBlockDrag($event, block)"
@dragend="finishBlockDrag"
>
<span aria-hidden="true"></span>
<span class="admin-block-editor__handle-container" aria-hidden="true">
<span class="admin-block-editor__handle-grabber"></span>
</span>
</button>
<hr v-if="block.type === 'divider'" class="admin-block-editor__divider border-line">
@@ -1513,6 +1559,62 @@ defineExpose({
color: #1f2328;
}
.admin-block-editor__row::before {
position: absolute;
inset: -4px -10px;
z-index: -1;
border-radius: 8px;
background: transparent;
content: "";
opacity: 0;
transform: scaleX(0.995);
transition:
background-color 160ms ease,
opacity 160ms ease,
transform 160ms ease;
}
.admin-block-editor__row:hover::before,
.admin-block-editor__row--selected::before {
background: #eff1f2;
opacity: 1;
transform: scaleX(1);
}
.admin-block-editor__handle {
min-height: 32px;
}
.admin-block-editor__handle-container {
display: flex;
width: 16px;
height: 100%;
align-items: center;
justify-content: center;
padding: 6px 0;
}
.admin-block-editor__handle-grabber {
display: block;
width: 4px;
height: 20px;
border-radius: 999px;
background: #8d969f;
opacity: 0.72;
transition:
height 160ms ease,
background-color 160ms ease,
opacity 160ms ease;
}
.admin-block-editor__row:hover .admin-block-editor__handle-grabber,
.admin-block-editor__row--selected .admin-block-editor__handle-grabber,
.admin-block-editor__handle:focus .admin-block-editor__handle-grabber {
height: 100%;
background: #5d6670;
opacity: 1;
}
.admin-block-editor__row + .admin-block-editor__row--text {
margin-top: 32px;
}