skillbox/client/src/pages/instrument.vue

268 lines
6.2 KiB
Vue

<template>
<div
class="instrument"
ref="instrumentDiv"
>
<h1
class="instrument__title"
data-cy="instrument-title"
>
{{ instrument.title }}
</h1>
<!-- eslint-disable vue/no-v-html -->
<div
class="instrument__intro intro"
data-cy="instrument-intro"
ref="highlightIntro"
v-html="instrument.intro"
/>
<content-component
:component="component"
:root="instrument.slug"
:parent="instrument"
:bookmarks="instrument.bookmarks"
:notes="instrument.notes"
:highlights="instrument.highlights"
:edit-mode="false"
v-for="component in instrument.contents"
:key="component.id"
/>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, nextTick, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { graphql } from '@/__generated__';
import { useQuery } from '@vue/apollo-composable';
import { computed } from '@vue/reactivity';
import { AddHighlightArgument, HighlightNode, InstrumentNode } from '@/__generated__/graphql';
import {
createHighlightCurry,
getSelectionHandler,
markHighlight,
SelectionHandlerOptions,
SelectionHandlerType,
} from '@/helpers/highlight';
import { doUpdateHighlight } from '@/graphql/mutations';
import highlightSidebar from '@/helpers/highlight-sidebar';
const instrumentDiv = ref<HTMLElement | null>(null);
const highlightIntro = ref<HTMLElement | null>(null);
const ContentComponent = defineAsyncComponent(() => import('@/components/content-blocks/ContentComponent.vue'));
graphql(`
fragment InstrumentParts on InstrumentNode {
id
title
intro
slug
language
bookmarks {
uuid
note {
id
text
}
}
type {
id
name
category {
id
name
foreground
background
}
type
}
contents
highlights {
...HighlightParts
}
}
`);
const route = useRoute();
const { result, onResult } = useQuery(
graphql(`
query InstrumentQuery($slug: String!) {
instrument(slug: $slug) {
...InstrumentParts
}
}
`),
{
slug: route.params.slug as string,
}
);
const instrument = computed(() => (result.value?.instrument as InstrumentNode) || {});
const instrumentHighlightsFragment = graphql(`
fragment instrumentHighlightsFragment on InstrumentNode {
id
slug
__typename
highlights {
...HighlightParts
}
}
`);
const markHighlights = (highlights: HighlightNode[], element: HTMLElement) => {
for (const highlight of highlights) {
markHighlight(highlight, element, element);
}
};
let contentSelectionHandler: SelectionHandlerType, introSelectionHandler: SelectionHandlerType;
onResult(async () => {
const element = instrumentDiv.value;
const intro = highlightIntro.value;
const fragment = instrumentHighlightsFragment;
const fragmentName = 'instrumentHighlightsFragment';
const cacheSignature = {
slug: instrument.value.slug, // this value is only known onResult
__typename: 'InstrumentNode',
};
const createContentHighlight = createHighlightCurry({
fragment,
fragmentName,
cacheSignature,
isContentHighlight: true,
});
const createIntroHighlight = createHighlightCurry({
fragment,
fragmentName,
cacheSignature,
isContentHighlight: false,
});
const openSidebar = (highlight: HighlightNode) => {
highlightSidebar.open({
highlight,
onUpdateText: (text: string) => {
doUpdateHighlight({
input: {
note: text,
id: highlight.id,
},
});
},
});
};
if (element !== null) {
const el = element;
const page = instrument.value;
const introOptions: SelectionHandlerOptions = {
el,
page,
onChangeColor: (newHighlight: AddHighlightArgument) => {
createIntroHighlight(newHighlight);
},
onCreateNote: (newHighlight: AddHighlightArgument) => {
// todo: the same as the other one in ContentBlock.vue, possible to merge
// we also open the sidebar when clicking on the note icon
createIntroHighlight(newHighlight).then(openSidebar);
},
parentSelector: 'intro',
};
const contentOptions: SelectionHandlerOptions = {
el,
page,
onChangeColor: (newHighlight: AddHighlightArgument) => {
createContentHighlight(newHighlight);
},
onCreateNote: (newHighlight: AddHighlightArgument) => {
// todo: the same as the other one in ContentBlock.vue, possible to merge
// we also open the sidebar when clicking on the note icon
createContentHighlight(newHighlight).then(openSidebar);
},
};
contentSelectionHandler = getSelectionHandler(contentOptions);
introSelectionHandler = getSelectionHandler(introOptions);
element.addEventListener('mouseup', contentSelectionHandler);
element.addEventListener('mouseup', introSelectionHandler);
}
if (intro !== null) {
const introHighlights = instrument.value.highlights.filter((h) => h.contentUuid === null);
await nextTick();
markHighlights(introHighlights, intro);
}
});
onUnmounted(() => {
const element = instrumentDiv.value;
if (element !== null) {
element.removeEventListener('mouseup', contentSelectionHandler);
element.removeEventListener('mouseup', introSelectionHandler);
}
});
</script>
<style scoped lang="scss">
@import 'styles/helpers';
.instrument {
padding-top: 2 * $large-spacing;
&__intro {
position: relative;
}
&__title {
font-size: toRem(35px);
margin-bottom: $large-spacing;
line-height: $default-heading-line-height;
overflow-wrap: break-word;
}
& :deep() {
& p {
margin-bottom: $large-spacing;
}
& p:last-child {
margin-bottom: 0;
}
& ul {
@include list-parent;
}
& p + ul {
margin-top: -30px;
}
& li {
@include list-child;
line-height: 1.5;
}
& b {
font-weight: 600;
& mark {
font-weight: 600;
}
}
.brand {
color: $color-brand;
font-weight: 600;
}
.secondary {
color: $secondary-color;
font-weight: 600;
}
}
}
</style>