173 lines
6.2 KiB
JavaScript
173 lines
6.2 KiB
JavaScript
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<string>} options.activeMode - 현재 에디터 모드
|
|
* @param {import('vue').Ref<string>} options.markdownValue - 마크다운 본문
|
|
* @param {import('vue').Ref<number|null>} options.liveSlashSourceLine - 슬래시 입력 줄
|
|
* @param {import('vue').Ref<string>} options.liveSlashQuery - 슬래시 검색어
|
|
* @param {import('vue').Ref<number|null>} 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
|
|
}
|
|
}
|