라이브 콜아웃 선택 정렬 보정
This commit is contained in:
@@ -33,19 +33,13 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['commit', 'delete-line', 'insert-below', 'merge-with-previous', 'leave-block', 'focus-line'])
|
||||
const emit = defineEmits(['commit', 'delete-line', 'insert-below', 'merge-with-previous', 'leave-block'])
|
||||
|
||||
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[]} contentLines - 본문 줄
|
||||
@@ -64,96 +58,26 @@ const commitCalloutLines = (contentLines) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 편집 값을 문자열로 정규화한다.
|
||||
* 콜아웃 본문 문자열을 줄 목록으로 정규화한다.
|
||||
* @param {string|{ value?: string }} payload - 편집 페이로드
|
||||
* @returns {string} 편집 값
|
||||
* @returns {string[]} 본문 줄
|
||||
*/
|
||||
const normalizeInlineValue = (payload) => {
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
const normalizeBodyLines = (payload) => {
|
||||
const value = typeof payload === 'string'
|
||||
? payload
|
||||
: String(payload?.value ?? '')
|
||||
|
||||
return String(payload?.value ?? '')
|
||||
const lines = String(value ?? '').replace(/\r/g, '').split('\n')
|
||||
return lines.length ? lines : ['']
|
||||
}
|
||||
|
||||
/**
|
||||
* 아래 줄 삽입 페이로드를 정규화한다.
|
||||
* @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 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)
|
||||
const onBodyCommit = (payload) => {
|
||||
commitCalloutLines(normalizeBodyLines(payload))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -168,27 +92,25 @@ const onBodyLineInsertBelow = (lineIndex, payload) => {
|
||||
>
|
||||
<div class="content-markdown-callout-editor__inner flex items-start gap-2">
|
||||
<span
|
||||
v-if="calloutEmojiEnabled"
|
||||
class="content-markdown-callout-editor__emoji inline-flex size-9 shrink-0 items-center justify-center rounded-md text-xl text-[var(--site-text)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span v-if="calloutEmojiEnabled">{{ calloutEmoji || '💡' }}</span>
|
||||
<span v-else class="text-base text-[#8e9cac]">+</span>
|
||||
{{ calloutEmoji || '💡' }}
|
||||
</span>
|
||||
<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>
|
||||
<ContentMarkdownEditableInline
|
||||
block-class="content-markdown-callout-editor__body min-w-0 flex-1 text-[15px] leading-8 text-[var(--site-text)]"
|
||||
enter-mode="multiline"
|
||||
plain-text
|
||||
:source-line="bodySourceLine"
|
||||
:source-line-count="bodyLines.length"
|
||||
:model-value="modelValue"
|
||||
@commit="onBodyCommit"
|
||||
@delete-line="emit('delete-line', $event)"
|
||||
@insert-below="emit('insert-below', $event)"
|
||||
@merge-with-previous="emit('merge-with-previous', bodySourceLine, $event)"
|
||||
@leave-block="emit('leave-block', $event)"
|
||||
/>
|
||||
</div>
|
||||
</ProseCallout>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,11 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
/** 이 편집 영역이 대표하는 원본 줄 수 */
|
||||
sourceLineCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/** 루트 요소 태그 */
|
||||
tag: {
|
||||
type: String,
|
||||
@@ -538,7 +543,7 @@ const navigateToAdjacentBlock = (direction, column, caretMode = 'column') => {
|
||||
if (!target) {
|
||||
if (
|
||||
direction === 1
|
||||
&& (resolvedEnterMode.value === 'multiline' || props.arrowExitCreatesLine)
|
||||
&& props.arrowExitCreatesLine
|
||||
) {
|
||||
emit('insert-below', buildInsertBelowPayload())
|
||||
}
|
||||
@@ -1017,6 +1022,7 @@ defineExpose({ focusEditor, readEditorValue })
|
||||
showingRaw ? 'content-markdown-editable-inline--raw' : ''
|
||||
]"
|
||||
:data-source-line="sourceLine ?? undefined"
|
||||
:data-source-line-end="sourceLine !== null ? sourceLine + Math.max(1, sourceLineCount) - 1 : undefined"
|
||||
contenteditable="true"
|
||||
spellcheck="true"
|
||||
@focus="onFocus"
|
||||
|
||||
@@ -858,9 +858,23 @@ const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', ca
|
||||
const matches = rendererRootRef.value
|
||||
? [...rendererRootRef.value.querySelectorAll(`[data-source-line="${lineIndex}"]`)]
|
||||
: []
|
||||
const rangedMatches = rendererRootRef.value
|
||||
? [...rendererRootRef.value.querySelectorAll('[data-source-line][data-source-line-end]')]
|
||||
.filter((node) => {
|
||||
const start = Number(node.getAttribute('data-source-line'))
|
||||
const end = Number(node.getAttribute('data-source-line-end'))
|
||||
|
||||
return node.getAttribute('contenteditable') === 'true'
|
||||
&& Number.isInteger(start)
|
||||
&& Number.isInteger(end)
|
||||
&& start <= lineIndex
|
||||
&& lineIndex <= end
|
||||
})
|
||||
: []
|
||||
|
||||
const element = matches.find((node) => node.getAttribute('contenteditable') === 'true')
|
||||
|| matches[0]
|
||||
|| rangedMatches[0]
|
||||
|| null
|
||||
|
||||
if (!element) {
|
||||
@@ -875,8 +889,10 @@ const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', ca
|
||||
|
||||
const line = getMarkdownLine(lineIndex)
|
||||
const isBlankMarker = /^>\s*$/.test(line) || /^[-*+]\s*$/.test(line) || /^\d+\.\s*$/.test(line.trim())
|
||||
const elementSourceLine = Number(element.getAttribute('data-source-line'))
|
||||
const isRangedLine = Number.isInteger(elementSourceLine) && elementSourceLine !== lineIndex
|
||||
|
||||
if (isBlankMarker || !line.trim()) {
|
||||
if (!isRangedLine && (isBlankMarker || !line.trim())) {
|
||||
if (element.getAttribute('contenteditable') === 'true') {
|
||||
element.textContent = ''
|
||||
element.innerHTML = ''
|
||||
@@ -896,6 +912,23 @@ const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', ca
|
||||
return
|
||||
}
|
||||
|
||||
if (isRangedLine && Number.isInteger(elementSourceLine)) {
|
||||
const text = readEditableTextFromElement(/** @type {HTMLElement} */ (element))
|
||||
const textLines = text.length ? text.split('\n') : ['']
|
||||
const targetLineIndex = Math.max(0, Math.min(lineIndex - elementSourceLine, textLines.length - 1))
|
||||
const lineOffset = textLines
|
||||
.slice(0, targetLineIndex)
|
||||
.reduce((sum, textLine) => sum + textLine.length + 1, 0)
|
||||
const lineText = textLines[targetLineIndex] ?? ''
|
||||
const nextOffset = cursorPosition === 'end'
|
||||
? lineOffset + lineText.length
|
||||
: lineOffset
|
||||
|
||||
setEditableCaretOffset(/** @type {HTMLElement} */ (element), nextOffset)
|
||||
element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
|
||||
return
|
||||
}
|
||||
|
||||
if (cursorPosition === 'start' || (cursorPosition === 'auto' && (isBlankMarker || !line.trim()))) {
|
||||
setEditableCaretOffset(/** @type {HTMLElement} */ (element), 0)
|
||||
element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
|
||||
|
||||
@@ -41,10 +41,10 @@ const backgroundClass = computed(() => {
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="prose-callout prose-callout-card mt-8 rounded-[10px] p-5 text-[15px] leading-8 text-[var(--site-text)]"
|
||||
class="prose-callout prose-callout-card mb-2.5 rounded-[10px] p-5 text-[15px] leading-8 text-[var(--site-text)]"
|
||||
:class="backgroundClass"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<span v-if="emojiEnabled" class="inline-flex shrink-0 text-[20px] leading-none">{{ emoji || '💡' }}</span>
|
||||
<div class="min-w-0 flex-1 whitespace-pre-line">
|
||||
<slot />
|
||||
|
||||
Reference in New Issue
Block a user