mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
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:
@@ -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) {
|
||||
|
||||
83
frontend/components/Form/MarkdownEditor.vue
Normal file
83
frontend/components/Form/MarkdownEditor.vue
Normal 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>
|
||||
@@ -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%;
|
||||
|
||||
@@ -279,7 +279,8 @@
|
||||
"updating": "Updating",
|
||||
"value": "Value",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Welcome, { username }"
|
||||
"welcome": "Welcome, { username }",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Labels",
|
||||
|
||||
@@ -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]"
|
||||
|
||||
Reference in New Issue
Block a user