diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index cef0cce..06b1a59 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -64,6 +64,7 @@ import { normalizeUploadMaxTotalSize } from '../../lib/upload-size-limit.js' import AdminMarkdownModeToggle from './AdminMarkdownModeToggle.vue' +import AdminMarkdownPreviewSurface from './AdminMarkdownPreviewSurface.vue' import AdminMarkdownSlashMenu from './AdminMarkdownSlashMenu.vue' import AdminMarkdownWriteSurface from './AdminMarkdownWriteSurface.vue' @@ -101,8 +102,7 @@ const activeMode = defineModel('editorMode', { const editorRootRef = ref(null) const writeSurfaceRef = ref(null) -const previewRef = ref(null) -const previewRendererRef = ref(null) +const previewSurfaceRef = ref(null) /** 커서가 있는 논리 줄(0-based, `\\n` 기준) */ const activeLogicalLineIndex = ref(0) /** 소스 모드 입력 이벤트 직후 textarea의 최신 본문 값 */ @@ -183,6 +183,36 @@ const MIN_TEXTAREA_HEIGHT_PX = 620 */ const getWriteTextareaElement = () => writeSurfaceRef.value?.getTextareaElement?.() ?? null +/** + * 미리보기 표면 DOM을 반환한다. + * @returns {HTMLElement|null} 미리보기 DOM + */ +const getPreviewSurfaceElement = () => previewSurfaceRef.value?.getPreviewElement?.() ?? null + +/** + * 미리보기 표면 안의 지정 줄 DOM을 반환한다. + * @param {number|string|null} sourceLine - 원본 줄 번호 + * @returns {Element|null} 줄 DOM + */ +const getPreviewSourceLineElement = (sourceLine) => previewSurfaceRef.value?.getSourceLineElement?.(sourceLine) ?? null + +/** + * 라이브 편집 줄에 포커스를 이동한다. + * @param {...unknown} args - 포커스 인자 + * @returns {void} + */ +const focusPreviewEditableAtLine = (...args) => { + previewSurfaceRef.value?.focusEditableAtLine?.(...args) +} + +/** + * 라이브 문서 전체를 선택한다. + * @returns {void} + */ +const selectAllPreviewDocument = () => { + previewSurfaceRef.value?.selectAllLiveDocument?.() +} + const markdownValue = computed({ get: () => normalizeMarkdownContent(props.modelValue), set: (value) => emit('update:modelValue', value) @@ -683,7 +713,7 @@ const restoreEditorFocusAfterHistory = (selection = null) => { ? Math.max(0, selection.start - getLineStartOffset(focusLine)) : 0 - previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'auto', offset, 'center') + focusPreviewEditableAtLine(focusLine, 0, 'auto', offset, 'center') }) } @@ -1214,7 +1244,7 @@ watch(activeMode, (mode) => { } nextTick(() => { - previewRendererRef.value?.focusEditableAtLine(previewFocus.line, 0, 'auto', previewFocus.offset, 'center') + focusPreviewEditableAtLine(previewFocus.line, 0, 'auto', previewFocus.offset, 'center') }) }) }) @@ -1224,8 +1254,8 @@ watch(activeMode, (mode) => { * @returns {void} */ const scrollPreviewToTop = () => { - if (previewRef.value) { - previewRef.value.scrollTop = 0 + if (getPreviewSurfaceElement()) { + getPreviewSurfaceElement().scrollTop = 0 } } @@ -1234,12 +1264,12 @@ const scrollPreviewToTop = () => { * @returns {number|null} 원본 줄 번호 */ const getCurrentPreviewSourceLine = () => { - if (!import.meta.client || !previewRef.value) { + if (!import.meta.client || !getPreviewSurfaceElement()) { return null } const activeElement = document.activeElement - const focusedLineElement = previewRef.value.contains(activeElement) + const focusedLineElement = getPreviewSurfaceElement().contains(activeElement) ? activeElement?.closest?.('[data-source-line]') : null const focusedLine = Number(focusedLineElement?.getAttribute?.('data-source-line')) @@ -1248,9 +1278,9 @@ const getCurrentPreviewSourceLine = () => { return focusedLine } - const previewRect = previewRef.value.getBoundingClientRect() + const previewRect = getPreviewSurfaceElement().getBoundingClientRect() const anchorTop = Math.max(previewRect.top, 0) + 24 - const candidates = [...previewRef.value.querySelectorAll('[data-source-line]')] + const candidates = [...getPreviewSurfaceElement().querySelectorAll('[data-source-line]')] .map((element) => { const rect = element.getBoundingClientRect() const line = Number(element.getAttribute('data-source-line')) @@ -1502,7 +1532,7 @@ const focusFirstBlock = () => { nextTick(() => { nextTick(() => { - previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'start') + focusPreviewEditableAtLine(focusLine, 0, 'start') }) }) @@ -2215,7 +2245,7 @@ const onPreviewInsertAfterLine = ({ afterLine, lines, focusLine }) => { syncBlockPanelState() nextTick(() => { - previewRendererRef.value?.focusEditableAtLine(nextFocusLine, 0, 'start') + focusPreviewEditableAtLine(nextFocusLine, 0, 'start') }) } @@ -2262,7 +2292,7 @@ const handlePreviewKeydownCapture = (event) => { event.preventDefault() event.stopPropagation() - previewRendererRef.value?.selectAllLiveDocument() + selectAllPreviewDocument() return } @@ -2355,7 +2385,7 @@ const getLiveSlashMenuElement = () => { * @returns {void} */ const updatePreviewSlashMenuPosition = () => { - const element = previewRef.value?.querySelector(`[data-source-line="${liveSlashSourceLine.value}"]`) + const element = getPreviewSourceLineElement(liveSlashSourceLine.value) if (!element) { return @@ -2534,7 +2564,7 @@ watch(liveSlashVisible, (visible) => { if (visible) { window.addEventListener('scroll', onReposition, true) window.addEventListener('resize', onReposition) - previewRef.value?.addEventListener('scroll', onReposition) + getPreviewSurfaceElement()?.addEventListener('scroll', onReposition) getWriteTextareaElement()?.addEventListener('scroll', onReposition) updateLiveSlashMenuPosition() scrollLiveSlashHighlightedIntoView() @@ -2543,7 +2573,7 @@ watch(liveSlashVisible, (visible) => { window.removeEventListener('scroll', onReposition, true) window.removeEventListener('resize', onReposition) - previewRef.value?.removeEventListener('scroll', onReposition) + getPreviewSurfaceElement()?.removeEventListener('scroll', onReposition) getWriteTextareaElement()?.removeEventListener('scroll', onReposition) }) @@ -2596,7 +2626,7 @@ const focusPreviewAfterSlashCommand = (line, replacementLines) => { nextTick(() => { requestAnimationFrame(() => { requestAnimationFrame(() => { - previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'end', offset) + focusPreviewEditableAtLine(focusLine, 0, 'end', offset) }) }) }) @@ -2936,7 +2966,7 @@ const updateActiveInlineColor = (color) => { offset: nextVisibleOffset } nextTick(() => { - previewRendererRef.value?.focusEditableAtLine( + focusPreviewEditableAtLine( context.sourceLine, 0, 'auto', @@ -3518,38 +3548,30 @@ const handleKeydown = (event) => { @blur="onTextareaBlur" /> -