387 lines
11 KiB
Vue
387 lines
11 KiB
Vue
<template>
|
|
<div
|
|
:class="componentClass"
|
|
:data-scrollto="component.id"
|
|
:data-uuid="component.id"
|
|
data-cy="content-component"
|
|
ref="contentComponentDiv"
|
|
>
|
|
<bookmark-actions
|
|
:bookmarked="bookmarked"
|
|
:note="note"
|
|
:edit-mode="editMode"
|
|
v-if="showBookmarkActions"
|
|
@add-note="addNote(component.id)"
|
|
@edit-note="editNote"
|
|
@bookmark="bookmarkContent(!bookmarked)"
|
|
/>
|
|
<component
|
|
v-bind="component"
|
|
:parent="parent"
|
|
class="content-component__content"
|
|
:is="componentType"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import { constructContentComponentBookmarkMutation } from '@/helpers/update-content-bookmark-mutation';
|
|
import { useMutation } from '@vue/apollo-composable';
|
|
import { HighlightNode } from '@/__generated__/graphql';
|
|
import Mark from 'mark.js';
|
|
import highlightSidebar from '@/helpers/highlight-sidebar';
|
|
|
|
export interface Props {
|
|
// todo: use useful types here
|
|
component: any;
|
|
parent: any;
|
|
highlights: HighlightNode[];
|
|
bookmarks: any[];
|
|
notes: any[];
|
|
root: string;
|
|
editMode: boolean;
|
|
}
|
|
import TextBlock from '@/components/content-blocks/TextBlock.vue';
|
|
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget.vue';
|
|
import ImageBlock from '@/components/content-blocks/ImageBlock.vue';
|
|
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock.vue';
|
|
import VideoBlock from '@/components/content-blocks/VideoBlock.vue';
|
|
import LinkBlock from '@/components/content-blocks/LinkBlock.vue';
|
|
import DocumentBlock from '@/components/content-blocks/DocumentBlock.vue';
|
|
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock.vue';
|
|
import InfogramBlock from '@/components/content-blocks/InfogramBlock.vue';
|
|
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock.vue';
|
|
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock.vue';
|
|
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock.vue';
|
|
import SectionTitleBlock from '@/components/content-blocks/SectionTitleBlock.vue';
|
|
import ContentListBlock from '@/components/content-blocks/ContentListBlock.vue';
|
|
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug.vue';
|
|
import Assignment from '@/components/content-blocks/assignment/Assignment.vue';
|
|
import Survey from '@/components/content-blocks/SurveyBlock.vue';
|
|
import Solution from '@/components/content-blocks/Solution.vue';
|
|
import Instruction from '@/components/content-blocks/Instruction.vue';
|
|
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
|
|
import popover from '@/helpers/popover';
|
|
import { graphql } from '@/__generated__';
|
|
|
|
type ContentComponentType =
|
|
| typeof TextBlock
|
|
| typeof InstrumentWidget
|
|
| typeof InstrumentWidget
|
|
| typeof ImageBlock
|
|
| typeof ImageUrlBlock
|
|
| typeof VideoBlock
|
|
| typeof LinkBlock
|
|
| typeof DocumentBlock
|
|
| typeof InfogramBlock
|
|
| typeof GeniallyBlock
|
|
| typeof SubtitleBlock
|
|
| typeof SectionTitleBlock
|
|
| typeof ContentListBlock
|
|
| typeof ModuleRoomSlug
|
|
| typeof ThinglinkBlock
|
|
| typeof CmsDocumentBlock
|
|
| typeof Survey
|
|
| typeof Solution
|
|
| typeof Instruction
|
|
| typeof Assignment;
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
component: () => ({}),
|
|
parent: () => ({}),
|
|
bookmarks: () => [],
|
|
highlights: () => [],
|
|
notes: () => [],
|
|
root: '',
|
|
editMode: false,
|
|
});
|
|
|
|
const store = useStore();
|
|
|
|
const components: Record<string, ContentComponentType> = {
|
|
text_block: TextBlock,
|
|
basic_knowledge: InstrumentWidget, // for legacy
|
|
instrument: InstrumentWidget,
|
|
image_block: ImageBlock,
|
|
image_url_block: ImageUrlBlock,
|
|
video_block: VideoBlock,
|
|
link_block: LinkBlock,
|
|
document_block: DocumentBlock,
|
|
infogram_block: InfogramBlock,
|
|
genially_block: GeniallyBlock,
|
|
subtitle: SubtitleBlock,
|
|
section_title: SectionTitleBlock,
|
|
content_list: ContentListBlock,
|
|
module_room_slug: ModuleRoomSlug,
|
|
thinglink_block: ThinglinkBlock,
|
|
cms_document_block: CmsDocumentBlock,
|
|
survey: Survey,
|
|
solution: Solution,
|
|
instruction: Instruction,
|
|
assignment: Assignment,
|
|
};
|
|
|
|
const contentComponentDiv = ref<HTMLElement | null>(null);
|
|
|
|
const componentType = computed(() => {
|
|
return components[props.component.type] || '';
|
|
});
|
|
|
|
const filteredHighlights = computed(
|
|
() => props.highlights.filter((highlight) => highlight.contentUuid === props.component.id) || []
|
|
);
|
|
const bookmarked = computed(
|
|
() => props.bookmarks && !!props.bookmarks.find((bookmark) => bookmark.uuid === props.component.id)
|
|
);
|
|
const note = computed(() => {
|
|
const bookmark = props.bookmarks && props.bookmarks.find((bookmark) => bookmark.uuid === props.component.id);
|
|
return bookmark && bookmark.note;
|
|
});
|
|
const showBookmarkActions = computed(
|
|
() => props.component.type !== 'content_list' && props.component.type !== 'basic_knowledge' && !props.editMode
|
|
);
|
|
const componentClass = computed(() => {
|
|
let classes = ['content-component', `content-component--${props.component.type}`];
|
|
if (bookmarked.value) {
|
|
classes.push('content-component--bookmarked');
|
|
}
|
|
return classes;
|
|
});
|
|
const childElements = computed(() => {
|
|
if (contentComponentDiv.value) {
|
|
const parent = contentComponentDiv.value.querySelector('.content-component__content');
|
|
if (!parent) {
|
|
console.warn('Parent does not exist, this should not be possible');
|
|
// or can it be, if the children did not load yet, e.g. with a dynamic component?
|
|
return [];
|
|
}
|
|
// make a flat list from all <li>-elements and all the others
|
|
const elements = Array.from(parent.children).reduce((acc: Element[], current: Element) => {
|
|
if (current.tagName.toLowerCase() === 'ul') {
|
|
return [...acc, ...current.children];
|
|
}
|
|
return [...acc, current];
|
|
}, []);
|
|
return elements as HTMLElement[];
|
|
}
|
|
return [];
|
|
});
|
|
|
|
watch(
|
|
() => filteredHighlights.value.map((h) => h.color),
|
|
() => {
|
|
for (const paragraph of childElements.value) {
|
|
const instance = new Mark(paragraph);
|
|
instance.unmark();
|
|
}
|
|
markHighlights();
|
|
}
|
|
);
|
|
|
|
const addNote = (id: string) => {
|
|
const type = Object.prototype.hasOwnProperty.call(props.parent, '__typename')
|
|
? props.parent.__typename
|
|
: 'ContentBlockNode';
|
|
|
|
store.dispatch('addNote', {
|
|
content: id,
|
|
type,
|
|
block: props.root,
|
|
});
|
|
};
|
|
|
|
const editNote = () => {
|
|
store.dispatch('editNote', note);
|
|
};
|
|
|
|
const { mutation, variables, update, optimisticResponse } = constructContentComponentBookmarkMutation(
|
|
props.component.id,
|
|
bookmarked.value,
|
|
props.parent,
|
|
props.root
|
|
);
|
|
const { mutate: mutateBookmarkContent } = useMutation(mutation, {
|
|
update,
|
|
optimisticResponse,
|
|
});
|
|
|
|
const bookmarkContent = (bookmarked: boolean) => {
|
|
const newVars = {
|
|
input: {
|
|
...variables.input,
|
|
bookmarked,
|
|
},
|
|
};
|
|
mutateBookmarkContent(newVars);
|
|
};
|
|
|
|
const { mutate: doUpdateHighlight } = useMutation(
|
|
graphql(`
|
|
mutation UpdateHighlight($input: UpdateHighlightInput!) {
|
|
updateHighlight(input: $input) {
|
|
highlight {
|
|
...HighlightParts
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
);
|
|
|
|
const { mutate: doDeleteHighlight } = useMutation(
|
|
graphql(`
|
|
mutation DeleteHighlight($input: DeleteHighlightInput!) {
|
|
deleteHighlight(input: $input) {
|
|
success
|
|
}
|
|
}
|
|
`)
|
|
);
|
|
|
|
const markHighlights = () => {
|
|
for (const highlight of filteredHighlights.value) {
|
|
const element = childElements.value[highlight.paragraphIndex];
|
|
const instance = new Mark(element);
|
|
const ranges: Mark.Range[] = [
|
|
{
|
|
start: highlight.startPosition,
|
|
length: highlight.selectionLength,
|
|
},
|
|
];
|
|
instance.markRanges(ranges, {
|
|
className: `highlight highlight--${highlight.color}`,
|
|
each: (newMark: HTMLElement) => {
|
|
newMark.dataset.id = highlight.id;
|
|
newMark.addEventListener('click', () => {
|
|
if (contentComponentDiv.value) {
|
|
popover.show({
|
|
wrapper: contentComponentDiv.value,
|
|
offsetTop: 0,
|
|
onChooseColor: (color: string) => {
|
|
doUpdateHighlight({
|
|
input: {
|
|
color: color,
|
|
id: highlight.id,
|
|
},
|
|
});
|
|
},
|
|
onDelete: () => {
|
|
console.log(`deleting ${highlight.id}`);
|
|
doDeleteHighlight(
|
|
{
|
|
input: {
|
|
id: highlight.id,
|
|
},
|
|
},
|
|
{
|
|
update: (
|
|
cache,
|
|
{
|
|
data: {
|
|
deleteHighlight: { success },
|
|
},
|
|
}
|
|
) => {
|
|
if (success) {
|
|
const fragment = graphql(`
|
|
fragment ContentBlockHighlightsWithIdOnlyFragment on ContentBlockNode {
|
|
highlights {
|
|
id
|
|
}
|
|
}
|
|
`);
|
|
const id = cache.identify({
|
|
__typename: 'ContentBlockNode',
|
|
id: highlight.contentBlock.id,
|
|
});
|
|
const contentBlock = cache.readFragment({
|
|
fragment,
|
|
id,
|
|
});
|
|
const data = {
|
|
...contentBlock,
|
|
highlights: contentBlock.highlights.filter((h) => h.id !== highlight.id),
|
|
};
|
|
cache.writeFragment({
|
|
fragment,
|
|
id,
|
|
data,
|
|
});
|
|
}
|
|
},
|
|
}
|
|
);
|
|
},
|
|
});
|
|
}
|
|
highlightSidebar.open({
|
|
highlight,
|
|
onUpdateText: (text: string) => {
|
|
doUpdateHighlight({
|
|
input: {
|
|
note: text,
|
|
id: highlight.id,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
console.log('onMounted ContentComponent called');
|
|
setTimeout(markHighlights, 500);
|
|
});
|
|
</script>
|
|
|
|
<style lang="postcss">
|
|
.highlight {
|
|
background-color: Highlight;
|
|
color: HighlightText;
|
|
cursor: pointer;
|
|
|
|
&--alpha {
|
|
background-color: var(--mark-alpha);
|
|
color: var(--color-charcoal-dark);
|
|
}
|
|
|
|
&--beta {
|
|
background-color: var(--mark-beta);
|
|
color: var(--color-charcoal-dark);
|
|
}
|
|
|
|
&--gamma {
|
|
background-color: var(--mark-gamma);
|
|
color: var(--color-charcoal-dark);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="postcss" scoped>
|
|
.content-component {
|
|
position: relative;
|
|
|
|
&--subtitle {
|
|
margin-top: 2 * var(--medium-spacing);
|
|
margin-bottom: var(--medium-spacing);
|
|
}
|
|
|
|
&--section_title {
|
|
margin-top: var(--section-spacing);
|
|
margin-bottom: var(--large-spacing);
|
|
}
|
|
|
|
&--text_block {
|
|
margin-bottom: var(--large-spacing);
|
|
}
|
|
|
|
&--document_block {
|
|
margin-bottom: var(--large-spacing);
|
|
}
|
|
}
|
|
</style>
|