v1.2.9: 라이브 에디터·홈 피드·메인 커버 개선
라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기, 사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,17 +3,20 @@ import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.
|
||||
import { resolveActiveBlockContext } from '../../lib/markdown-block-context.js'
|
||||
import { getImageDefaultAltLabel, serializeImageMarkdown } from '../../lib/markdown-image.js'
|
||||
import { convertHtmlToMarkdown } from '../../lib/markdown-inline.js'
|
||||
import {
|
||||
filterSlashCommands,
|
||||
getSlashCommandFocusTarget,
|
||||
parseSlashInput,
|
||||
resolveSlashCommand
|
||||
} from '../../lib/markdown-slash-commands.js'
|
||||
import { getTextareaCaretCoordinates } from '../../lib/textarea-caret-coordinates.js'
|
||||
import AdminSlashCommandIcon from './AdminSlashCommandIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array, Object],
|
||||
default: ''
|
||||
},
|
||||
/** 헤더 아래 고정 툴바 슬롯(AdminPostForm `#admin-post-form-editor-toolbar-host`) */
|
||||
toolbarTeleportTo: {
|
||||
type: String,
|
||||
default: '#admin-post-form-editor-toolbar-host'
|
||||
},
|
||||
/** 작성/미리보기 토글 버튼 Teleport 대상(AdminPostForm 헤더) */
|
||||
modeToggleTeleportTo: {
|
||||
type: String,
|
||||
@@ -30,6 +33,7 @@ const activeMode = defineModel('editorMode', {
|
||||
const editorRootRef = ref(null)
|
||||
const textareaRef = ref(null)
|
||||
const previewRef = ref(null)
|
||||
const previewRendererRef = ref(null)
|
||||
const gutterRef = ref(null)
|
||||
/** 커서가 있는 논리 줄(0-based, `\\n` 기준) */
|
||||
const activeLogicalLineIndex = ref(0)
|
||||
@@ -45,6 +49,32 @@ const lastSelectionState = ref({
|
||||
start: 0,
|
||||
end: 0
|
||||
})
|
||||
/** @type {import('vue').Ref<number|null>} 슬래시 명령이 입력된 줄 */
|
||||
const liveSlashSourceLine = ref(null)
|
||||
/** @type {import('vue').Ref<string>} 슬래시 검색어 */
|
||||
const liveSlashQuery = ref('')
|
||||
/** @type {import('vue').Ref<number>} 슬래시 메뉴 하이라이트 인덱스 */
|
||||
const liveSlashHighlightedIndex = ref(0)
|
||||
/** @type {import('vue').Ref<{ top: number, left: number }>} 슬래시 메뉴 위치 */
|
||||
const liveSlashMenuPosition = ref({ top: 0, left: 0 })
|
||||
/** @type {import('vue').Ref<HTMLElement|null>} 슬래시 메뉴 DOM */
|
||||
const liveSlashMenuRef = ref(null)
|
||||
|
||||
/** 슬래시 메뉴 예상 높이(px) — 렌더 전 위치 계산용 */
|
||||
const LIVE_SLASH_MENU_ESTIMATED_HEIGHT = 280
|
||||
/** @type {import('vue').Ref<number|null>} 슬래시 후 미디어 삽입 줄 */
|
||||
const liveSlashInsertLine = ref(null)
|
||||
/** @type {import('vue').Ref<Set<number>>} ESC로 슬래시 메뉴만 닫은 줄(0-based) */
|
||||
const liveSlashSuppressedLines = ref(new Set())
|
||||
/** 키보드로 슬래시 메뉴를 탐색 중일 때 mouseenter 하이라이트 무시 */
|
||||
const liveSlashKeyboardNav = ref(false)
|
||||
let liveSlashKeyboardNavTimer = null
|
||||
|
||||
const liveSlashVisible = computed(() => liveSlashSourceLine.value !== null)
|
||||
|
||||
const liveSlashSuppressedLineList = computed(() => [...liveSlashSuppressedLines.value])
|
||||
|
||||
const visibleLiveSlashCommands = computed(() => filterSlashCommands(liveSlashQuery.value))
|
||||
|
||||
/** 작성 textarea 최소 높이(px) */
|
||||
const MIN_TEXTAREA_HEIGHT_PX = 620
|
||||
@@ -215,9 +245,47 @@ const refreshCaretLogicalLine = () => {
|
||||
activeLogicalLineIndex.value = Math.max(0, lineIndex)
|
||||
syncTextareaHeight()
|
||||
syncBlockPanelState()
|
||||
|
||||
if (activeMode.value === 'write' && document.activeElement === textarea) {
|
||||
syncWriteSlashState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스(작성) 모드 textarea 현재 줄의 슬래시 입력 상태를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const syncWriteSlashState = () => {
|
||||
if (activeMode.value !== 'write') {
|
||||
return
|
||||
}
|
||||
|
||||
const line = activeLogicalLineIndex.value
|
||||
|
||||
if (liveSlashSuppressedLines.value.has(line)) {
|
||||
if (liveSlashSourceLine.value !== null) {
|
||||
liveSlashSourceLine.value = null
|
||||
liveSlashQuery.value = ''
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const lines = (markdownValue.value ?? '').split('\n')
|
||||
const parsed = parseSlashInput(lines[line] ?? '')
|
||||
|
||||
if (!parsed) {
|
||||
onLiveSlashEnd({ sourceLine: line })
|
||||
return
|
||||
}
|
||||
|
||||
liveSlashSourceLine.value = line
|
||||
liveSlashQuery.value = parsed.query
|
||||
nextTick(updateLiveSlashMenuPosition)
|
||||
}
|
||||
|
||||
/**
|
||||
* textarea의 선택 영역과 스크롤 위치를 기억한다.
|
||||
* @returns {void}
|
||||
@@ -341,12 +409,58 @@ onMounted(() => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDocumentKeydown = (event) => {
|
||||
if (event.key === 'Escape' && isMediaPickerOpen.value) {
|
||||
event.preventDefault()
|
||||
closeMediaPicker()
|
||||
return
|
||||
}
|
||||
|
||||
const root = editorRootRef.value
|
||||
|
||||
if (!root || !root.contains(document.activeElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (liveSlashVisible.value) {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
const count = visibleLiveSlashCommands.value.length
|
||||
|
||||
if (count) {
|
||||
liveSlashKeyboardNav.value = true
|
||||
window.clearTimeout(liveSlashKeyboardNavTimer)
|
||||
liveSlashKeyboardNavTimer = window.setTimeout(() => {
|
||||
liveSlashKeyboardNav.value = false
|
||||
}, 200)
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
liveSlashHighlightedIndex.value = (liveSlashHighlightedIndex.value + 1) % count
|
||||
} else {
|
||||
liveSlashHighlightedIndex.value = (liveSlashHighlightedIndex.value - 1 + count) % count
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
const command = visibleLiveSlashCommands.value[liveSlashHighlightedIndex.value]
|
||||
|
||||
if (command) {
|
||||
applyLiveSlashCommand(command)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
cancelLiveSlashCommand()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'e') {
|
||||
return
|
||||
}
|
||||
@@ -360,6 +474,7 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearTimeout(blockPanelFocusTimer)
|
||||
window.clearTimeout(liveSlashKeyboardNavTimer)
|
||||
document.removeEventListener('selectionchange', onSelectionChange)
|
||||
document.removeEventListener('keydown', onDocumentKeydown, true)
|
||||
})
|
||||
@@ -439,25 +554,6 @@ const replaceSelection = (replacement, cursorOffset = replacement.length, select
|
||||
setTextareaSelection(nextStart, nextStart + (selectionLength ?? 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter 입력을 문단/줄바꿈 규칙에 맞게 처리한다.
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
* @returns {boolean} 직접 처리했는지 여부
|
||||
*/
|
||||
const handleParagraphEnter = (event) => {
|
||||
if (event.key !== 'Enter' || event.metaKey || event.ctrlKey || event.altKey || event.isComposing) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!event.shiftKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
replaceSelection('\\\n')
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록형 마크다운 조각을 커서 위치에 삽입한다.
|
||||
* @param {string} snippet - 삽입할 마크다운
|
||||
@@ -737,6 +833,392 @@ const onPreviewDeleteLine = (lineIndex) => {
|
||||
markdownValue.value = nextLines.length ? nextLines.join('\n') : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이브 모드에서 현재 줄을 이전 줄 끝에 병합한다.
|
||||
* @param {{ lineIndex: number, mergedLine: string }} payload - 병합 위치·결과 줄
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* 슬래시 메뉴 좌표를 뷰포트 안으로 맞춘다.
|
||||
* @param {number} top - 후보 top(px)
|
||||
* @param {number} left - 후보 left(px)
|
||||
* @param {number} menuHeight - 메뉴 높이
|
||||
* @param {number} menuWidth - 메뉴 너비
|
||||
* @param {number} anchorTop - 기준 요소 top(위로 넘칠 때)
|
||||
* @returns {{ top: number, left: number }}
|
||||
*/
|
||||
const clampSlashMenuPosition = (top, left, menuHeight, menuWidth, anchorTop) => {
|
||||
const gap = 4
|
||||
const padding = 8
|
||||
const viewportBottom = window.innerHeight - padding
|
||||
const viewportRight = window.innerWidth - padding
|
||||
let nextTop = top
|
||||
|
||||
if (nextTop + menuHeight > viewportBottom) {
|
||||
const aboveTop = anchorTop - menuHeight - gap
|
||||
|
||||
if (aboveTop >= padding) {
|
||||
nextTop = aboveTop
|
||||
} else {
|
||||
nextTop = Math.max(padding, viewportBottom - menuHeight)
|
||||
}
|
||||
}
|
||||
|
||||
let nextLeft = left
|
||||
|
||||
if (nextLeft + menuWidth > viewportRight) {
|
||||
nextLeft = viewportRight - menuWidth
|
||||
}
|
||||
|
||||
return {
|
||||
top: nextTop,
|
||||
left: Math.max(padding, nextLeft)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리보기 모드 슬래시 메뉴 위치를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updatePreviewSlashMenuPosition = () => {
|
||||
const element = previewRef.value?.querySelector(`[data-source-line="${liveSlashSourceLine.value}"]`)
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const menuEl = liveSlashMenuRef.value
|
||||
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
|
||||
const menuWidth = menuEl?.offsetWidth || 288
|
||||
const gap = 4
|
||||
|
||||
liveSlashMenuPosition.value = clampSlashMenuPosition(
|
||||
rect.bottom + gap,
|
||||
rect.left,
|
||||
menuHeight,
|
||||
menuWidth,
|
||||
rect.top
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스 모드 textarea 슬래시 메뉴 위치를 갱신한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateWriteSlashMenuPosition = () => {
|
||||
const textarea = textareaRef.value
|
||||
const line = liveSlashSourceLine.value
|
||||
|
||||
if (!textarea || line === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = markdownValue.value ?? ''
|
||||
const lineStart = value.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0)
|
||||
const coords = getTextareaCaretCoordinates(textarea, lineStart)
|
||||
const rect = textarea.getBoundingClientRect()
|
||||
const menuEl = liveSlashMenuRef.value
|
||||
const menuHeight = menuEl?.offsetHeight || LIVE_SLASH_MENU_ESTIMATED_HEIGHT
|
||||
const menuWidth = menuEl?.offsetWidth || 288
|
||||
const gap = 4
|
||||
const anchorTop = rect.top + coords.top - textarea.scrollTop
|
||||
|
||||
liveSlashMenuPosition.value = clampSlashMenuPosition(
|
||||
anchorTop + coords.height + gap,
|
||||
rect.left + coords.left - textarea.scrollLeft,
|
||||
menuHeight,
|
||||
menuWidth,
|
||||
anchorTop
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 위치를 갱신한다(뷰포트 아래로 넘치면 위쪽·좌우 클램프).
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateLiveSlashMenuPosition = () => {
|
||||
if (!import.meta.client || liveSlashSourceLine.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
if (activeMode.value === 'write') {
|
||||
updateWriteSlashMenuPosition()
|
||||
return
|
||||
}
|
||||
|
||||
updatePreviewSlashMenuPosition()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 키보드로 선택한 슬래시 메뉴 항목이 보이도록 스크롤한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const scrollLiveSlashHighlightedIntoView = () => {
|
||||
if (!import.meta.client || !liveSlashVisible.value) {
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const menu = liveSlashMenuRef.value
|
||||
|
||||
if (!menu) {
|
||||
return
|
||||
}
|
||||
|
||||
const highlightedItem = menu.querySelector('.admin-markdown-editor__slash-item--active')
|
||||
|
||||
if (!highlightedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const itemTop = highlightedItem.offsetTop
|
||||
const itemBottom = itemTop + highlightedItem.offsetHeight
|
||||
const viewTop = menu.scrollTop
|
||||
const viewBottom = viewTop + menu.clientHeight
|
||||
|
||||
if (itemTop < viewTop) {
|
||||
menu.scrollTop = itemTop
|
||||
} else if (itemBottom > viewBottom) {
|
||||
menu.scrollTop = itemBottom - menu.clientHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 메뉴 항목 mouseenter 시 하이라이트(키보드 탐색 중에는 무시).
|
||||
* @param {number} commandIndex - 명령 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const onSlashMenuItemMouseEnter = (commandIndex) => {
|
||||
if (liveSlashKeyboardNav.value) {
|
||||
return
|
||||
}
|
||||
|
||||
liveSlashHighlightedIndex.value = commandIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령 입력을 갱신한다.
|
||||
* @param {{ sourceLine: number, query: string }} payload - 줄·검색어
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLiveSlashUpdate = (payload) => {
|
||||
if (typeof payload?.sourceLine !== 'number') {
|
||||
return
|
||||
}
|
||||
|
||||
if (liveSlashSuppressedLines.value.has(payload.sourceLine)) {
|
||||
if (liveSlashSourceLine.value !== null) {
|
||||
liveSlashSourceLine.value = null
|
||||
liveSlashQuery.value = ''
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
liveSlashSourceLine.value = payload.sourceLine
|
||||
liveSlashQuery.value = String(payload.query ?? '')
|
||||
nextTick(updateLiveSlashMenuPosition)
|
||||
}
|
||||
|
||||
watch(liveSlashQuery, () => {
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
})
|
||||
|
||||
watch(visibleLiveSlashCommands, (commands) => {
|
||||
if (liveSlashHighlightedIndex.value >= commands.length) {
|
||||
liveSlashHighlightedIndex.value = Math.max(0, commands.length - 1)
|
||||
}
|
||||
|
||||
updateLiveSlashMenuPosition()
|
||||
})
|
||||
|
||||
watch(liveSlashHighlightedIndex, () => {
|
||||
scrollLiveSlashHighlightedIntoView()
|
||||
})
|
||||
|
||||
watch(liveSlashVisible, (visible) => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
const onReposition = () => updateLiveSlashMenuPosition()
|
||||
|
||||
if (visible) {
|
||||
window.addEventListener('scroll', onReposition, true)
|
||||
window.addEventListener('resize', onReposition)
|
||||
previewRef.value?.addEventListener('scroll', onReposition)
|
||||
textareaRef.value?.addEventListener('scroll', onReposition)
|
||||
updateLiveSlashMenuPosition()
|
||||
scrollLiveSlashHighlightedIntoView()
|
||||
return
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', onReposition, true)
|
||||
window.removeEventListener('resize', onReposition)
|
||||
previewRef.value?.removeEventListener('scroll', onReposition)
|
||||
textareaRef.value?.removeEventListener('scroll', onReposition)
|
||||
})
|
||||
|
||||
/**
|
||||
* 슬래시 명령 입력을 종료한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLiveSlashEnd = (payload) => {
|
||||
if (typeof payload?.sourceLine === 'number') {
|
||||
const next = new Set(liveSlashSuppressedLines.value)
|
||||
next.delete(payload.sourceLine)
|
||||
liveSlashSuppressedLines.value = next
|
||||
}
|
||||
|
||||
liveSlashSourceLine.value = null
|
||||
liveSlashQuery.value = ''
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령 입력을 취소한다(줄 내용은 유지, 메뉴만 닫음).
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelLiveSlashCommand = () => {
|
||||
const line = liveSlashSourceLine.value
|
||||
|
||||
if (line !== null) {
|
||||
liveSlashSuppressedLines.value = new Set([...liveSlashSuppressedLines.value, line])
|
||||
}
|
||||
|
||||
liveSlashSourceLine.value = null
|
||||
liveSlashQuery.value = ''
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령 적용 후 미리보기 인라인 편집에 포커스를 복원한다.
|
||||
* @param {number} line - 줄 번호(0-based)
|
||||
* @param {string[]} replacementLines - 삽입된 줄
|
||||
* @returns {void}
|
||||
*/
|
||||
const focusPreviewAfterSlashCommand = (line, replacementLines) => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
const { line: focusLine, offset } = getSlashCommandFocusTarget(line, replacementLines)
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
previewRendererRef.value?.focusEditableAtLine(focusLine, 0, 'end', offset)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 소스 모드 textarea에 슬래시 명령 적용 후 커서를 복원한다.
|
||||
* @param {number} line - 줄 번호(0-based)
|
||||
* @param {string[]} replacementLines - 삽입된 줄
|
||||
* @returns {void}
|
||||
*/
|
||||
const focusTextareaAfterSlashCommand = (line, replacementLines) => {
|
||||
const value = markdownValue.value ?? ''
|
||||
const lineStart = value.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0)
|
||||
const { offset } = getSlashCommandFocusTarget(line, replacementLines)
|
||||
setTextareaSelection(lineStart + offset, lineStart + offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령을 적용한다.
|
||||
* @param {import('../../lib/markdown-slash-commands.js').MarkdownSlashCommand} command - 명령
|
||||
* @returns {void}
|
||||
*/
|
||||
const applyLiveSlashCommand = (command) => {
|
||||
if (!command || liveSlashSourceLine.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const line = liveSlashSourceLine.value
|
||||
|
||||
if (command.action === 'media-image' || command.action === 'media-gallery') {
|
||||
liveSlashInsertLine.value = line
|
||||
replaceLineRange(line, line, [], false)
|
||||
onLiveSlashEnd()
|
||||
openMediaPicker(command.action === 'media-image' ? 'image' : 'gallery')
|
||||
return
|
||||
}
|
||||
|
||||
if (command.action === 'lines' && Array.isArray(command.lines)) {
|
||||
const replacementLines = command.lines
|
||||
const nextSuppressed = new Set(liveSlashSuppressedLines.value)
|
||||
nextSuppressed.delete(line)
|
||||
liveSlashSuppressedLines.value = nextSuppressed
|
||||
replaceLineRange(line, line, replacementLines, false)
|
||||
liveSlashSourceLine.value = null
|
||||
liveSlashQuery.value = ''
|
||||
liveSlashHighlightedIndex.value = 0
|
||||
|
||||
if (activeMode.value === 'write') {
|
||||
focusTextareaAfterSlashCommand(line, replacementLines)
|
||||
} else {
|
||||
focusPreviewAfterSlashCommand(line, replacementLines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 슬래시 명령 Enter 적용
|
||||
* @param {{ sourceLine: number, value: string }} payload - 줄·입력 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLiveSlashApply = (payload) => {
|
||||
const parsed = parseSlashInput(payload?.value)
|
||||
const command = resolveSlashCommand(parsed?.query ?? liveSlashQuery.value)
|
||||
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
|
||||
applyLiveSlashCommand(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정 줄에 마크다운 줄을 삽입한다.
|
||||
* @param {number} lineIndex - 삽입 위치(0-based)
|
||||
* @param {string[]} insertLines - 삽입 줄
|
||||
* @returns {void}
|
||||
*/
|
||||
const insertMarkdownAtLine = (lineIndex, insertLines) => {
|
||||
const sourceLines = (markdownValue.value ?? '').split('\n')
|
||||
|
||||
markdownValue.value = [
|
||||
...sourceLines.slice(0, lineIndex),
|
||||
...insertLines,
|
||||
...sourceLines.slice(lineIndex)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
|
||||
if (typeof lineIndex !== 'number' || lineIndex <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceLines = (markdownValue.value ?? '').split('\n')
|
||||
|
||||
if (lineIndex >= sourceLines.length) {
|
||||
return
|
||||
}
|
||||
|
||||
markdownValue.value = [
|
||||
...sourceLines.slice(0, lineIndex - 1),
|
||||
String(mergedLine ?? ''),
|
||||
...sourceLines.slice(lineIndex + 1)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 갤러리 이미지 순서를 바꾼다.
|
||||
* @param {number} imageIndex - 이동할 이미지 인덱스
|
||||
@@ -894,6 +1376,7 @@ const closeMediaPicker = () => {
|
||||
selectedMediaUrls.value = []
|
||||
activeMediaPickerTab.value = 'library'
|
||||
mediaSearchQuery.value = ''
|
||||
liveSlashInsertLine.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,6 +1433,36 @@ const applyMediaSelection = () => {
|
||||
.map((url) => mediaItems.value.find((item) => item.url === url))
|
||||
.filter(Boolean)
|
||||
|
||||
if (liveSlashInsertLine.value !== null) {
|
||||
const line = liveSlashInsertLine.value
|
||||
|
||||
if (mediaPickerTarget.value === 'image') {
|
||||
const [item] = selectedItems
|
||||
|
||||
if (item) {
|
||||
insertMarkdownAtLine(line, [createImageMarkdown({
|
||||
url: item.url,
|
||||
useAlt: false,
|
||||
caption: ''
|
||||
})])
|
||||
}
|
||||
} else if (selectedItems.length) {
|
||||
insertMarkdownAtLine(line, [
|
||||
':::gallery',
|
||||
...selectedItems.map((item) => createImageMarkdown({
|
||||
url: item.url,
|
||||
useAlt: false,
|
||||
caption: ''
|
||||
})),
|
||||
':::'
|
||||
])
|
||||
}
|
||||
|
||||
liveSlashInsertLine.value = null
|
||||
closeMediaPicker()
|
||||
return
|
||||
}
|
||||
|
||||
if (mediaPickerTarget.value === 'image') {
|
||||
const [item] = selectedItems
|
||||
if (item) {
|
||||
@@ -1141,10 +1654,6 @@ const handleDrop = async (event) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleKeydown = (event) => {
|
||||
if (handleParagraphEnter(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(event.metaKey || event.ctrlKey)) {
|
||||
return
|
||||
}
|
||||
@@ -1163,49 +1672,6 @@ const handleKeydown = (event) => {
|
||||
|
||||
<template>
|
||||
<div ref="editorRootRef" class="admin-markdown-editor grid gap-3">
|
||||
<Teleport v-if="activeMode === 'write'" :to="toolbarTeleportTo">
|
||||
<div class="admin-markdown-editor__toolbar flex w-full min-h-[40px] items-center gap-1.5 border-b border-[#e3e6e8] bg-white px-8 py-1.5">
|
||||
<div class="admin-markdown-editor__toolbar-tools flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(1)">
|
||||
H1
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(2)">
|
||||
H2
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(3)">
|
||||
H3
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-bold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('**', '**', '굵은 글씨')">
|
||||
B
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm italic text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('*', '*', '기울임')">
|
||||
I
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 font-mono text-sm text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('`', '`', 'code')">
|
||||
Code
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyQuote">
|
||||
인용
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyList">
|
||||
목록
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="insertCodeBlock">
|
||||
코드블록
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="insertBlockSnippet('---')">
|
||||
구분선
|
||||
</button>
|
||||
<span class="admin-markdown-editor__divider mx-1 h-6 w-px bg-[#e3e6e8]" aria-hidden="true" />
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('image')">
|
||||
이미지
|
||||
</button>
|
||||
<button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('gallery')">
|
||||
갤러리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<Teleport v-if="modeToggleTeleportTo" :to="modeToggleTeleportTo">
|
||||
<button
|
||||
@@ -1299,16 +1765,61 @@ const handleKeydown = (event) => {
|
||||
tabindex="0"
|
||||
>
|
||||
<ContentMarkdownRenderer
|
||||
ref="previewRendererRef"
|
||||
:content="markdownValue"
|
||||
interactive
|
||||
:slash-menu-active="liveSlashVisible"
|
||||
:slash-suppressed-lines="liveSlashSuppressedLineList"
|
||||
@gallery-reorder="onPreviewGalleryReorder"
|
||||
@block-content-change="onPreviewBlockContentChange"
|
||||
@append-paragraph="onPreviewAppendParagraph"
|
||||
@insert-after-line="onPreviewInsertAfterLine"
|
||||
@delete-line="onPreviewDeleteLine"
|
||||
@merge-with-previous-line="onPreviewMergeWithPreviousLine"
|
||||
@slash-update="onLiveSlashUpdate"
|
||||
@slash-end="onLiveSlashEnd"
|
||||
@slash-apply="onLiveSlashApply"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="liveSlashVisible"
|
||||
ref="liveSlashMenuRef"
|
||||
class="admin-markdown-editor__slash-menu fixed z-[60] w-72 max-h-[min(52vh,360px)] overflow-y-auto rounded-lg border border-[#e3e6e8] bg-white p-1 shadow-lg"
|
||||
:style="{ top: `${liveSlashMenuPosition.top}px`, left: `${liveSlashMenuPosition.left}px` }"
|
||||
role="listbox"
|
||||
aria-label="슬래시 명령"
|
||||
>
|
||||
<button
|
||||
v-for="(command, commandIndex) in visibleLiveSlashCommands"
|
||||
:key="`live-slash-${command.id}`"
|
||||
class="admin-markdown-editor__slash-item flex w-full items-center justify-between gap-3 rounded-md px-2.5 py-2 text-left transition-colors"
|
||||
:class="commandIndex === liveSlashHighlightedIndex ? 'admin-markdown-editor__slash-item--active bg-[#f4f5f6]' : ''"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="commandIndex === liveSlashHighlightedIndex"
|
||||
@mouseenter="onSlashMenuItemMouseEnter(commandIndex)"
|
||||
@mousedown.prevent="applyLiveSlashCommand(command)"
|
||||
>
|
||||
<span class="admin-markdown-editor__slash-leading flex min-w-0 items-center gap-2.5">
|
||||
<AdminSlashCommandIcon
|
||||
:command-id="command.id"
|
||||
class="admin-markdown-editor__slash-icon shrink-0 text-[#394047]"
|
||||
/>
|
||||
<span class="admin-markdown-editor__slash-label truncate text-sm text-[#15171a]">{{ command.label }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="admin-markdown-editor__slash-slug shrink-0 text-xs text-[#8b959e] transition-opacity"
|
||||
:class="commandIndex === liveSlashHighlightedIndex ? 'opacity-100' : 'opacity-0'"
|
||||
>/{{ command.id }}</span>
|
||||
</button>
|
||||
<p v-if="!visibleLiveSlashCommands.length" class="admin-markdown-editor__slash-empty px-4 py-3 text-sm text-[#6b7280]">
|
||||
일치하는 명령이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div v-if="isMediaPickerOpen" class="admin-markdown-editor__media-modal fixed inset-0 z-50 grid place-items-center bg-black/40 px-4 py-6" @click.self="closeMediaPicker">
|
||||
<div class="admin-markdown-editor__media-panel flex max-h-[86vh] w-full max-w-5xl flex-col overflow-hidden rounded bg-white shadow-2xl">
|
||||
<header class="admin-markdown-editor__media-header flex items-center justify-between border-b border-[#e3e6e8] px-5 py-4">
|
||||
|
||||
@@ -1338,12 +1338,6 @@ defineExpose({
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-show="editorMode === 'write'"
|
||||
id="admin-post-form-editor-toolbar-host"
|
||||
class="admin-post-form__editor-toolbar-host min-h-[40px] shrink-0 bg-white"
|
||||
/>
|
||||
|
||||
<main class="admin-post-form__editor-scroll min-h-0 flex-1 overflow-y-auto">
|
||||
<section class="admin-post-form__content mx-auto w-full max-w-[804px] px-8 pb-16 pt-16">
|
||||
<div class="admin-post-form__feature-block mb-9">
|
||||
|
||||
135
components/admin/AdminSlashCommandIcon.vue
Normal file
135
components/admin/AdminSlashCommandIcon.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 슬래시 명령 메뉴 아이콘 (Ghost 스타일 라인 아이콘)
|
||||
*/
|
||||
const props = defineProps({
|
||||
/** @type {import('vue').PropType<string>} */
|
||||
commandId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
class="admin-slash-command-icon"
|
||||
:class="`admin-slash-command-icon--${commandId}`"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
width="20"
|
||||
height="20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<!-- image -->
|
||||
<template v-if="commandId === 'image'">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.8 2H3.2C1.985 2 1 2.895 1 4v16c0 1.105.985 2 2.2 2h17.6c1.215 0 2.2-.895 2.2-2V4c0-1.105-.985-2-2.2-2Z"></path><path fill="currentColor" d="m19.642 16.276-3.85-7a.517.517 0 0 0-.181-.189.585.585 0 0 0-.749.115l-4.533 5.494-2.307-2.516a.548.548 0 0 0-.206-.14.598.598 0 0 0-.499.031.529.529 0 0 0-.183.164l-2.75 4a.468.468 0 0 0-.015.507.526.526 0 0 0 .202.189c.084.045.18.069.28.069H19.15a.594.594 0 0 0 .268-.063.532.532 0 0 0 .2-.174.462.462 0 0 0 .024-.487ZM9.25 9c.911 0 1.65-.672 1.65-1.5S10.161 6 9.25 6c-.91 0-1.65.672-1.65 1.5S8.34 9 9.25 9Z"></path>
|
||||
</template>
|
||||
|
||||
<!-- gallery -->
|
||||
<template v-else-if="commandId === 'gallery'">
|
||||
<g clip-path="url(#a)"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.8 5H3.2C1.985 5 1 5.806 1 6.8v14.4c0 .994.985 1.8 2.2 1.8h17.6c1.215 0 2.2-.806 2.2-1.8V6.8c0-.994-.985-1.8-2.2-1.8ZM6 1h12"></path><path fill="currentColor" d="M15.142 10.264a.75.75 0 0 1 .529.4l4 8A.75.75 0 0 1 19 19.75H6a.75.75 0 0 1-.498-1.31l9-8a.75.75 0 0 1 .64-.176ZM7 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h24v24H0z"></path></clipPath></defs>
|
||||
</template>
|
||||
|
||||
<!-- h1 -->
|
||||
<template v-else-if="commandId === 'h1'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M17.926 6.578v10.898c0 .602.33.963.862.963.541 0 .862-.351.862-.963V5.726c0-.682-.451-1.153-1.093-1.153-.39 0-.742.15-1.373.622l-2.026 1.504c-.4.29-.591.561-.591.852 0 .38.3.692.672.692.22 0 .43-.08.721-.291l1.885-1.374zM4.42 4.903a.77.77 0 0 1 .77.77v5.35h6.168v-5.35a.77.77 0 1 1 1.54 0v12.242a.77.77 0 0 1-1.54 0v-5.351H5.19v5.351a.77.77 0 1 1-1.54 0V5.673a.77.77 0 0 1 .77-.77"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- h2 -->
|
||||
<template v-else-if="commandId === 'h2'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M14.159 16c-.562.638-.725.905-.725 1.248 0 .553.439.886 1.135.886h6.289c.524 0 .829-.276.829-.724 0-.457-.324-.734-.83-.734H15.57v-.114l3.526-4.031c1.715-1.953 2.201-2.859 2.201-4.098 0-2.096-1.648-3.583-3.993-3.583-2.515 0-4.088 1.697-4.088 3.317 0 .514.305.867.772.867.39 0 .658-.258.791-.763.286-1.238 1.191-1.972 2.42-1.972 1.449 0 2.411.896 2.411 2.24 0 .895-.41 1.695-1.486 2.925zM3.419 5.364c.404 0 .731.327.731.731v5.087h5.863V6.095a.732.732 0 1 1 1.464 0v11.637a.732.732 0 0 1-1.464 0v-5.087H4.15v5.087a.732.732 0 1 1-1.463 0V6.095c0-.404.327-.731.732-.731"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- h3 -->
|
||||
<template v-else-if="commandId === 'h3'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M4.05 5.856a.75.75 0 0 0-1.5 0v11.921a.75.75 0 1 0 1.5 0v-5.21h6.006v5.21a.75.75 0 0 0 1.5 0V5.856a.75.75 0 0 0-1.5 0v5.21H4.05zm9.479 9.234c-.418 0-.713.304-.713.732 0 1.454 1.796 2.936 4.248 2.936 2.642 0 4.486-1.558 4.486-3.782 0-1.635-1.226-3.041-2.832-3.222v-.095c1.321-.228 2.395-1.596 2.395-3.031 0-1.977-1.692-3.393-4.068-3.393-2.338 0-3.925 1.425-3.925 2.898 0 .476.285.79.723.79.37 0 .608-.2.798-.723.38-.979 1.235-1.54 2.366-1.54 1.454 0 2.433.875 2.433 2.177s-1.007 2.242-2.395 2.242h-1.121c-.456 0-.76.295-.76.713 0 .409.323.723.76.723h1.188c1.654 0 2.765.978 2.765 2.432s-1.083 2.386-2.784 2.386c-1.293 0-2.281-.57-2.775-1.587-.247-.485-.456-.656-.789-.656"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- h4 -->
|
||||
<template v-else-if="commandId === 'h4'">
|
||||
<path fill="currentColor" d="M140-290v-380h60v160h180v-160h60v380h-60v-160H200v160h-60Zm580 0v-120H520v-260h60v200h140v-200h60v200h80v60h-80v120h-60Z"/>
|
||||
</template>
|
||||
|
||||
<!-- quote -->
|
||||
<template v-else-if="commandId === 'quote'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M5 10.966v4.095c0 .565.458 1.024 1.024 1.024h4.095c.566 0 1.024-.459 1.024-1.024v-4.095c0-.566-.459-1.024-1.024-1.024H6.704A3.35 3.35 0 0 1 9.845 7.75v-1.5A4.845 4.845 0 0 0 5 10.966m8 0v4.095c0 .565.458 1.024 1.024 1.024h4.095c.566 0 1.024-.459 1.024-1.024v-4.095c0-.566-.459-1.024-1.024-1.024h-3.415a3.35 3.35 0 0 1 3.141-2.192v-1.5A4.845 4.845 0 0 0 13 10.966"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- list -->
|
||||
<template v-else-if="commandId === 'list'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M4.3 7.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2m4.033-1.75a.75.75 0 0 0 0 1.5l11.917.001a.75.75 0 0 0 0-1.5zm0 5.5a.75.75 0 1 0 0 1.5l11.917.002a.75.75 0 1 0 0-1.5zm0 5.5a.75.75 0 1 0 0 1.5l11.917.002a.75.75 0 0 0 0-1.5zM5.3 12a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-1 6.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- code -->
|
||||
<template v-else-if="commandId === 'code'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M2.25 6A2.75 2.75 0 0 1 5 3.25h14A2.75 2.75 0 0 1 21.75 6v12A2.75 2.75 0 0 1 19 20.75H5A2.75 2.75 0 0 1 2.25 18zM5 4.75c-.69 0-1.25.56-1.25 1.25v12c0 .69.56 1.25 1.25 1.25h14c.69 0 1.25-.56 1.25-1.25V6c0-.69-.56-1.25-1.25-1.25zm5.53 4.62a.75.75 0 0 1 0 1.06l-1.59 1.591 1.59 1.591a.75.75 0 0 1-1.06 1.06l-2.122-2.12a.75.75 0 0 1 0-1.061L9.47 9.37a.75.75 0 0 1 1.06 0m2.94 1.06a.75.75 0 1 1 1.06-1.06l2.122 2.12a.75.75 0 0 1 0 1.062l-2.122 2.12a.75.75 0 1 1-1.06-1.06l1.59-1.59z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- divider -->
|
||||
<template v-else-if="commandId === 'divider'">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M4 4.25a.75.75 0 0 1 .75.75v1c0 .69.56 1.25 1.25 1.25h12c.69 0 1.25-.56 1.25-1.25V5a.75.75 0 0 1 1.5 0v1A2.75 2.75 0 0 1 18 8.75H6A2.75 2.75 0 0 1 3.25 6V5A.75.75 0 0 1 4 4.25m0 15.5a.75.75 0 0 0 .75-.75v-1c0-.69.56-1.25 1.25-1.25h12c.69 0 1.25.56 1.25 1.25v1a.75.75 0 0 0 1.5 0v-1A2.75 2.75 0 0 0 18 15.25H6A2.75 2.75 0 0 0 3.25 18v1c0 .414.336.75.75.75m-1-8.5a.75.75 0 0 0 0 1.5h1.2a.75.75 0 0 0 0-1.5H3m3.45.75a.75.75 0 0 1 .75-.75h1.2a.75.75 0 0 1 0 1.5H7.2a.75.75 0 0 1-.75-.75m4.95-.75a.75.75 0 0 0 0 1.5h1.2a.75.75 0 0 0 0-1.5h-1.2m3.45.75a.75.75 0 0 1 .75-.75h1.2a.75.75 0 0 1 0 1.5h-1.2a.75.75 0 0 1-.75-.75m4.95-.75a.75.75 0 0 0 0 1.5H21a.75.75 0 0 0 0-1.5h-1.2"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- callout -->
|
||||
<template v-else-if="commandId === 'callout'">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.8 2H3.2C1.985 2 1 2.895 1 4v16c0 1.105.985 2 2.2 2h17.6c1.215 0 2.2-.895 2.2-2V4c0-1.105-.985-2-2.2-2Z"></path><path fill="currentColor" d="M12 18a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.5v6"></path>
|
||||
</template>
|
||||
|
||||
<!-- toggle -->
|
||||
<template v-else-if="commandId === 'toggle'">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.8 2H3.2C1.985 2 1 2.895 1 4v16c0 1.105.985 2 2.2 2h17.6c1.215 0 2.2-.895 2.2-2V4c0-1.105-.985-2-2.2-2Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.5 11 12 15l-4.5-4"></path>
|
||||
</template>
|
||||
|
||||
<!-- embed -->
|
||||
<template v-else-if="commandId === 'embed'">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V9.8l4.7-5.3H19c.3 0 .5.2.5.5v14zm-6-9.5L16 12l-2.5 2.8 1.1 1L18 12l-3.5-3.5-1 1zm-3 0l-1-1L6 12l3.5 3.8 1.1-1L8 12l2.5-2.5z"></path>
|
||||
</template>
|
||||
|
||||
<!-- fallback -->
|
||||
<template v-else>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M12 4.5c.46 0 .833.373.833.833v5.834h5.834a.833.833 0 0 1 0 1.666h-5.834v5.834a.833.833 0 0 1-1.666 0v-5.834H5.333a.833.833 0 0 1 0-1.666h5.834V5.333c0-.46.373-.833.833-.833"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</template>
|
||||
</svg>
|
||||
</template>
|
||||
Reference in New Issue
Block a user