코드 블록 슬래시와 되돌리기 수정
This commit is contained in:
@@ -104,6 +104,16 @@ const liveSlashVisible = computed(() => liveSlashSourceLine.value !== null)
|
||||
const liveSlashSuppressedLineList = computed(() => [...liveSlashSuppressedLines.value])
|
||||
|
||||
const visibleLiveSlashCommands = computed(() => filterSlashCommands(liveSlashQuery.value))
|
||||
/** 되돌리기 히스토리 최대 보관 개수 */
|
||||
const EDITOR_UNDO_LIMIT = 100
|
||||
/** @type {import('vue').Ref<string[]>} 본문 되돌리기 스택 */
|
||||
const editorUndoStack = ref([])
|
||||
/** @type {import('vue').Ref<string[]>} 본문 다시 실행 스택 */
|
||||
const editorRedoStack = ref([])
|
||||
/** 히스토리 복원 중 변경 기록을 막는다. */
|
||||
const isRestoringEditorHistory = ref(false)
|
||||
/** 마지막 히스토리 기록 시각 */
|
||||
let lastEditorHistoryRecordAt = 0
|
||||
const mediaPickerAccept = computed(() => {
|
||||
if (['image', 'gallery', 'active-gallery'].includes(mediaPickerTarget.value)) {
|
||||
return 'image/*'
|
||||
@@ -378,6 +388,97 @@ const findCodeFenceRangeAtLine = (lines, targetLine) => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정 줄이 코드 펜스 본문 줄인지 확인한다.
|
||||
* @param {string[]} lines - 마크다운 줄 목록
|
||||
* @param {number} lineIndex - 줄 번호
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isCodeFenceBodyLine = (lines, lineIndex) => {
|
||||
for (let index = 0; index <= lineIndex; index += 1) {
|
||||
if (!String(lines[index] ?? '').trim().startsWith('```')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const closingLine = findNextFencedClosingLine(lines, index, '```')
|
||||
|
||||
if (lineIndex > index && (closingLine === null || lineIndex < closingLine)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (closingLine !== null) {
|
||||
index = closingLine
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택 위치에 맞춰 에디터 포커스를 복원한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const restoreEditorFocusAfterHistory = () => {
|
||||
const focusLine = activeLogicalLineIndex.value
|
||||
|
||||
if (activeMode.value === 'write') {
|
||||
focusTextareaAtLine(focusLine)
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'auto')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 에디터 히스토리 항목을 기록한다.
|
||||
* @param {string} previousValue - 변경 전 본문
|
||||
* @returns {void}
|
||||
*/
|
||||
const recordEditorHistory = (previousValue) => {
|
||||
if (isRestoringEditorHistory.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = String(previousValue ?? '')
|
||||
const now = Date.now()
|
||||
const shouldGroup = now - lastEditorHistoryRecordAt < 900
|
||||
|
||||
if (!shouldGroup || !editorUndoStack.value.length) {
|
||||
editorUndoStack.value = [...editorUndoStack.value, value].slice(-EDITOR_UNDO_LIMIT)
|
||||
}
|
||||
|
||||
editorRedoStack.value = []
|
||||
lastEditorHistoryRecordAt = now
|
||||
}
|
||||
|
||||
/**
|
||||
* 에디터 본문 히스토리를 복원한다.
|
||||
* @param {'undo'|'redo'} direction - 복원 방향
|
||||
* @returns {void}
|
||||
*/
|
||||
const restoreEditorHistory = (direction) => {
|
||||
const sourceStack = direction === 'undo' ? editorUndoStack : editorRedoStack
|
||||
const targetStack = direction === 'undo' ? editorRedoStack : editorUndoStack
|
||||
const nextValue = sourceStack.value[sourceStack.value.length - 1]
|
||||
|
||||
if (typeof nextValue !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
sourceStack.value = sourceStack.value.slice(0, -1)
|
||||
targetStack.value = [...targetStack.value, markdownValue.value ?? ''].slice(-EDITOR_UNDO_LIMIT)
|
||||
isRestoringEditorHistory.value = true
|
||||
markdownValue.value = nextValue
|
||||
lastEditorHistoryRecordAt = 0
|
||||
|
||||
nextTick(() => {
|
||||
isRestoringEditorHistory.value = false
|
||||
restoreEditorFocusAfterHistory()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 모드에 실제로 존재하는 편집 줄로 포커스 대상을 보정한다.
|
||||
* @param {number} line - 원본 줄 번호
|
||||
@@ -652,6 +753,12 @@ const syncWriteSlashState = () => {
|
||||
}
|
||||
|
||||
const lines = (markdownValue.value ?? '').split('\n')
|
||||
|
||||
if (isCodeFenceBodyLine(lines, line)) {
|
||||
onLiveSlashEnd({ sourceLine: line })
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = parseSlashInput(lines[line] ?? '')
|
||||
|
||||
if (!parsed) {
|
||||
@@ -816,6 +923,14 @@ watch(() => props.modelValue, () => {
|
||||
refreshCaretLogicalLine()
|
||||
})
|
||||
|
||||
watch(markdownValue, (nextValue, previousValue) => {
|
||||
if (nextValue === previousValue) {
|
||||
return
|
||||
}
|
||||
|
||||
recordEditorHistory(previousValue)
|
||||
})
|
||||
|
||||
watch(activeMode, (mode) => {
|
||||
if (mode === 'write') {
|
||||
nextTick(() => {
|
||||
@@ -1007,6 +1122,22 @@ 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')
|
||||
|
||||
if (isUndoShortcut || isRedoShortcut) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
restoreEditorHistory(isUndoShortcut ? 'undo' : 'redo')
|
||||
return
|
||||
}
|
||||
|
||||
if (liveSlashVisible.value) {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
@@ -2128,6 +2259,13 @@ const onLiveSlashUpdate = (payload) => {
|
||||
return
|
||||
}
|
||||
|
||||
const lines = (markdownValue.value ?? '').split('\n')
|
||||
|
||||
if (isCodeFenceBodyLine(lines, payload.sourceLine)) {
|
||||
onLiveSlashEnd({ sourceLine: payload.sourceLine })
|
||||
return
|
||||
}
|
||||
|
||||
liveSlashSourceLine.value = payload.sourceLine
|
||||
liveSlashQuery.value = String(payload.query ?? '')
|
||||
nextTick(updateLiveSlashMenuPosition)
|
||||
|
||||
Reference in New Issue
Block a user