feat: Rich-text component for ext. link handling

This commit is contained in:
Reto Aebersold 2023-11-01 14:27:53 +01:00
parent 5833c29817
commit 4776206bb8
13 changed files with 113 additions and 79 deletions

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import RichText from "@/components/ui/RichText.vue";
export interface Props { export interface Props {
title: string; title: string;
description: string; description: string;
@ -18,8 +20,7 @@ withDefaults(defineProps<Props>(), {
<div class="flex flex-col justify-between bg-white p-8 pb-4 lg:flex-row lg:pb-8"> <div class="flex flex-col justify-between bg-white p-8 pb-4 lg:flex-row lg:pb-8">
<div class="mb-4 lg:mb-0"> <div class="mb-4 lg:mb-0">
<h3 class="mb-4">{{ title }}</h3> <h3 class="mb-4">{{ title }}</h3>
<!-- eslint-disable vue/no-v-html --> <RichText class="mb-4" :content="description" />
<p class="default-wagtail-rich-text mb-4" v-html="description"></p>
<a <a
v-if="externalLink" v-if="externalLink"
:href="link" :href="link"

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { computed } from "vue";
import { setExternalLinksToOpenInNewTab } from "@/utils/dom";
const props = withDefaults(
defineProps<{
content?: string;
openLinksInNewTab?: boolean;
}>(),
{
content: "",
openLinksInNewTab: false,
}
);
const updatedContent = computed(() => {
if (!props.openLinksInNewTab || !props.content) return props.content;
const parser = new DOMParser();
const doc = parser.parseFromString(props.content, "text/html");
const anchors = doc.querySelectorAll("a");
setExternalLinksToOpenInNewTab(anchors);
return doc.body.innerHTML;
});
</script>
<template>
<!-- eslint-disable vue/no-v-html -->
<section class="default-wagtail-rich-text" v-html="updatedContent"></section>
</template>

View File

@ -16,6 +16,7 @@ import { useMutation } from "@urql/vue";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
assignmentUser: CourseSessionUser; assignmentUser: CourseSessionUser;
@ -188,10 +189,11 @@ const evaluationUser = computed(() => {
</div> </div>
</div> </div>
<div <RichText
class="default-wagtail-rich-text mb-2 font-bold" class="mb-2 font-bold"
v-html="task.value.description" :content="task.value.description"
></div> open-links-in-new-tab
/>
<section class="mb-4"> <section class="mb-4">
<div <div
@ -199,12 +201,14 @@ const evaluationUser = computed(() => {
subTaskByPoints(task, evaluationForTask(task).points)?.value.title subTaskByPoints(task, evaluationForTask(task).points)?.value.title
" "
></div> ></div>
<p
class="default-wagtail-rich-text" <RichText
v-html=" :content="
subTaskByPoints(task, evaluationForTask(task).points)?.value.description subTaskByPoints(task, evaluationForTask(task).points)?.value.description
" "
></p> open-links-in-new-tab
/>
<div class="text-sm text-gray-800"> <div class="text-sm text-gray-800">
{{ evaluationForTask(task).points }} Punkte {{ evaluationForTask(task).points }} Punkte
</div> </div>

View File

@ -13,6 +13,7 @@ import { useMutation } from "@urql/vue";
import { useDebounceFn } from "@vueuse/core"; import { useDebounceFn } from "@vueuse/core";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
assignmentUser: CourseSessionUser; assignmentUser: CourseSessionUser;
@ -114,11 +115,10 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
/> />
<label :for="String(index)" class="ml-4 block"> <label :for="String(index)" class="ml-4 block">
<div>{{ subTask.value.title }}</div> <div>{{ subTask.value.title }}</div>
<div <RichText
v-if="subTask.value.description" v-if="subTask.value.description"
class="default-wagtail-rich-text" :content="subTask.value.description"
v-html="subTask.value.description" />
></div>
<div class="text-sm text-gray-800">{{ subTask.value.points }} Punkte</div> <div class="text-sm text-gray-800">{{ subTask.value.points }} Punkte</div>
</label> </label>
</div> </div>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import type { CircleType } from "@/types"; import type { CircleType } from "@/types";
import RichText from "@/components/ui/RichText.vue";
defineProps<{ defineProps<{
circle: CircleType; circle: CircleType;
@ -14,13 +15,7 @@ const emit = defineEmits(["closemodal"]);
<ItFullScreenModal :show="show" @closemodal="emit('closemodal')"> <ItFullScreenModal :show="show" @closemodal="emit('closemodal')">
<div v-if="circle" class="container-medium"> <div v-if="circle" class="container-medium">
<h2 data-cy="lc-title">{{ $t("a.Übersicht") }}: Circle «{{ circle.title }}»</h2> <h2 data-cy="lc-title">{{ $t("a.Übersicht") }}: Circle «{{ circle.title }}»</h2>
<RichText v-if="circle.goals" class="my-4" :content="circle.goals" />
<!-- eslint-disable vue/no-v-html -->
<div
v-if="circle.goals"
class="default-wagtail-rich-text my-4"
v-html="circle.goals"
></div>
</div> </div>
</ItFullScreenModal> </ItFullScreenModal>
</template> </template>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue"; import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed, getCurrentInstance, onUpdated } from "vue"; import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables"; import { useCourseDataWithCompletion } from "@/composables";
import { stringifyParse } from "@/utils/utils"; import { stringifyParse } from "@/utils/utils";
@ -20,24 +20,6 @@ const learningContent = computed(() =>
const circle = computed(() => { const circle = computed(() => {
return courseData.findCircle(props.circleSlug); return courseData.findCircle(props.circleSlug);
}); });
onUpdated(() => {
const vueInstance = getCurrentInstance();
if (vueInstance) {
// VBV-489: open external links in new tab
const rootElement: HTMLElement = vueInstance.proxy?.$el;
const anchors = rootElement.querySelectorAll("a");
anchors.forEach((anchor: HTMLAnchorElement) => {
if (
/^https?:\/\//i.test(anchor.href) &&
!anchor.href.includes(window.location.hostname)
) {
anchor.setAttribute("target", "_blank");
anchor.setAttribute("rel", "noopener noreferrer");
}
});
}
});
</script> </script>
<template> <template>

View File

@ -4,6 +4,7 @@ import type { Assignment } from "@/types";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import log from "loglevel"; import log from "loglevel";
import dayjs from "dayjs"; import dayjs from "dayjs";
import RichText from "@/components/ui/RichText.vue";
interface Props { interface Props {
assignment: Assignment; assignment: Assignment;
@ -20,12 +21,12 @@ const step = useRouteQuery("step");
</script> </script>
<template> <template>
<!-- eslint-disable vue/no-v-html --> <RichText
<p
v-if="props.assignment.intro_text" v-if="props.assignment.intro_text"
class="default-wagtail-rich-text text-large" class="text-large"
v-html="props.assignment.intro_text" :content="props.assignment.intro_text"
></p> open-links-in-new-tab
/>
<h3 class="mb-4 mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3> <h3 class="mb-4 mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
<p class="text-large"> <p class="text-large">
@ -74,11 +75,12 @@ const step = useRouteQuery("step");
" "
> >
<h3 class="mb-4 mt-8">{{ $t("a.Bewertung") }}</h3> <h3 class="mb-4 mt-8">{{ $t("a.Bewertung") }}</h3>
<p <RichText
v-if="props.assignment.evaluation_description" v-if="props.assignment.evaluation_description"
class="default-wagtail-rich-text text-large" class="text-large"
v-html="props.assignment.evaluation_description" :content="props.assignment.evaluation_description"
></p> open-links-in-new-tab
/>
<p v-if="props.assignment.evaluation_document_url"> <p v-if="props.assignment.evaluation_document_url">
<a <a
:href="props.assignment.evaluation_document_url" :href="props.assignment.evaluation_document_url"

View File

@ -6,6 +6,7 @@ import type {
AssignmentTaskCompletionData, AssignmentTaskCompletionData,
UserDataText, UserDataText,
} from "@/types"; } from "@/types";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
assignment: Assignment; assignment: Assignment;
@ -19,7 +20,6 @@ const emit = defineEmits<{
}>(); }>();
</script> </script>
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div <div
v-for="task in props.assignment.tasks ?? []" v-for="task in props.assignment.tasks ?? []"
:key="task.id" :key="task.id"
@ -36,10 +36,11 @@ const emit = defineEmits<{
</button> </button>
</div> </div>
<div v-for="taskBlock in task.value.content" :key="taskBlock.id"> <div v-for="taskBlock in task.value.content" :key="taskBlock.id">
<p <RichText
class="default-wagtail-rich-text pt-6 text-base font-bold" class="pt-6 text-base font-bold"
v-html="taskBlock.value.text" :content="taskBlock.value.text"
></p> open-links-in-new-tab
/>
<p <p
v-if=" v-if="
props.assignmentCompletionData && props.assignmentCompletionData &&

View File

@ -16,6 +16,7 @@ import { useDebounceFn } from "@vueuse/core";
import log from "loglevel"; import log from "loglevel";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue"; import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
assignmentId: string; assignmentId: string;
@ -123,7 +124,11 @@ const taskFileInfo = computed(() => {
<div class="flex flex-col space-y-10"> <div class="flex flex-col space-y-10">
<div v-for="(block, index) in props.task.value.content" :key="block.id"> <div v-for="(block, index) in props.task.value.content" :key="block.id">
<div v-if="block.type === 'explanation'"> <div v-if="block.type === 'explanation'">
<p class="default-wagtail-rich-text text-large" v-html="block.value.text"></p> <RichText
class="text-large"
:content="block.value.text"
open-links-in-new-tab
/>
</div> </div>
<div v-if="block.type === 'user_confirmation'"> <div v-if="block.type === 'user_confirmation'">

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue"; import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContent } from "@/types"; import type { LearningContent } from "@/types";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
content: LearningContent; content: LearningContent;
@ -12,13 +13,12 @@ const props = defineProps<{
:title="props.content.title" :title="props.content.title"
:learning-content="props.content" :learning-content="props.content"
> >
<!-- eslint-disable vue/no-v-html -->
<div class="container-medium"> <div class="container-medium">
<p <RichText
v-if="props.content.description" v-if="props.content.description"
class="default-wagtail-rich-text my-4" class="my-4"
v-html="props.content.description" :content="props.content.description"
></p> />
</div> </div>
</LearningContentSimpleLayout> </LearningContentSimpleLayout>
</template> </template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue"; import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
import type { LearningContentRichText } from "@/types"; import type { LearningContentRichText } from "@/types";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
content: LearningContentRichText; content: LearningContentRichText;
@ -14,17 +15,13 @@ const props = defineProps<{
> >
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="container-medium"> <div class="container-medium">
<p <RichText
v-if="props.content.description" v-if="props.content.description"
class="default-wagtail-rich-text my-4" class="my-4"
v-html="props.content.description" :content="props.content.description"
></p> />
<div <RichText v-if="props.content.text" class="my-4" :content="props.content.text" />
v-if="props.content.text"
class="default-wagtail-rich-text my-4"
v-html="props.content.text"
></div>
</div> </div>
</LearningContentSimpleLayout> </LearningContentSimpleLayout>
</template> </template>

View File

@ -3,6 +3,7 @@ import { useMediaLibraryStore } from "@/stores/mediaLibrary";
import * as log from "loglevel"; import * as log from "loglevel";
import { computed } from "vue"; import { computed } from "vue";
import type { MediaLibraryCategoryPage, MediaLibraryContentPage } from "@/types"; import type { MediaLibraryCategoryPage, MediaLibraryContentPage } from "@/types";
import RichText from "@/components/ui/RichText.vue";
const props = defineProps<{ const props = defineProps<{
categorySlug: string; categorySlug: string;
@ -47,11 +48,11 @@ const mediaCategory = computed(() => {
<div class="flex justify-between md:flex-col lg:flex-row"> <div class="flex justify-between md:flex-col lg:flex-row">
<div class="lg:w-6/12"> <div class="lg:w-6/12">
<h1 class="mb-4 lg:mb-8" data-cy="hf-title">{{ mediaCategory.title }}</h1> <h1 class="mb-4 lg:mb-8" data-cy="hf-title">{{ mediaCategory.title }}</h1>
<!-- eslint-disable vue/no-v-html --> <RichText
<p class="text-large"
class="default-wagtail-rich-text text-large" :content="mediaCategory.description"
v-html="mediaCategory.description" open-links-in-new-tab
></p> ></RichText>
</div> </div>
<div> <div>
<img <img
@ -64,11 +65,11 @@ const mediaCategory = computed(() => {
</div> </div>
</div> </div>
<div class="container-large"> <div class="container-large">
<!-- eslint-disable vue/no-v-html --> <RichText
<section class="mb-20 mt-8 lg:w-2/3"
class="default-wagtail-rich-text mb-20 mt-8 lg:w-2/3" :content="mediaCategory.body"
v-html="mediaCategory.body" open-links-in-new-tab
></section> />
</div> </div>
</div> </div>
</template> </template>

14
client/src/utils/dom.ts Normal file
View File

@ -0,0 +1,14 @@
export const setExternalLinksToOpenInNewTab = (
anchors: NodeListOf<HTMLAnchorElement>
) => {
const httpRegex = /^https?:\/\//i;
anchors.forEach((anchor: HTMLAnchorElement) => {
if (
httpRegex.test(anchor.href) &&
!anchor.href.includes(window.location.hostname)
) {
anchor.setAttribute("target", "_blank");
anchor.setAttribute("rel", "noopener noreferrer");
}
});
};