fix(front): add a required attribute to the text editor and show error message if text empty on blur

Also improve text editor borders

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2024-01-09 16:10:56 +01:00
parent ef20585f8c
commit ba66874cc3
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
4 changed files with 48 additions and 6 deletions

View file

@ -32,6 +32,7 @@
v-if="currentActor" v-if="currentActor"
:currentActor="currentActor" :currentActor="currentActor"
:placeholder="t('Write a new message')" :placeholder="t('Write a new message')"
:required="true"
/> />
<o-notification <o-notification
class="my-2" class="my-2"

View file

@ -217,7 +217,15 @@
</button> </button>
</bubble-menu> </bubble-menu>
<editor-content class="editor__content" :editor="editor" v-if="editor" /> <editor-content
class="editor__content"
:class="{ editorErrorStatus }"
:editor="editor"
v-if="editor"
/>
<p v-if="editorErrorMessage" class="text-sm text-mbz-danger">
{{ editorErrorMessage }}
</p>
</div> </div>
</div> </div>
</template> </template>
@ -249,7 +257,7 @@ import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
import { AutoDir } from "./Editor/Autodir"; import { AutoDir } from "./Editor/Autodir";
// import sanitizeHtml from "sanitize-html"; // import sanitizeHtml from "sanitize-html";
import { computed, inject, onBeforeUnmount, watch } from "vue"; import { computed, inject, onBeforeUnmount, ref, watch } from "vue";
import { Dialog } from "@/plugins/dialog"; import { Dialog } from "@/plugins/dialog";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
@ -279,11 +287,13 @@ const props = withDefaults(
currentActor: IPerson; currentActor: IPerson;
placeholder?: string; placeholder?: string;
headingLevel?: Level[]; headingLevel?: Level[];
required?: boolean;
}>(), }>(),
{ {
mode: "description", mode: "description",
maxSize: 100_000_000, maxSize: 100_000_000,
headingLevel: () => [3, 4, 5], headingLevel: () => [3, 4, 5],
required: false,
} }
); );
@ -333,7 +343,7 @@ const editor = useEditor({
"aria-label": ariaLabel.value ?? "", "aria-label": ariaLabel.value ?? "",
role: "textbox", role: "textbox",
class: class:
"prose dark:prose-invert prose-sm lg:prose-lg xl:prose-xl bg-zinc-50 dark:bg-zinc-700 focus:outline-none !max-w-full", "prose dark:prose-invert prose-sm lg:prose-lg xl:prose-xl bg-white dark:bg-zinc-700 focus:outline-none !max-w-full",
}, },
transformPastedHTML: transformPastedHTML, transformPastedHTML: transformPastedHTML,
}, },
@ -373,6 +383,13 @@ const editor = useEditor({
onUpdate: () => { onUpdate: () => {
emit("update:modelValue", editor.value?.getHTML()); emit("update:modelValue", editor.value?.getHTML());
}, },
onBlur: () => {
checkEditorEmpty();
},
onFocus: () => {
editorErrorStatus.value = false;
editorErrorMessage.value = "";
},
}); });
watch(value, (val: string) => { watch(value, (val: string) => {
@ -470,6 +487,18 @@ defineExpose({ replyToComment, focus });
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy(); editor.value?.destroy();
}); });
const editorErrorStatus = ref(false);
const editorErrorMessage = ref("");
const isEmpty = computed(
() => props.required === true && editor.value?.isEmpty === true
);
const checkEditorEmpty = () => {
editorErrorStatus.value = isEmpty.value;
editorErrorMessage.value = isEmpty.value ? t("You need to enter a text") : "";
};
</script> </script>
<style lang="scss"> <style lang="scss">
@use "@/styles/_mixins" as *; @use "@/styles/_mixins" as *;
@ -525,7 +554,6 @@ onBeforeUnmount(() => {
min-height: 2.5rem; min-height: 2.5rem;
box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
border-radius: 4px; border-radius: 4px;
border: 1px solid #dbdbdb;
padding: 12px 6px; padding: 12px 6px;
&:focus { &:focus {
@ -655,4 +683,15 @@ onBeforeUnmount(() => {
.mention[data-id] { .mention[data-id] {
@apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1; @apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1;
} }
.editor__content {
@apply border focus:border-[#2563eb] rounded border-[#6b7280];
}
.editorErrorStatus {
@apply border-red-500;
}
.editor__content p.is-editor-empty:first-child::before {
@apply text-slate-300;
}
</style> </style>

View file

@ -1642,5 +1642,6 @@
"Visit {instance_domain}": "Visit {instance_domain}", "Visit {instance_domain}": "Visit {instance_domain}",
"Software details: {software_details}": "Software details: {software_details}", "Software details: {software_details}": "Software details: {software_details}",
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed", "Only instances with an application actor can be followed": "Only instances with an application actor can be followed",
"Domain or instance name": "Domain or instance name" "Domain or instance name": "Domain or instance name",
"You need to enter a text": "You need to enter a text"
} }

View file

@ -1636,5 +1636,6 @@
"Visit {instance_domain}": "Visiter {instance_domain}", "Visit {instance_domain}": "Visiter {instance_domain}",
"Software details: {software_details}": "Détails du logiciel : {software_details}", "Software details: {software_details}": "Détails du logiciel : {software_details}",
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies", "Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies",
"Domain or instance name": "Domaine ou nom de l'instance" "Domain or instance name": "Domaine ou nom de l'instance",
"You need to enter a text": "Vous devez entrer un texte"
} }