에디터 슬래시 상태와 단축키 판별 분리

This commit is contained in:
2026-07-27 12:09:47 +09:00
parent 358199e595
commit bf48af7bd3
10 changed files with 361 additions and 69 deletions

View File

@@ -21,6 +21,11 @@ import {
getEditorHistoryEntrySelection,
getEditorHistoryEntryValue
} from '../../lib/markdown-editor-history.js'
import {
getEditorHistoryKeyboardAction,
isEditorModeToggleShortcut,
isMediaPickerCloseShortcut
} from '../../lib/markdown-editor-keyboard.js'
import {
buildMediaPickerApplyState,
buildMediaPickerEmptyMessage,
@@ -44,9 +49,6 @@ import {
toggleMediaSelectionState
} from '../../lib/markdown-editor-media.js'
import {
addSlashSuppressedLine,
clampSlashHighlightedIndex,
filterSlashCommands,
getMediaPickerKindFromSlashAction,
getNextSlashHighlightedIndex,
getSlashCommandFocusTarget,
@@ -55,7 +57,6 @@ import {
insertMarkdownLinesAtSlashTarget,
isMediaSlashAction,
parseSlashInput,
removeSlashSuppressedLine,
resolveSlashCommand,
shouldIgnorePendingMediaSlashCommit
} from '../../lib/markdown-slash-commands.js'
@@ -69,6 +70,7 @@ import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OP
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
import { buildToggleOpenerLine } from '../../lib/markdown-toggle.js'
import { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
import { useAdminMarkdownSlashMenu } from '../../composables/useAdminMarkdownSlashMenu.js'
import {
buildDefaultUploadSizeLimits,
DEFAULT_UPLOAD_MAX_FILE_COUNT,
@@ -147,44 +149,32 @@ const lastSelectionState = ref({
start: 0,
end: 0
})
/** @type {import('vue').Ref<number|null>} 슬래시 명령이 입력된 줄 */
const liveSlashSourceLine = ref(null)
/** @type {import('vue').Ref<string>} 슬래시 검색어 */
const liveSlashQuery = ref('')
/** @type {import('vue').Ref<number>} 슬래시 메뉴 하이라이트 인덱스 */
const liveSlashHighlightedIndex = ref(0)
/** @type {import('vue').Ref<{ top: number, left: number }>} 슬래시 메뉴 위치 */
const liveSlashMenuPosition = ref({ top: 0, left: 0 })
/** @type {import('vue').Ref<HTMLElement|null>} 슬래시 메뉴 DOM */
const liveSlashMenuRef = ref(null)
/** @type {import('vue').Ref<number|null>} 슬래시 후 미디어 삽입 줄 */
const liveSlashInsertLine = ref(null)
/** @type {import('vue').Ref<Set<number>>} ESC로 슬래시 메뉴만 닫은 줄(0-based) */
const liveSlashSuppressedLines = ref(new Set())
/** 키보드로 슬래시 메뉴를 탐색 중일 때 mouseenter 하이라이트 무시 */
const liveSlashKeyboardNav = ref(false)
let liveSlashKeyboardNavTimer = null
const {
liveSlashSourceLine,
liveSlashQuery,
liveSlashHighlightedIndex,
liveSlashMenuPosition,
liveSlashMenuRef,
liveSlashInsertLine,
liveSlashKeyboardNav,
liveSlashVisible,
liveSlashSuppressedLineList,
visibleLiveSlashCommands,
clearLiveSlashState,
openLiveSlashMenu,
isLiveSlashLineSuppressed,
suppressLiveSlashLine,
releaseLiveSlashLine,
resetLiveSlashHighlightedIndex,
clampLiveSlashHighlightedIndex,
startLiveSlashKeyboardNavigation,
stopLiveSlashKeyboardNavigation
} = useAdminMarkdownSlashMenu()
/** @type {HTMLElement|null} 슬래시 메뉴 위치 갱신을 위해 구독한 미리보기 스크롤 영역 */
let liveSlashPreviewScrollElement = null
/** @type {HTMLElement|null} 슬래시 메뉴 위치 갱신을 위해 구독한 작성 textarea */
let liveSlashTextareaScrollElement = null
const liveSlashVisible = computed(() => liveSlashSourceLine.value !== null)
const liveSlashSuppressedLineList = computed(() => [...liveSlashSuppressedLines.value])
const visibleLiveSlashCommands = computed(() => filterSlashCommands(liveSlashQuery.value))
/**
* 슬래시 메뉴 입력 상태를 초기화한다.
* @returns {void}
*/
const clearLiveSlashState = () => {
liveSlashSourceLine.value = null
liveSlashQuery.value = ''
liveSlashHighlightedIndex.value = 0
}
/** 되돌리기 히스토리 최대 보관 개수 */
const EDITOR_UNDO_LIMIT = 100
/** @type {import('vue').Ref<Array<string|{ value: string, selectionStart: number, selectionEnd: number }>>} 본문 되돌리기 스택 */
@@ -1054,7 +1044,7 @@ const syncWriteSlashState = () => {
const line = activeLogicalLineIndex.value
if (liveSlashSuppressedLines.value.has(line)) {
if (isLiveSlashLineSuppressed(line)) {
if (liveSlashSourceLine.value !== null) {
clearLiveSlashState()
}
@@ -1076,8 +1066,7 @@ const syncWriteSlashState = () => {
return
}
liveSlashSourceLine.value = line
liveSlashQuery.value = parsed.query
openLiveSlashMenu(line, parsed.query)
nextTick(updateLiveSlashMenuPosition)
}
@@ -1423,7 +1412,7 @@ onMounted(() => {
* @returns {void}
*/
const onDocumentKeydown = (event) => {
if (event.key === 'Escape' && isMediaPickerOpen.value) {
if (isMediaPickerCloseShortcut(event) && isMediaPickerOpen.value) {
event.preventDefault()
closeMediaPicker()
return
@@ -1435,19 +1424,12 @@ onMounted(() => {
return
}
const hasCommandModifier = event.metaKey || event.ctrlKey
const isUndoShortcut = hasCommandModifier
&& !event.altKey
&& !event.shiftKey
&& event.key.toLowerCase() === 'z'
const isRedoShortcut = hasCommandModifier
&& !event.altKey
&& (event.shiftKey && event.key.toLowerCase() === 'z' || event.key.toLowerCase() === 'y')
const historyKeyboardAction = getEditorHistoryKeyboardAction(event)
if (isUndoShortcut || isRedoShortcut) {
if (historyKeyboardAction) {
event.preventDefault()
event.stopPropagation()
restoreEditorHistory(isUndoShortcut ? 'undo' : 'redo')
restoreEditorHistory(historyKeyboardAction)
return
}
@@ -1459,11 +1441,7 @@ onMounted(() => {
const count = visibleLiveSlashCommands.value.length
if (count) {
liveSlashKeyboardNav.value = true
window.clearTimeout(liveSlashKeyboardNavTimer)
liveSlashKeyboardNavTimer = window.setTimeout(() => {
liveSlashKeyboardNav.value = false
}, 200)
startLiveSlashKeyboardNavigation()
liveSlashHighlightedIndex.value = getNextSlashHighlightedIndex({
currentIndex: liveSlashHighlightedIndex.value,
@@ -1493,7 +1471,7 @@ onMounted(() => {
}
}
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'e') {
if (!isEditorModeToggleShortcut(event)) {
return
}
@@ -1533,7 +1511,7 @@ onMounted(() => {
onBeforeUnmount(() => {
window.clearTimeout(blockPanelFocusTimer)
window.clearTimeout(liveSlashKeyboardNavTimer)
stopLiveSlashKeyboardNavigation()
removeLiveSlashRepositionListeners()
document.removeEventListener('selectionchange', onSelectionChange)
document.removeEventListener('keydown', onDocumentKeydown, true)
@@ -2547,7 +2525,7 @@ const onLiveSlashUpdate = (payload) => {
return
}
if (liveSlashSuppressedLines.value.has(payload.sourceLine)) {
if (isLiveSlashLineSuppressed(payload.sourceLine)) {
if (liveSlashSourceLine.value !== null) {
clearLiveSlashState()
}
@@ -2562,17 +2540,16 @@ const onLiveSlashUpdate = (payload) => {
return
}
liveSlashSourceLine.value = payload.sourceLine
liveSlashQuery.value = String(payload.query ?? '')
openLiveSlashMenu(payload.sourceLine, payload.query)
nextTick(updateLiveSlashMenuPosition)
}
watch(liveSlashQuery, () => {
liveSlashHighlightedIndex.value = 0
resetLiveSlashHighlightedIndex()
})
watch(visibleLiveSlashCommands, (commands) => {
liveSlashHighlightedIndex.value = clampSlashHighlightedIndex(liveSlashHighlightedIndex.value, commands.length)
clampLiveSlashHighlightedIndex(commands.length)
updateLiveSlashMenuPosition()
})
@@ -2602,7 +2579,7 @@ watch(liveSlashVisible, (visible) => {
*/
const onLiveSlashEnd = (payload) => {
if (typeof payload?.sourceLine === 'number') {
liveSlashSuppressedLines.value = removeSlashSuppressedLine(liveSlashSuppressedLines.value, payload.sourceLine)
releaseLiveSlashLine(payload.sourceLine)
}
clearLiveSlashState()
@@ -2616,7 +2593,7 @@ const cancelLiveSlashCommand = () => {
const line = liveSlashSourceLine.value
if (line !== null) {
liveSlashSuppressedLines.value = addSlashSuppressedLine(liveSlashSuppressedLines.value, line)
suppressLiveSlashLine(line)
}
clearLiveSlashState()
@@ -2689,7 +2666,7 @@ const applyLiveSlashCommand = (command) => {
if (command.action === 'lines' && Array.isArray(command.lines)) {
const replacementLines = command.lines
liveSlashSuppressedLines.value = removeSlashSuppressedLine(liveSlashSuppressedLines.value, line)
releaseLiveSlashLine(line)
replaceLineRange(line, line, replacementLines, false)
clearLiveSlashState()