슬래시 메뉴 위치 계산 분리

This commit is contained in:
2026-07-27 10:28:23 +09:00
parent ce64ab7a9f
commit 63c657d7a3
7 changed files with 163 additions and 57 deletions

View File

@@ -52,6 +52,11 @@ import {
resolveSlashCommand,
shouldIgnorePendingMediaSlashCommit
} from '../../lib/markdown-slash-commands.js'
import {
calculateSlashMenuPosition,
DEFAULT_SLASH_MENU_HEIGHT,
DEFAULT_SLASH_MENU_WIDTH
} from '../../lib/markdown-slash-menu-position.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'
@@ -145,8 +150,6 @@ const liveSlashMenuPosition = ref({ top: 0, left: 0 })
/** @type {import('vue').Ref<HTMLElement|null>} 슬래시 메뉴 DOM */
const liveSlashMenuRef = ref(null)
/** 슬래시 메뉴 예상 높이(px) — 렌더 전 위치 계산용 */
const LIVE_SLASH_MENU_ESTIMATED_HEIGHT = 280
/** @type {import('vue').Ref<number|null>} 슬래시 후 미디어 삽입 줄 */
const liveSlashInsertLine = ref(null)
/** @type {import('vue').Ref<Set<number>>} ESC로 슬래시 메뉴만 닫은 줄(0-based) */
@@ -2324,44 +2327,6 @@ const handlePreviewKeydownCapture = (event) => {
* @param {{ lineIndex: number, mergedLine: string }} payload - 병합 위치·결과 줄
* @returns {void}
*/
/**
* 슬래시 메뉴 좌표를 뷰포트 안으로 맞춘다.
* @param {number} top - 후보 top(px)
* @param {number} left - 후보 left(px)
* @param {number} menuHeight - 메뉴 높이
* @param {number} menuWidth - 메뉴 너비
* @param {number} anchorTop - 기준 요소 top(위로 넘칠 때)
* @returns {{ top: number, left: number }}
*/
const clampSlashMenuPosition = (top, left, menuHeight, menuWidth, anchorTop) => {
const gap = 4
const padding = 8
const viewportBottom = window.innerHeight - padding
const viewportRight = window.innerWidth - padding
let nextTop = top
if (nextTop + menuHeight > viewportBottom) {
const aboveTop = anchorTop - menuHeight - gap
if (aboveTop >= padding) {
nextTop = aboveTop
} else {
nextTop = Math.max(padding, viewportBottom - menuHeight)
}
}
let nextLeft = left
if (nextLeft + menuWidth > viewportRight) {
nextLeft = viewportRight - menuWidth
}
return {
top: nextTop,
left: Math.max(padding, nextLeft)
}
}
/**
* 슬래시 메뉴 컴포넌트가 노출한 실제 DOM 엘리먼트를 반환한다.
* @returns {HTMLElement|null} 메뉴 엘리먼트
@@ -2393,17 +2358,19 @@ const updatePreviewSlashMenuPosition = () => {
const rect = element.getBoundingClientRect()
const menuEl = getLiveSlashMenuElement()
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
const menuWidth = menuEl?.offsetWidth || 288
const menuHeight = menuEl?.offsetHeight || DEFAULT_SLASH_MENU_HEIGHT
const menuWidth = menuEl?.offsetWidth || DEFAULT_SLASH_MENU_WIDTH
const gap = 4
liveSlashMenuPosition.value = clampSlashMenuPosition(
rect.bottom + gap,
rect.left,
liveSlashMenuPosition.value = calculateSlashMenuPosition({
top: rect.bottom + gap,
left: rect.left,
menuHeight,
menuWidth,
rect.top
)
anchorTop: rect.top,
viewportHeight: window.innerHeight,
viewportWidth: window.innerWidth
})
}
/**
@@ -2423,18 +2390,20 @@ const updateWriteSlashMenuPosition = () => {
const coords = getTextareaCaretCoordinates(textarea, lineStart)
const rect = textarea.getBoundingClientRect()
const menuEl = getLiveSlashMenuElement()
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
const menuWidth = menuEl?.offsetWidth || 288
const menuHeight = menuEl?.offsetHeight || DEFAULT_SLASH_MENU_HEIGHT
const menuWidth = menuEl?.offsetWidth || DEFAULT_SLASH_MENU_WIDTH
const gap = 4
const anchorTop = rect.top + coords.top - textarea.scrollTop
liveSlashMenuPosition.value = clampSlashMenuPosition(
anchorTop + coords.height + gap,
rect.left + coords.left - textarea.scrollLeft,
liveSlashMenuPosition.value = calculateSlashMenuPosition({
top: anchorTop + coords.height + gap,
left: rect.left + coords.left - textarea.scrollLeft,
menuHeight,
menuWidth,
anchorTop
)
anchorTop,
viewportHeight: window.innerHeight,
viewportWidth: window.innerWidth
})
}
/**