diff --git a/components/admin/AdminEditorBlockPanel.vue b/components/admin/AdminEditorBlockPanel.vue index b944381..5c8b569 100644 --- a/components/admin/AdminEditorBlockPanel.vue +++ b/components/admin/AdminEditorBlockPanel.vue @@ -1,6 +1,12 @@ diff --git a/components/content/ContentMarkdownRenderer.vue b/components/content/ContentMarkdownRenderer.vue index 9fc9a8a..764f929 100644 --- a/components/content/ContentMarkdownRenderer.vue +++ b/components/content/ContentMarkdownRenderer.vue @@ -20,7 +20,7 @@ import { } from '../../lib/markdown-live-edit.js' import { buildCodeBlockLines, parseCodeFenceLine } from '../../lib/markdown-code-block.js' import { buildToggleBlockLines, parseToggleOpenerLine } from '../../lib/markdown-toggle.js' -import { CALLOUT_BACKGROUND_OPTIONS, parseCalloutOptions } from '../../lib/markdown-callout.js' +import { CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS, parseCalloutOptions } from '../../lib/markdown-callout.js' import { createHeadingIdFactory } from '../../lib/markdown-toc.js' import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue' import ProseCodeBlock from './ProseCodeBlock.vue' @@ -61,6 +61,8 @@ const emit = defineEmits([ 'delete-line', 'merge-with-previous-line', 'edit-image', + 'line-focus', + 'line-blur', 'slash-update', 'slash-end', 'slash-apply' @@ -141,7 +143,7 @@ const createBlock = (type = 'paragraph', text = '', level = null, id = '', optio calloutEmojiEnabled: options.calloutEmojiEnabled ?? true, calloutEmoji: options.calloutEmoji || '💡', calloutBackground: options.calloutBackground || 'blue', - quoteBackground: options.quoteBackground || 'pink', + quoteBackground: options.quoteBackground || 'gray', codeLanguage: options.codeLanguage || '', codeShowLineNumbers: options.codeShowLineNumbers !== false }) @@ -192,7 +194,7 @@ const parseQuoteOptions = (value) => { const [key, rawOptionValue] = token.split('=') const optionValue = String(rawOptionValue || '').trim() - if (key?.toLowerCase() === 'bg' && CALLOUT_BACKGROUND_OPTIONS.includes(optionValue)) { + if (key?.toLowerCase() === 'bg' && QUOTE_BACKGROUND_OPTIONS.includes(optionValue)) { quoteBackground = optionValue } }) @@ -1181,6 +1183,51 @@ const commitInlineBlockLines = (block, replacementLines) => { }) } +/** + * 라이브 편집 포커스/클릭 위치의 원본 줄을 상위에 알린다. + * @param {Event} event - 포커스 또는 포인터 이벤트 + * @returns {void} + */ +const emitLiveLineFocus = (event) => { + if (!props.interactive) { + return + } + + const target = event.target + + if (!(target instanceof Element)) { + return + } + + const sourceElement = target.closest('[data-source-line]') + const sourceLine = Number(sourceElement?.getAttribute('data-source-line')) + + if (!Number.isInteger(sourceLine) || sourceLine < 0) { + return + } + + emit('line-focus', sourceLine) +} + +/** + * 라이브 편집 영역을 벗어난 포커스를 상위에 알린다. + * @param {FocusEvent} event - 포커스 이탈 이벤트 + * @returns {void} + */ +const emitLiveLineBlur = (event) => { + if (!props.interactive || !rendererRootRef.value) { + return + } + + const nextTarget = event.relatedTarget + + if (nextTarget instanceof Node && rendererRootRef.value.contains(nextTarget)) { + return + } + + emit('line-blur') +} + /** * 문단 인라인 편집 반영 * @param {Object} block - 블록 @@ -2297,7 +2344,13 @@ onBeforeUnmount(() => {