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">
|
||||
업로드 중
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.2.3
|
||||
|
||||
- 관리자 마크다운 에디터: textarea 내부 스크롤·`resize-y` 제거, 본문 높이 자동 확장·`admin-post-form__editor-scroll` 외부 스크롤만 사용, 줄 번호 거터 flex 정렬.
|
||||
- 패키지 버전 `1.2.3`으로 갱신.
|
||||
|
||||
## v1.2.2
|
||||
|
||||
- 이미지 `useAlt` 판별: 대괄호 내용이 URL 파일명과 일치할 때만 파일명 대체 텍스트 모드로 처리(임의 문자열·레거시 표시문구와 분리).
|
||||
|
||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
@@ -615,7 +615,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"license": "MIT",
|
||||
@@ -5470,7 +5470,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
@@ -5905,7 +5905,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"license": "Apache-2.0"
|
||||
@@ -6340,7 +6340,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-string-truncated-width": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz",
|
||||
"integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==",
|
||||
"license": "MIT"
|
||||
@@ -6847,7 +6847,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-shutdown": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz",
|
||||
"integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==",
|
||||
"license": "MIT",
|
||||
@@ -6885,7 +6885,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
@@ -6983,7 +6983,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/iron-webcrypto": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
|
||||
"license": "MIT",
|
||||
@@ -7156,7 +7156,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||
"license": "MIT",
|
||||
@@ -7165,7 +7165,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||
"license": "MIT",
|
||||
@@ -9885,7 +9885,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
@@ -10459,7 +10459,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
@@ -10693,7 +10693,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
Reference in New Issue
Block a user