라이브 인용 콜아웃 입력 보정
This commit is contained in:
@@ -1979,7 +1979,11 @@ const focusPreviewAfterSlashCommand = (line, replacementLines) => {
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'end', offset)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'end', offset)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,16 +33,25 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['commit', 'delete-line', 'insert-below', 'merge-with-previous', 'leave-block'])
|
||||
const emit = defineEmits(['commit', 'delete-line', 'insert-below', 'merge-with-previous', 'leave-block', 'focus-line'])
|
||||
|
||||
const bodyLines = computed(() => {
|
||||
const lines = String(props.modelValue ?? '').replace(/\r/g, '').split('\n')
|
||||
return lines.length ? lines : ['']
|
||||
})
|
||||
|
||||
const bodyLineEntries = computed(() => bodyLines.value.map((text, index) => ({
|
||||
text,
|
||||
index,
|
||||
sourceLine: props.bodySourceLine + index
|
||||
})))
|
||||
|
||||
/**
|
||||
* 콜아웃 마크다운 줄을 반영한다.
|
||||
* @param {string} body - 본문
|
||||
* @param {string[]} contentLines - 본문 줄
|
||||
* @returns {void}
|
||||
*/
|
||||
const commitCallout = (body) => {
|
||||
const contentLines = String(body ?? '').replace(/\r/g, '').split('\n')
|
||||
|
||||
const commitCalloutLines = (contentLines) => {
|
||||
emit('commit', [
|
||||
buildCalloutOpenerLine({
|
||||
calloutEmojiEnabled: props.calloutEmojiEnabled,
|
||||
@@ -55,21 +64,96 @@ const commitCallout = (body) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 본문 편집 반영
|
||||
* @param {string} body - 본문
|
||||
* @returns {void}
|
||||
* 인라인 편집 값을 문자열로 정규화한다.
|
||||
* @param {string|{ value?: string }} payload - 편집 페이로드
|
||||
* @returns {string} 편집 값
|
||||
*/
|
||||
const onBodyCommit = (body) => {
|
||||
commitCallout(body)
|
||||
const normalizeInlineValue = (payload) => {
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
|
||||
return String(payload?.value ?? '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 콜아웃 아래로 이탈한다.
|
||||
* @param {Object} payload - 이탈 페이로드
|
||||
* 아래 줄 삽입 페이로드를 정규화한다.
|
||||
* @param {string|Object} payload - 아래 줄 삽입 페이로드
|
||||
* @returns {{ value: string, before: string, after: string, caretAtStart: boolean }}
|
||||
*/
|
||||
const normalizeInsertPayload = (payload) => {
|
||||
if (typeof payload === 'string') {
|
||||
return {
|
||||
value: payload,
|
||||
before: '',
|
||||
after: '',
|
||||
caretAtStart: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: String(payload?.value ?? ''),
|
||||
before: String(payload?.before ?? ''),
|
||||
after: String(payload?.after ?? ''),
|
||||
caretAtStart: payload?.caretAtStart === true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정한 콜아웃 본문 줄에 포커스를 요청한다.
|
||||
* @param {number} sourceLine - 원본 줄 번호
|
||||
* @returns {void}
|
||||
*/
|
||||
const onExitBelow = (payload) => {
|
||||
emit('insert-below', payload)
|
||||
const focusCalloutLine = (sourceLine) => {
|
||||
emit('focus-line', {
|
||||
line: sourceLine,
|
||||
position: 'start',
|
||||
offset: 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 본문 한 줄 편집 반영
|
||||
* @param {number} lineIndex - 본문 줄 인덱스
|
||||
* @param {string|{ value?: string }} payload - 편집 페이로드
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBodyLineCommit = (lineIndex, payload) => {
|
||||
const nextLines = [...bodyLines.value]
|
||||
nextLines[lineIndex] = normalizeInlineValue(payload)
|
||||
commitCalloutLines(nextLines)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 줄 아래에 콜아웃 본문 줄을 추가한다.
|
||||
* @param {number} lineIndex - 본문 줄 인덱스
|
||||
* @param {Object|string} payload - 아래 줄 삽입 페이로드
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBodyLineInsertBelow = (lineIndex, payload) => {
|
||||
const { value, before, after, caretAtStart } = normalizeInsertPayload(payload)
|
||||
const nextLines = [...bodyLines.value]
|
||||
|
||||
if (caretAtStart && after.length) {
|
||||
nextLines[lineIndex] = after
|
||||
nextLines.splice(lineIndex, 0, '')
|
||||
focusCalloutLine(props.bodySourceLine + lineIndex)
|
||||
commitCalloutLines(nextLines)
|
||||
return
|
||||
}
|
||||
|
||||
if (before.length && after.length) {
|
||||
nextLines[lineIndex] = before
|
||||
nextLines.splice(lineIndex + 1, 0, after)
|
||||
focusCalloutLine(props.bodySourceLine + lineIndex + 1)
|
||||
commitCalloutLines(nextLines)
|
||||
return
|
||||
}
|
||||
|
||||
nextLines[lineIndex] = value
|
||||
nextLines.splice(lineIndex + 1, 0, '')
|
||||
focusCalloutLine(props.bodySourceLine + lineIndex + 1)
|
||||
commitCalloutLines(nextLines)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -90,17 +174,21 @@ const onExitBelow = (payload) => {
|
||||
<span v-if="calloutEmojiEnabled">{{ calloutEmoji || '💡' }}</span>
|
||||
<span v-else class="text-base text-[#8e9cac]">+</span>
|
||||
</span>
|
||||
<ContentMarkdownEditableInline
|
||||
block-class="content-markdown-callout-editor__body min-w-0 flex-1 text-[15px] leading-8 text-[var(--site-text)]"
|
||||
enter-mode="multiline"
|
||||
:source-line="bodySourceLine"
|
||||
:model-value="modelValue"
|
||||
@commit="onBodyCommit"
|
||||
@delete-line="emit('delete-line', $event)"
|
||||
@insert-below="onExitBelow"
|
||||
@merge-with-previous="emit('merge-with-previous', $event)"
|
||||
@leave-block="emit('leave-block', $event)"
|
||||
/>
|
||||
<div class="content-markdown-callout-editor__body-lines min-w-0 flex-1">
|
||||
<ContentMarkdownEditableInline
|
||||
v-for="line in bodyLineEntries"
|
||||
:key="`${blockSourceLine}-callout-line-${line.sourceLine}`"
|
||||
block-class="content-markdown-callout-editor__body min-w-0 text-[15px] leading-8 text-[var(--site-text)]"
|
||||
enter-mode="insert-below"
|
||||
:source-line="line.sourceLine"
|
||||
:model-value="line.text"
|
||||
@commit="onBodyLineCommit(line.index, $event)"
|
||||
@delete-line="emit('delete-line', $event)"
|
||||
@insert-below="onBodyLineInsertBelow(line.index, $event)"
|
||||
@merge-with-previous="emit('merge-with-previous', line.sourceLine, $event)"
|
||||
@leave-block="emit('leave-block', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ProseCallout>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,8 @@ const suppressBlurCommit = ref(false)
|
||||
const splitLock = ref(false)
|
||||
/** 조합 중 Enter 후 compositionend에서 분리할지 */
|
||||
const pendingSplitAfterComposition = ref(false)
|
||||
/** 조합 종료 직후 중복 Enter를 무시할 기준 시각 */
|
||||
const suppressComposedEnterUntil = ref(0)
|
||||
const showingRaw = ref(false)
|
||||
|
||||
/** @returns {string} Enter 동작 모드 */
|
||||
@@ -526,7 +528,7 @@ const navigateToAdjacentBlock = (direction, column, caretMode = 'column') => {
|
||||
const target = elements[currentIndex + direction]
|
||||
|
||||
if (!target) {
|
||||
if (resolvedEnterMode.value === 'multiline' && direction === 1) {
|
||||
if (['insert-below', 'multiline'].includes(resolvedEnterMode.value) && direction === 1) {
|
||||
emit('insert-below', buildInsertBelowPayload())
|
||||
}
|
||||
|
||||
@@ -771,9 +773,14 @@ const onKeydown = (event) => {
|
||||
&& props.sourceLine !== null
|
||||
&& !readEditorValue().trim()
|
||||
) {
|
||||
const lineContext = getCaretLineContext()
|
||||
const sourceLine = resolvedEnterMode.value === 'multiline'
|
||||
? props.sourceLine + lineContext.lineIndex
|
||||
: props.sourceLine
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('delete-line', props.sourceLine)
|
||||
emit('delete-line', sourceLine)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -863,6 +870,17 @@ const onKeydown = (event) => {
|
||||
|
||||
const enterMode = showingRaw.value ? 'insert-below' : resolvedEnterMode.value
|
||||
|
||||
if (
|
||||
event.key === 'Enter'
|
||||
&& !event.shiftKey
|
||||
&& suppressComposedEnterUntil.value
|
||||
&& Date.now() < suppressComposedEnterUntil.value
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey && parseSlashInput(readEditorValue())) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -880,6 +898,7 @@ const onKeydown = (event) => {
|
||||
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
pendingSplitAfterComposition.value = true
|
||||
suppressComposedEnterUntil.value = Date.now() + 240
|
||||
return
|
||||
}
|
||||
|
||||
@@ -917,6 +936,7 @@ const onCompositionEnd = () => {
|
||||
}
|
||||
|
||||
pendingSplitAfterComposition.value = false
|
||||
suppressComposedEnterUntil.value = Date.now() + 240
|
||||
const enterMode = showingRaw.value ? 'insert-below' : resolvedEnterMode.value
|
||||
|
||||
if (enterMode !== 'split-paragraph' && enterMode !== 'insert-below') {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
hasListMarker,
|
||||
hasQuoteMarker,
|
||||
isEmptyListMarkerLine,
|
||||
isEmptyQuoteMarkerLine,
|
||||
parseOrderedListMarker,
|
||||
stripListMarker,
|
||||
stripQuoteMarker
|
||||
@@ -1291,6 +1290,21 @@ const onCalloutBlockInsertBelow = (block, payload) => {
|
||||
onInsertBelowBlock(block, { lines: [''] })
|
||||
}
|
||||
|
||||
/**
|
||||
* 자식 편집기가 요청한 원본 줄에 포커스를 둔다.
|
||||
* @param {{ line?: number, position?: 'start'|'end'|'auto', offset?: number|null }} payload - 포커스 요청
|
||||
* @returns {void}
|
||||
*/
|
||||
const onEditorFocusLine = (payload) => {
|
||||
if (typeof payload?.line !== 'number') {
|
||||
return
|
||||
}
|
||||
|
||||
pendingFocusLine.value = payload.line
|
||||
pendingFocusPosition.value = payload.position || 'auto'
|
||||
pendingFocusOffset.value = typeof payload.offset === 'number' ? payload.offset : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 토글 편집 반영
|
||||
* @param {Object} block - 토글 블록
|
||||
@@ -1751,16 +1765,6 @@ const onQuoteLineInsertBelow = (block, lineIndex, payload) => {
|
||||
nextLines[lineIndex] = formatQuoteLine(value, raw)
|
||||
}
|
||||
|
||||
const committedLine = nextLines[lineIndex] ?? ''
|
||||
|
||||
if (isEmptyQuoteMarkerLine(committedLine)) {
|
||||
nextLines[lineIndex] = ''
|
||||
pendingFocusLine.value = block.meta.startLine + lineIndex
|
||||
pendingFocusPosition.value = 'start'
|
||||
commitInlineBlockLines(block, nextLines)
|
||||
return
|
||||
}
|
||||
|
||||
nextLines.splice(lineIndex + 1, 0, '> ')
|
||||
pendingFocusLine.value = block.meta.startLine + lineIndex + 1
|
||||
pendingFocusPosition.value = 'start'
|
||||
@@ -2610,7 +2614,8 @@ onBeforeUnmount(() => {
|
||||
@commit="onCalloutBlockCommit(block, $event)"
|
||||
@delete-line="onDeleteLine"
|
||||
@insert-below="onCalloutBlockInsertBelow(block, $event)"
|
||||
@merge-with-previous="onMergeWithPreviousLine(block.meta.startLine + 1, $event)"
|
||||
@merge-with-previous="onMergeWithPreviousLine"
|
||||
@focus-line="onEditorFocusLine"
|
||||
/>
|
||||
<ProseCallout
|
||||
v-else-if="block.type === 'callout'"
|
||||
|
||||
@@ -27,15 +27,15 @@ const backgroundClass = computed(() => {
|
||||
return 'prose-callout--yellow'
|
||||
}
|
||||
|
||||
if (props.background === 'red') {
|
||||
return 'prose-callout--red'
|
||||
if (props.background === 'blue') {
|
||||
return 'prose-callout--blue'
|
||||
}
|
||||
|
||||
if (props.background === 'purple') {
|
||||
return 'prose-callout--purple'
|
||||
}
|
||||
|
||||
return 'prose-callout--blue'
|
||||
return 'prose-callout--red'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user