본문 문단과 줄바꿈 처리 정리

This commit is contained in:
2026-05-14 16:33:30 +09:00
parent f5bfb560e2
commit 113c974ee5
9 changed files with 200 additions and 31 deletions

View File

@@ -24,6 +24,11 @@ const isUploading = ref(false)
const mediaPickerTarget = ref('image')
const selectedMediaUrls = ref([])
const selectedImageWidth = ref('regular')
const lastSelectionState = ref({
start: 0,
end: 0,
scrollTop: 0
})
const imageWidthOptions = [
{ value: 'regular', label: '기본' },
@@ -154,26 +159,79 @@ const refreshCaretLogicalLine = () => {
const pos = Math.min(textarea.selectionStart, value.length)
const lineIndex = value.slice(0, pos).split('\n').length - 1
lastSelectionState.value = {
start: Math.min(textarea.selectionStart, value.length),
end: Math.min(textarea.selectionEnd, value.length),
scrollTop: textarea.scrollTop
}
activeLogicalLineIndex.value = Math.max(0, lineIndex)
syncGutterScroll()
})
}
/**
* textarea 스크롤 시 거터 동기화한다.
* textarea 스크롤 시 선택 위치를 기억하고 거터 동기화한다.
* @returns {void}
*/
const onTextareaScroll = () => {
rememberTextareaSelection()
syncGutterScroll()
}
/**
* textarea의 선택 영역과 스크롤 위치를 기억한다.
* @returns {void}
*/
const rememberTextareaSelection = () => {
const textarea = textareaRef.value
const value = markdownValue.value ?? ''
if (!textarea) {
lastSelectionState.value = {
start: value.length,
end: value.length,
scrollTop: 0
}
return
}
lastSelectionState.value = {
start: Math.min(textarea.selectionStart, value.length),
end: Math.min(textarea.selectionEnd, value.length),
scrollTop: textarea.scrollTop
}
}
/**
* 기억한 선택 영역과 스크롤 위치로 작성 textarea 포커스를 복원한다.
* @returns {void}
*/
const restoreTextareaFocus = () => {
nextTick(() => {
const textarea = textareaRef.value
const value = markdownValue.value ?? ''
if (!textarea) {
return
}
const start = Math.min(lastSelectionState.value.start, value.length)
const end = Math.min(lastSelectionState.value.end, value.length)
textarea.focus()
textarea.setSelectionRange(start, end)
textarea.scrollTop = lastSelectionState.value.scrollTop
refreshCaretLogicalLine()
})
}
watch(() => props.modelValue, () => {
refreshCaretLogicalLine()
})
watch(activeMode, (mode) => {
if (mode === 'write') {
refreshCaretLogicalLine()
restoreTextareaFocus()
return
}
@@ -187,9 +245,30 @@ watch(activeMode, (mode) => {
* @returns {void}
*/
const toggleEditorMode = () => {
if (activeMode.value === 'write') {
rememberTextareaSelection()
}
activeMode.value = activeMode.value === 'write' ? 'preview' : 'write'
}
/**
* 작성/미리보기 모드를 지정한다.
* @param {'write'|'preview'} mode - 전환할 모드
* @returns {void}
*/
const setEditorMode = (mode) => {
if (activeMode.value === mode) {
return
}
if (activeMode.value === 'write') {
rememberTextareaSelection()
}
activeMode.value = mode
}
onMounted(() => {
/**
* document selectionchange에서 작성 textarea가 포커스일 때만 활성 줄을 갱신한다.
@@ -311,6 +390,21 @@ const replaceSelection = (replacement, cursorOffset = replacement.length, select
setTextareaSelection(nextStart, nextStart + (selectionLength ?? 0))
}
/**
* Enter 입력을 문단 분리 규칙에 맞게 처리한다.
* @param {KeyboardEvent} event - 키보드 이벤트
* @returns {boolean} 직접 처리했는지 여부
*/
const handleParagraphEnter = (event) => {
if (event.key !== 'Enter' || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey || event.isComposing) {
return false
}
event.preventDefault()
replaceSelection('\n\n')
return true
}
/**
* 블록형 마크다운 조각을 커서 위치에 삽입한다.
* @param {string} snippet - 삽입할 마크다운
@@ -882,6 +976,10 @@ const handleDrop = async (event) => {
* @returns {void}
*/
const handleKeydown = (event) => {
if (handleParagraphEnter(event)) {
return
}
if (!(event.metaKey || event.ctrlKey)) {
return
}
@@ -956,7 +1054,7 @@ const handleKeydown = (event) => {
class="admin-markdown-editor__mode-button rounded px-3 py-1.5 text-sm font-semibold"
:class="activeMode === 'write' ? 'bg-white text-black shadow-sm' : 'text-[#6b7280]'"
type="button"
@click="activeMode = 'write'"
@click="setEditorMode('write')"
>
작성
</button>
@@ -964,7 +1062,7 @@ const handleKeydown = (event) => {
class="admin-markdown-editor__mode-button rounded px-3 py-1.5 text-sm font-semibold"
:class="activeMode === 'preview' ? 'bg-white text-black shadow-sm' : 'text-[#6b7280]'"
type="button"
@click="activeMode = 'preview'"
@click="setEditorMode('preview')"
>
미리보기
</button>