관리자 블록 에디터 입력 안정화
This commit is contained in:
@@ -12,6 +12,7 @@ const editorBlocks = ref([])
|
||||
const blockRefs = ref([])
|
||||
const activeBlockId = ref('')
|
||||
const slashQuery = ref('')
|
||||
const slashMenuDirection = ref('down')
|
||||
const isApplyingExternalValue = ref(false)
|
||||
let blockIdSeed = 0
|
||||
|
||||
@@ -211,6 +212,10 @@ const emitContent = () => {
|
||||
const setBlockRef = (element, index) => {
|
||||
if (element) {
|
||||
blockRefs.value[index] = element
|
||||
|
||||
if (element.innerText !== editorBlocks.value[index]?.text) {
|
||||
element.innerText = editorBlocks.value[index]?.text || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +242,28 @@ const focusBlock = (index) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 표시 방향 갱신
|
||||
* @param {number} index - 블록 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateSlashMenuDirection = (index) => {
|
||||
nextTick(() => {
|
||||
const element = blockRefs.value[index]
|
||||
|
||||
if (!element) {
|
||||
slashMenuDirection.value = 'down'
|
||||
return
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const menuHeight = 280
|
||||
slashMenuDirection.value = window.innerHeight - rect.bottom < menuHeight && rect.top > menuHeight
|
||||
? 'up'
|
||||
: 'down'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 타입에 맞는 태그명 반환
|
||||
* @param {Object} block - 에디터 블록
|
||||
@@ -291,6 +318,7 @@ const updateBlockText = (event, index) => {
|
||||
activeBlockId.value = block.id
|
||||
applyMarkdownShortcut(block, index)
|
||||
updateSlashQuery(block)
|
||||
updateSlashMenuDirection(index)
|
||||
emitContent()
|
||||
}
|
||||
|
||||
@@ -378,6 +406,12 @@ const applyCommand = (command) => {
|
||||
block.type = command.type
|
||||
block.level = command.level || null
|
||||
block.text = ''
|
||||
const element = blockRefs.value[index]
|
||||
|
||||
if (element) {
|
||||
element.innerText = ''
|
||||
}
|
||||
|
||||
slashQuery.value = ''
|
||||
|
||||
if (command.type === 'divider') {
|
||||
@@ -406,6 +440,10 @@ const handleEnter = (event, index) => {
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (!currentBlock.text.trim() && currentBlock.type === 'paragraph') {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBlock.type === 'divider') {
|
||||
editorBlocks.value.splice(index + 1, 0, createEditorBlock())
|
||||
emitContent()
|
||||
@@ -452,10 +490,23 @@ const handleBackspace = (event, index) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const activateBlock = (block) => {
|
||||
const index = editorBlocks.value.findIndex((item) => item.id === block.id)
|
||||
activeBlockId.value = block.id
|
||||
updateSlashQuery(block)
|
||||
updateSlashMenuDirection(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 placeholder 표시 여부 반환
|
||||
* @param {Object} block - 에디터 블록
|
||||
* @param {number} index - 블록 인덱스
|
||||
* @returns {boolean} placeholder 표시 여부
|
||||
*/
|
||||
const shouldShowPlaceholder = (block, index) => !block.text && (
|
||||
activeBlockId.value === block.id ||
|
||||
(index === 0 && editorBlocks.value.length === 1)
|
||||
)
|
||||
|
||||
watch(() => props.modelValue, (value) => {
|
||||
if (isApplyingExternalValue.value) {
|
||||
return
|
||||
@@ -479,7 +530,7 @@ watch(editorBlocks, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-block-editor rounded border border-line bg-white px-5 py-5">
|
||||
<div class="admin-block-editor min-h-[32rem] bg-transparent py-4">
|
||||
<div class="admin-block-editor__surface post-prose grid gap-1">
|
||||
<div
|
||||
v-for="(block, index) in editorBlocks"
|
||||
@@ -495,13 +546,12 @@ watch(editorBlocks, () => {
|
||||
contenteditable="true"
|
||||
spellcheck="true"
|
||||
:data-placeholder="index === 0 ? '본문을 입력하거나 / 를 눌러 블록을 선택하세요' : '/ 를 눌러 블록 선택'"
|
||||
:data-show-placeholder="shouldShowPlaceholder(block, index)"
|
||||
@focus="activateBlock(block)"
|
||||
@input="updateBlockText($event, index)"
|
||||
@keydown.enter="handleEnter($event, index)"
|
||||
@keydown.backspace="handleBackspace($event, index)"
|
||||
>
|
||||
{{ block.text }}
|
||||
</component>
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="block.type === 'divider'"
|
||||
@@ -515,7 +565,8 @@ watch(editorBlocks, () => {
|
||||
|
||||
<div
|
||||
v-if="visibleCommands.length && activeBlockId === block.id"
|
||||
class="admin-block-editor__slash-menu absolute left-0 top-full z-20 mt-2 w-72 overflow-hidden rounded border border-line bg-white shadow-lg"
|
||||
class="admin-block-editor__slash-menu absolute left-0 z-20 w-72 overflow-hidden rounded border border-line bg-white shadow-lg"
|
||||
:class="slashMenuDirection === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'"
|
||||
>
|
||||
<button
|
||||
v-for="command in visibleCommands"
|
||||
@@ -534,7 +585,7 @@ watch(editorBlocks, () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-block-editor__block:empty::before {
|
||||
.admin-block-editor__block:empty[data-show-placeholder="true"]::before {
|
||||
color: var(--site-soft);
|
||||
content: attr(data-placeholder);
|
||||
}
|
||||
|
||||
@@ -92,22 +92,15 @@ const submitPost = () => {
|
||||
<form class="admin-post-form grid gap-6" @submit.prevent="submitPost">
|
||||
<div class="admin-post-form__main grid gap-5 lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<section class="admin-post-form__content grid gap-4">
|
||||
<label class="admin-post-form__field grid gap-2 text-sm">
|
||||
<span class="admin-post-form__label font-medium">제목</span>
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
type="text"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="admin-post-form__title-input border-0 bg-transparent px-0 py-2 text-5xl font-semibold leading-tight outline-none placeholder:text-soft"
|
||||
type="text"
|
||||
placeholder="제목"
|
||||
required
|
||||
>
|
||||
|
||||
<div class="admin-post-form__field grid gap-2 text-sm">
|
||||
<div class="admin-post-form__editor-header flex flex-wrap items-center justify-between gap-3">
|
||||
<span class="admin-post-form__label font-medium">본문</span>
|
||||
<span class="admin-post-form__editor-note text-xs text-muted">/ 명령과 마크다운 단축 입력 지원</span>
|
||||
</div>
|
||||
|
||||
<AdminBlockEditor v-model="form.content" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user