dml_frontend/components/Accordion.vue
2025-05-16 13:22:13 +02:00

219 lines
6.0 KiB
Vue

<template>
<div class="myAccordion">
<div v-for="(item, index) in items" :key="index" class="accordion-item">
<div
class="accordion-header"
:ref="el => setHeaderRef(el, index)"
@click="toggleSection(index)"
>
<div class="accordion-title">{{ item.title }}</div>
<div class="accordion-toggle" :class="{ open: openIndex === index }">
<span></span>
<span></span>
</div>
</div>
<div
class="accordion-content"
:ref="el => setContentRef(el, index)"
:style="{ maxHeight: openIndex === index ? `${contentHeights[index]}px` : '0px' }"
>
<p><span v-html="htmlContent(item.content)"></span></p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import { useHtmlConverter } from '../composables/useHTMLConverter';
const { convertToHTML } = useHtmlConverter();
const htmlContent = (data) => {
return convertToHTML(data); // Nutze die convertToHTML Funktion der Composable
};
// Props für die Items
const props = defineProps({
items: {
type: Array,
required: true,
default: () => [],
},
});
// Zustand für das geöffnete Element
const openIndex = ref(null);
// Referenzen für die Inhalte
const contentRefs = ref([]);
// Höhen der Inhalte
const contentHeights = ref([]);
// Referenzen für die Header-Elemente
const headerRefs = ref([]);
// Funktion zum Umschalten des geöffneten Abschnitts
const toggleSection = async (index) => {
// Umschalten des offenen Indexes
openIndex.value = openIndex.value === index ? null : index;
// Wenn ein neuer Abschnitt geöffnet wurde
if (openIndex.value !== null) {
await nextTick(); // Warten, bis das DOM aktualisiert wurde
setTimeout(() => {
const header = headerRefs.value[openIndex.value]; // Header des offenen Elements
const fixedHeaderHeight = document.querySelector('.fixed-header')?.offsetHeight || 0;
if (header) {
const topPosition = header.getBoundingClientRect().top + window.scrollY - fixedHeaderHeight - 20; // 20px Puffer
// Scrollen zur berechneten Position
window.scrollTo({
top: topPosition > 0 ? topPosition : 0, // Verhindert negatives Scrollen
behavior: 'smooth',
});
}
}, 100); // Kleiner Timeout für das reibungslose Scrollen
}
};
// Setze die Referenzen dynamisch
const setContentRef = (el, index) => {
if (el) {
contentRefs.value[index] = el;
}
};
const setHeaderRef = (el, index) => {
if (el) {
headerRefs.value[index] = el;
}
};
// Berechne die Höhen der Inhalte
const calculateHeights = () => {
contentHeights.value = contentRefs.value.map((el) => {
if (!el) return 0;
const additionalHeight = 50; // Fester Wert für oben und unten
return el.scrollHeight + additionalHeight; // Dynamische Höhe + fester Wert
});
};
// Initialisiere die Berechnung nach dem Mounten
onMounted(async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
});
// Aktualisiere die Höhen, wenn sich die Items ändern
watch(
() => props.items,
async () => {
contentRefs.value = Array.from({ length: props.items.length });
headerRefs.value = Array.from({ length: props.items.length });
await nextTick();
calculateHeights();
}
);
</script>
<style lang="scss">
.myAccordion {
.accordion-item {
border-bottom: 1px solid #ccc;
&:last-of-type {
border-bottom: none;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 1rem;
font-family: 'Mainfont-Bold';
background-color: white;
transition: background-color 0.3s ease;
&:hover {
background-color: lighten(#EEEBE5, 6%);
}
.accordion-title {
flex: 1; // Nimmt den freien Platz ein
text-align: left; // Links ausgerichtet
font-weight: bold;
font-size: 1rem;
margin-right: 1rem;
}
.accordion-toggle {
position: relative;
flex-shrink: 0; // Verhindert, dass das Icon skaliert wird
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
span {
position: absolute;
width: 15px; // Kürzere Linien für saubere Spitzen
height: 2px;
background-color: #333;
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
top: 8px;
&:first-child {
transform: rotate(135deg); // Linie 1: Oben links zur Mitte
left: 5px; // Leicht nach links verschoben
}
&:last-child {
transform: rotate(-135deg); // Linie 2: Oben rechts zur Mitte
left: -5px; // Leicht nach rechts verschoben
}
}
&.open {
span:first-child {
transform: rotate(45deg); // Linie 1: Teil des "X"
left: 0; // Zentriert
}
span:last-child {
transform: rotate(-45deg); // Linie 2: Teil des "X"
left: 0; // Zentriert
}
}
}
}
.accordion-content {
overflow: hidden;
transition: max-height 0.3s ease-out;
padding: 0 1rem;
background-color: white;
h3 {
font-size: 1rem;
line-height: 1.4rem;
margin-bottom: .5rem;
}
p {
margin: 1rem 0.5rem;
font-size: 1rem;
}
}
}
}
</style>