블록 설정 패널 확장 v1.5.37
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { getImageAltAttribute, getImageDefaultAltLabel } from '../../lib/markdown-image.js'
|
||||
import { CALLOUT_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
import { CALLOUT_BACKGROUND_OPTIONS, CALLOUT_EMOJI_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** 패널 표시 여부 */
|
||||
@@ -24,10 +24,13 @@ const emit = defineEmits([
|
||||
'remove-media-image',
|
||||
'add-gallery-images',
|
||||
'update-embed-url',
|
||||
'update-quote-background'
|
||||
'update-quote-background',
|
||||
'update-callout-options',
|
||||
'update-code-options',
|
||||
'update-toggle-options'
|
||||
])
|
||||
|
||||
const quoteBackgroundLabels = {
|
||||
const backgroundLabels = {
|
||||
gray: '회색',
|
||||
blue: '파랑',
|
||||
green: '초록',
|
||||
@@ -37,7 +40,7 @@ const quoteBackgroundLabels = {
|
||||
pink: '분홍'
|
||||
}
|
||||
|
||||
const quoteBackgroundSwatches = {
|
||||
const backgroundSwatches = {
|
||||
gray: 'rgba(100,116,139,0.28)',
|
||||
blue: 'rgba(59,130,246,0.3)',
|
||||
green: 'rgba(34,197,94,0.3)',
|
||||
@@ -47,6 +50,22 @@ const quoteBackgroundSwatches = {
|
||||
pink: 'rgba(236,72,153,0.28)'
|
||||
}
|
||||
|
||||
const languageOptions = ['', 'javascript', 'html', 'css', 'vue', 'json', 'bash', 'markdown', 'sql']
|
||||
|
||||
/**
|
||||
* 배경 라벨을 반환한다.
|
||||
* @param {string} background - 배경 키
|
||||
* @returns {string} 배경 라벨
|
||||
*/
|
||||
const getBackgroundLabel = (background) => backgroundLabels[background] || background
|
||||
|
||||
/**
|
||||
* 배경 스와치를 반환한다.
|
||||
* @param {string} background - 배경 키
|
||||
* @returns {string} CSS 배경
|
||||
*/
|
||||
const getBackgroundSwatch = (background) => backgroundSwatches[background] || 'rgba(100,116,139,0.28)'
|
||||
|
||||
/**
|
||||
* 블록 종류 라벨
|
||||
* @returns {string}
|
||||
@@ -68,6 +87,18 @@ const panelTitle = computed(() => {
|
||||
return '인용'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'callout') {
|
||||
return '콜아웃'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'code') {
|
||||
return '코드 블록'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'toggle') {
|
||||
return '토글'
|
||||
}
|
||||
|
||||
return '이미지'
|
||||
})
|
||||
|
||||
@@ -92,6 +123,18 @@ const panelMeta = computed(() => {
|
||||
return '인용 배경색'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'callout') {
|
||||
return '아이콘·배경색'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'code') {
|
||||
return '언어·줄번호'
|
||||
}
|
||||
|
||||
if (props.panel.kind === 'toggle') {
|
||||
return '기본 펼침 상태'
|
||||
}
|
||||
|
||||
return '커서가 위치한 이미지 줄'
|
||||
})
|
||||
|
||||
@@ -178,15 +221,135 @@ const onPanelFocusOut = (event) => {
|
||||
>
|
||||
<span
|
||||
class="size-5 shrink-0 rounded-full border border-black/5"
|
||||
:style="{ background: quoteBackgroundSwatches[background] }"
|
||||
:style="{ background: getBackgroundSwatch(background) }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ quoteBackgroundLabels[background] || background }}</span>
|
||||
<span>{{ getBackgroundLabel(background) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="panel.kind === 'callout'">
|
||||
<div class="admin-editor-block-panel__callout-settings grid gap-5">
|
||||
<label class="flex cursor-pointer items-center justify-between gap-3 rounded border border-[#edf0f2] bg-[#fafafa] px-3 py-3 text-sm font-semibold text-[#394047]">
|
||||
<span>아이콘 표시</span>
|
||||
<input
|
||||
class="size-4 rounded border-[#c8ced3] text-[#15171a]"
|
||||
type="checkbox"
|
||||
:checked="panel.calloutEmojiEnabled"
|
||||
@change="emit('update-callout-options', { calloutEmojiEnabled: $event.target.checked })"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<p class="text-sm font-semibold text-[#394047]">
|
||||
아이콘
|
||||
</p>
|
||||
<input
|
||||
class="rounded border border-[#d7dde2] bg-[#eff1f2] px-3 py-2 text-sm text-[#15171a] outline-none transition-colors focus:border-[#8e9cac] disabled:opacity-50"
|
||||
:value="panel.calloutEmoji"
|
||||
type="text"
|
||||
maxlength="4"
|
||||
placeholder="💡"
|
||||
:disabled="!panel.calloutEmojiEnabled"
|
||||
@input="emit('update-callout-options', { calloutEmojiEnabled: true, calloutEmoji: $event.target.value })"
|
||||
>
|
||||
<div class="grid grid-cols-5 gap-2">
|
||||
<button
|
||||
v-for="emoji in CALLOUT_EMOJI_OPTIONS"
|
||||
:key="`callout-emoji-${emoji}`"
|
||||
class="grid h-10 place-items-center rounded border text-lg transition"
|
||||
:class="panel.calloutEmoji === emoji && panel.calloutEmojiEnabled ? 'border-[#15171a] bg-white' : 'border-[#dce0e5] bg-[#fafafa] hover:bg-white'"
|
||||
type="button"
|
||||
:disabled="!panel.calloutEmojiEnabled"
|
||||
@click="emit('update-callout-options', { calloutEmojiEnabled: true, calloutEmoji: emoji })"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<p class="text-sm font-semibold text-[#394047]">
|
||||
배경색
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="background in CALLOUT_BACKGROUND_OPTIONS"
|
||||
:key="`callout-background-${background}`"
|
||||
class="flex items-center gap-2 rounded border px-3 py-2 text-left text-xs font-semibold transition"
|
||||
:class="panel.calloutBackground === background ? 'border-[#15171a] bg-white text-[#15171a]' : 'border-[#dce0e5] bg-[#fafafa] text-[#657080] hover:bg-white'"
|
||||
type="button"
|
||||
@click="emit('update-callout-options', { calloutBackground: background })"
|
||||
>
|
||||
<span
|
||||
class="size-5 shrink-0 rounded-full border border-black/5"
|
||||
:style="{ background: getBackgroundSwatch(background) }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ getBackgroundLabel(background) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="panel.kind === 'code'">
|
||||
<div class="admin-editor-block-panel__code-settings grid gap-4">
|
||||
<label class="admin-editor-block-panel__field grid gap-2 text-sm">
|
||||
<span class="font-semibold text-[#394047]">언어</span>
|
||||
<input
|
||||
class="rounded border border-[#d7dde2] bg-[#eff1f2] px-3 py-2 text-sm text-[#15171a] outline-none transition-colors focus:border-[#8e9cac]"
|
||||
:value="panel.language"
|
||||
type="text"
|
||||
list="admin-editor-block-panel-languages"
|
||||
placeholder="javascript"
|
||||
@input="emit('update-code-options', { language: $event.target.value })"
|
||||
>
|
||||
<datalist id="admin-editor-block-panel-languages">
|
||||
<option
|
||||
v-for="language in languageOptions"
|
||||
:key="`code-language-${language || 'plain'}`"
|
||||
:value="language"
|
||||
/>
|
||||
</datalist>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center justify-between gap-3 rounded border border-[#edf0f2] bg-[#fafafa] px-3 py-3 text-sm font-semibold text-[#394047]">
|
||||
<span>줄번호 표시</span>
|
||||
<input
|
||||
class="size-4 rounded border-[#c8ced3] text-[#15171a]"
|
||||
type="checkbox"
|
||||
:checked="panel.showLineNumbers"
|
||||
@change="emit('update-code-options', { showLineNumbers: $event.target.checked })"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="panel.kind === 'toggle'">
|
||||
<div class="admin-editor-block-panel__toggle-settings grid gap-3">
|
||||
<button
|
||||
class="flex items-center justify-between rounded border px-3 py-3 text-left text-sm font-semibold transition"
|
||||
:class="panel.defaultOpen ? 'border-[#15171a] bg-white text-[#15171a]' : 'border-[#dce0e5] bg-[#fafafa] text-[#657080] hover:bg-white'"
|
||||
type="button"
|
||||
@click="emit('update-toggle-options', { defaultOpen: true })"
|
||||
>
|
||||
<span>기본 펼침</span>
|
||||
<span v-if="panel.defaultOpen" class="text-xs text-[#15171a]">선택됨</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-between rounded border px-3 py-3 text-left text-sm font-semibold transition"
|
||||
:class="!panel.defaultOpen ? 'border-[#15171a] bg-white text-[#15171a]' : 'border-[#dce0e5] bg-[#fafafa] text-[#657080] hover:bg-white'"
|
||||
type="button"
|
||||
@click="emit('update-toggle-options', { defaultOpen: false })"
|
||||
>
|
||||
<span>기본 닫힘</span>
|
||||
<span v-if="!panel.defaultOpen" class="text-xs text-[#15171a]">선택됨</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="admin-editor-block-panel__media-list grid gap-3">
|
||||
<div
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
parseSlashInput,
|
||||
resolveSlashCommand
|
||||
} from '../../lib/markdown-slash-commands.js'
|
||||
import { CALLOUT_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
||||
import { buildToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
||||
import { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
|
||||
import {
|
||||
buildDefaultUploadSizeLimits,
|
||||
@@ -1212,7 +1214,7 @@ const createMediaBlockMarkdown = (blockType, item) => {
|
||||
return [
|
||||
':::file',
|
||||
`url=${item.url}`,
|
||||
`title=${title}`,
|
||||
`title=${item.name || title}`,
|
||||
'description=',
|
||||
`name=${item.name || title}`,
|
||||
`size=${formatMediaFileSize(item.size)}`,
|
||||
@@ -2107,6 +2109,82 @@ const updateActiveQuoteBackground = (background) => {
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 콜아웃 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ calloutEmojiEnabled: boolean, calloutEmoji: string, calloutBackground: string }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCalloutOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
|
||||
if (!block || block.kind !== 'callout') {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextBackground = CALLOUT_BACKGROUND_OPTIONS.includes(patch.calloutBackground)
|
||||
? patch.calloutBackground
|
||||
: block.calloutBackground
|
||||
const nextLine = buildCalloutOpenerLine({
|
||||
calloutEmojiEnabled: patch.calloutEmojiEnabled ?? block.calloutEmojiEnabled,
|
||||
calloutEmoji: patch.calloutEmoji ?? block.calloutEmoji,
|
||||
calloutBackground: nextBackground
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 코드 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ language: string, showLineNumbers: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCodeOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
|
||||
if (!block || block.kind !== 'code') {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextLine = buildCodeFenceOpener({
|
||||
language: patch.language ?? block.language,
|
||||
showLineNumbers: patch.showLineNumbers ?? block.showLineNumbers
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 토글 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ defaultOpen: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveToggleOptions = (patch = {}) => {
|
||||
const block = activeBlockContext.value
|
||||
|
||||
if (!block || block.kind !== 'toggle') {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
const nextLine = buildToggleOpenerLine({
|
||||
title: block.title,
|
||||
defaultOpen: patch.defaultOpen ?? block.defaultOpen
|
||||
})
|
||||
const lines = (markdownValue.value || '').split('\n')
|
||||
const nextLines = [...lines]
|
||||
|
||||
nextLines.splice(block.startLine, 1, nextLine)
|
||||
markdownValue.value = nextLines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 마크다운을 삽입한다.
|
||||
* @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
|
||||
@@ -2174,6 +2252,9 @@ defineExpose({
|
||||
appendImagesToActiveGallery,
|
||||
updateActiveEmbedUrl,
|
||||
updateActiveQuoteBackground,
|
||||
updateActiveCalloutOptions,
|
||||
updateActiveCodeOptions,
|
||||
updateActiveToggleOptions,
|
||||
openMediaPicker
|
||||
})
|
||||
|
||||
|
||||
@@ -1062,6 +1062,33 @@ const onBlockPanelUpdateQuoteBackground = (background) => {
|
||||
blockEditor.value?.updateActiveQuoteBackground?.(background)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 패널에서 콜아웃 옵션을 수정한다.
|
||||
* @param {Object} patch - 콜아웃 변경 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockPanelUpdateCalloutOptions = (patch) => {
|
||||
blockEditor.value?.updateActiveCalloutOptions?.(patch)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 패널에서 코드 블록 옵션을 수정한다.
|
||||
* @param {Object} patch - 코드 블록 변경 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockPanelUpdateCodeOptions = (patch) => {
|
||||
blockEditor.value?.updateActiveCodeOptions?.(patch)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 패널에서 토글 옵션을 수정한다.
|
||||
* @param {Object} patch - 토글 변경 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const onBlockPanelUpdateToggleOptions = (patch) => {
|
||||
blockEditor.value?.updateActiveToggleOptions?.(patch)
|
||||
}
|
||||
|
||||
const focusContentEditor = (event) => {
|
||||
if (event?.isComposing || isTitleInputComposing.value || event?.keyCode === 229) {
|
||||
return
|
||||
@@ -1884,6 +1911,9 @@ defineExpose({
|
||||
@add-gallery-images="onBlockPanelAddGalleryImages"
|
||||
@update-embed-url="onBlockPanelUpdateEmbedUrl"
|
||||
@update-quote-background="onBlockPanelUpdateQuoteBackground"
|
||||
@update-callout-options="onBlockPanelUpdateCalloutOptions"
|
||||
@update-code-options="onBlockPanelUpdateCodeOptions"
|
||||
@update-toggle-options="onBlockPanelUpdateToggleOptions"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user