import { nextTick, unref } from 'vue' import { createSlashCommandApplyPlan, createSlashCommandEditorEffectPlan, getSlashCommandFocusTarget, getSlashCommandTextareaSelection, parseSlashInput, resolveSlashCommand } from '../lib/markdown-slash-commands.js' /** * 관리자 Markdown 에디터의 슬래시 명령 적용 실행을 관리한다. * @param {Object} options - 슬래시 명령 실행 옵션 * @param {import('vue').Ref} options.activeMode - 현재 에디터 모드 * @param {import('vue').Ref} options.markdownValue - 마크다운 본문 * @param {import('vue').Ref} options.liveSlashSourceLine - 슬래시 입력 줄 * @param {import('vue').Ref} options.liveSlashQuery - 슬래시 검색어 * @param {import('vue').Ref} options.liveSlashInsertLine - 미디어 삽입 대기 줄 * @param {() => void} options.clearLiveSlashState - 슬래시 상태 초기화 함수 * @param {(line: number) => void} options.releaseLiveSlashLine - 슬래시 억제 해제 함수 * @param {(line: number, lineValue?: string) => void} options.suppressLiveSlashLine - 슬래시 억제 함수 * @param {(startLine: number, endLine: number, replacementLines: string[], focusEditor?: boolean) => void} options.replaceLineRange - 줄 교체 함수 * @param {(mediaKind: 'image'|'gallery'|'video'|'audio'|'file') => void} options.openMediaPicker - 미디어 모달 열기 함수 * @param {(start: number, end: number) => void} options.setTextareaSelection - textarea 선택 복원 함수 * @param {(line: number, column?: number, position?: string, offset?: number) => void} options.focusPreviewEditableAtLine - 미리보기 줄 포커스 함수 * @param {boolean} [options.isClient] - 클라이언트 실행 여부 * @returns {{ * applyLiveSlashCommand: (command: import('../lib/markdown-slash-commands.js').MarkdownSlashCommand) => void, * cancelLiveSlashCommand: () => void, * focusPreviewAfterSlashCommand: (line: number, replacementLines: string[]) => void, * focusTextareaAfterSlashCommand: (line: number, replacementLines: string[]) => void, * onLiveSlashApply: (payload?: { sourceLine?: number, value?: string }) => void, * onLiveSlashEnd: (payload?: { sourceLine?: number }) => void * }} 슬래시 명령 실행 액션 */ export const useAdminMarkdownSlashCommandActions = (options) => { const isClient = options.isClient ?? import.meta.client /** * 슬래시 명령 입력을 종료한다. * @param {{ sourceLine?: number }} [payload] - 종료 대상 줄 * @returns {void} */ const onLiveSlashEnd = (payload) => { if (typeof payload?.sourceLine === 'number') { options.releaseLiveSlashLine(payload.sourceLine) } options.clearLiveSlashState() } /** * 슬래시 명령 입력을 취소한다(줄 내용은 유지, 메뉴만 닫음). * @returns {void} */ const cancelLiveSlashCommand = () => { const line = options.liveSlashSourceLine.value if (line !== null) { const lines = String(unref(options.markdownValue) ?? '').split('\n') options.suppressLiveSlashLine(line, lines[line] ?? '') } options.clearLiveSlashState() } /** * 슬래시 명령 적용 후 미리보기 인라인 편집에 포커스를 복원한다. * @param {number} line - 줄 번호(0-based) * @param {string[]} replacementLines - 삽입된 줄 * @returns {void} */ const focusPreviewAfterSlashCommand = (line, replacementLines) => { if (!isClient) { return } const { line: focusLine, offset } = getSlashCommandFocusTarget(line, replacementLines) const requestFrame = globalThis.requestAnimationFrame ?? ((callback) => globalThis.setTimeout(callback, 0)) nextTick(() => { nextTick(() => { requestFrame(() => { requestFrame(() => { options.focusPreviewEditableAtLine(focusLine, 0, 'end', offset) }) }) }) }) } /** * 소스 모드 textarea에 슬래시 명령 적용 후 커서를 복원한다. * @param {number} line - 줄 번호(0-based) * @param {string[]} replacementLines - 삽입된 줄 * @returns {void} */ const focusTextareaAfterSlashCommand = (line, replacementLines) => { const selection = getSlashCommandTextareaSelection({ markdown: unref(options.markdownValue) ?? '', startLine: line, lines: replacementLines }) options.setTextareaSelection(selection.start, selection.end) } /** * 슬래시 명령을 적용한다. * @param {import('../lib/markdown-slash-commands.js').MarkdownSlashCommand} command - 명령 * @returns {void} */ const applyLiveSlashCommand = (command) => { const plan = createSlashCommandApplyPlan({ command, sourceLine: options.liveSlashSourceLine.value }) const effectPlan = createSlashCommandEditorEffectPlan({ plan, activeMode: options.activeMode.value }) if (effectPlan.kind === 'none') { return } if (effectPlan.kind === 'media') { options.liveSlashInsertLine.value = effectPlan.line options.replaceLineRange(effectPlan.line, effectPlan.line, [], false) onLiveSlashEnd() options.openMediaPicker(effectPlan.mediaKind) return } if (effectPlan.kind === 'lines') { options.releaseLiveSlashLine(effectPlan.line) options.replaceLineRange(effectPlan.line, effectPlan.line, effectPlan.lines, false) options.clearLiveSlashState() if (effectPlan.focusTarget === 'textarea') { focusTextareaAfterSlashCommand(effectPlan.line, effectPlan.lines) } else { focusPreviewAfterSlashCommand(effectPlan.line, effectPlan.lines) } } } /** * 슬래시 명령 Enter 적용. * @param {{ sourceLine?: number, value?: string }} [payload] - 줄·입력 값 * @returns {void} */ const onLiveSlashApply = (payload) => { const parsed = parseSlashInput(payload?.value) const command = resolveSlashCommand(parsed?.query ?? options.liveSlashQuery.value) if (!command) { onLiveSlashEnd({ sourceLine: payload?.sourceLine }) return } applyLiveSlashCommand(command) } return { applyLiveSlashCommand, cancelLiveSlashCommand, focusPreviewAfterSlashCommand, focusTextareaAfterSlashCommand, onLiveSlashApply, onLiveSlashEnd } }