에디터 모드 전환 포커스 분리
This commit is contained in:
112
composables/useAdminMarkdownFocusBridge.js
Normal file
112
composables/useAdminMarkdownFocusBridge.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
|
||||
/**
|
||||
* 관리자 Markdown 에디터의 작성/라이브 모드 포커스 복원 상태를 만든다.
|
||||
* @param {Object} options - 포커스 옵션
|
||||
* @param {import('vue').Ref<'write'|'preview'|string>} options.activeMode - 현재 에디터 모드
|
||||
* @param {import('vue').Ref<string>} options.value - Markdown 본문
|
||||
* @param {import('vue').Ref<{ start: number, end: number }>} options.lastSelectionState - 마지막 textarea 선택 위치
|
||||
* @param {import('vue').Ref<number>} options.activeLogicalLineIndex - 현재 논리 줄
|
||||
* @param {() => HTMLTextAreaElement|null} options.getWriteTextareaElement - 작성 textarea 반환 함수
|
||||
* @param {(offset: number) => number} options.getLineIndexAtOffset - 오프셋 기준 줄 계산 함수
|
||||
* @param {(lineIndex: number) => number} options.getLineStartOffset - 줄 시작 오프셋 계산 함수
|
||||
* @param {(line: number, offset: number) => { line: number, offset: number }} options.resolvePreviewFocusPosition - 라이브 포커스 보정 함수
|
||||
* @param {() => number|null} options.getCurrentPreviewSourceLine - 현재 라이브 원본 줄 반환 함수
|
||||
* @param {() => void} options.syncTextareaHeight - textarea 높이 동기화 함수
|
||||
* @param {(lineIndex: number) => void} options.focusTextareaAtLine - 지정 줄 textarea 포커스 함수
|
||||
* @param {() => void} options.restoreTextareaFocus - textarea 선택 복원 함수
|
||||
* @param {() => void} options.scrollPreviewToTop - 라이브 미리보기 상단 이동 함수
|
||||
* @param {(line: number, childIndex: number, position: string, offset?: number, scrollAlign?: string) => void} options.focusPreviewEditableAtLine - 라이브 편집 줄 포커스 함수
|
||||
* @returns {Object} 포커스 브리지 상태와 이벤트
|
||||
*/
|
||||
export const useAdminMarkdownFocusBridge = (options) => {
|
||||
/** 라이브 모드에서 소스 모드로 돌아올 때 복원할 줄(0-based) */
|
||||
const pendingWriteFocusLine = ref(null)
|
||||
/** 소스 모드에서 라이브 모드로 넘어갈 때 복원할 줄·오프셋 */
|
||||
const pendingPreviewFocus = ref(null)
|
||||
|
||||
/**
|
||||
* 현재 textarea 선택 위치를 라이브 모드 포커스 대상으로 저장한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const rememberWritePositionForPreview = () => {
|
||||
const textarea = options.getWriteTextareaElement()
|
||||
const value = options.value.value ?? ''
|
||||
const start = textarea
|
||||
? Math.min(textarea.selectionStart, value.length)
|
||||
: Math.min(options.lastSelectionState.value.start, value.length)
|
||||
const line = options.getLineIndexAtOffset(start)
|
||||
const offset = Math.max(0, start - options.getLineStartOffset(line))
|
||||
const focusPosition = options.resolvePreviewFocusPosition(line, offset)
|
||||
|
||||
pendingPreviewFocus.value = {
|
||||
line: focusPosition.line,
|
||||
offset: focusPosition.offset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 모드의 현재 위치를 소스 모드 복원 대상으로 저장한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const rememberPreviewPosition = () => {
|
||||
pendingWriteFocusLine.value = options.getCurrentPreviewSourceLine()
|
||||
?? options.activeLogicalLineIndex.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 모드 포커스 복원을 예약한다.
|
||||
* @param {{ line: number, offset?: number }} focus - 포커스 위치
|
||||
* @returns {void}
|
||||
*/
|
||||
const requestPreviewFocus = (focus) => {
|
||||
if (!focus || !Number.isFinite(focus.line)) {
|
||||
pendingPreviewFocus.value = null
|
||||
return
|
||||
}
|
||||
|
||||
pendingPreviewFocus.value = {
|
||||
line: focus.line,
|
||||
offset: Math.max(0, Number(focus.offset) || 0)
|
||||
}
|
||||
}
|
||||
|
||||
watch(options.activeMode, (mode) => {
|
||||
if (mode === 'write') {
|
||||
nextTick(() => {
|
||||
options.syncTextareaHeight()
|
||||
const focusLine = pendingWriteFocusLine.value
|
||||
pendingWriteFocusLine.value = null
|
||||
|
||||
if (typeof focusLine === 'number') {
|
||||
options.focusTextareaAtLine(focusLine)
|
||||
} else {
|
||||
options.restoreTextareaFocus()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const previewFocus = pendingPreviewFocus.value
|
||||
pendingPreviewFocus.value = null
|
||||
|
||||
if (!previewFocus) {
|
||||
options.scrollPreviewToTop()
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
options.focusPreviewEditableAtLine(previewFocus.line, 0, 'auto', previewFocus.offset, 'center')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
pendingWriteFocusLine,
|
||||
pendingPreviewFocus,
|
||||
rememberWritePositionForPreview,
|
||||
rememberPreviewPosition,
|
||||
requestPreviewFocus
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user