mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
184 lines
5.0 KiB
Vue
184 lines
5.0 KiB
Vue
<template>
|
|
<li>
|
|
<h2>{{ props.item.title }}</h2>
|
|
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
|
|
<div class="content" v-if="shouldTruncate(item.content)">
|
|
<div v-html="prepareContent(item.content)"></div>
|
|
<div class="float-end">
|
|
<button class="btn btn-sm btn-show read-more" @click="() => openModal(item)">{{ $t('widget.news.readMore') }}</button>
|
|
</div>
|
|
</div>
|
|
<div class="content" v-else>
|
|
<div v-html="convertMarkdownToHtml(item.content)"></div>
|
|
</div>
|
|
|
|
<modal v-if="showModal" @close="closeModal">
|
|
<template #header>
|
|
<p class="news-title">{{ item.title }}</p>
|
|
</template>
|
|
<template #body>
|
|
<p class="news-date">
|
|
<time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
|
|
</p>
|
|
<div v-html="convertMarkdownToHtml(item.content)"></div>
|
|
</template>
|
|
</modal>
|
|
</li>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
import { marked } from 'marked';
|
|
import DOMPurify from 'dompurify';
|
|
import { DateTime, NewsItemType } from "../../../types";
|
|
import type { PropType } from 'vue'
|
|
import { ref } from "vue";
|
|
import {ISOToDatetime} from '../../../chill/js/date';
|
|
|
|
|
|
const props = defineProps({
|
|
item: {
|
|
type: Object as PropType<NewsItemType>,
|
|
required: true
|
|
},
|
|
maxLength: {
|
|
type: Number,
|
|
required: false,
|
|
default: 350,
|
|
},
|
|
maxLines: {
|
|
type: Number,
|
|
required: false,
|
|
default: 3
|
|
}
|
|
})
|
|
|
|
const selectedArticle = ref<NewsItemType | null>(null);
|
|
const showModal = ref(false);
|
|
|
|
const openModal = (item: NewsItemType) => {
|
|
selectedArticle.value = item;
|
|
showModal.value = true;
|
|
};
|
|
|
|
const closeModal = () => {
|
|
selectedArticle.value = null;
|
|
showModal.value = false;
|
|
};
|
|
|
|
const shouldTruncate = (content: string): boolean => {
|
|
const lines = content.split('\n');
|
|
|
|
// Check if any line exceeds the maximum length
|
|
const tooManyLines = lines.length > props.maxLines;
|
|
|
|
return content.length > props.maxLength || tooManyLines;
|
|
};
|
|
|
|
const truncateContent = (content: string): string => {
|
|
let truncatedContent = content.slice(0, props.maxLength);
|
|
let linkDepth = 0;
|
|
let linkStartIndex = -1;
|
|
const lines = content.split('\n');
|
|
|
|
// Truncate if amount of lines are too many
|
|
if (lines.length > props.maxLines && content.length < props.maxLength) {
|
|
const truncatedContent = lines.slice(0, props.maxLines).join('\n').trim();
|
|
return truncatedContent + '...';
|
|
}
|
|
|
|
for (let i = 0; i < truncatedContent.length; i++) {
|
|
const char = truncatedContent[i];
|
|
|
|
if (char === '[') {
|
|
linkDepth++;
|
|
if (linkDepth === 1) {
|
|
linkStartIndex = i;
|
|
}
|
|
} else if (char === ']') {
|
|
linkDepth = Math.max(0, linkDepth - 1);
|
|
} else if (char === '(' && linkDepth === 0) {
|
|
truncatedContent = truncatedContent.slice(0, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
while (linkDepth > 0) {
|
|
truncatedContent += ']';
|
|
linkDepth--;
|
|
}
|
|
|
|
// If a link was found, append the URL inside the parentheses
|
|
if (linkStartIndex !== -1) {
|
|
const linkEndIndex = content.indexOf(')', linkStartIndex);
|
|
const url = content.slice(linkStartIndex + 1, linkEndIndex);
|
|
truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`;
|
|
}
|
|
|
|
truncatedContent += '...';
|
|
|
|
return truncatedContent;
|
|
};
|
|
|
|
const preprocess = (markdown: string): string => {
|
|
return markdown;
|
|
}
|
|
|
|
const postprocess = (html: string): string => {
|
|
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
|
if ('target' in node) {
|
|
node.setAttribute('target', '_blank');
|
|
node.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
|
|
node.setAttribute('xlink:show', 'new');
|
|
}
|
|
})
|
|
|
|
return DOMPurify.sanitize(html);
|
|
}
|
|
|
|
const convertMarkdownToHtml = (markdown: string): string => {
|
|
marked.use({'hooks': {postprocess, preprocess}});
|
|
const rawHtml = marked(markdown);
|
|
return rawHtml;
|
|
};
|
|
|
|
const prepareContent = (content: string): string => {
|
|
const htmlContent = convertMarkdownToHtml(content);
|
|
return truncateContent(htmlContent);
|
|
};
|
|
|
|
const newsItemStartDate = (): null|Date => {
|
|
return ISOToDatetime(props.item?.startDate.datetime);
|
|
}
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
|
|
li {
|
|
margin-bottom: 20px;
|
|
overflow: hidden;
|
|
padding: .8rem;
|
|
background-color: #fbfbfb;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1rem !important;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.content {
|
|
overflow: hidden;
|
|
font-size: .9rem;
|
|
position: relative;
|
|
}
|
|
|
|
.news-title {
|
|
font-weight: bold;
|
|
}
|
|
|
|
</style>
|