Add highlighting to instrument intro

This commit is contained in:
Ramon Wenger 2024-02-29 12:30:52 +01:00
parent 2ee56aa3c0
commit 650f8c05d5
5 changed files with 97 additions and 43 deletions

View File

@ -811,7 +811,7 @@ export type InstrumentNode = Node & {
__typename?: 'InstrumentNode'; __typename?: 'InstrumentNode';
bookmarks?: Maybe<Array<Maybe<InstrumentBookmarkNode>>>; bookmarks?: Maybe<Array<Maybe<InstrumentBookmarkNode>>>;
contents?: Maybe<Scalars['GenericStreamFieldType']['output']>; contents?: Maybe<Scalars['GenericStreamFieldType']['output']>;
highlights?: Maybe<Array<Maybe<HighlightNode>>>; highlights: Array<HighlightNode>;
/** The ID of the object */ /** The ID of the object */
id: Scalars['ID']['output']; id: Scalars['ID']['output'];
intro: Scalars['String']['output']; intro: Scalars['String']['output'];
@ -2773,7 +2773,7 @@ export type AddContentHighlightMutation = { __typename?: 'Mutation', addContentH
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
) | null } | null }; ) | null } | null };
export type InstrumentHighlightsWithIdOnlyFragmentFragment = { __typename?: 'InstrumentNode', highlights?: Array<{ __typename?: 'HighlightNode', id: string } | null> | null } & { ' $fragmentName'?: 'InstrumentHighlightsWithIdOnlyFragmentFragment' }; export type InstrumentHighlightsWithIdOnlyFragmentFragment = { __typename?: 'InstrumentNode', highlights: Array<{ __typename?: 'HighlightNode', id: string }> } & { ' $fragmentName'?: 'InstrumentHighlightsWithIdOnlyFragmentFragment' };
export type ContentBlockHighlightsWithIdOnlyFragmentFragment = { __typename?: 'ContentBlockNode', highlights?: Array<{ __typename?: 'HighlightNode', id: string } | null> | null } & { ' $fragmentName'?: 'ContentBlockHighlightsWithIdOnlyFragmentFragment' }; export type ContentBlockHighlightsWithIdOnlyFragmentFragment = { __typename?: 'ContentBlockNode', highlights?: Array<{ __typename?: 'HighlightNode', id: string } | null> | null } & { ' $fragmentName'?: 'ContentBlockHighlightsWithIdOnlyFragmentFragment' };
@ -2794,10 +2794,10 @@ export type UpdateContentBookmarkMutation = { __typename?: 'Mutation', updateCon
export type MyActivitiesQueryQueryVariables = Exact<{ [key: string]: never; }>; export type MyActivitiesQueryQueryVariables = Exact<{ [key: string]: never; }>;
export type MyActivitiesQueryQuery = { __typename?: 'Query', myActivities?: { __typename?: 'ActivityNode', instruments: Array<{ __typename?: 'InstrumentNode', id: string, slug: string, title: string, path: string, highlights?: Array<( export type MyActivitiesQueryQuery = { __typename?: 'Query', myActivities?: { __typename?: 'ActivityNode', instruments: Array<{ __typename?: 'InstrumentNode', id: string, slug: string, title: string, path: string, highlights: Array<(
{ __typename?: 'HighlightNode' } { __typename?: 'HighlightNode' }
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
) | null> | null, bookmarks?: Array<{ __typename?: 'InstrumentBookmarkNode', path: string } | null> | null }>, topics: Array<{ __typename?: 'TopicNode', id: string, title: string, modules?: Array<{ __typename?: 'ModuleNode', id: string, slug: string, title: string, metaTitle: string, myHighlights: Array<( )>, bookmarks?: Array<{ __typename?: 'InstrumentBookmarkNode', path: string } | null> | null }>, topics: Array<{ __typename?: 'TopicNode', id: string, title: string, modules?: Array<{ __typename?: 'ModuleNode', id: string, slug: string, title: string, metaTitle: string, myHighlights: Array<(
{ __typename?: 'HighlightNode' } { __typename?: 'HighlightNode' }
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
)>, myBookmarks?: Array<{ __typename?: 'ChapterBookmarkNode', path?: string | null, chapter: { __typename?: 'ChapterNode', path?: string | null }, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | { __typename?: 'ContentBlockBookmarkNode', id: string, uuid?: any | null, path?: string | null, contentBlock: { __typename?: 'ContentBlockNode', id: string, path?: string | null }, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | { __typename?: 'InstrumentBookmarkNode' } | { __typename?: 'ModuleBookmarkNode', path?: string | null, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | null> | null, mySubmissions?: Array<{ __typename?: 'StudentSubmissionNode', id: string, text: string, assignment: { __typename?: 'AssignmentNode', id: string, title: string, path: string, module: { __typename?: 'ModuleNode', slug: string } } } | null> | null, myAnswers?: Array<{ __typename?: 'AnswerNode', id: string, survey: { __typename?: 'SurveyNode', path: string, id: string, title: string } } | null> | null }> | null }> } | null }; )>, myBookmarks?: Array<{ __typename?: 'ChapterBookmarkNode', path?: string | null, chapter: { __typename?: 'ChapterNode', path?: string | null }, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | { __typename?: 'ContentBlockBookmarkNode', id: string, uuid?: any | null, path?: string | null, contentBlock: { __typename?: 'ContentBlockNode', id: string, path?: string | null }, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | { __typename?: 'InstrumentBookmarkNode' } | { __typename?: 'ModuleBookmarkNode', path?: string | null, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | null> | null, mySubmissions?: Array<{ __typename?: 'StudentSubmissionNode', id: string, text: string, assignment: { __typename?: 'AssignmentNode', id: string, title: string, path: string, module: { __typename?: 'ModuleNode', slug: string } } } | null> | null, myAnswers?: Array<{ __typename?: 'AnswerNode', id: string, survey: { __typename?: 'SurveyNode', path: string, id: string, title: string } } | null> | null }> | null }> } | null };
@ -2816,10 +2816,10 @@ export type ContentBlockQueryQueryVariables = Exact<{
export type ContentBlockQueryQuery = { __typename?: 'Query', contentBlock?: { __typename?: 'ContentBlockNode', path?: string | null } | null }; export type ContentBlockQueryQuery = { __typename?: 'Query', contentBlock?: { __typename?: 'ContentBlockNode', path?: string | null } | null };
export type InstrumentPartsFragment = { __typename?: 'InstrumentNode', id: string, title: string, intro: string, slug: string, language?: string | null, contents?: any | null, bookmarks?: Array<{ __typename?: 'InstrumentBookmarkNode', uuid?: any | null, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | null> | null, type?: { __typename?: 'InstrumentTypeNode', id: string, name: string, type: string, category?: { __typename?: 'InstrumentCategoryNode', id: string, name: string, foreground: string, background: string } | null } | null, highlights?: Array<( export type InstrumentPartsFragment = { __typename?: 'InstrumentNode', id: string, title: string, intro: string, slug: string, language?: string | null, contents?: any | null, bookmarks?: Array<{ __typename?: 'InstrumentBookmarkNode', uuid?: any | null, note?: { __typename?: 'NoteNode', id: string, text: string } | null } | null> | null, type?: { __typename?: 'InstrumentTypeNode', id: string, name: string, type: string, category?: { __typename?: 'InstrumentCategoryNode', id: string, name: string, foreground: string, background: string } | null } | null, highlights: Array<(
{ __typename?: 'HighlightNode' } { __typename?: 'HighlightNode' }
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
) | null> | null } & { ' $fragmentName'?: 'InstrumentPartsFragment' }; )> } & { ' $fragmentName'?: 'InstrumentPartsFragment' };
export type InstrumentQueryQueryVariables = Exact<{ export type InstrumentQueryQueryVariables = Exact<{
slug: Scalars['String']['input']; slug: Scalars['String']['input'];
@ -2831,10 +2831,10 @@ export type InstrumentQueryQuery = { __typename?: 'Query', instrument?: (
& { ' $fragmentRefs'?: { 'InstrumentPartsFragment': InstrumentPartsFragment } } & { ' $fragmentRefs'?: { 'InstrumentPartsFragment': InstrumentPartsFragment } }
) | null }; ) | null };
export type InstrumentHighlightsFragmentFragment = { __typename: 'InstrumentNode', id: string, slug: string, highlights?: Array<( export type InstrumentHighlightsFragmentFragment = { __typename: 'InstrumentNode', id: string, slug: string, highlights: Array<(
{ __typename?: 'HighlightNode' } { __typename?: 'HighlightNode' }
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
) | null> | null } & { ' $fragmentName'?: 'InstrumentHighlightsFragmentFragment' }; )> } & { ' $fragmentName'?: 'InstrumentHighlightsFragmentFragment' };
export type MeLanguageQueryVariables = Exact<{ [key: string]: never; }>; export type MeLanguageQueryVariables = Exact<{ [key: string]: never; }>;

View File

@ -384,7 +384,6 @@ export const deleteHighlightCurry = (highlight: HighlightNode) => () => {
if (success) { if (success) {
const page = highlight.page; const page = highlight.page;
let fragment: DocumentNode, id; let fragment: DocumentNode, id;
console.log(page?.__typename);
if (page?.__typename === 'InstrumentNode') { if (page?.__typename === 'InstrumentNode') {
fragment = graphql(` fragment = graphql(`
fragment InstrumentHighlightsWithIdOnlyFragment on InstrumentNode { fragment InstrumentHighlightsWithIdOnlyFragment on InstrumentNode {

View File

@ -14,6 +14,7 @@
<div <div
class="instrument__intro intro" class="instrument__intro intro"
data-cy="instrument-intro" data-cy="instrument-intro"
ref="highlightIntro"
v-html="instrument.intro" v-html="instrument.intro"
/> />
@ -32,17 +33,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineAsyncComponent, onUnmounted, ref } from 'vue'; import { defineAsyncComponent, nextTick, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { graphql } from '@/__generated__'; import { graphql } from '@/__generated__';
import { useQuery } from '@vue/apollo-composable'; import { useQuery } from '@vue/apollo-composable';
import { computed } from '@vue/reactivity'; import { computed } from '@vue/reactivity';
import { AddHighlightArgument, InstrumentNode } from '@/__generated__/graphql'; import { AddHighlightArgument, HighlightNode, InstrumentNode } from '@/__generated__/graphql';
import { createHighlightCurry, getSelectionHandler, SelectionHandlerOptions } from '@/helpers/highlight'; import {
createHighlightCurry,
getSelectionHandler,
markHighlight,
SelectionHandlerOptions,
SelectionHandlerType,
} from '@/helpers/highlight';
import { doUpdateHighlight } from '@/graphql/mutations'; import { doUpdateHighlight } from '@/graphql/mutations';
import highlightSidebar from '@/helpers/highlight-sidebar'; import highlightSidebar from '@/helpers/highlight-sidebar';
const instrumentDiv = ref<HTMLElement | null>(null); const instrumentDiv = ref<HTMLElement | null>(null);
const highlightIntro = ref<HTMLElement | null>(null);
const ContentComponent = defineAsyncComponent(() => import('@/components/content-blocks/ContentComponent.vue')); const ContentComponent = defineAsyncComponent(() => import('@/components/content-blocks/ContentComponent.vue'));
@ -106,46 +114,86 @@ const instrumentHighlightsFragment = graphql(`
} }
`); `);
let selectionHandler; const markHighlights = (highlights: HighlightNode[], element: HTMLElement) => {
onResult(() => { for (const highlight of highlights) {
markHighlight(highlight, element, element);
}
};
let contentSelectionHandler: SelectionHandlerType, introSelectionHandler: SelectionHandlerType;
onResult(async () => {
const element = instrumentDiv.value; const element = instrumentDiv.value;
const createHighlight = createHighlightCurry({ const intro = highlightIntro.value;
fragment: instrumentHighlightsFragment, const fragment = instrumentHighlightsFragment;
fragmentName: 'instrumentHighlightsFragment', const fragmentName = 'instrumentHighlightsFragment';
cacheSignature: { const cacheSignature = {
slug: instrument.value.slug, // this value is only known onResult slug: instrument.value.slug, // this value is only known onResult
__typename: 'InstrumentNode', __typename: 'InstrumentNode',
}, };
const createContentHighlight = createHighlightCurry({
fragment,
fragmentName,
cacheSignature,
isContentHighlight: true, 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) { if (element !== null) {
const options: SelectionHandlerOptions = { const el = element;
el: element, const page = instrument.value;
page: instrument.value, const introOptions: SelectionHandlerOptions = {
el,
page,
onChangeColor: (newHighlight: AddHighlightArgument) => { onChangeColor: (newHighlight: AddHighlightArgument) => {
createHighlight(newHighlight); createIntroHighlight(newHighlight);
}, },
onCreateNote: (newHighlight: AddHighlightArgument) => { onCreateNote: (newHighlight: AddHighlightArgument) => {
// todo: the same as the other one in ContentBlock.vue, possible to merge // todo: the same as the other one in ContentBlock.vue, possible to merge
// we also open the sidebar when clicking on the note icon // we also open the sidebar when clicking on the note icon
createHighlight(newHighlight).then((highlight) => { createIntroHighlight(newHighlight).then(openSidebar);
highlightSidebar.open({ },
highlight, parentSelector: 'intro',
onUpdateText: (text: string) => { };
doUpdateHighlight({ const contentOptions: SelectionHandlerOptions = {
input: { el,
note: text, page,
id: highlight.id, 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);
}, },
}; };
selectionHandler = getSelectionHandler(options); contentSelectionHandler = getSelectionHandler(contentOptions);
element.addEventListener('mouseup', selectionHandler); 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);
} }
}); });
@ -153,7 +201,8 @@ onUnmounted(() => {
const element = instrumentDiv.value; const element = instrumentDiv.value;
if (element !== null) { if (element !== null) {
element.removeEventListener('mouseup', selectionHandler); element.removeEventListener('mouseup', contentSelectionHandler);
element.removeEventListener('mouseup', introSelectionHandler);
} }
}); });
</script> </script>
@ -164,6 +213,10 @@ onUnmounted(() => {
.instrument { .instrument {
padding-top: 2 * $large-spacing; padding-top: 2 * $large-spacing;
&__intro {
position: relative;
}
&__title { &__title {
font-size: toRem(35px); font-size: toRem(35px);
margin-bottom: $large-spacing; margin-bottom: $large-spacing;

View File

@ -44,7 +44,9 @@ class InstrumentNode(DjangoObjectType):
type = graphene.Field(InstrumentTypeNode) type = graphene.Field(InstrumentTypeNode)
contents = GenericStreamFieldType() contents = GenericStreamFieldType()
language = graphene.String() language = graphene.String()
highlights = graphene.List("notes.schema.HighlightNode") highlights = graphene.List(
graphene.NonNull("notes.schema.HighlightNode"), required=True
)
path = graphene.String(required=True) path = graphene.String(required=True)
class Meta: class Meta:

View File

@ -450,7 +450,7 @@ type InstrumentNode implements Node {
bookmarks: [InstrumentBookmarkNode] bookmarks: [InstrumentBookmarkNode]
type: InstrumentTypeNode type: InstrumentTypeNode
language: String language: String
highlights: [HighlightNode] highlights: [HighlightNode!]!
path: String! path: String!
} }