feat: add a markdown preview for description and notes (#1043)

* feat: add a markdown preview for description and notes

* feat: add char count for md
This commit is contained in:
Tonya
2025-10-10 13:37:57 +01:00
committed by GitHub
parent 116e39531b
commit 28c3e102a2
5 changed files with 99 additions and 8 deletions

View File

@@ -1005,12 +1005,12 @@
.markdown :where(ul) {
list-style: disc;
margin-left: 2rem;
margin-left: 1rem;
}
.markdown :where(ol) {
list-style: decimal;
margin-left: 2rem;
margin-left: 1rem;
}
/* Heading Styles */
.markdown :where(h1) {

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import Markdown from "@/components/global/Markdown.vue";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
const props = withDefaults(
defineProps<{
modelValue?: string | null;
label?: string | null;
maxLength?: number;
minLength?: number;
}>(),
{
modelValue: null,
label: null,
maxLength: -1,
minLength: -1,
}
);
const emit = defineEmits(["update:modelValue"]);
const local = ref(props.modelValue ?? "");
watch(
() => props.modelValue,
v => {
if (v !== local.value) local.value = v ?? "";
}
);
watch(local, v => emit("update:modelValue", v === "" ? null : v));
const showPreview = ref(false);
const id = useId();
const isLengthInvalid = computed(() => {
if (typeof local.value !== "string") return false;
const len = local.value.length;
const max = props.maxLength ?? -1;
const min = props.minLength ?? -1;
return (max !== -1 && len > max) || (min !== -1 && len < min);
});
const lengthIndicator = computed(() => {
if (typeof local.value !== "string") return "";
const max = props.maxLength ?? -1;
if (max !== -1) {
return `${local.value.length}/${max}`;
}
return "";
});
</script>
<template>
<div class="w-full">
<div class="mb-2 grid grid-cols-1 items-center gap-2 md:grid-cols-4">
<div class="min-w-0">
<Label :for="id" class="flex min-w-0 items-center gap-2 px-1">
<span class="truncate" :title="props.label ?? ''">{{ props.label }}</span>
<span class="grow" />
<span class="ml-2 text-sm" :class="{ 'text-destructive': isLengthInvalid }">{{ lengthIndicator }}</span>
</Label>
</div>
<div class="col-span-1 flex items-center justify-start gap-2 md:col-span-3 md:justify-end">
<label class="text-xs text-slate-500">{{ $t("global.preview") }}</label>
<Checkbox v-model="showPreview" />
</div>
</div>
<div class="flex w-full flex-col gap-4">
<Textarea :id="id" v-model="local" autosize class="resize-none" />
<div v-if="showPreview">
<Markdown :source="local" />
</div>
</div>
</div>
</template>

View File

@@ -25,14 +25,13 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="markdown text-wrap break-words" v-html="raw" />
<div class="markdown prose text-wrap break-words" v-html="raw" />
</template>
<style scoped>
* {
word-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
overflow-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
white-space: pre-wrap; /*Fix for long words going out of emelent bounds and issue #407 */
}
.markdown {
max-width: 100%;

View File

@@ -279,7 +279,8 @@
"updating": "Updating",
"value": "Value",
"version": "Version: { version }",
"welcome": "Welcome, { username }"
"welcome": "Welcome, { username }",
"preview": "Preview"
},
"home": {
"labels": "Labels",

View File

@@ -22,6 +22,7 @@
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import MarkdownEditor from "~/components/Form/MarkdownEditor.vue";
import FormDatePicker from "~/components/Form/DatePicker.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import LocationSelector from "~/components/Location/Selector.vue";
@@ -147,7 +148,7 @@
type DateKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends Date | string ? K : never]: any }>;
type TextFormField = {
type: "text" | "textarea";
type: "text" | "textarea" | "markdown";
label: string;
ref: NonNullableStringKeys<ItemOut>;
maxLength?: number;
@@ -188,7 +189,7 @@
ref: "quantity",
},
{
type: "textarea",
type: "markdown",
label: "items.description",
ref: "description",
maxLength: 1000,
@@ -212,7 +213,7 @@
maxLength: 255,
},
{
type: "textarea",
type: "markdown",
label: "items.notes",
ref: "notes",
maxLength: 1000,
@@ -613,6 +614,13 @@
:max-length="field.maxLength"
:min-length="field.minLength"
/>
<MarkdownEditor
v-else-if="field.type === 'markdown'"
v-model="item[field.ref]"
:label="$t(field.label)"
:max-length="field.maxLength"
:min-length="field.minLength"
/>
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"