feat: entry editor at /content/:key/new and /content/:key/:id
This commit is contained in:
1 parent
91d916fb74
commit
fba20f83c5
94 files changed
+7116
-423
No files matched your search
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { renderMarkdown } from '@/lib/markdown'
|
||||
import { MARKDOWN_ACTIONS, applyMarkdownAction, type MarkdownAction } from '@/lib/markdownEdit'
|
||||
|
||||
type Mode = 'write' | 'preview'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
disabled?: boolean
|
||||
invalid?: boolean
|
||||
required?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
const mode = ref<Mode>('write')
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ label: t('Write'), value: 'write' },
|
||||
{ label: t('Preview'), value: 'preview' },
|
||||
])
|
||||
const actions = computed(() => ({
|
||||
bold: { label: t('Bold'), icon: 'i-lucide-bold' },
|
||||
italic: { label: t('Italic'), icon: 'i-lucide-italic' },
|
||||
heading: { label: t('Heading'), icon: 'i-lucide-heading' },
|
||||
link: { label: t('Link'), icon: 'i-lucide-link' },
|
||||
list: { label: t('List'), icon: 'i-lucide-list' },
|
||||
}))
|
||||
const rendered = computed(() => renderMarkdown(props.modelValue))
|
||||
const empty = computed(() => props.modelValue.trim() === '')
|
||||
|
||||
function textarea() {
|
||||
return root.value?.querySelector<HTMLTextAreaElement>('textarea') ?? null
|
||||
}
|
||||
|
||||
async function run(action: MarkdownAction) {
|
||||
const field = textarea()
|
||||
if (!field) {
|
||||
return
|
||||
}
|
||||
const edit = applyMarkdownAction(
|
||||
action,
|
||||
props.modelValue,
|
||||
field.selectionStart,
|
||||
field.selectionEnd,
|
||||
)
|
||||
emit('update:modelValue', edit.value)
|
||||
await nextTick()
|
||||
field.focus()
|
||||
field.setSelectionRange(edit.start, edit.end)
|
||||
}
|
||||
|
||||
async function onMode(value: string | number) {
|
||||
mode.value = value === 'preview' ? 'preview' : 'write'
|
||||
if (mode.value === 'write') {
|
||||
await nextTick()
|
||||
textarea()?.focus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
data-testid="markdown-field"
|
||||
class="flex min-w-0 flex-col border bg-default"
|
||||
:class="[
|
||||
invalid ? 'border-error shadow-[inset_0_0_0_1px_var(--ui-error)]' : 'border-accented',
|
||||
disabled ? 'bg-elevated' : '',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 overflow-hidden border-b border-default px-1.5">
|
||||
<UTabs
|
||||
:model-value="mode"
|
||||
:items="tabs"
|
||||
:content="false"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
:ui="{
|
||||
root: 'mr-2 w-auto gap-0',
|
||||
list: 'gap-3.5 px-1.5',
|
||||
trigger: 'h-11 px-0 text-base md:h-8',
|
||||
indicator: 'bg-primary',
|
||||
}"
|
||||
@update:model-value="onMode"
|
||||
/>
|
||||
<template v-if="mode === 'write'">
|
||||
<UButton
|
||||
v-for="action in MARKDOWN_ACTIONS"
|
||||
:key="action"
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:icon="actions[action].icon"
|
||||
:aria-label="actions[action].label"
|
||||
:disabled="disabled"
|
||||
class="size-10 justify-center text-toned md:size-7"
|
||||
:ui="{ leadingIcon: 'size-4' }"
|
||||
@click="run(action)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<UTextarea
|
||||
v-show="mode === 'write'"
|
||||
:model-value="modelValue"
|
||||
:placeholder="t('Write in Markdown')"
|
||||
:disabled="disabled"
|
||||
:aria-invalid="invalid ? 'true' : undefined"
|
||||
:aria-required="required ? 'true' : undefined"
|
||||
variant="none"
|
||||
:rows="4"
|
||||
autoresize
|
||||
class="w-full"
|
||||
:ui="{
|
||||
base: 'min-h-30 px-2.5 py-2 font-mono text-lg/5.5 md:min-h-24 md:text-xs/4.75',
|
||||
}"
|
||||
@update:model-value="emit('update:modelValue', String($event))"
|
||||
/>
|
||||
<div
|
||||
v-if="mode === 'preview'"
|
||||
data-testid="markdown-preview"
|
||||
class="flex min-h-30 flex-col gap-2 px-3 py-2.5 text-lg md:min-h-24 md:text-sm [&_a]:text-primary [&_a]:underline [&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold [&_ul]:list-[square] [&_ul]:ps-4.5"
|
||||
>
|
||||
<p v-if="empty" class="text-muted">{{ t('Nothing to preview yet.') }}</p>
|
||||
<div v-else class="flex flex-col gap-2" v-html="rendered" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user