feat: Rich-text component for ext. link handling
This commit is contained in:
parent
5833c29817
commit
4776206bb8
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
export interface Props {
|
||||
title: 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="mb-4 lg:mb-0">
|
||||
<h3 class="mb-4">{{ title }}</h3>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p class="default-wagtail-rich-text mb-4" v-html="description"></p>
|
||||
<RichText class="mb-4" :content="description" />
|
||||
<a
|
||||
v-if="externalLink"
|
||||
:href="link"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -16,6 +16,7 @@ import { useMutation } from "@urql/vue";
|
|||
import dayjs, { Dayjs } from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
|
|
@ -188,10 +189,11 @@ const evaluationUser = computed(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="default-wagtail-rich-text mb-2 font-bold"
|
||||
v-html="task.value.description"
|
||||
></div>
|
||||
<RichText
|
||||
class="mb-2 font-bold"
|
||||
:content="task.value.description"
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
|
||||
<section class="mb-4">
|
||||
<div
|
||||
|
|
@ -199,12 +201,14 @@ const evaluationUser = computed(() => {
|
|||
subTaskByPoints(task, evaluationForTask(task).points)?.value.title
|
||||
"
|
||||
></div>
|
||||
<p
|
||||
class="default-wagtail-rich-text"
|
||||
v-html="
|
||||
|
||||
<RichText
|
||||
:content="
|
||||
subTaskByPoints(task, evaluationForTask(task).points)?.value.description
|
||||
"
|
||||
></p>
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
|
||||
<div class="text-sm text-gray-800">
|
||||
{{ evaluationForTask(task).points }} Punkte
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useMutation } from "@urql/vue";
|
|||
import { useDebounceFn } from "@vueuse/core";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
|
|
@ -114,11 +115,10 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
|
|||
/>
|
||||
<label :for="String(index)" class="ml-4 block">
|
||||
<div>{{ subTask.value.title }}</div>
|
||||
<div
|
||||
<RichText
|
||||
v-if="subTask.value.description"
|
||||
class="default-wagtail-rich-text"
|
||||
v-html="subTask.value.description"
|
||||
></div>
|
||||
:content="subTask.value.description"
|
||||
/>
|
||||
<div class="text-sm text-gray-800">{{ subTask.value.points }} Punkte</div>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import type { CircleType } from "@/types";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
defineProps<{
|
||||
circle: CircleType;
|
||||
|
|
@ -14,13 +15,7 @@ const emit = defineEmits(["closemodal"]);
|
|||
<ItFullScreenModal :show="show" @closemodal="emit('closemodal')">
|
||||
<div v-if="circle" class="container-medium">
|
||||
<h2 data-cy="lc-title">{{ $t("a.Übersicht") }}: Circle «{{ circle.title }}»</h2>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
v-if="circle.goals"
|
||||
class="default-wagtail-rich-text my-4"
|
||||
v-html="circle.goals"
|
||||
></div>
|
||||
<RichText v-if="circle.goals" class="my-4" :content="circle.goals" />
|
||||
</div>
|
||||
</ItFullScreenModal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
||||
import * as log from "loglevel";
|
||||
import { computed, getCurrentInstance, onUpdated } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
||||
|
|
@ -20,24 +20,6 @@ const learningContent = computed(() =>
|
|||
const circle = computed(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { Assignment } from "@/types";
|
|||
import { useRouteQuery } from "@vueuse/router";
|
||||
import log from "loglevel";
|
||||
import dayjs from "dayjs";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
interface Props {
|
||||
assignment: Assignment;
|
||||
|
|
@ -20,12 +21,12 @@ const step = useRouteQuery("step");
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p
|
||||
<RichText
|
||||
v-if="props.assignment.intro_text"
|
||||
class="default-wagtail-rich-text text-large"
|
||||
v-html="props.assignment.intro_text"
|
||||
></p>
|
||||
class="text-large"
|
||||
:content="props.assignment.intro_text"
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
|
||||
<h3 class="mb-4 mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
||||
<p class="text-large">
|
||||
|
|
@ -74,11 +75,12 @@ const step = useRouteQuery("step");
|
|||
"
|
||||
>
|
||||
<h3 class="mb-4 mt-8">{{ $t("a.Bewertung") }}</h3>
|
||||
<p
|
||||
<RichText
|
||||
v-if="props.assignment.evaluation_description"
|
||||
class="default-wagtail-rich-text text-large"
|
||||
v-html="props.assignment.evaluation_description"
|
||||
></p>
|
||||
class="text-large"
|
||||
:content="props.assignment.evaluation_description"
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
<p v-if="props.assignment.evaluation_document_url">
|
||||
<a
|
||||
:href="props.assignment.evaluation_document_url"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
AssignmentTaskCompletionData,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
|
|
@ -19,7 +20,6 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
v-for="task in props.assignment.tasks ?? []"
|
||||
:key="task.id"
|
||||
|
|
@ -36,10 +36,11 @@ const emit = defineEmits<{
|
|||
</button>
|
||||
</div>
|
||||
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||
<p
|
||||
class="default-wagtail-rich-text pt-6 text-base font-bold"
|
||||
v-html="taskBlock.value.text"
|
||||
></p>
|
||||
<RichText
|
||||
class="pt-6 text-base font-bold"
|
||||
:content="taskBlock.value.text"
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
<p
|
||||
v-if="
|
||||
props.assignmentCompletionData &&
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useDebounceFn } from "@vueuse/core";
|
|||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentId: string;
|
||||
|
|
@ -123,7 +124,11 @@ const taskFileInfo = computed(() => {
|
|||
<div class="flex flex-col space-y-10">
|
||||
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||
<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 v-if="block.type === 'user_confirmation'">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
||||
import type { LearningContent } from "@/types";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContent;
|
||||
|
|
@ -12,13 +13,12 @@ const props = defineProps<{
|
|||
:title="props.content.title"
|
||||
:learning-content="props.content"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="container-medium">
|
||||
<p
|
||||
<RichText
|
||||
v-if="props.content.description"
|
||||
class="default-wagtail-rich-text my-4"
|
||||
v-html="props.content.description"
|
||||
></p>
|
||||
class="my-4"
|
||||
:content="props.content.description"
|
||||
/>
|
||||
</div>
|
||||
</LearningContentSimpleLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
||||
import type { LearningContentRichText } from "@/types";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentRichText;
|
||||
|
|
@ -14,17 +15,13 @@ const props = defineProps<{
|
|||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="container-medium">
|
||||
<p
|
||||
<RichText
|
||||
v-if="props.content.description"
|
||||
class="default-wagtail-rich-text my-4"
|
||||
v-html="props.content.description"
|
||||
></p>
|
||||
class="my-4"
|
||||
:content="props.content.description"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="props.content.text"
|
||||
class="default-wagtail-rich-text my-4"
|
||||
v-html="props.content.text"
|
||||
></div>
|
||||
<RichText v-if="props.content.text" class="my-4" :content="props.content.text" />
|
||||
</div>
|
||||
</LearningContentSimpleLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useMediaLibraryStore } from "@/stores/mediaLibrary";
|
|||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import type { MediaLibraryCategoryPage, MediaLibraryContentPage } from "@/types";
|
||||
import RichText from "@/components/ui/RichText.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
categorySlug: string;
|
||||
|
|
@ -47,11 +48,11 @@ const mediaCategory = computed(() => {
|
|||
<div class="flex justify-between md:flex-col lg:flex-row">
|
||||
<div class="lg:w-6/12">
|
||||
<h1 class="mb-4 lg:mb-8" data-cy="hf-title">{{ mediaCategory.title }}</h1>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p
|
||||
class="default-wagtail-rich-text text-large"
|
||||
v-html="mediaCategory.description"
|
||||
></p>
|
||||
<RichText
|
||||
class="text-large"
|
||||
:content="mediaCategory.description"
|
||||
open-links-in-new-tab
|
||||
></RichText>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
|
|
@ -64,11 +65,11 @@ const mediaCategory = computed(() => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="container-large">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<section
|
||||
class="default-wagtail-rich-text mb-20 mt-8 lg:w-2/3"
|
||||
v-html="mediaCategory.body"
|
||||
></section>
|
||||
<RichText
|
||||
class="mb-20 mt-8 lg:w-2/3"
|
||||
:content="mediaCategory.body"
|
||||
open-links-in-new-tab
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
};
|
||||
Loading…
Reference in New Issue