글쓰기 문단 입력과 편집 영역 정리
This commit is contained in:
@@ -391,17 +391,21 @@ const replaceSelection = (replacement, cursorOffset = replacement.length, select
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter 입력을 문단 분리 규칙에 맞게 처리한다.
|
||||
* Enter 입력을 문단/줄바꿈 규칙에 맞게 처리한다.
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
* @returns {boolean} 직접 처리했는지 여부
|
||||
*/
|
||||
const handleParagraphEnter = (event) => {
|
||||
if (event.key !== 'Enter' || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey || event.isComposing) {
|
||||
if (event.key !== 'Enter' || event.metaKey || event.ctrlKey || event.altKey || event.isComposing) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!event.shiftKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
replaceSelection('\n\n')
|
||||
replaceSelection(' \n')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1069,18 +1073,17 @@ const handleKeydown = (event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeMode === 'write'" class="admin-markdown-editor__write relative">
|
||||
<div class="admin-markdown-editor__editor-surface flex min-h-[620px] overflow-hidden rounded border border-[#e3e6e8] bg-white">
|
||||
<div v-if="activeMode === 'write'" class="admin-markdown-editor__write relative pl-12">
|
||||
<div class="admin-markdown-editor__editor-surface min-h-[620px]">
|
||||
<div
|
||||
ref="gutterRef"
|
||||
class="admin-markdown-editor__gutter min-w-[2.75rem] shrink-0 select-none overflow-y-auto overflow-x-hidden border-r border-[#e3e6e8] bg-[#f6f7f8] py-5 font-mono text-[13px] leading-7 text-[#8e9cac]"
|
||||
class="admin-markdown-editor__gutter absolute bottom-0 left-0 top-0 w-10 select-none overflow-y-auto overflow-x-hidden py-5 font-mono text-[13px] leading-7 text-[#a0a8b0]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="ln in gutterLineCount"
|
||||
:key="`gutter-line-${ln}`"
|
||||
class="admin-markdown-editor__gutter-line min-h-[28px] pr-2 text-right tabular-nums"
|
||||
:class="{ 'admin-markdown-editor__gutter-line--active': ln - 1 === activeLogicalLineIndex }"
|
||||
>
|
||||
{{ ln }}
|
||||
</div>
|
||||
@@ -1088,7 +1091,7 @@ const handleKeydown = (event) => {
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="markdownValue"
|
||||
class="admin-markdown-editor__textarea min-h-[620px] flex-1 resize-y border-0 bg-transparent py-5 pl-2 pr-5 font-mono text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:ring-0"
|
||||
class="admin-markdown-editor__textarea min-h-[620px] w-full resize-y border-0 bg-transparent py-5 pl-0 pr-5 font-mono text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:ring-0"
|
||||
placeholder="마크다운으로 글을 작성하세요."
|
||||
spellcheck="false"
|
||||
@keydown="handleKeydown"
|
||||
@@ -1272,11 +1275,3 @@ const handleKeydown = (event) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-markdown-editor__gutter-line--active {
|
||||
background-color: rgba(46, 182, 234, 0.16);
|
||||
color: #15171a;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -124,6 +124,20 @@ const isMarkdownBlockStart = (line) => {
|
||||
Boolean(parseImageLine(trimmedLine))
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 hard break 표식이 있는 행인지 확인한다.
|
||||
* @param {string} line - 마크다운 행
|
||||
* @returns {boolean} hard break 여부
|
||||
*/
|
||||
const hasMarkdownHardBreak = (line) => / {2,}$/.test(line)
|
||||
|
||||
/**
|
||||
* 문단 행에서 hard break 표식을 제거한다.
|
||||
* @param {string} line - 마크다운 행
|
||||
* @returns {string} 정리된 문단 행
|
||||
*/
|
||||
const cleanParagraphLine = (line) => line.replace(/ {2,}$/, '').trim()
|
||||
|
||||
/**
|
||||
* 닫힘 표식까지의 행 목록을 반환
|
||||
* @param {Array<string>} lines - 전체 마크다운 행
|
||||
@@ -398,10 +412,11 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraphLines = [trimmedLine]
|
||||
const paragraphLines = [cleanParagraphLine(line)]
|
||||
let shouldJoinNextLine = hasMarkdownHardBreak(line)
|
||||
index += 1
|
||||
|
||||
while (index < lines.length) {
|
||||
while (shouldJoinNextLine && index < lines.length) {
|
||||
const nextLine = lines[index]
|
||||
const nextTrimmedLine = nextLine.trim()
|
||||
|
||||
@@ -409,7 +424,8 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
break
|
||||
}
|
||||
|
||||
paragraphLines.push(nextTrimmedLine)
|
||||
paragraphLines.push(cleanParagraphLine(nextLine))
|
||||
shouldJoinNextLine = hasMarkdownHardBreak(nextLine)
|
||||
index += 1
|
||||
}
|
||||
|
||||
@@ -616,7 +632,7 @@ const showNextImage = () => {
|
||||
class="content-markdown-renderer__code my-6 overflow-x-auto rounded bg-[#15171a] px-4 py-3 text-sm leading-6 text-white"
|
||||
><code>{{ block.text }}</code></pre>
|
||||
<hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-10 border-line">
|
||||
<p v-else class="content-markdown-renderer__paragraph mb-6 text-[15px] leading-8 text-[var(--site-text)] last:mb-0">
|
||||
<p v-else class="content-markdown-renderer__paragraph mb-2.5 text-[15px] leading-8 text-[var(--site-text)] last:mb-0">
|
||||
<template v-for="(lineSegments, lineIndex) in parseInlineSegmentLines(block.text)" :key="`${block.id}-paragraph-line-${lineIndex}`">
|
||||
<br v-if="lineIndex > 0">
|
||||
<template v-for="(segment, segmentIndex) in lineSegments" :key="`${block.id}-paragraph-${lineIndex}-${segmentIndex}`">
|
||||
|
||||
@@ -12,7 +12,7 @@ const tagName = computed(() => `h${Math.min(Math.max(props.level, 1), 6)}`)
|
||||
<template>
|
||||
<component
|
||||
:is="tagName"
|
||||
class="prose-heading mt-12 font-semibold leading-[1.25] tracking-normal first:mt-0"
|
||||
class="prose-heading mb-2.5 mt-12 font-semibold leading-[1.25] tracking-normal first:mt-0"
|
||||
:class="{
|
||||
'text-[clamp(1.35rem,1.25rem+0.35vw,1.6rem)] leading-[1.15]': level === 1,
|
||||
'text-[clamp(1.2rem,1.15rem+0.3vw,1.4rem)]': level === 2,
|
||||
|
||||
Reference in New Issue
Block a user