From a867269d9bb85c3634401eb475cae9c1c81689de Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 15 May 2026 18:34:38 +0900 Subject: [PATCH] =?UTF-8?q?v1.2.3:=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=99=B8=EB=B6=80=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EB=B0=8F=20=EC=A4=84=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit textarea 내부 스크롤을 없애고 본문 높이를 자동으로 늘려, 글 편집 영역 스크롤과 줄 번호가 어긋나지 않도록 했다. Co-authored-by: Cursor --- components/admin/AdminMarkdownEditor.vue | 102 +++++++++++------------ docs/update.md | 5 ++ package-lock.json | 28 +++---- package.json | 2 +- 4 files changed, 69 insertions(+), 68 deletions(-) diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 8301429..854ca40 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -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) => {
-