diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 7a14d16..7eacc05 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -57,6 +57,7 @@ import { DEFAULT_SLASH_MENU_HEIGHT, DEFAULT_SLASH_MENU_WIDTH } from '../../lib/markdown-slash-menu-position.js' +import { calculateSlashMenuScrollTop } from '../../lib/markdown-slash-menu-scroll.js' import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js' import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js' import { buildToggleOpenerLine } from '../../lib/markdown-toggle.js' @@ -2497,16 +2498,12 @@ const scrollLiveSlashHighlightedIntoView = () => { return } - const itemTop = highlightedItem.offsetTop - const itemBottom = itemTop + highlightedItem.offsetHeight - const viewTop = menu.scrollTop - const viewBottom = viewTop + menu.clientHeight - - if (itemTop < viewTop) { - menu.scrollTop = itemTop - } else if (itemBottom > viewBottom) { - menu.scrollTop = itemBottom - menu.clientHeight - } + menu.scrollTop = calculateSlashMenuScrollTop({ + itemTop: highlightedItem.offsetTop, + itemHeight: highlightedItem.offsetHeight, + viewTop: menu.scrollTop, + viewHeight: menu.clientHeight + }) }) } diff --git a/docs/map.md b/docs/map.md index 85664de..39cba3d 100644 --- a/docs/map.md +++ b/docs/map.md @@ -62,6 +62,7 @@ | lib/markdown-toc.js | 공개 게시글 TOC용 H1~H3 제목 추출과 앵커 ID 생성 | | lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의, 미디어 명령 action 판별, 미디어 명령 지연 commit 무시·슬래시 원문 줄 교체 삽입 helper | | lib/markdown-slash-menu-position.js | 관리자 Markdown-first 에디터 슬래시 메뉴 뷰포트 위치 보정 helper | +| lib/markdown-slash-menu-scroll.js | 관리자 Markdown-first 에디터 슬래시 메뉴 선택 항목 표시용 scrollTop 계산 helper | | lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `
` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 | | lib/post-tag-limit.js | 게시물별 태그 최대 개수 기본값·허용 범위·정규화 | | lib/analytics-shared.js | 통계 추적 경로 필터·체류/스크롤 상수(클라이언트·서버 공용) | @@ -123,6 +124,7 @@ | tests/markdown-editor-history.test.js | 관리자 Markdown 에디터 undo/redo 히스토리 항목과 선택 범위 보정 단위 회귀 테스트 | | tests/markdown-editor-media.test.js | 관리자 Markdown 에디터 미디어 선택 helper의 종류·선택 가능 여부·미리보기·선택 상태·블록 생성 단위 회귀 테스트 | | tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 | +| tests/markdown-slash-menu-scroll.test.js | 관리자 슬래시 메뉴 선택 항목 scrollTop 보정 단위 테스트 | | tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 | | tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 | | tests/markdown-quote.test.js | Markdown 인용 옵션 파싱과 편집 줄 변환 단위 회귀 테스트 | diff --git a/docs/update.md b/docs/update.md index 78f7bf9..ad4897a 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.85 + +- 관리자 Markdown 에디터의 슬래시 메뉴 선택 항목 스크롤 계산을 `markdown-slash-menu-scroll` helper로 분리. +- 슬래시 메뉴 선택 항목의 위·아래 스크롤 보정 단위 테스트 추가. + ## v1.11.84 - 관리자 Markdown 에디터의 슬래시 메뉴 재배치 이벤트 리스너를 안정적인 참조로 정리. diff --git a/lib/markdown-slash-menu-scroll.js b/lib/markdown-slash-menu-scroll.js new file mode 100644 index 0000000..3e13ed3 --- /dev/null +++ b/lib/markdown-slash-menu-scroll.js @@ -0,0 +1,32 @@ +/** + * 슬래시 메뉴 선택 항목이 보이도록 필요한 scrollTop을 계산한다. + * @param {Object} options - 계산 옵션 + * @param {number} options.itemTop - 항목의 메뉴 내부 위쪽 위치 + * @param {number} options.itemHeight - 항목 높이 + * @param {number} options.viewTop - 현재 메뉴 scrollTop + * @param {number} options.viewHeight - 메뉴 표시 높이 + * @returns {number} 적용할 scrollTop + */ +export const calculateSlashMenuScrollTop = ({ + itemTop, + itemHeight, + viewTop, + viewHeight +}) => { + const safeItemTop = Number.isFinite(itemTop) ? itemTop : 0 + const safeItemHeight = Number.isFinite(itemHeight) ? Math.max(0, itemHeight) : 0 + const safeViewTop = Number.isFinite(viewTop) ? Math.max(0, viewTop) : 0 + const safeViewHeight = Number.isFinite(viewHeight) ? Math.max(0, viewHeight) : 0 + const itemBottom = safeItemTop + safeItemHeight + const viewBottom = safeViewTop + safeViewHeight + + if (safeItemTop < safeViewTop) { + return Math.max(0, safeItemTop) + } + + if (itemBottom > viewBottom) { + return Math.max(0, itemBottom - safeViewHeight) + } + + return safeViewTop +} diff --git a/package-lock.json b/package-lock.json index 31e401e..1feff03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.84", + "version": "1.11.85", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.84", + "version": "1.11.85", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 1c65c5d..3a2a891 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.84", + "version": "1.11.85", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-slash-menu-scroll.test.js b/tests/markdown-slash-menu-scroll.test.js new file mode 100644 index 0000000..d30b912 --- /dev/null +++ b/tests/markdown-slash-menu-scroll.test.js @@ -0,0 +1,40 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { calculateSlashMenuScrollTop } from '../lib/markdown-slash-menu-scroll.js' + +test('슬래시 메뉴 항목이 보이는 범위 안이면 현재 scrollTop을 유지한다', () => { + assert.equal(calculateSlashMenuScrollTop({ + itemTop: 48, + itemHeight: 32, + viewTop: 40, + viewHeight: 120 + }), 40) +}) + +test('슬래시 메뉴 항목이 위로 벗어나면 항목 위쪽으로 스크롤한다', () => { + assert.equal(calculateSlashMenuScrollTop({ + itemTop: 20, + itemHeight: 32, + viewTop: 60, + viewHeight: 120 + }), 20) +}) + +test('슬래시 메뉴 항목이 아래로 벗어나면 항목 아래쪽이 보이도록 스크롤한다', () => { + assert.equal(calculateSlashMenuScrollTop({ + itemTop: 180, + itemHeight: 32, + viewTop: 60, + viewHeight: 120 + }), 92) +}) + +test('슬래시 메뉴 스크롤 계산은 잘못된 숫자를 안전한 기본값으로 보정한다', () => { + assert.equal(calculateSlashMenuScrollTop({ + itemTop: Number.NaN, + itemHeight: -10, + viewTop: -20, + viewHeight: Number.POSITIVE_INFINITY + }), 0) +})