/** * 관리자 Markdown 라이브 미리보기 포커스 실행 함수를 만든다. * @param {Object} options - 미리보기 포커스 옵션 * @param {import('vue').Ref} options.previewSurfaceRef - 미리보기 표면 컴포넌트 ref * @param {boolean} [options.isClient] - 클라이언트 실행 여부 테스트 값 * @param {() => Element|null} [options.getActiveElement] - 현재 포커스 요소 반환 함수 * @returns {Object} 미리보기 포커스 실행 함수 */ export const useAdminMarkdownPreviewFocus = (options) => { /** * 클라이언트 런타임 여부를 반환한다. * @returns {boolean} 클라이언트 여부 */ const isClientRuntime = () => options.isClient ?? import.meta.client /** * 미리보기 표면 컴포넌트 인스턴스를 반환한다. * @returns {Object|null} 미리보기 표면 컴포넌트 */ const getPreviewSurfaceApi = () => options.previewSurfaceRef.value ?? null /** * 미리보기 표면 DOM을 반환한다. * @returns {HTMLElement|null} 미리보기 DOM */ const getPreviewSurfaceElement = () => getPreviewSurfaceApi()?.getPreviewElement?.() ?? null /** * 미리보기 표면 안의 지정 줄 DOM을 반환한다. * @param {number|string|null} sourceLine - 원본 줄 번호 * @returns {Element|null} 줄 DOM */ const getPreviewSourceLineElement = (sourceLine) => getPreviewSurfaceApi()?.getSourceLineElement?.(sourceLine) ?? null /** * 라이브 편집 줄에 포커스를 이동한다. * @param {...unknown} args - 포커스 인자 * @returns {void} */ const focusPreviewEditableAtLine = (...args) => { getPreviewSurfaceApi()?.focusEditableAtLine?.(...args) } /** * 라이브 문서 전체를 선택한다. * @returns {void} */ const selectAllPreviewDocument = () => { getPreviewSurfaceApi()?.selectAllLiveDocument?.() } /** * 라이브 미리보기 스크롤을 맨 위로 맞춘다. * @returns {void} */ const scrollPreviewToTop = () => { const surface = getPreviewSurfaceElement() if (surface) { surface.scrollTop = 0 } } /** * 포커스된 라이브 줄에서 원본 줄 번호를 읽는다. * @param {HTMLElement} surface - 미리보기 표면 DOM * @returns {number|null} 원본 줄 번호 */ const getFocusedPreviewSourceLine = (surface) => { const activeElement = typeof options.getActiveElement === 'function' ? options.getActiveElement() : document.activeElement const focusedLineElement = surface.contains?.(activeElement) ? activeElement?.closest?.('[data-source-line]') : null const focusedLine = Number(focusedLineElement?.getAttribute?.('data-source-line')) return Number.isFinite(focusedLine) ? focusedLine : null } /** * 화면 상단에 가까운 라이브 줄에서 원본 줄 번호를 읽는다. * @param {HTMLElement} surface - 미리보기 표면 DOM * @returns {number|null} 원본 줄 번호 */ const getNearestPreviewSourceLine = (surface) => { const previewRect = surface.getBoundingClientRect() const anchorTop = Math.max(previewRect.top, 0) + 24 const candidates = [...surface.querySelectorAll('[data-source-line]')] .map((element) => { const rect = element.getBoundingClientRect() const line = Number(element.getAttribute('data-source-line')) return { rect, line } }) .filter(({ rect, line }) => Number.isFinite(line) && rect.height > 0 && rect.bottom >= anchorTop) .sort((a, b) => Math.abs(a.rect.top - anchorTop) - Math.abs(b.rect.top - anchorTop)) return candidates[0]?.line ?? null } /** * 라이브 미리보기에서 현재 포커스 또는 화면 상단에 가까운 원본 줄을 찾는다. * @returns {number|null} 원본 줄 번호 */ const getCurrentPreviewSourceLine = () => { const surface = getPreviewSurfaceElement() if (!isClientRuntime() || !surface) { return null } return getFocusedPreviewSourceLine(surface) ?? getNearestPreviewSourceLine(surface) } return { focusPreviewEditableAtLine, getCurrentPreviewSourceLine, getPreviewSourceLineElement, getPreviewSurfaceElement, scrollPreviewToTop, selectAllPreviewDocument } }