Add highlight query to client
This commit is contained in:
parent
a6974853ef
commit
9213d57be5
|
|
@ -83,6 +83,7 @@
|
|||
:root="root"
|
||||
:parent="contentBlock"
|
||||
:bookmarks="contentBlock.bookmarks"
|
||||
:highlights="contentBlock.highlights"
|
||||
:notes="contentBlock.notes"
|
||||
:edit-mode="editMode"
|
||||
v-for="component in contentBlocksWithContentLists.contents"
|
||||
|
|
@ -260,7 +261,9 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
// add the listener from highlights
|
||||
element.addEventListener('mouseup', getSelectionHandler(element, props.contentBlock));
|
||||
element.addEventListener('mouseup', getSelectionHandler(element, props.contentBlock), (newHighlight) => {
|
||||
console.log(newHighlight);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
:data-scrollto="component.id"
|
||||
:data-uuid="component.id"
|
||||
data-cy="content-component"
|
||||
ref="contentComponentDiv"
|
||||
>
|
||||
<bookmark-actions
|
||||
:bookmarked="bookmarked"
|
||||
|
|
@ -14,84 +15,51 @@
|
|||
@edit-note="editNote"
|
||||
@bookmark="bookmarkContent(!bookmarked)"
|
||||
/>
|
||||
<component v-bind="component" :parent="parent" :is="componentType" />
|
||||
<component
|
||||
v-bind="component"
|
||||
:parent="parent"
|
||||
:is="componentType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { constructContentComponentBookmarkMutation } from "@/helpers/update-content-bookmark-mutation";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { defineAsyncComponent, ref, computed, onMounted } 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 * as Mark from 'mark.js';
|
||||
|
||||
export interface Props {
|
||||
component: any;
|
||||
parent: any;
|
||||
highlights: HighlightNode[];
|
||||
bookmarks: any[];
|
||||
notes: any[];
|
||||
root: string;
|
||||
editMode: boolean;
|
||||
}
|
||||
const TextBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/TextBlock.vue")
|
||||
);
|
||||
const InstrumentWidget = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/InstrumentWidget.vue")
|
||||
);
|
||||
const ImageBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/ImageBlock.vue")
|
||||
);
|
||||
const ImageUrlBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/ImageUrlBlock.vue")
|
||||
);
|
||||
const VideoBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/VideoBlock.vue")
|
||||
);
|
||||
const LinkBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/LinkBlock.vue")
|
||||
);
|
||||
const DocumentBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/DocumentBlock.vue")
|
||||
);
|
||||
const CmsDocumentBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/CmsDocumentBlock.vue")
|
||||
);
|
||||
const InfogramBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/InfogramBlock.vue")
|
||||
);
|
||||
const ThinglinkBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/ThinglinkBlock.vue")
|
||||
);
|
||||
const GeniallyBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/GeniallyBlock.vue")
|
||||
);
|
||||
const SubtitleBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/SubtitleBlock.vue")
|
||||
);
|
||||
const SectionTitleBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/SectionTitleBlock.vue")
|
||||
);
|
||||
const ContentListBlock = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/ContentListBlock.vue")
|
||||
);
|
||||
const ModuleRoomSlug = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/ModuleRoomSlug.vue")
|
||||
);
|
||||
const Assignment = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/assignment/Assignment.vue")
|
||||
);
|
||||
const Survey = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/SurveyBlock.vue")
|
||||
);
|
||||
const Solution = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/Solution.vue")
|
||||
);
|
||||
const Instruction = defineAsyncComponent(
|
||||
() => import("@/components/content-blocks/Instruction.vue")
|
||||
);
|
||||
const BookmarkActions = defineAsyncComponent(
|
||||
() => import("@/components/notes/BookmarkActions.vue")
|
||||
);
|
||||
const TextBlock = defineAsyncComponent(() => import('@/components/content-blocks/TextBlock.vue'));
|
||||
const InstrumentWidget = defineAsyncComponent(() => import('@/components/content-blocks/InstrumentWidget.vue'));
|
||||
const ImageBlock = defineAsyncComponent(() => import('@/components/content-blocks/ImageBlock.vue'));
|
||||
const ImageUrlBlock = defineAsyncComponent(() => import('@/components/content-blocks/ImageUrlBlock.vue'));
|
||||
const VideoBlock = defineAsyncComponent(() => import('@/components/content-blocks/VideoBlock.vue'));
|
||||
const LinkBlock = defineAsyncComponent(() => import('@/components/content-blocks/LinkBlock.vue'));
|
||||
const DocumentBlock = defineAsyncComponent(() => import('@/components/content-blocks/DocumentBlock.vue'));
|
||||
const CmsDocumentBlock = defineAsyncComponent(() => import('@/components/content-blocks/CmsDocumentBlock.vue'));
|
||||
const InfogramBlock = defineAsyncComponent(() => import('@/components/content-blocks/InfogramBlock.vue'));
|
||||
const ThinglinkBlock = defineAsyncComponent(() => import('@/components/content-blocks/ThinglinkBlock.vue'));
|
||||
const GeniallyBlock = defineAsyncComponent(() => import('@/components/content-blocks/GeniallyBlock.vue'));
|
||||
const SubtitleBlock = defineAsyncComponent(() => import('@/components/content-blocks/SubtitleBlock.vue'));
|
||||
const SectionTitleBlock = defineAsyncComponent(() => import('@/components/content-blocks/SectionTitleBlock.vue'));
|
||||
const ContentListBlock = defineAsyncComponent(() => import('@/components/content-blocks/ContentListBlock.vue'));
|
||||
const ModuleRoomSlug = defineAsyncComponent(() => import('@/components/content-blocks/ModuleRoomSlug.vue'));
|
||||
const Assignment = defineAsyncComponent(() => import('@/components/content-blocks/assignment/Assignment.vue'));
|
||||
const Survey = defineAsyncComponent(() => import('@/components/content-blocks/SurveyBlock.vue'));
|
||||
const Solution = defineAsyncComponent(() => import('@/components/content-blocks/Solution.vue'));
|
||||
const Instruction = defineAsyncComponent(() => import('@/components/content-blocks/Instruction.vue'));
|
||||
const BookmarkActions = defineAsyncComponent(() => import('@/components/notes/BookmarkActions.vue'));
|
||||
|
||||
type ContentComponentType =
|
||||
| typeof TextBlock
|
||||
|
|
@ -119,8 +87,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
component: () => ({}),
|
||||
parent: () => ({}),
|
||||
bookmarks: () => [],
|
||||
highlights: () => [],
|
||||
notes: () => [],
|
||||
root: "",
|
||||
root: '',
|
||||
editMode: false,
|
||||
});
|
||||
|
||||
|
|
@ -149,44 +118,39 @@ const components: Record<string, ContentComponentType> = {
|
|||
assignment: Assignment,
|
||||
};
|
||||
|
||||
const contentComponentDiv = ref<HTMLElement | null>(null);
|
||||
|
||||
const componentType = computed(() => {
|
||||
return components[props.component.type] || "";
|
||||
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)
|
||||
() => 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);
|
||||
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
|
||||
() => props.component.type !== 'content_list' && props.component.type !== 'basic_knowledge' && !props.editMode
|
||||
);
|
||||
const componentClass = computed(() => {
|
||||
let classes = [
|
||||
"content-component",
|
||||
`content-component--${props.component.type}`,
|
||||
];
|
||||
let classes = ['content-component', `content-component--${props.component.type}`];
|
||||
if (bookmarked.value) {
|
||||
classes.push("content-component--bookmarked");
|
||||
classes.push('content-component--bookmarked');
|
||||
}
|
||||
return classes;
|
||||
});
|
||||
|
||||
const addNote = (id: string) => {
|
||||
const type = Object.prototype.hasOwnProperty.call(props.parent, "__typename")
|
||||
const type = Object.prototype.hasOwnProperty.call(props.parent, '__typename')
|
||||
? props.parent.__typename
|
||||
: "ContentBlockNode";
|
||||
: 'ContentBlockNode';
|
||||
|
||||
store.dispatch("addNote", {
|
||||
store.dispatch('addNote', {
|
||||
content: id,
|
||||
type,
|
||||
block: props.root,
|
||||
|
|
@ -194,16 +158,15 @@ const addNote = (id: string) => {
|
|||
};
|
||||
|
||||
const editNote = () => {
|
||||
store.dispatch("editNote", note);
|
||||
store.dispatch('editNote', note);
|
||||
};
|
||||
|
||||
const { mutation, variables, update, optimisticResponse } =
|
||||
constructContentComponentBookmarkMutation(
|
||||
props.component.id,
|
||||
bookmarked.value,
|
||||
props.parent,
|
||||
props.root
|
||||
);
|
||||
const { mutation, variables, update, optimisticResponse } = constructContentComponentBookmarkMutation(
|
||||
props.component.id,
|
||||
bookmarked.value,
|
||||
props.parent,
|
||||
props.root
|
||||
);
|
||||
const { mutate: mutateBookmarkContent } = useMutation(mutation, {
|
||||
update,
|
||||
optimisticResponse,
|
||||
|
|
@ -219,10 +182,35 @@ const bookmarkContent = (bookmarked: boolean) => {
|
|||
console.log(newVars);
|
||||
mutateBookmarkContent(newVars);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (contentComponentDiv.value) {
|
||||
const paragraphs = contentComponentDiv.value.getElementsByTagName('p');
|
||||
for (const highlight of filteredHighlights.value) {
|
||||
console.log(paragraphs);
|
||||
console.log(highlight);
|
||||
|
||||
const element = paragraphs[highlight.paragraphIndex];
|
||||
console.log(element);
|
||||
const instance = new Mark(element);
|
||||
console.log(instance);
|
||||
const ranges: Mark.Range[] = [
|
||||
{
|
||||
start: highlight.startPosition,
|
||||
length: highlight.startPosition,
|
||||
},
|
||||
];
|
||||
console.log(ranges);
|
||||
instance.markRanges(ranges);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "styles/helpers";
|
||||
@import 'styles/helpers';
|
||||
|
||||
.content-component {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import popover from '@/helpers/popover';
|
|||
// Which start and endpoint of the selection? Or rather start and length?
|
||||
// Also, we probably want to store the text of the selection at least, and maybe even some more for context?
|
||||
|
||||
interface Highlight {
|
||||
export interface Highlight {
|
||||
contentBlock: string; // the id
|
||||
contentIndex: number; // which content of the .contents array do I come from?
|
||||
contentUUID: string; // the wagtail UUID of the content element
|
||||
|
|
@ -22,6 +22,7 @@ interface Highlight {
|
|||
startPosition: number; // start of the selection
|
||||
selectionLength: number; // length of the selection
|
||||
text: string; // text that was actually selected
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const TOP_TRESHOLD = 0;
|
||||
|
|
@ -86,78 +87,84 @@ const findPositionInParent = (element: HTMLElement, className: string = 'content
|
|||
return children.indexOf(element);
|
||||
};
|
||||
|
||||
export const getSelectionHandler = (el: HTMLElement, contentBlock: ContentBlockNode) => (_e: Event) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (isInsideViewport(rect.top) || isInsideViewport(rect.bottom)) {
|
||||
// the listener only does something if the `el` is visible in the viewport, to save resources
|
||||
export const getSelectionHandler =
|
||||
(el: HTMLElement, contentBlock: ContentBlockNode, onUpdateHighlight: (highlight: any) => void = () => {}) =>
|
||||
(_e: Event) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (isInsideViewport(rect.top) || isInsideViewport(rect.bottom)) {
|
||||
// the listener only does something if the `el` is visible in the viewport, to save resources
|
||||
|
||||
// now we check if the selection is inside our container
|
||||
// now we check if the selection is inside our container
|
||||
|
||||
const selection = rangy.getSelection();
|
||||
if (selection.rangeCount && !selection.isCollapsed) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (range.intersectsNode(el)) {
|
||||
// the selection is inside our container, so we can do something with it
|
||||
const { startContainer, endContainer } = range;
|
||||
const paragraph = findClosestAncestorWithTag(startContainer, 'p');
|
||||
const endAncestor = findClosestAncestorWithTag(endContainer, 'p');
|
||||
const selection = rangy.getSelection();
|
||||
if (selection.rangeCount && !selection.isCollapsed) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (range.intersectsNode(el)) {
|
||||
// the selection is inside our container, so we can do something with it
|
||||
const { startContainer, endContainer } = range;
|
||||
const paragraph = findClosestAncestorWithTag(startContainer, 'p');
|
||||
const endAncestor = findClosestAncestorWithTag(endContainer, 'p');
|
||||
|
||||
if (paragraph && paragraph === endAncestor) {
|
||||
// todo: rename this variable, `parent` is not correct anymore
|
||||
const contentComponent = findClosestAncestorWithClass(startContainer, 'content-component');
|
||||
if (contentComponent) {
|
||||
// our selection is wholly inside the container node, we continue with it
|
||||
const position = findPositionInParent(contentComponent);
|
||||
const siblings = Array.from(paragraph.parentElement?.children || []);
|
||||
const positionInTextBlock = siblings.indexOf(paragraph);
|
||||
if (!paragraph) {
|
||||
throw Error('This should not be happening, but there was no paragraph');
|
||||
}
|
||||
if (paragraph === endAncestor) {
|
||||
const contentComponent = findClosestAncestorWithClass(startContainer, 'content-component');
|
||||
if (contentComponent) {
|
||||
// our selection is wholly inside the container node, we continue with it
|
||||
const position = findPositionInParent(contentComponent);
|
||||
const siblings = Array.from(paragraph.parentElement?.children || []);
|
||||
const positionInTextBlock = siblings.indexOf(paragraph);
|
||||
|
||||
const numOfParagraphs = Array.from(contentComponent.children[1].children).filter(
|
||||
(child) => child.nodeName.toLowerCase() === 'p'
|
||||
).length;
|
||||
const numOfParagraphs = Array.from(contentComponent.children[1].children).filter(
|
||||
(child) => child.nodeName.toLowerCase() === 'p'
|
||||
).length;
|
||||
|
||||
const { start, end } = range.toCharacterRange(paragraph);
|
||||
const uuid = contentComponent.dataset.uuid || '';
|
||||
const highlightedText: Highlight = {
|
||||
contentBlock: contentBlock.id,
|
||||
contentIndex: position,
|
||||
contentUUID: uuid,
|
||||
startPosition: start,
|
||||
paragraphIndex: positionInTextBlock,
|
||||
selectionLength: end - start,
|
||||
text: range.toString(),
|
||||
};
|
||||
console.log(highlightedText);
|
||||
const { start, end } = range.toCharacterRange(paragraph);
|
||||
const uuid = contentComponent.dataset.uuid || '';
|
||||
const highlightedText: Highlight = {
|
||||
contentBlock: contentBlock.id,
|
||||
contentIndex: position,
|
||||
contentUUID: uuid,
|
||||
startPosition: start,
|
||||
paragraphIndex: positionInTextBlock,
|
||||
selectionLength: end - start,
|
||||
text: range.toString(),
|
||||
};
|
||||
console.log(highlightedText);
|
||||
|
||||
const positionOfContentBlock = el.getBoundingClientRect();
|
||||
const positionOfSelection = paragraph?.getBoundingClientRect();
|
||||
const offsetTop = positionOfSelection.top - positionOfContentBlock.top;
|
||||
popover
|
||||
.show({
|
||||
wrapper: el,
|
||||
offsetTop,
|
||||
onChooseColor: (color: string) => {
|
||||
console.log('chosenColor', color);
|
||||
// const newHighlight: Highlight = {
|
||||
// contentBlock: contentBlock.id,
|
||||
// contentIndex:
|
||||
//
|
||||
// };
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
console.log('confirmed');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('canceled');
|
||||
});
|
||||
// todo: how do we save this? maybe we need to move away from the directive and do this inside the mounted
|
||||
// and unmounted hooks of the ContentBlock
|
||||
const positionOfContentBlock = el.getBoundingClientRect();
|
||||
const positionOfSelection = paragraph?.getBoundingClientRect();
|
||||
const offsetTop = positionOfSelection.top - positionOfContentBlock.top;
|
||||
popover
|
||||
.show({
|
||||
wrapper: el,
|
||||
offsetTop,
|
||||
onChooseColor: (color: string) => {
|
||||
console.log('chosenColor', color);
|
||||
highlightedText.color = color;
|
||||
// const newHighlight: Highlight = {
|
||||
// contentBlock: contentBlock.id,
|
||||
// contentIndex:
|
||||
//
|
||||
// };
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
console.log('confirmed here');
|
||||
onUpdateHighlight();
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('canceled');
|
||||
});
|
||||
// todo: how do we save this? maybe we need to move away from the directive and do this inside the mounted
|
||||
// and unmounted hooks of the ContentBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const highlight = (element: HTMLElement, ranges: Mark.Range[]) => {
|
||||
// todo: make the ContentComponent / TextBlock component handle the highlighting
|
||||
|
|
@ -177,11 +184,7 @@ const mounted = (el: HTMLElement, binding: DirectiveBinding) => {
|
|||
// code inside this setTimeout would run too early and yield no elements
|
||||
setTimeout(() => {
|
||||
const elements = el.querySelectorAll<HTMLElement>('.content-component');
|
||||
// console.log(el);
|
||||
// console.log(elements);
|
||||
for (const element of elements) {
|
||||
// console.log(element);
|
||||
// console.log(typeof element);
|
||||
highlight(element, demoRanges);
|
||||
}
|
||||
// highlight(el, demoRanges);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@ fragment ContentBlockParts on ContentBlockNode {
|
|||
slug
|
||||
userCreated
|
||||
mine
|
||||
highlights {
|
||||
contentIndex
|
||||
paragraphIndex
|
||||
selectionLength
|
||||
contentUuid
|
||||
startPosition
|
||||
text
|
||||
color
|
||||
}
|
||||
instrumentCategory {
|
||||
id
|
||||
foreground
|
||||
|
|
|
|||
Loading…
Reference in New Issue