v1.2.9: 라이브 에디터·홈 피드·메인 커버 개선
라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기, 사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import {
|
||||
convertEditableHtmlToMarkdown,
|
||||
getRangeInnerHtml,
|
||||
escapeHtml,
|
||||
getEditableCaretOffset,
|
||||
markdownInlineToHtml,
|
||||
markdownMultilineInlineToHtml
|
||||
readEditableTextFromElement,
|
||||
setEditableCaretOffset
|
||||
} from '../../lib/markdown-inline.js'
|
||||
import { parseSlashInput } from '../../lib/markdown-slash-commands.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** 인라인 마크다운 원문 */
|
||||
@@ -12,31 +14,32 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 여러 줄(Shift+Enter 줄바꿈) 허용 */
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* Enter 동작
|
||||
* - split-paragraph: 문단 분리
|
||||
* - insert-below: 블록 아래 새 줄 삽입
|
||||
* - none: 기본(제목 등 단일 줄 blur)
|
||||
* - focus-next: Enter 시 blur 없이 enter-advance 발생
|
||||
* - multiline: Enter 기본 동작(코드·콜아웃 본문 등)
|
||||
*/
|
||||
enterMode: {
|
||||
type: String,
|
||||
default: 'none'
|
||||
},
|
||||
/**
|
||||
* ↑↓ 이동 범위
|
||||
* - document: 렌더러 전체 블록
|
||||
* - parent: 가장 가까운 data-editable-scope 컨테이너 안
|
||||
*/
|
||||
navigationScope: {
|
||||
type: String,
|
||||
default: 'document'
|
||||
},
|
||||
/** @deprecated enterMode 사용 */
|
||||
splitOnEnter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** Shift+Enter 줄바꿈 허용 */
|
||||
allowHardBreak: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 원본 마크다운 줄 번호(0-based) */
|
||||
sourceLine: {
|
||||
type: Number,
|
||||
@@ -61,10 +64,38 @@ const props = defineProps({
|
||||
allowRawToggle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 슬래시 메뉴가 열려 키보드 탐색 중인지 */
|
||||
slashMenuActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** ESC 등으로 이 줄의 슬래시 메뉴를 닫은 상태(문자 `/`는 유지) */
|
||||
slashCommandSuppressed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** true면 인라인 마크다운 변환 없이 줄바꿈 유지(코드 블록 등) */
|
||||
plainText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['commit', 'split', 'insert-below', 'delete-line', 'raw-mode'])
|
||||
const emit = defineEmits([
|
||||
'commit',
|
||||
'input',
|
||||
'split',
|
||||
'insert-below',
|
||||
'delete-line',
|
||||
'merge-with-previous',
|
||||
'raw-mode',
|
||||
'slash-update',
|
||||
'slash-end',
|
||||
'slash-apply',
|
||||
'enter-advance',
|
||||
'leave-block'
|
||||
])
|
||||
|
||||
const rootRef = ref(null)
|
||||
const isFocused = ref(false)
|
||||
@@ -87,9 +118,14 @@ const resolvedEnterMode = computed(() => {
|
||||
* modelValue를 편집용 HTML로 변환한다.
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
const toEditorHtml = () => (props.multiline
|
||||
? markdownMultilineInlineToHtml(props.modelValue)
|
||||
: markdownInlineToHtml(props.modelValue))
|
||||
const toEditorHtml = () => markdownInlineToHtml(props.modelValue.replace(/\n/g, ' '))
|
||||
|
||||
/**
|
||||
* plainText 모드용 편집 HTML을 만든다.
|
||||
* @param {string} value - 본문
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
const plainTextToEditorHtml = (value) => escapeHtml(String(value ?? '')).replace(/\n/g, '<br>')
|
||||
|
||||
/**
|
||||
* 편집 영역 HTML을 동기화한다.
|
||||
@@ -105,15 +141,38 @@ const syncEditorHtml = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.plainText) {
|
||||
rootRef.value.innerHTML = plainTextToEditorHtml(props.modelValue)
|
||||
return
|
||||
}
|
||||
|
||||
rootRef.value.innerHTML = toEditorHtml()
|
||||
}
|
||||
|
||||
watch(() => [props.modelValue, props.rawLine], () => {
|
||||
if (!isFocused.value) {
|
||||
showingRaw.value = false
|
||||
syncEditorHtml()
|
||||
return
|
||||
}
|
||||
|
||||
syncEditorHtml()
|
||||
if (showingRaw.value) {
|
||||
if (props.rawLine && rootRef.value && rootRef.value.textContent !== props.rawLine) {
|
||||
rootRef.value.textContent = props.rawLine
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const current = readEditorValue()
|
||||
|
||||
if (current !== props.modelValue) {
|
||||
if (props.plainText) {
|
||||
rootRef.value.innerHTML = plainTextToEditorHtml(props.modelValue)
|
||||
} else {
|
||||
rootRef.value.innerHTML = toEditorHtml()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -126,6 +185,43 @@ onMounted(() => {
|
||||
*/
|
||||
const onFocus = () => {
|
||||
isFocused.value = true
|
||||
syncSlashState()
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령 입력 상태를 부모에 알린다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const syncSlashState = () => {
|
||||
if (props.sourceLine === null || showingRaw.value) {
|
||||
emit('slash-end')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.slashCommandSuppressed) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = parseSlashInput(readEditorValue())
|
||||
|
||||
if (!parsed) {
|
||||
emit('slash-end', { sourceLine: props.sourceLine })
|
||||
return
|
||||
}
|
||||
|
||||
emit('slash-update', {
|
||||
sourceLine: props.sourceLine,
|
||||
query: parsed.query,
|
||||
value: parsed.raw
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 시 슬래시 상태를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onEditorInput = () => {
|
||||
syncSlashState()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +241,7 @@ const readEditorValue = () => {
|
||||
return rootRef.value.textContent ?? ''
|
||||
}
|
||||
|
||||
return convertEditableHtmlToMarkdown(rootRef.value.innerHTML)
|
||||
return readEditableTextFromElement(rootRef.value)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
@@ -265,6 +361,30 @@ const getRawPrefixLength = () => {
|
||||
return match ? match[1].length : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 영역에 접힙지 않은 텍스트 선택이 있는지 확인한다.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const hasNonCollapsedSelection = () => {
|
||||
if (!import.meta.client || !rootRef.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const selection = window.getSelection()
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
if (!rootRef.value.contains(range.commonAncestorContainer)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !range.collapsed
|
||||
}
|
||||
|
||||
/**
|
||||
* 커서가 줄의 논리적 맨 앞인지 확인한다(원문 접두사 직후 포함).
|
||||
* @returns {boolean}
|
||||
@@ -290,25 +410,109 @@ const isCaretAtLogicalStart = () => {
|
||||
const isCaretAtLogicalEnd = () => isCaretAtEdge('end')
|
||||
|
||||
/**
|
||||
* 이전·다음 편집 줄로 포커스를 옮긴다.
|
||||
* @param {number} direction - -1 이전 줄, 1 다음 줄
|
||||
* @param {boolean} cursorAtStart - 커서를 줄 앞에 둘지
|
||||
* 커서가 위치한 시각 줄 정보를 반환한다.
|
||||
* @returns {{ column: number, lineIndex: number, lines: string[], isFirstLine: boolean, isLastLine: boolean }}
|
||||
*/
|
||||
const getCaretLineContext = () => {
|
||||
const fallback = {
|
||||
column: 0,
|
||||
lineIndex: 0,
|
||||
lines: [''],
|
||||
isFirstLine: true,
|
||||
isLastLine: true
|
||||
}
|
||||
|
||||
if (!import.meta.client || !rootRef.value) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const selection = window.getSelection()
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
if (!rootRef.value.contains(range.commonAncestorContainer)) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const full = readEditorValue()
|
||||
const offset = getEditableCaretOffset(rootRef.value, range)
|
||||
const lines = full.length ? full.split('\n') : ['']
|
||||
const before = full.slice(0, offset)
|
||||
const lineIndex = before.split('\n').length - 1
|
||||
const lineStart = before.lastIndexOf('\n') + 1
|
||||
const column = offset - lineStart
|
||||
|
||||
return {
|
||||
column,
|
||||
lineIndex,
|
||||
lines,
|
||||
isFirstLine: lineIndex === 0,
|
||||
isLastLine: lineIndex >= lines.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 줄 배열에서 지정 줄·열의 오프셋을 계산한다.
|
||||
* @param {string[]} lines - 줄 목록
|
||||
* @param {number} lineIndex - 줄 인덱스
|
||||
* @param {number} column - 열
|
||||
* @returns {number} 오프셋
|
||||
*/
|
||||
const getOffsetForLineColumn = (lines, lineIndex, column) => {
|
||||
let offset = 0
|
||||
|
||||
for (let index = 0; index < lineIndex; index += 1) {
|
||||
offset += (lines[index] ?? '').length + 1
|
||||
}
|
||||
|
||||
const line = lines[lineIndex] ?? ''
|
||||
|
||||
return offset + Math.min(Math.max(column, 0), line.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 인접 편집 블록으로 포커스를 옮긴다.
|
||||
* @param {number} direction - -1 이전, 1 다음
|
||||
* @param {number} column - 유지할 열(column 모드)
|
||||
* @param {'column'|'block-start'|'block-end'} [caretMode='column'] - 커서 배치
|
||||
* @returns {void}
|
||||
*/
|
||||
const navigateToAdjacentLine = (direction, cursorAtStart) => {
|
||||
/**
|
||||
* ↑↓ 이동 대상 편집 요소 목록을 반환한다.
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
const getNavigationElements = () => {
|
||||
if (!import.meta.client || !rootRef.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const root = props.navigationScope === 'parent'
|
||||
? rootRef.value.closest('[data-editable-scope]')
|
||||
: rootRef.value.closest('.content-markdown-renderer')
|
||||
|
||||
if (!root) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [...root.querySelectorAll('[data-source-line]')]
|
||||
.sort((left, right) => Number(left.dataset.sourceLine) - Number(right.dataset.sourceLine))
|
||||
}
|
||||
|
||||
const navigateToAdjacentBlock = (direction, column, caretMode = 'column') => {
|
||||
if (!import.meta.client || props.sourceLine === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = rootRef.value?.closest('.content-markdown-renderer')
|
||||
const elements = getNavigationElements()
|
||||
|
||||
if (!container) {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const elements = [...container.querySelectorAll('[data-source-line]')]
|
||||
.sort((left, right) => Number(left.dataset.sourceLine) - Number(right.dataset.sourceLine))
|
||||
|
||||
const currentIndex = elements.findIndex(
|
||||
(element) => Number(element.dataset.sourceLine) === props.sourceLine
|
||||
)
|
||||
@@ -320,16 +524,113 @@ const navigateToAdjacentLine = (direction, cursorAtStart) => {
|
||||
const target = elements[currentIndex + direction]
|
||||
|
||||
if (!target) {
|
||||
if (resolvedEnterMode.value === 'multiline' && direction === 1) {
|
||||
emit('insert-below', buildInsertBelowPayload())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
target.focus()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(target)
|
||||
range.collapse(cursorAtStart)
|
||||
if (props.navigationScope === 'parent') {
|
||||
emit('leave-block', { direction })
|
||||
}
|
||||
|
||||
target.focus({ preventScroll: true })
|
||||
|
||||
const scopedCaretMode = props.navigationScope === 'parent'
|
||||
? (direction === 1 ? 'block-start' : 'block-end')
|
||||
: caretMode
|
||||
|
||||
const targetText = readEditableTextFromElement(target)
|
||||
const targetLines = targetText.length ? targetText.split('\n') : ['']
|
||||
let targetLineIndex = direction === -1 ? targetLines.length - 1 : 0
|
||||
let targetColumn = column
|
||||
|
||||
if (scopedCaretMode === 'block-start') {
|
||||
targetLineIndex = 0
|
||||
targetColumn = 0
|
||||
} else if (scopedCaretMode === 'block-end') {
|
||||
targetLineIndex = targetLines.length - 1
|
||||
targetColumn = (targetLines[targetLineIndex] ?? '').length
|
||||
} else {
|
||||
targetColumn = Math.min(column, (targetLines[targetLineIndex] ?? '').length)
|
||||
}
|
||||
|
||||
const targetOffset = getOffsetForLineColumn(targetLines, targetLineIndex, targetColumn)
|
||||
|
||||
setEditableCaretOffset(target, targetOffset)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 블록의 맨 앞·맨 끝으로 커서를 옮긴다.
|
||||
* @param {'start'|'end'} edge - 위치
|
||||
* @returns {void}
|
||||
*/
|
||||
const setCaretAtBlockEdge = (edge) => {
|
||||
if (!rootRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (edge === 'start') {
|
||||
if (showingRaw.value) {
|
||||
placeCursorAfterPrefix()
|
||||
return
|
||||
}
|
||||
|
||||
setEditableCaretOffset(rootRef.value, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const full = readEditorValue()
|
||||
|
||||
setEditableCaretOffset(rootRef.value, full.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 커서 기준 앞·뒤 텍스트를 읽는다.
|
||||
* @returns {{ before: string, after: string }}
|
||||
*/
|
||||
const readCaretSplit = () => {
|
||||
if (!import.meta.client || !rootRef.value) {
|
||||
return { before: props.modelValue, after: '' }
|
||||
}
|
||||
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return { before: props.modelValue, after: '' }
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
if (!rootRef.value.contains(range.commonAncestorContainer)) {
|
||||
return { before: props.modelValue, after: '' }
|
||||
}
|
||||
|
||||
const full = readEditorValue()
|
||||
const offset = getEditableCaretOffset(rootRef.value, range)
|
||||
|
||||
return {
|
||||
before: full.slice(0, offset),
|
||||
after: full.slice(offset)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* insert-below 이벤트 페이로드를 만든다.
|
||||
* @returns {{ value: string, raw: boolean, before: string, after: string, caretAtStart: boolean, caretAtEnd: boolean }}
|
||||
*/
|
||||
const buildInsertBelowPayload = () => {
|
||||
const { before, after } = readCaretSplit()
|
||||
|
||||
return {
|
||||
value: readEditorValue(),
|
||||
raw: showingRaw.value,
|
||||
before,
|
||||
after,
|
||||
caretAtStart: isCaretAtLogicalStart(),
|
||||
caretAtEnd: isCaretAtLogicalEnd()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,40 +638,7 @@ const navigateToAdjacentLine = (direction, cursorAtStart) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const splitAtCaret = () => {
|
||||
if (!import.meta.client || !rootRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const selection = window.getSelection()
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
emit('split', { before: props.modelValue, after: '' })
|
||||
return
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
if (!rootRef.value.contains(range.commonAncestorContainer)) {
|
||||
return
|
||||
}
|
||||
|
||||
const beforeRange = document.createRange()
|
||||
beforeRange.selectNodeContents(rootRef.value)
|
||||
beforeRange.setEnd(range.startContainer, range.startOffset)
|
||||
|
||||
const afterRange = document.createRange()
|
||||
afterRange.setStart(range.endContainer, range.endOffset)
|
||||
|
||||
if (rootRef.value.lastChild) {
|
||||
afterRange.setEndAfter(rootRef.value.lastChild)
|
||||
} else {
|
||||
afterRange.setEnd(rootRef.value, 0)
|
||||
}
|
||||
|
||||
const before = convertEditableHtmlToMarkdown(getRangeInnerHtml(beforeRange))
|
||||
const after = convertEditableHtmlToMarkdown(getRangeInnerHtml(afterRange))
|
||||
|
||||
emit('split', { before, after })
|
||||
emit('split', readCaretSplit())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,15 +657,14 @@ const scheduleEnterAction = (action) => {
|
||||
if (action === 'split') {
|
||||
splitAtCaret()
|
||||
} else {
|
||||
const nextValue = readEditorValue()
|
||||
emit('insert-below', showingRaw.value ? { value: nextValue, raw: true } : nextValue)
|
||||
emit('insert-below', buildInsertBelowPayload())
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
splitLock.value = false
|
||||
window.setTimeout(() => {
|
||||
suppressBlurCommit.value = false
|
||||
}, 50)
|
||||
}, 180)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,7 +744,22 @@ const onKeydown = (event) => {
|
||||
|
||||
if (
|
||||
event.key === 'Backspace'
|
||||
&& isCaretAtEdge('start')
|
||||
&& !hasNonCollapsedSelection()
|
||||
&& isCaretAtLogicalStart()
|
||||
&& !showingRaw.value
|
||||
&& props.sourceLine !== null
|
||||
&& readEditorValue().trim()
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('merge-with-previous', buildInsertBelowPayload())
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === 'Backspace'
|
||||
&& !hasNonCollapsedSelection()
|
||||
&& isCaretAtLogicalStart()
|
||||
&& !showingRaw.value
|
||||
&& props.sourceLine !== null
|
||||
&& !readEditorValue().trim()
|
||||
@@ -490,6 +772,7 @@ const onKeydown = (event) => {
|
||||
|
||||
if (
|
||||
event.key === 'Backspace'
|
||||
&& !hasNonCollapsedSelection()
|
||||
&& props.allowRawToggle
|
||||
&& props.rawLine
|
||||
&& !showingRaw.value
|
||||
@@ -510,37 +793,80 @@ const onKeydown = (event) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && isCaretAtLogicalStart()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
navigateToAdjacentLine(-1, false)
|
||||
const hasCommandModifier = event.metaKey || event.ctrlKey
|
||||
|
||||
if (hasCommandModifier && !event.shiftKey && !event.altKey) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setCaretAtBlockEdge('start')
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setCaretAtBlockEdge('end')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (props.slashMenuActive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown' && isCaretAtLogicalEnd()) {
|
||||
if (!hasCommandModifier && !event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
return
|
||||
}
|
||||
|
||||
const lineContext = getCaretLineContext()
|
||||
const direction = event.key === 'ArrowUp' ? -1 : 1
|
||||
const isMultilineEditor = resolvedEnterMode.value === 'multiline'
|
||||
|
||||
if (isMultilineEditor) {
|
||||
if (direction === -1 && !lineContext.isFirstLine) {
|
||||
return
|
||||
}
|
||||
|
||||
if (direction === 1 && !lineContext.isLastLine) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
navigateToAdjacentLine(1, true)
|
||||
navigateToAdjacentBlock(direction, lineContext.column)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowLeft' && isCaretAtLogicalStart()) {
|
||||
if (!hasCommandModifier && !event.altKey && event.key === 'ArrowLeft' && isCaretAtLogicalStart()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
navigateToAdjacentLine(-1, false)
|
||||
navigateToAdjacentBlock(-1, 0, 'block-end')
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight' && isCaretAtLogicalEnd()) {
|
||||
if (!hasCommandModifier && !event.altKey && event.key === 'ArrowRight' && isCaretAtLogicalEnd()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
navigateToAdjacentLine(1, true)
|
||||
navigateToAdjacentBlock(1, 0, 'block-start')
|
||||
return
|
||||
}
|
||||
|
||||
const enterMode = showingRaw.value ? 'insert-below' : resolvedEnterMode.value
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey && (enterMode === 'split-paragraph' || enterMode === 'insert-below')) {
|
||||
if (event.key === 'Enter' && !event.shiftKey && parseSlashInput(readEditorValue())) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('slash-apply', {
|
||||
sourceLine: props.sourceLine,
|
||||
value: readEditorValue()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && (enterMode === 'split-paragraph' || enterMode === 'insert-below')) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -554,11 +880,19 @@ const onKeydown = (event) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && event.shiftKey && props.allowHardBreak) {
|
||||
if (event.key === 'Enter' && enterMode === 'focus-next') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('enter-advance')
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !props.multiline && enterMode === 'none') {
|
||||
if (event.key === 'Enter' && enterMode === 'none') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
rootRef.value?.blur()
|
||||
@@ -596,16 +930,13 @@ const focusEditor = (position = 'end') => {
|
||||
return
|
||||
}
|
||||
|
||||
rootRef.value.focus()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(rootRef.value)
|
||||
range.collapse(position === 'start')
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
rootRef.value.focus({ preventScroll: true })
|
||||
const textLength = readEditorValue().length
|
||||
const offset = position === 'start' ? 0 : textLength
|
||||
setEditableCaretOffset(rootRef.value, offset)
|
||||
}
|
||||
|
||||
defineExpose({ focusEditor })
|
||||
defineExpose({ focusEditor, readEditorValue })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -623,6 +954,7 @@ defineExpose({ focusEditor })
|
||||
spellcheck="true"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onEditorInput"
|
||||
@paste="onPaste"
|
||||
@keydown="onKeydown"
|
||||
@compositionend="onCompositionEnd"
|
||||
@@ -630,6 +962,10 @@ defineExpose({ focusEditor })
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content-markdown-editable-inline {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.content-markdown-editable-inline--idle {
|
||||
cursor: text;
|
||||
border-radius: 4px;
|
||||
|
||||
Reference in New Issue
Block a user