diff --git a/client/cypress/e2e/frontend/modules/highlights.cy.ts b/client/cypress/e2e/frontend/modules/highlights.cy.ts index d5cbbe80..2eac3565 100644 --- a/client/cypress/e2e/frontend/modules/highlights.cy.ts +++ b/client/cypress/e2e/frontend/modules/highlights.cy.ts @@ -331,14 +331,14 @@ describe('Highlights', () => { cy.getByDataCy('highlight-mark').should('have.length', 1); }); - it.skip('visits a module and highlights the chapter description', () => { + it('visits a module and highlights the chapter description', () => { cy.mockGraphqlOps({ operations, }); cy.visit('/module/my-module'); markText('chapter-intro'); - const highlightedText = 'is is somet'; + const highlightedText = 'is is some'; // delete doesn't make sense before the highlight exists cy.getByDataCy('highlight-delete').should('not.exist'); diff --git a/client/cypress/fixtures/mocks.ts b/client/cypress/fixtures/mocks.ts index 36d44787..6b876d53 100644 --- a/client/cypress/fixtures/mocks.ts +++ b/client/cypress/fixtures/mocks.ts @@ -47,6 +47,7 @@ export default { bookmark: null, descriptionHiddenFor: [], titleHiddenFor: [], + highlights: [], }), ContentBlockNode: () => ({ contents: [], diff --git a/client/src/__generated__/gql.ts b/client/src/__generated__/gql.ts index 7c38f6d3..c45e0226 100644 --- a/client/src/__generated__/gql.ts +++ b/client/src/__generated__/gql.ts @@ -13,6 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { + "\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n": types.ChapterHighlightsFragmentFragmentDoc, "\n fragment HighlightParts on HighlightNode {\n id\n contentIndex\n paragraphIndex\n selectionLength\n contentUuid\n startPosition\n color\n note {\n text\n }\n text\n page {\n # only one of them should be necessary, but the client somehow doesn't like just the Node inline fragment\n __typename\n ... on ContentBlockNode {\n id\n }\n ... on InstrumentNode {\n id\n slug\n }\n ... on ModuleNode {\n id\n slug\n }\n ... on ChapterNode {\n id\n slug\n }\n }\n }\n": types.HighlightPartsFragmentDoc, "\n fragment ContentBlockHighlightsFragment on ContentBlockNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n": types.ContentBlockHighlightsFragmentFragmentDoc, "\n query LanguageQuery {\n me {\n language @client\n }\n }\n ": types.LanguageQueryDocument, @@ -59,6 +60,10 @@ const documents = { */ export function graphql(source: string): unknown; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n"): (typeof documents)["\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/__generated__/graphql.ts b/client/src/__generated__/graphql.ts index 407dddcf..17861962 100644 --- a/client/src/__generated__/graphql.ts +++ b/client/src/__generated__/graphql.ts @@ -369,6 +369,7 @@ export type ChapterNode = ChapterInterface & Node & { contentBlocks?: Maybe>>; description?: Maybe; descriptionHiddenFor?: Maybe>>; + highlights?: Maybe>>; /** The ID of the object */ id: Scalars['ID']['output']; path?: Maybe; @@ -2691,6 +2692,11 @@ export type UserGroupBlockVisibility = { schoolClassId: Scalars['ID']['input']; }; +export type ChapterHighlightsFragmentFragment = { __typename: 'ChapterNode', id: string, highlights?: Array<( + { __typename?: 'HighlightNode' } + & { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } } + ) | null> | null } & { ' $fragmentName'?: 'ChapterHighlightsFragmentFragment' }; + export type HighlightPartsFragment = { __typename?: 'HighlightNode', id: string, contentIndex?: number | null, paragraphIndex: number, selectionLength: number, contentUuid?: any | null, startPosition: number, color: string, text: string, note?: { __typename?: 'NoteNode', text: string } | null, page?: { __typename: 'ChapterNode', id: string, slug: string } | { __typename: 'ContentBlockNode', id: string } | { __typename: 'InstrumentNode', id: string, slug: string } | { __typename: 'ModuleNode', id: string, slug: string } | null } & { ' $fragmentName'?: 'HighlightPartsFragment' }; export type ContentBlockHighlightsFragmentFragment = { __typename: 'ContentBlockNode', id: string, highlights?: Array<( @@ -2872,6 +2878,7 @@ export type ModuleSolutionsQueryVariables = Exact<{ export type ModuleSolutionsQuery = { __typename?: 'Query', module?: { __typename?: 'ModuleNode', solutionsEnabled?: boolean | null, slug: string } | null }; export const HighlightPartsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ChapterHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ChapterHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; export const ContentBlockHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContentBlockHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; export const ModuleHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModuleHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; export const ModuleLevelFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModuleLevelFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleLevelNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"filterAttributeType"}}]}}]} as unknown as DocumentNode; diff --git a/client/src/components/Chapter.vue b/client/src/components/Chapter.vue index 12b1d87c..fd000a08 100644 --- a/client/src/components/Chapter.vue +++ b/client/src/components/Chapter.vue @@ -40,6 +40,7 @@ class="chapter__intro intro hideable-element" data-cy="chapter-intro" v-if="!descriptionHidden" + ref="chapterIntro" > (null); +const chapterIntro = ref(null); +const props = withDefaults(defineProps(), { + 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; @@ -111,11 +159,46 @@ onMounted(() => { }, 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(); }); -const props = withDefaults(defineProps(), { - index: 0, - editMode: false, +onUnmounted(() => { + const intro = chapterIntro.value; + + if (intro !== null) { + intro.removeEventListener('mouseup', selectionHandler); + } }); const filteredContentBlocks = computed(() => { @@ -230,9 +313,26 @@ const textHidden = (type: string) => { 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(); + } +); +