에디터 블록 옵션 계산 분리
This commit is contained in:
@@ -2,18 +2,15 @@
|
||||
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
|
||||
import { resolveActiveBlockContext } from '../../lib/markdown-block-context.js'
|
||||
import {
|
||||
getImageDefaultAltLabel,
|
||||
isImageUrl,
|
||||
parseImageMarkdownLine,
|
||||
serializeImageMarkdown
|
||||
} from '../../lib/markdown-image.js'
|
||||
import {
|
||||
convertHtmlToMarkdown,
|
||||
formatInlineColorMarkdown,
|
||||
readEditableTextFromElement
|
||||
} from '../../lib/markdown-inline.js'
|
||||
import {
|
||||
getMarkdownLineBodyPrefixLength,
|
||||
getSelectionOffsetInElement
|
||||
} from '../../lib/markdown-live-selection.js'
|
||||
import {
|
||||
@@ -42,6 +39,21 @@ import {
|
||||
resolveImageUploadInsertTarget,
|
||||
resolveUploadFetchErrorMessage
|
||||
} from '../../lib/markdown-editor-media.js'
|
||||
import {
|
||||
createCalloutOptionsReplacementPlan,
|
||||
createCodeOptionsReplacementPlan,
|
||||
createEmbedUrlReplacementPlan,
|
||||
createGalleryImageMovePlan,
|
||||
createGalleryImagesAppendPlan,
|
||||
createLiveInlineColorValue,
|
||||
createMediaImageRemovePlan,
|
||||
createMediaImagesReplacementPlan,
|
||||
createMediaImageUpdatePlan,
|
||||
createMediaUseAltPatch,
|
||||
createQuoteBackgroundValue,
|
||||
createSourceInlineColorValue,
|
||||
createToggleOptionsReplacementPlan
|
||||
} from '../../lib/markdown-editor-block-options.js'
|
||||
import {
|
||||
createSlashCommandApplyPlan,
|
||||
createSlashCommandEditorEffectPlan,
|
||||
@@ -61,9 +73,6 @@ import {
|
||||
DEFAULT_SLASH_MENU_WIDTH
|
||||
} from '../../lib/markdown-slash-menu-position.js'
|
||||
import { calculateSlashMenuScrollTop } from '../../lib/markdown-slash-menu-scroll.js'
|
||||
import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
||||
import { buildToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
||||
import { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
|
||||
import { useAdminMarkdownSlashMenu } from '../../composables/useAdminMarkdownSlashMenu.js'
|
||||
import { useAdminMarkdownHistory } from '../../composables/useAdminMarkdownHistory.js'
|
||||
@@ -1213,6 +1222,21 @@ const replaceLineRange = (startLine, endLine, replacementLines, focusEditor = tr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 줄 범위 교체 계획을 현재 마크다운 본문에 적용한다.
|
||||
* @param {{ startLine: number, endLine: number, replacementLines: string[] }|null} plan - 교체 계획
|
||||
* @param {boolean} focusEditor - 교체 후 textarea에 포커스를 돌릴지 여부
|
||||
* @returns {boolean} 적용 여부
|
||||
*/
|
||||
const applyLineReplacementPlan = (plan, focusEditor = false) => {
|
||||
if (!plan) {
|
||||
return false
|
||||
}
|
||||
|
||||
replaceLineRange(plan.startLine, plan.endLine, plan.replacementLines, focusEditor)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 문서 기준 fenced 블록의 닫는 줄을 찾는다.
|
||||
* @param {number} startLine - 시작 줄
|
||||
@@ -1242,32 +1266,10 @@ const resolveCurrentFencedBlockEndLine = (startLine, fallbackEndLine) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const replaceActiveMediaImages = (images) => {
|
||||
const block = activeMediaBlock.value
|
||||
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
|
||||
if (block.kind === 'image') {
|
||||
if (!images[0]) {
|
||||
replaceLineRange(block.startLine, block.endLine, [], false)
|
||||
return
|
||||
}
|
||||
|
||||
replaceLineRange(block.startLine, block.endLine, [createImageMarkdown(images[0])], false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!images.length) {
|
||||
replaceLineRange(block.startLine, block.endLine, [], false)
|
||||
return
|
||||
}
|
||||
|
||||
replaceLineRange(block.startLine, block.endLine, [
|
||||
':::gallery',
|
||||
...images.map(createImageMarkdown),
|
||||
':::'
|
||||
], false)
|
||||
applyLineReplacementPlan(
|
||||
createMediaImagesReplacementPlan(activeMediaBlock.value, images),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1277,15 +1279,14 @@ const replaceActiveMediaImages = (images) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveMediaImage = (imageIndex, patch) => {
|
||||
const block = activeMediaBlock.value
|
||||
const plan = createMediaImageUpdatePlan(activeMediaBlock.value, imageIndex, patch)
|
||||
|
||||
if (!block) {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const images = block.images.map((image, index) => index === imageIndex ? { ...image, ...patch } : image)
|
||||
replaceActiveMediaImages(images)
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1296,24 +1297,12 @@ const updateActiveMediaImage = (imageIndex, patch) => {
|
||||
*/
|
||||
const setActiveMediaUseAlt = (imageIndex, enabled) => {
|
||||
const image = activeMediaBlock.value?.images[imageIndex]
|
||||
const patch = createMediaUseAltPatch(image, enabled)
|
||||
|
||||
if (!image) {
|
||||
if (!patch) {
|
||||
return
|
||||
}
|
||||
|
||||
const filename = getImageDefaultAltLabel(image.url)
|
||||
const patch = { useAlt: enabled }
|
||||
|
||||
if (enabled) {
|
||||
if (!String(image.caption || '').trim()) {
|
||||
const legacy = String(image.legacyBracketLabel || '').trim()
|
||||
|
||||
patch.caption = legacy && legacy !== filename ? legacy : filename
|
||||
}
|
||||
} else if (String(image.caption || '').trim() === filename) {
|
||||
patch.caption = ''
|
||||
}
|
||||
|
||||
updateActiveMediaImage(imageIndex, patch)
|
||||
}
|
||||
|
||||
@@ -2078,22 +2067,13 @@ const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const moveActiveGalleryImage = (imageIndex, direction) => {
|
||||
const block = activeMediaBlock.value
|
||||
const plan = createGalleryImageMovePlan(activeMediaBlock.value, imageIndex, direction)
|
||||
|
||||
if (!block || block.kind !== 'gallery') {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextIndex = imageIndex + direction
|
||||
|
||||
if (nextIndex < 0 || nextIndex >= block.images.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const images = [...block.images]
|
||||
const [target] = images.splice(imageIndex, 1)
|
||||
images.splice(nextIndex, 0, target)
|
||||
replaceActiveMediaImages(images)
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2102,13 +2082,10 @@ const moveActiveGalleryImage = (imageIndex, direction) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const removeActiveMediaImage = (imageIndex) => {
|
||||
const block = activeMediaBlock.value
|
||||
|
||||
if (!block) {
|
||||
return
|
||||
}
|
||||
|
||||
replaceActiveMediaImages(block.images.filter((_, index) => index !== imageIndex))
|
||||
applyLineReplacementPlan(
|
||||
createMediaImageRemovePlan(activeMediaBlock.value, imageIndex),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2117,14 +2094,14 @@ const removeActiveMediaImage = (imageIndex) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const appendImagesToActiveGallery = (images) => {
|
||||
const block = activeMediaBlock.value
|
||||
const plan = createGalleryImagesAppendPlan(activeMediaBlock.value, images)
|
||||
|
||||
if (!block || block.kind !== 'gallery') {
|
||||
if (!plan) {
|
||||
insertGallery(images)
|
||||
return
|
||||
}
|
||||
|
||||
replaceActiveMediaImages([...block.images, ...images])
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2133,21 +2110,14 @@ const appendImagesToActiveGallery = (images) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveEmbedUrl = (url) => {
|
||||
const block = activeBlockContext.value
|
||||
const plan = createEmbedUrlReplacementPlan(activeBlockContext.value, url)
|
||||
|
||||
if (!block || !['link', 'iframe', 'html'].includes(block.kind)) {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const trimmed = String(url || '').trim()
|
||||
|
||||
if (!trimmed) {
|
||||
replaceLineRange(block.startLine, block.endLine, [], false)
|
||||
return
|
||||
}
|
||||
|
||||
replaceLineRange(block.startLine, block.endLine, [`:::${block.kind}`, trimmed, ':::'], false)
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2156,30 +2126,18 @@ const updateActiveEmbedUrl = (url) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveQuoteBackground = (background) => {
|
||||
const block = activeBlockContext.value
|
||||
const nextValue = createQuoteBackgroundValue({
|
||||
value: markdownValue.value,
|
||||
block: activeBlockContext.value,
|
||||
background
|
||||
})
|
||||
|
||||
if (!block || block.kind !== 'quote') {
|
||||
if (nextValue === null) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const value = String(background || '').trim()
|
||||
|
||||
if (!QUOTE_BACKGROUND_OPTIONS.includes(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
const optionLine = `> [!bg=${value}]`
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
if (block.hasQuoteOptions) {
|
||||
nextLines.splice(block.startLine, 1, optionLine)
|
||||
} else {
|
||||
nextLines.splice(block.startLine, 0, optionLine)
|
||||
}
|
||||
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
markdownValue.value = nextValue
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2188,27 +2146,14 @@ const updateActiveQuoteBackground = (background) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCalloutOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
const plan = createCalloutOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!block || block.kind !== 'callout') {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextBackground = CALLOUT_BACKGROUND_OPTIONS.includes(patch.calloutBackground)
|
||||
? patch.calloutBackground
|
||||
: block.calloutBackground
|
||||
const nextLine = buildCalloutOpenerLine({
|
||||
calloutEmojiEnabled: patch.calloutEmojiEnabled ?? block.calloutEmojiEnabled,
|
||||
calloutEmoji: patch.calloutEmoji ?? block.calloutEmoji,
|
||||
calloutBackground: nextBackground,
|
||||
title: patch.title ?? block.title
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2217,22 +2162,14 @@ const updateActiveCalloutOptions = (patch = {}) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCodeOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
const plan = createCodeOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!block || block.kind !== 'code') {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextLine = buildCodeFenceOpener({
|
||||
language: patch.language ?? block.language,
|
||||
showLineNumbers: patch.showLineNumbers ?? block.showLineNumbers
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2241,22 +2178,14 @@ const updateActiveCodeOptions = (patch = {}) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveToggleOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
const plan = createToggleOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!block || block.kind !== 'toggle') {
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextLine = buildToggleOpenerLine({
|
||||
title: block.title,
|
||||
defaultOpen: patch.defaultOpen ?? block.defaultOpen
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2273,51 +2202,44 @@ const updateActiveInlineColor = (color) => {
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
|
||||
if (context.mode === 'source') {
|
||||
const value = markdownValue.value || ''
|
||||
const start = Math.max(0, Math.min(context.start ?? 0, value.length))
|
||||
const end = Math.max(start, Math.min(context.end ?? start, value.length))
|
||||
const selectedText = value.slice(start, end)
|
||||
const replacement = formatInlineColorMarkdown(color, selectedText)
|
||||
const sourceResult = createSourceInlineColorValue({
|
||||
value: markdownValue.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
markdownValue.value = `${value.slice(0, start)}${replacement}${value.slice(end)}`
|
||||
if (sourceResult) {
|
||||
markdownValue.value = sourceResult.value
|
||||
inlineColorSelectionContext.value = null
|
||||
setTextareaSelection(start + replacement.length, start + replacement.length, { preserveScroll: true })
|
||||
setTextareaSelection(sourceResult.selectionStart, sourceResult.selectionEnd, { preserveScroll: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (context.mode === 'live' && Number.isInteger(context.sourceLine)) {
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const line = lines[context.sourceLine]
|
||||
const liveResult = createLiveInlineColorValue({
|
||||
value: markdownValue.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
if (typeof line !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const prefixLength = getMarkdownLineBodyPrefixLength(line, context.bodyText || '')
|
||||
const absoluteStart = prefixLength + Math.max(0, context.startOffset ?? 0)
|
||||
const absoluteEnd = prefixLength + Math.max(context.startOffset ?? 0, context.endOffset ?? 0)
|
||||
const selectedText = line.slice(absoluteStart, absoluteEnd)
|
||||
const replacement = formatInlineColorMarkdown(color, selectedText)
|
||||
const nextVisibleOffset = (context.startOffset ?? 0) + selectedText.length
|
||||
|
||||
lines.splice(context.sourceLine, 1, `${line.slice(0, absoluteStart)}${replacement}${line.slice(absoluteEnd)}`)
|
||||
markdownValue.value = lines.join('\n')
|
||||
inlineColorSelectionContext.value = null
|
||||
requestPreviewFocus({
|
||||
line: context.sourceLine,
|
||||
offset: nextVisibleOffset
|
||||
})
|
||||
nextTick(() => {
|
||||
focusPreviewEditableAtLine(
|
||||
context.sourceLine,
|
||||
0,
|
||||
'auto',
|
||||
nextVisibleOffset,
|
||||
'nearest'
|
||||
)
|
||||
})
|
||||
if (!liveResult) {
|
||||
return
|
||||
}
|
||||
|
||||
markdownValue.value = liveResult.value
|
||||
inlineColorSelectionContext.value = null
|
||||
requestPreviewFocus({
|
||||
line: liveResult.focusLine,
|
||||
offset: liveResult.focusOffset
|
||||
})
|
||||
nextTick(() => {
|
||||
focusPreviewEditableAtLine(
|
||||
liveResult.focusLine,
|
||||
0,
|
||||
'auto',
|
||||
liveResult.focusOffset,
|
||||
'nearest'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user