슬래시 메뉴 위치 관리 분리
This commit is contained in:
@@ -50,14 +50,8 @@ import {
|
||||
resolveSlashCommand,
|
||||
shouldCaptureSlashMenuKeyboardAction
|
||||
} from '../../lib/markdown-slash-commands.js'
|
||||
import {
|
||||
calculateSlashMenuPosition,
|
||||
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 { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
|
||||
import { useAdminMarkdownSlashMenu } from '../../composables/useAdminMarkdownSlashMenu.js'
|
||||
import { useAdminMarkdownSlashMenuPosition } from '../../composables/useAdminMarkdownSlashMenuPosition.js'
|
||||
import { useAdminMarkdownHistory } from '../../composables/useAdminMarkdownHistory.js'
|
||||
import { useAdminMarkdownMediaPicker } from '../../composables/useAdminMarkdownMediaPicker.js'
|
||||
import { useAdminMarkdownFocusBridge } from '../../composables/useAdminMarkdownFocusBridge.js'
|
||||
@@ -184,10 +178,6 @@ const {
|
||||
uploadMaxFileCount,
|
||||
uploadMaxTotalSize
|
||||
})
|
||||
/** @type {HTMLElement|null} 슬래시 메뉴 위치 갱신을 위해 구독한 미리보기 스크롤 영역 */
|
||||
let liveSlashPreviewScrollElement = null
|
||||
/** @type {HTMLElement|null} 슬래시 메뉴 위치 갱신을 위해 구독한 작성 textarea */
|
||||
let liveSlashTextareaScrollElement = null
|
||||
|
||||
/**
|
||||
* 작성 표면의 textarea DOM을 반환한다.
|
||||
@@ -211,6 +201,25 @@ const markdownValue = computed({
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const {
|
||||
updateLiveSlashMenuPosition
|
||||
} = useAdminMarkdownSlashMenuPosition({
|
||||
activeMode,
|
||||
markdownValue,
|
||||
liveSlashSourceLine,
|
||||
liveSlashQuery,
|
||||
liveSlashHighlightedIndex,
|
||||
liveSlashMenuPosition,
|
||||
liveSlashMenuRef,
|
||||
liveSlashVisible,
|
||||
visibleLiveSlashCommands,
|
||||
resetLiveSlashHighlightedIndex,
|
||||
clampLiveSlashHighlightedIndex,
|
||||
getPreviewSourceLineElement,
|
||||
getPreviewSurfaceElement,
|
||||
getWriteTextareaElement
|
||||
})
|
||||
|
||||
watch(markdownValue, () => {
|
||||
const textarea = getWriteTextareaElement()
|
||||
|
||||
@@ -802,7 +811,6 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
clearBlockPanelFocusTimer()
|
||||
stopLiveSlashKeyboardNavigation()
|
||||
removeLiveSlashRepositionListeners()
|
||||
document.removeEventListener('selectionchange', onSelectionChange)
|
||||
document.removeEventListener('keydown', onDocumentKeydown, true)
|
||||
document.removeEventListener('pointerdown', onDocumentPointerDown, true)
|
||||
@@ -1224,180 +1232,6 @@ const handlePreviewKeydownCapture = (event) => {
|
||||
onPreviewDeleteLine(lineIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 컴포넌트가 노출한 실제 DOM 엘리먼트를 반환한다.
|
||||
* @returns {HTMLElement|null} 메뉴 엘리먼트
|
||||
*/
|
||||
const getLiveSlashMenuElement = () => {
|
||||
const menu = liveSlashMenuRef.value
|
||||
|
||||
if (!menu) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof menu.getRootElement === 'function') {
|
||||
return menu.getRootElement()
|
||||
}
|
||||
|
||||
return menu.rootRef ?? menu
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리보기 모드 슬래시 메뉴 위치를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updatePreviewSlashMenuPosition = () => {
|
||||
const element = getPreviewSourceLineElement(liveSlashSourceLine.value)
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const menuEl = getLiveSlashMenuElement()
|
||||
const menuHeight = menuEl?.offsetHeight || DEFAULT_SLASH_MENU_HEIGHT
|
||||
const menuWidth = menuEl?.offsetWidth || DEFAULT_SLASH_MENU_WIDTH
|
||||
const gap = 4
|
||||
|
||||
liveSlashMenuPosition.value = calculateSlashMenuPosition({
|
||||
top: rect.bottom + gap,
|
||||
left: rect.left,
|
||||
menuHeight,
|
||||
menuWidth,
|
||||
anchorTop: rect.top,
|
||||
viewportHeight: window.innerHeight,
|
||||
viewportWidth: window.innerWidth
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스 모드 textarea 슬래시 메뉴 위치를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateWriteSlashMenuPosition = () => {
|
||||
const textarea = getWriteTextareaElement()
|
||||
const line = liveSlashSourceLine.value
|
||||
|
||||
if (!textarea || line === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = markdownValue.value ?? ''
|
||||
const lineStart = value.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0)
|
||||
const coords = getTextareaCaretCoordinates(textarea, lineStart)
|
||||
const rect = textarea.getBoundingClientRect()
|
||||
const menuEl = getLiveSlashMenuElement()
|
||||
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 = calculateSlashMenuPosition({
|
||||
top: anchorTop + coords.height + gap,
|
||||
left: rect.left + coords.left - textarea.scrollLeft,
|
||||
menuHeight,
|
||||
menuWidth,
|
||||
anchorTop,
|
||||
viewportHeight: window.innerHeight,
|
||||
viewportWidth: window.innerWidth
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 위치를 갱신한다(뷰포트 아래로 넘치면 위쪽·좌우 클램프).
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateLiveSlashMenuPosition = () => {
|
||||
if (!import.meta.client || liveSlashSourceLine.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
if (activeMode.value === 'write') {
|
||||
updateWriteSlashMenuPosition()
|
||||
return
|
||||
}
|
||||
|
||||
updatePreviewSlashMenuPosition()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 위치 재계산 이벤트를 실행한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const repositionLiveSlashMenu = () => {
|
||||
updateLiveSlashMenuPosition()
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 위치 재계산 이벤트 리스너를 제거한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const removeLiveSlashRepositionListeners = () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', repositionLiveSlashMenu, true)
|
||||
window.removeEventListener('resize', repositionLiveSlashMenu)
|
||||
liveSlashPreviewScrollElement?.removeEventListener('scroll', repositionLiveSlashMenu)
|
||||
liveSlashTextareaScrollElement?.removeEventListener('scroll', repositionLiveSlashMenu)
|
||||
liveSlashPreviewScrollElement = null
|
||||
liveSlashTextareaScrollElement = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 위치 재계산 이벤트 리스너를 등록한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const addLiveSlashRepositionListeners = () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
removeLiveSlashRepositionListeners()
|
||||
liveSlashPreviewScrollElement = getPreviewSurfaceElement()
|
||||
liveSlashTextareaScrollElement = getWriteTextareaElement()
|
||||
window.addEventListener('scroll', repositionLiveSlashMenu, true)
|
||||
window.addEventListener('resize', repositionLiveSlashMenu)
|
||||
liveSlashPreviewScrollElement?.addEventListener('scroll', repositionLiveSlashMenu)
|
||||
liveSlashTextareaScrollElement?.addEventListener('scroll', repositionLiveSlashMenu)
|
||||
}
|
||||
|
||||
/**
|
||||
* 키보드로 선택한 슬래시 메뉴 항목이 보이도록 스크롤한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const scrollLiveSlashHighlightedIntoView = () => {
|
||||
if (!import.meta.client || !liveSlashVisible.value) {
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const menu = getLiveSlashMenuElement()
|
||||
|
||||
if (!menu) {
|
||||
return
|
||||
}
|
||||
|
||||
const highlightedItem = menu.querySelector('.admin-markdown-editor__slash-item--active')
|
||||
|
||||
if (!highlightedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
menu.scrollTop = calculateSlashMenuScrollTop({
|
||||
itemTop: highlightedItem.offsetTop,
|
||||
itemHeight: highlightedItem.offsetHeight,
|
||||
viewTop: menu.scrollTop,
|
||||
viewHeight: menu.clientHeight
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 항목 mouseenter 시 하이라이트(키보드 탐색 중에는 무시).
|
||||
* @param {number} commandIndex - 명령 인덱스
|
||||
@@ -1443,35 +1277,6 @@ const onLiveSlashUpdate = (payload) => {
|
||||
nextTick(updateLiveSlashMenuPosition)
|
||||
}
|
||||
|
||||
watch(liveSlashQuery, () => {
|
||||
resetLiveSlashHighlightedIndex()
|
||||
})
|
||||
|
||||
watch(visibleLiveSlashCommands, (commands) => {
|
||||
clampLiveSlashHighlightedIndex(commands.length)
|
||||
|
||||
updateLiveSlashMenuPosition()
|
||||
})
|
||||
|
||||
watch(liveSlashHighlightedIndex, () => {
|
||||
scrollLiveSlashHighlightedIntoView()
|
||||
})
|
||||
|
||||
watch(liveSlashVisible, (visible) => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
addLiveSlashRepositionListeners()
|
||||
updateLiveSlashMenuPosition()
|
||||
scrollLiveSlashHighlightedIntoView()
|
||||
return
|
||||
}
|
||||
|
||||
removeLiveSlashRepositionListeners()
|
||||
})
|
||||
|
||||
/**
|
||||
* 슬래시 명령 입력을 종료한다.
|
||||
* @returns {void}
|
||||
|
||||
Reference in New Issue
Block a user