Files
sori.studio/composables/useAdminMarkdownSlashMenu.js

187 lines
6.3 KiB
JavaScript

import { computed, ref } from 'vue'
import {
addSlashSuppressedLine,
clampSlashHighlightedIndex,
filterSlashCommands,
removeSlashSuppressedLine
} from '../lib/markdown-slash-commands.js'
/**
* 관리자 Markdown 에디터의 슬래시 메뉴 상태를 관리한다.
* @returns {{
* liveSlashSourceLine: import('vue').Ref<number|null>,
* liveSlashQuery: import('vue').Ref<string>,
* liveSlashHighlightedIndex: import('vue').Ref<number>,
* liveSlashMenuPosition: import('vue').Ref<{ top: number, left: number }>,
* liveSlashMenuRef: import('vue').Ref<HTMLElement|null>,
* liveSlashInsertLine: import('vue').Ref<number|null>,
* liveSlashSuppressedLines: import('vue').Ref<Set<number>>,
* liveSlashSuppressedLineValues: import('vue').Ref<Record<string, string>>,
* liveSlashKeyboardNav: import('vue').Ref<boolean>,
* liveSlashVisible: import('vue').ComputedRef<boolean>,
* liveSlashSuppressedLineList: import('vue').ComputedRef<number[]>,
* visibleLiveSlashCommands: import('vue').ComputedRef<import('../lib/markdown-slash-commands.js').MarkdownSlashCommand[]>,
* clearLiveSlashState: () => void,
* openLiveSlashMenu: (sourceLine: number, query: string) => void,
* isLiveSlashLineSuppressed: (line: number, lineValue?: string) => boolean,
* suppressLiveSlashLine: (line: number, lineValue?: string) => void,
* releaseLiveSlashLine: (line: number) => void,
* resetLiveSlashHighlightedIndex: () => void,
* clampLiveSlashHighlightedIndex: (commandCount: number) => void,
* startLiveSlashKeyboardNavigation: (durationMs?: number) => void,
* stopLiveSlashKeyboardNavigation: () => void
* }} 슬래시 메뉴 상태와 액션
*/
export const useAdminMarkdownSlashMenu = () => {
const liveSlashSourceLine = ref(null)
const liveSlashQuery = ref('')
const liveSlashHighlightedIndex = ref(0)
const liveSlashMenuPosition = ref({ top: 0, left: 0 })
const liveSlashMenuRef = ref(null)
const liveSlashInsertLine = ref(null)
const liveSlashSuppressedLines = ref(new Set())
const liveSlashSuppressedLineValues = ref({})
const liveSlashKeyboardNav = ref(false)
let liveSlashKeyboardNavTimer = 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
}
/**
* 슬래시 메뉴를 지정 줄과 검색어로 연다.
* @param {number} sourceLine - 슬래시 명령이 입력된 줄
* @param {string} query - 검색어
* @returns {void}
*/
const openLiveSlashMenu = (sourceLine, query) => {
liveSlashSourceLine.value = sourceLine
liveSlashQuery.value = String(query ?? '')
}
/**
* 슬래시 메뉴가 억제된 줄인지 확인한다.
* @param {number} line - 줄 번호
* @returns {boolean} 억제 여부
*/
const isLiveSlashLineSuppressed = (line, lineValue) => {
if (!liveSlashSuppressedLines.value.has(line)) {
return false
}
const suppressedValue = liveSlashSuppressedLineValues.value[String(line)]
if (suppressedValue === undefined || lineValue === undefined) {
return true
}
return suppressedValue === lineValue
}
/**
* 지정 줄에서 슬래시 메뉴가 다시 열리지 않도록 억제한다.
* @param {number} line - 줄 번호
* @param {string} [lineValue] - 억제 당시 줄 내용
* @returns {void}
*/
const suppressLiveSlashLine = (line, lineValue) => {
liveSlashSuppressedLines.value = addSlashSuppressedLine(liveSlashSuppressedLines.value, line)
if (Number.isFinite(line)) {
liveSlashSuppressedLineValues.value = {
...liveSlashSuppressedLineValues.value,
[String(line)]: String(lineValue ?? '')
}
}
}
/**
* 지정 줄의 슬래시 메뉴 억제를 해제한다.
* @param {number} line - 줄 번호
* @returns {void}
*/
const releaseLiveSlashLine = (line) => {
liveSlashSuppressedLines.value = removeSlashSuppressedLine(liveSlashSuppressedLines.value, line)
if (Number.isFinite(line)) {
const nextValues = { ...liveSlashSuppressedLineValues.value }
delete nextValues[String(line)]
liveSlashSuppressedLineValues.value = nextValues
}
}
/**
* 슬래시 메뉴 하이라이트를 첫 항목으로 되돌린다.
* @returns {void}
*/
const resetLiveSlashHighlightedIndex = () => {
liveSlashHighlightedIndex.value = 0
}
/**
* 슬래시 메뉴 하이라이트를 명령 개수 안으로 보정한다.
* @param {number} commandCount - 표시 가능한 명령 개수
* @returns {void}
*/
const clampLiveSlashHighlightedIndex = (commandCount) => {
liveSlashHighlightedIndex.value = clampSlashHighlightedIndex(liveSlashHighlightedIndex.value, commandCount)
}
/**
* 키보드 탐색 중 mouseenter 하이라이트를 잠시 무시한다.
* @param {number} [durationMs=200] - 유지 시간(ms)
* @returns {void}
*/
const startLiveSlashKeyboardNavigation = (durationMs = 200) => {
liveSlashKeyboardNav.value = true
globalThis.clearTimeout(liveSlashKeyboardNavTimer)
liveSlashKeyboardNavTimer = globalThis.setTimeout(() => {
liveSlashKeyboardNav.value = false
}, durationMs)
}
/**
* 슬래시 메뉴 키보드 탐색 타이머를 정리한다.
* @returns {void}
*/
const stopLiveSlashKeyboardNavigation = () => {
globalThis.clearTimeout(liveSlashKeyboardNavTimer)
liveSlashKeyboardNavTimer = null
liveSlashKeyboardNav.value = false
}
return {
liveSlashSourceLine,
liveSlashQuery,
liveSlashHighlightedIndex,
liveSlashMenuPosition,
liveSlashMenuRef,
liveSlashInsertLine,
liveSlashSuppressedLines,
liveSlashSuppressedLineValues,
liveSlashKeyboardNav,
liveSlashVisible,
liveSlashSuppressedLineList,
visibleLiveSlashCommands,
clearLiveSlashState,
openLiveSlashMenu,
isLiveSlashLineSuppressed,
suppressLiveSlashLine,
releaseLiveSlashLine,
resetLiveSlashHighlightedIndex,
clampLiveSlashHighlightedIndex,
startLiveSlashKeyboardNavigation,
stopLiveSlashKeyboardNavigation
}
}