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) { .markdown :where(ul) {
list-style: disc; list-style: disc;
margin-left: 2rem; margin-left: 1rem;
} }
.markdown :where(ol) { .markdown :where(ol) {
list-style: decimal; list-style: decimal;
margin-left: 2rem; margin-left: 1rem;
} }
/* Heading Styles */ /* Heading Styles */
.markdown :where(h1) { .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> <template>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- 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> </template>
<style scoped> <style scoped>
* { * {
word-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */ 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 */ 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 { .markdown {
max-width: 100%; max-width: 100%;

View File

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

View File

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