364 lines
8.8 KiB
Vue
364 lines
8.8 KiB
Vue
<template>
|
|
<div
|
|
:data-scrollto="chapter.id"
|
|
class="chapter"
|
|
data-cy="chapter"
|
|
ref="wrapper"
|
|
>
|
|
<div
|
|
:class="{ 'hideable-element--greyed-out': titleGreyedOut }"
|
|
class="hideable-element"
|
|
v-if="!titleHidden"
|
|
>
|
|
<h3
|
|
class="chapter__title"
|
|
:id="'chapter-' + index"
|
|
>
|
|
{{ chapter.title }}
|
|
<copy-link :id="chapter.id" />
|
|
</h3>
|
|
</div>
|
|
|
|
<visibility-action
|
|
:block="chapter"
|
|
type="chapter-title"
|
|
v-if="editMode"
|
|
/>
|
|
|
|
<bookmark-actions
|
|
:bookmarked="!!chapter.bookmark"
|
|
:note="note"
|
|
:edit-mode="editMode"
|
|
class="chapter__bookmark-actions"
|
|
data-cy="chapter-bookmark-actions"
|
|
@add-note="addNote"
|
|
@edit-note="editNote"
|
|
@bookmark="bookmark()"
|
|
/>
|
|
<div
|
|
:class="{ 'hideable-element--greyed-out': descriptionGreyedOut }"
|
|
class="chapter__intro intro hideable-element"
|
|
data-cy="chapter-intro"
|
|
v-if="!descriptionHidden"
|
|
ref="chapterIntro"
|
|
>
|
|
<visibility-action
|
|
:block="chapter"
|
|
:chapter="true"
|
|
type="chapter-description"
|
|
v-if="editMode"
|
|
/>
|
|
<p
|
|
data-cy="chapter-description"
|
|
class="chapter__description"
|
|
>
|
|
{{ chapter.description }}
|
|
</p>
|
|
</div>
|
|
|
|
<add-content-button
|
|
:where="{ parent: chapter }"
|
|
v-if="editMode"
|
|
/>
|
|
|
|
<content-block
|
|
:content-block="contentBlock"
|
|
:parent="chapter"
|
|
:edit-mode="editMode"
|
|
v-for="contentBlock in filteredContentBlocks"
|
|
:key="contentBlock.id"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import ContentBlock from '@/components/ContentBlock.vue';
|
|
import AddContentButton from '@/components/AddContentButton.vue';
|
|
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
|
|
import VisibilityAction from '@/components/visibility/VisibilityAction.vue';
|
|
import CopyLink from '@/components/CopyLink.vue';
|
|
import { hidden } from '@/helpers/visibility';
|
|
import { CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE } from '@/consts/types';
|
|
import { useStore } from 'vuex';
|
|
|
|
import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql';
|
|
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
|
|
import { getMe } from '@/mixins/me';
|
|
import { useMutation } from '@vue/apollo-composable';
|
|
import { useRoute } from 'vue-router';
|
|
import { PAGE_LOAD_TIMEOUT } from '@/consts/navigation.consts';
|
|
import {
|
|
SelectionHandlerOptions,
|
|
SelectionHandlerType,
|
|
createHighlightCurry,
|
|
getSelectionHandler,
|
|
markHighlight,
|
|
} from '@/helpers/highlight';
|
|
import { onUnmounted } from 'vue';
|
|
import { AddHighlightArgument, ChapterNode, HighlightNode } from '@/__generated__/graphql';
|
|
import { graphql } from '@/__generated__';
|
|
import highlightSidebar from '@/helpers/highlight-sidebar';
|
|
import { doUpdateHighlight } from '@/graphql/mutations';
|
|
import { watch } from 'vue';
|
|
import Mark from 'mark.js';
|
|
|
|
export interface Props {
|
|
chapter: ChapterNode;
|
|
index: number;
|
|
editMode: boolean;
|
|
}
|
|
|
|
const { schoolClass } = getMe();
|
|
|
|
const store = useStore();
|
|
const route = useRoute();
|
|
const wrapper = ref<HTMLElement | null>(null);
|
|
const chapterIntro = ref<HTMLElement | null>(null);
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
index: 0,
|
|
editMode: false,
|
|
});
|
|
|
|
const chapterHighlightsFragment = graphql(`
|
|
fragment ChapterHighlightsFragment on ChapterNode {
|
|
id
|
|
__typename
|
|
highlights {
|
|
...HighlightParts
|
|
}
|
|
}
|
|
`);
|
|
|
|
const createHighlight = createHighlightCurry({
|
|
fragment: chapterHighlightsFragment,
|
|
fragmentName: 'ChapterHighlightsFragment',
|
|
cacheSignature: { id: props.chapter.id, __typename: 'ChapterNode' },
|
|
isContentHighlight: false,
|
|
});
|
|
const markHighlights = () => {
|
|
if (props.chapter.highlights && chapterIntro.value) {
|
|
for (const highlight of props.chapter.highlights) {
|
|
const highlightNode = highlight as HighlightNode;
|
|
const element = chapterIntro.value.children[highlightNode.paragraphIndex] as HTMLElement;
|
|
markHighlight(highlightNode, element, element);
|
|
}
|
|
}
|
|
};
|
|
|
|
let selectionHandler: SelectionHandlerType;
|
|
|
|
onMounted(() => {
|
|
const element = wrapper.value;
|
|
if (element !== null) {
|
|
if (route.hash === `#${props.chapter.id}`) {
|
|
setTimeout(() => {
|
|
const rect = element.getBoundingClientRect();
|
|
window.scrollTo({ top: rect.y, behavior: 'smooth' });
|
|
}, PAGE_LOAD_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
const intro = chapterIntro.value;
|
|
if (intro !== null) {
|
|
const options: SelectionHandlerOptions = {
|
|
el: intro,
|
|
page: props.chapter,
|
|
parentSelector: 'chapter__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);
|
|
intro.addEventListener('mouseup', selectionHandler);
|
|
}
|
|
|
|
markHighlights();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
const intro = chapterIntro.value;
|
|
|
|
if (intro !== null) {
|
|
intro.removeEventListener('mouseup', selectionHandler);
|
|
}
|
|
});
|
|
|
|
const filteredContentBlocks = computed(() => {
|
|
if (!(props.chapter && props.chapter.contentBlocks)) {
|
|
return [];
|
|
}
|
|
if (props.editMode) {
|
|
return props.chapter.contentBlocks;
|
|
}
|
|
return props.chapter.contentBlocks.filter(
|
|
(contentBlock) =>
|
|
!hidden({
|
|
block: contentBlock,
|
|
schoolClass: schoolClass.value,
|
|
type: CONTENT_TYPE,
|
|
})
|
|
);
|
|
});
|
|
const note = computed(() => {
|
|
if (props.chapter && props.chapter.bookmark) {
|
|
return props.chapter.bookmark.note;
|
|
}
|
|
return false;
|
|
});
|
|
const titleGreyedOut = computed(() => {
|
|
return textHidden(CHAPTER_TITLE_TYPE) && props.editMode;
|
|
});
|
|
const titleHidden = computed(() => {
|
|
// never hidden when editing the module
|
|
if (props.chapter.titleHidden === true) {
|
|
return true;
|
|
}
|
|
return textHidden(CHAPTER_TITLE_TYPE) && !props.editMode;
|
|
});
|
|
const descriptionGreyedOut = computed(() => {
|
|
return textHidden(CHAPTER_DESCRIPTION_TYPE) && props.editMode;
|
|
});
|
|
const descriptionHidden = computed(() => {
|
|
// never hidden when editing the module
|
|
if (props.chapter.descriptionHidden === true) {
|
|
return true;
|
|
}
|
|
return textHidden(CHAPTER_DESCRIPTION_TYPE) && !props.editMode;
|
|
});
|
|
|
|
const { mutate: bookmark } = useMutation(UPDATE_CHAPTER_BOOKMARK_MUTATION, () => {
|
|
const bookmarked = !props.chapter.bookmark;
|
|
const id = props.chapter.id;
|
|
return {
|
|
variables: {
|
|
input: {
|
|
chapter: id,
|
|
bookmarked,
|
|
},
|
|
},
|
|
update: (store: any) => {
|
|
const query = CHAPTER_QUERY;
|
|
const variables = { id };
|
|
const { chapter } = store.readQuery({
|
|
query,
|
|
variables,
|
|
});
|
|
|
|
let bookmark;
|
|
|
|
if (bookmarked) {
|
|
bookmark = {
|
|
__typename: 'ChapterBookmarkNode',
|
|
note: null,
|
|
};
|
|
} else {
|
|
bookmark = null;
|
|
}
|
|
|
|
const data = {
|
|
chapter: {
|
|
...chapter,
|
|
bookmark,
|
|
},
|
|
};
|
|
|
|
store.writeQuery({
|
|
data,
|
|
query,
|
|
variables,
|
|
});
|
|
},
|
|
|
|
optimisticResponse: {
|
|
__typename: 'Mutation',
|
|
updateChapterBookmark: {
|
|
__typename: 'UpdateChapterBookmarkPayload',
|
|
success: true,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const addNote = (id: string) => {
|
|
store.dispatch('addNote', {
|
|
content: id,
|
|
parent: props.chapter.id,
|
|
});
|
|
};
|
|
const editNote = () => {
|
|
store.dispatch('editNote', props.chapter.bookmark.note);
|
|
};
|
|
const textHidden = (type: string) => {
|
|
return hidden({
|
|
block: props.chapter,
|
|
schoolClass: schoolClass.value,
|
|
type,
|
|
});
|
|
};
|
|
|
|
const unmark = () => {
|
|
for (const paragraph of chapterIntro.value.children) {
|
|
const instance = new Mark(paragraph);
|
|
instance.unmark();
|
|
}
|
|
};
|
|
|
|
const highlights = computed(() => {
|
|
return props.chapter ? props.chapter.highlights : [];
|
|
});
|
|
|
|
watch(
|
|
() => highlights.value?.filter((h) => h.color),
|
|
() => {
|
|
unmark();
|
|
markHighlights();
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
/* todo: re-add `scoped`, same as in ContentBlock.vue */
|
|
@import 'styles/helpers';
|
|
|
|
.chapter {
|
|
position: relative;
|
|
|
|
&__bookmark-actions {
|
|
margin-top: 3px;
|
|
}
|
|
|
|
&__intro {
|
|
position: relative;
|
|
}
|
|
|
|
&__title {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
&__description {
|
|
@include lead-paragraph;
|
|
|
|
margin-bottom: $large-spacing;
|
|
}
|
|
}
|
|
</style>
|