에디터 슬래시 메뉴 컴포넌트 분리
This commit is contained in:
@@ -63,7 +63,7 @@ import {
|
||||
normalizeUploadMaxFileCount,
|
||||
normalizeUploadMaxTotalSize
|
||||
} from '../../lib/upload-size-limit.js'
|
||||
import AdminSlashCommandIcon from './AdminSlashCommandIcon.vue'
|
||||
import AdminMarkdownSlashMenu from './AdminMarkdownSlashMenu.vue'
|
||||
|
||||
const { toast, showToast } = useAdminToast()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
@@ -2325,6 +2325,24 @@ const clampSlashMenuPosition = (top, left, menuHeight, menuWidth, anchorTop) =>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 컴포넌트가 노출한 실제 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}
|
||||
@@ -2337,7 +2355,7 @@ const updatePreviewSlashMenuPosition = () => {
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const menuEl = liveSlashMenuRef.value
|
||||
const menuEl = getLiveSlashMenuElement()
|
||||
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
|
||||
const menuWidth = menuEl?.offsetWidth || 288
|
||||
const gap = 4
|
||||
@@ -2367,7 +2385,7 @@ const updateWriteSlashMenuPosition = () => {
|
||||
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 = liveSlashMenuRef.value
|
||||
const menuEl = getLiveSlashMenuElement()
|
||||
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
|
||||
const menuWidth = menuEl?.offsetWidth || 288
|
||||
const gap = 4
|
||||
@@ -2413,7 +2431,7 @@ const scrollLiveSlashHighlightedIntoView = () => {
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const menu = liveSlashMenuRef.value
|
||||
const menu = getLiveSlashMenuElement()
|
||||
|
||||
if (!menu) {
|
||||
return
|
||||
@@ -3589,41 +3607,15 @@ const handleKeydown = (event) => {
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
<AdminMarkdownSlashMenu
|
||||
v-if="liveSlashVisible"
|
||||
ref="liveSlashMenuRef"
|
||||
class="admin-markdown-editor__slash-menu fixed z-[60] w-72 max-h-[min(52vh,360px)] overflow-y-auto rounded-lg border border-[#e3e6e8] bg-white p-1 shadow-lg"
|
||||
:style="{ top: `${liveSlashMenuPosition.top}px`, left: `${liveSlashMenuPosition.left}px` }"
|
||||
role="listbox"
|
||||
aria-label="슬래시 명령"
|
||||
>
|
||||
<button
|
||||
v-for="(command, commandIndex) in visibleLiveSlashCommands"
|
||||
:key="`live-slash-${command.id}`"
|
||||
class="admin-markdown-editor__slash-item flex w-full items-center justify-between gap-3 rounded-md px-2.5 py-2 text-left transition-colors"
|
||||
:class="commandIndex === liveSlashHighlightedIndex ? 'admin-markdown-editor__slash-item--active bg-[#f4f5f6]' : ''"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="commandIndex === liveSlashHighlightedIndex"
|
||||
@mouseenter="onSlashMenuItemMouseEnter(commandIndex)"
|
||||
@mousedown.prevent="applyLiveSlashCommand(command)"
|
||||
>
|
||||
<span class="admin-markdown-editor__slash-leading flex min-w-0 items-center gap-2.5">
|
||||
<AdminSlashCommandIcon
|
||||
:command-id="command.id"
|
||||
class="admin-markdown-editor__slash-icon shrink-0 text-[#394047]"
|
||||
/>
|
||||
<span class="admin-markdown-editor__slash-label truncate text-sm text-[#15171a]">{{ command.label }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="admin-markdown-editor__slash-slug shrink-0 text-xs text-[#8b959e] transition-opacity"
|
||||
:class="commandIndex === liveSlashHighlightedIndex ? 'opacity-100' : 'opacity-0'"
|
||||
>/{{ command.id }}</span>
|
||||
</button>
|
||||
<p v-if="!visibleLiveSlashCommands.length" class="admin-markdown-editor__slash-empty px-4 py-3 text-sm text-[#6b7280]">
|
||||
일치하는 명령이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
:commands="visibleLiveSlashCommands"
|
||||
:highlighted-index="liveSlashHighlightedIndex"
|
||||
:position="liveSlashMenuPosition"
|
||||
@highlight="onSlashMenuItemMouseEnter"
|
||||
@select="applyLiveSlashCommand"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
<AdminMarkdownMediaPickerModal
|
||||
|
||||
71
components/admin/AdminMarkdownSlashMenu.vue
Normal file
71
components/admin/AdminMarkdownSlashMenu.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import AdminSlashCommandIcon from './AdminSlashCommandIcon.vue'
|
||||
|
||||
defineProps({
|
||||
/** @type {import('vue').PropType<Array<{ id: string, label: string }>>} */
|
||||
commands: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
highlightedIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** @type {import('vue').PropType<{ top: number, left: number }>} */
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ top: 0, left: 0 })
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['highlight', 'select'])
|
||||
const rootRef = ref(null)
|
||||
|
||||
/**
|
||||
* 부모 컴포넌트의 위치 보정·스크롤 계산에 사용할 메뉴 루트 엘리먼트를 반환한다.
|
||||
* @returns {HTMLElement|null} 메뉴 루트 엘리먼트
|
||||
*/
|
||||
const getRootElement = () => rootRef.value
|
||||
|
||||
defineExpose({
|
||||
getRootElement,
|
||||
rootRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="admin-markdown-editor__slash-menu fixed z-[60] w-72 max-h-[min(52vh,360px)] overflow-y-auto rounded-lg border border-[#e3e6e8] bg-white p-1 shadow-lg"
|
||||
:style="{ top: `${position.top}px`, left: `${position.left}px` }"
|
||||
role="listbox"
|
||||
aria-label="슬래시 명령"
|
||||
>
|
||||
<button
|
||||
v-for="(command, commandIndex) in commands"
|
||||
:key="`live-slash-${command.id}`"
|
||||
class="admin-markdown-editor__slash-item flex w-full items-center justify-between gap-3 rounded-md px-2.5 py-2 text-left transition-colors"
|
||||
:class="commandIndex === highlightedIndex ? 'admin-markdown-editor__slash-item--active bg-[#f4f5f6]' : ''"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="commandIndex === highlightedIndex"
|
||||
@mouseenter="emit('highlight', commandIndex)"
|
||||
@mousedown.prevent="emit('select', command)"
|
||||
>
|
||||
<span class="admin-markdown-editor__slash-leading flex min-w-0 items-center gap-2.5">
|
||||
<AdminSlashCommandIcon
|
||||
:command-id="command.id"
|
||||
class="admin-markdown-editor__slash-icon shrink-0 text-[#394047]"
|
||||
/>
|
||||
<span class="admin-markdown-editor__slash-label truncate text-sm text-[#15171a]">{{ command.label }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="admin-markdown-editor__slash-slug shrink-0 text-xs text-[#8b959e] transition-opacity"
|
||||
:class="commandIndex === highlightedIndex ? 'opacity-100' : 'opacity-0'"
|
||||
>/{{ command.id }}</span>
|
||||
</button>
|
||||
<p v-if="!commands.length" class="admin-markdown-editor__slash-empty px-4 py-3 text-sm text-[#6b7280]">
|
||||
일치하는 명령이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user