skillbox/client/src/components/modules/Module.vue

273 lines
5.9 KiB
Vue

<template>
<!-- eslint-disable vue/no-v-html -->
<div
class="module"
v-if="module.id"
>
<div class="module__header">
<h2
class="module__meta-title"
id="meta-title"
>
{{ module.metaTitle }}
</h2>
<div
class="module__categoryindicators"
v-if="$flavor.showModuleFilter"
>
<pill :text="module.level?.name"></pill>
<pill :text="module.category?.name"></pill>
</div>
</div>
<h1
class="module__title"
data-cy="module-title"
>
{{ module.title }}
</h1>
<div class="module__hero">
<img
:src="module.heroImage"
alt=""
class="module__hero-image"
/>
<h5
class="module__hero-source"
v-if="module.heroSource"
>
Quelle: {{ module.heroSource }}
</h5>
</div>
<div class="module__intro-wrapper">
<bookmark-actions
:bookmarked="!!module.bookmark"
:note="note"
:edit-mode="module.inEditMode"
class="module__bookmark-actions"
data-cy="module-bookmark-actions"
@add-note="$emit('addNote')"
@edit-note="$emit('editNote')"
@bookmark="$emit('bookmark', !module.bookmark)"
/>
<div
class="module__intro intro"
data-cy="module-intro"
ref="introDiv"
v-html="module.intro"
/>
</div>
<chapter
:chapter="chapter"
:index="index"
:edit-mode="module.inEditMode"
v-for="(chapter, index) in module.chapters"
:key="chapter.id"
/>
</div>
</template>
<script setup lang="ts">
import Chapter from '@/components/Chapter.vue';
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
import Pill from '@/components/ui/Pill.vue';
import {
SelectionHandlerType,
SelectionHandlerOptions,
getSelectionHandler,
createHighlightCurry,
markHighlight,
} from '@/helpers/highlight';
import { onMounted, onUnmounted, ref, watch, computed } from 'vue';
import { AddHighlightArgument, HighlightNode, ModuleNode } from '@/__generated__/graphql';
import { graphql } from '@/__generated__';
import highlightSidebar from '@/helpers/highlight-sidebar';
import { doUpdateHighlight } from '@/graphql/mutations';
import Mark from 'mark.js';
export interface Props {
module: ModuleNode;
}
const props = defineProps<Props>();
let selectionHandler: SelectionHandlerType;
const introDiv = ref<HTMLElement | null>(null);
const moduleHighlightsFragment = graphql(`
fragment ModuleHighlightsFragment on ModuleNode {
id
__typename
slug
highlights {
...HighlightParts
}
}
`);
const createHighlight = createHighlightCurry({
fragment: moduleHighlightsFragment,
fragmentName: 'ModuleHighlightsFragment',
cacheSignature: { slug: props.module.slug, __typename: 'ModuleNode' },
isContentHighlight: false,
});
onMounted(() => {
const element = introDiv.value;
if (element !== null) {
const options: SelectionHandlerOptions = {
el: element,
page: props.module,
parentSelector: 'module__intro',
onChangeColor: (newHighlight: AddHighlightArgument) => {
createHighlight(newHighlight);
},
onCreateNote: async (newHighlight: AddHighlightArgument) => {
// we also open the sidebar when clicking on the note icon
const highlight = await createHighlight(newHighlight);
highlightSidebar.open({
highlight,
onUpdateText: (text: string) => {
doUpdateHighlight({
input: {
note: text,
id: highlight.id,
},
});
},
});
},
};
selectionHandler = getSelectionHandler(options);
element.addEventListener('mouseup', selectionHandler);
}
markHighlights();
});
const markHighlights = () => {
if (props.module.highlights && introDiv.value) {
for (const highlight of props.module.highlights) {
const highlightNode = highlight as HighlightNode;
const element = introDiv.value.children[highlightNode.paragraphIndex] as HTMLElement;
markHighlight(highlightNode, element, element);
}
}
};
onUnmounted(() => {
const element = introDiv.value;
if (element !== null) {
element.removeEventListener('mouseup', selectionHandler);
}
});
const unmark = () => {
for (const paragraph of introDiv.value.children) {
const instance = new Mark(paragraph);
instance.unmark();
}
};
const highlights = computed(() => {
return props.module ? props.module.highlights : [];
});
watch(
() => highlights.value?.filter((h) => h.color),
() => {
unmark();
markHighlights();
}
);
const note = computed(() => {
if (!(props.module && props.module.bookmark)) {
return;
}
return props.module.bookmark.note;
});
</script>
<style scoped lang="scss">
@import 'styles/helpers';
.module {
display: flex;
justify-self: center;
max-width: 100vw;
padding: $large-spacing 0;
@include desktop {
width: 800px;
padding: $large-spacing 15px;
}
flex-direction: column;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&__hero {
margin-bottom: 35px;
}
&__hero-image {
max-width: 100%;
border-radius: 12px;
}
&__hero-source {
@include tiny-text;
line-height: 25px;
}
&__header {
display: flex;
justify-content: flex-start;
align-items: stretch;
margin-bottom: $small-spacing;
}
&__meta-title {
@include meta-title;
margin-right: $medium-spacing;
}
&__intro-wrapper {
position: relative;
}
&__intro {
> :deep(p) {
margin-bottom: $large-spacing;
@include lead-paragraph;
&:last-child {
margin-bottom: 0;
}
}
> :deep(ul) {
@include list-parent;
> li {
@include list-child;
@include lead-paragraph;
}
}
}
&__bookmark-actions {
margin-top: 3px;
}
}
</style>