v1.2.3: 마크다운 에디터 외부 스크롤 및 줄 번호 정렬
textarea 내부 스크롤을 없애고 본문 높이를 자동으로 늘려, 글 편집 영역 스크롤과 줄 번호가 어긋나지 않도록 했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -42,10 +42,12 @@ const mediaSearchQuery = ref('')
|
||||
const selectedMediaUrls = ref([])
|
||||
const lastSelectionState = ref({
|
||||
start: 0,
|
||||
end: 0,
|
||||
scrollTop: 0
|
||||
end: 0
|
||||
})
|
||||
|
||||
/** 작성 textarea 최소 높이(px) */
|
||||
const MIN_TEXTAREA_HEIGHT_PX = 620
|
||||
|
||||
const markdownValue = computed({
|
||||
get: () => normalizeMarkdownContent(props.modelValue),
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
@@ -120,16 +122,20 @@ const gutterLineCount = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* textarea와 줄 번호 거터의 세로 스크롤을 맞춘다.
|
||||
* textarea 높이를 본문 길이에 맞춘다. 내부 스크롤 없이 부모(`editor-scroll`)만 스크롤한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const syncGutterScroll = () => {
|
||||
const gutter = gutterRef.value
|
||||
const textarea = textareaRef.value
|
||||
const syncTextareaHeight = () => {
|
||||
nextTick(() => {
|
||||
const textarea = textareaRef.value
|
||||
|
||||
if (gutter && textarea) {
|
||||
gutter.scrollTop = textarea.scrollTop
|
||||
}
|
||||
if (!textarea) {
|
||||
return
|
||||
}
|
||||
|
||||
textarea.style.height = '0px'
|
||||
textarea.style.height = `${Math.max(MIN_TEXTAREA_HEIGHT_PX, textarea.scrollHeight)}px`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,24 +209,14 @@ const refreshCaretLogicalLine = () => {
|
||||
|
||||
lastSelectionState.value = {
|
||||
start: Math.min(textarea.selectionStart, value.length),
|
||||
end: Math.min(textarea.selectionEnd, value.length),
|
||||
scrollTop: textarea.scrollTop
|
||||
end: Math.min(textarea.selectionEnd, value.length)
|
||||
}
|
||||
activeLogicalLineIndex.value = Math.max(0, lineIndex)
|
||||
syncGutterScroll()
|
||||
syncTextareaHeight()
|
||||
syncBlockPanelState()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* textarea 스크롤 시 선택 위치를 기억하고 거터 스크롤을 맞춘다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onTextareaScroll = () => {
|
||||
rememberTextareaSelection()
|
||||
syncGutterScroll()
|
||||
}
|
||||
|
||||
/**
|
||||
* textarea의 선택 영역과 스크롤 위치를 기억한다.
|
||||
* @returns {void}
|
||||
@@ -232,16 +228,14 @@ const rememberTextareaSelection = () => {
|
||||
if (!textarea) {
|
||||
lastSelectionState.value = {
|
||||
start: value.length,
|
||||
end: value.length,
|
||||
scrollTop: 0
|
||||
end: value.length
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lastSelectionState.value = {
|
||||
start: Math.min(textarea.selectionStart, value.length),
|
||||
end: Math.min(textarea.selectionEnd, value.length),
|
||||
scrollTop: textarea.scrollTop
|
||||
end: Math.min(textarea.selectionEnd, value.length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +257,8 @@ const restoreTextareaFocus = () => {
|
||||
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(start, end)
|
||||
textarea.scrollTop = lastSelectionState.value.scrollTop
|
||||
syncTextareaHeight()
|
||||
textarea.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
||||
refreshCaretLogicalLine()
|
||||
})
|
||||
}
|
||||
@@ -1278,37 +1273,38 @@ const handleKeydown = (event) => {
|
||||
|
||||
<div v-if="activeMode === 'write'" class="admin-markdown-editor__write relative">
|
||||
<div class="admin-markdown-editor__editor-surface min-h-[620px]">
|
||||
<div
|
||||
ref="gutterRef"
|
||||
class="admin-markdown-editor__gutter absolute bottom-0 left-[-40px] top-0 w-10 select-none overflow-y-hidden overflow-x-hidden py-5 font-mono text-[13px] leading-7 text-[#a0a8b0]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="admin-markdown-editor__write-columns flex items-start">
|
||||
<div
|
||||
v-for="ln in gutterLineCount"
|
||||
:key="`gutter-line-${ln}`"
|
||||
class="admin-markdown-editor__gutter-line min-h-[28px] pr-2 text-right tabular-nums"
|
||||
ref="gutterRef"
|
||||
class="admin-markdown-editor__gutter w-10 shrink-0 select-none py-5 pr-2 font-mono text-[13px] leading-7 text-[#a0a8b0]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ ln }}
|
||||
<div
|
||||
v-for="ln in gutterLineCount"
|
||||
:key="`gutter-line-${ln}`"
|
||||
class="admin-markdown-editor__gutter-line min-h-[28px] text-right tabular-nums"
|
||||
>
|
||||
{{ ln }}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="markdownValue"
|
||||
class="admin-markdown-editor__textarea min-h-[620px] min-w-0 flex-1 resize-none overflow-hidden break-words border-0 bg-transparent py-5 pl-0 pr-5 text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:ring-0"
|
||||
placeholder="마크다운으로 글을 작성하세요."
|
||||
spellcheck="false"
|
||||
@keydown="handleKeydown"
|
||||
@paste="handlePaste"
|
||||
@drop="handleDrop"
|
||||
@dragover.prevent
|
||||
@input="refreshCaretLogicalLine"
|
||||
@click="refreshCaretLogicalLine"
|
||||
@keyup="refreshCaretLogicalLine"
|
||||
@select="refreshCaretLogicalLine"
|
||||
@focus="onTextareaFocus"
|
||||
@blur="onTextareaBlur"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="markdownValue"
|
||||
class="admin-markdown-editor__textarea min-h-[620px] w-full resize-y border-0 bg-transparent py-5 pl-0 pr-5 text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:ring-0"
|
||||
placeholder="마크다운으로 글을 작성하세요."
|
||||
spellcheck="false"
|
||||
@keydown="handleKeydown"
|
||||
@paste="handlePaste"
|
||||
@drop="handleDrop"
|
||||
@dragover.prevent
|
||||
@scroll="onTextareaScroll"
|
||||
@input="refreshCaretLogicalLine"
|
||||
@click="refreshCaretLogicalLine"
|
||||
@keyup="refreshCaretLogicalLine"
|
||||
@select="refreshCaretLogicalLine"
|
||||
@focus="onTextareaFocus"
|
||||
@blur="onTextareaBlur"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isUploading" class="admin-markdown-editor__uploading absolute right-3 top-3 rounded bg-[#15171a] px-3 py-1.5 text-xs font-semibold text-white">
|
||||
업로드 중
|
||||
|
||||
Reference in New Issue
Block a user