From 5db00539b750d16ffac981c22d5230b5dbb3d168 Mon Sep 17 00:00:00 2001 From: orlinmalkja <54899269+orlinmalkja@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:59:22 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20now=20use=20'annotations=20Pinia=20?= =?UTF-8?q?store'=20and=20remove=20the=20'Vuex=20anno=E2=80=A6=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: now use 'annotations Pinia store' and remove the 'Vuex annotations module' * refactor: add type checking (Typescript) for the function addActiveAnnotation() * refactor: use 'typescript' for the function `setFilteredAnnotations()` --------- Co-authored-by: malkja --- src/App.vue | 7 +- src/components/ContentView.vue | 6 +- .../annotations/AnnotationsView.vue | 16 +- src/components/panels/Panel.vue | 32 +- src/store/annotations/actions.js | 291 --------------- src/store/annotations/getters.js | 14 - src/store/annotations/index.js | 12 - src/store/annotations/mutations.js | 15 - src/store/annotations/state.js | 9 - src/store/contents/actions.js | 11 +- src/store/index.js | 2 - src/stores/annotations.ts | 331 ++++++++++++++++++ src/types.d.ts | 22 +- 13 files changed, 387 insertions(+), 381 deletions(-) delete mode 100644 src/store/annotations/actions.js delete mode 100644 src/store/annotations/getters.js delete mode 100644 src/store/annotations/index.js delete mode 100644 src/store/annotations/mutations.js delete mode 100644 src/store/annotations/state.js create mode 100644 src/stores/annotations.ts diff --git a/src/App.vue b/src/App.vue index 12186b1c..ce7f8453 100644 --- a/src/App.vue +++ b/src/App.vue @@ -38,6 +38,7 @@ import { } from 'vue'; import { useStore } from 'vuex'; import { useConfigStore } from '@/stores/config'; +import { useAnnotationsStore } from './stores/annotations'; import { useI18n } from 'vue-i18n'; import GlobalHeader from '@/components/header/GlobalHeader.vue'; @@ -50,6 +51,7 @@ import { initUseDark } from '@/utils/is-dark'; const store = useStore(); const configStore = useConfigStore() +const annotationStore = useAnnotationsStore() const { t, locale: i18nLocale } = useI18n(); const errorTitle = ref(''); @@ -76,7 +78,7 @@ const ready = computed(() => { return true; }); -const annotations = computed(() => store.getters['annotations/annotations']); +const annotations = computed(() => annotationStore.annotations); const config = computed(() => configStore.config); const collection = computed(() => store.getters['contents/collection']); const item = computed(() => store.getters['contents/item']); @@ -150,4 +152,5 @@ function setColorMode(configValue: string) { document.querySelector(config.value.container).setAttribute('color-scheme', configValue); } } - + + \ No newline at end of file diff --git a/src/components/ContentView.vue b/src/components/ContentView.vue index 8beac3fb..100e649c 100644 --- a/src/components/ContentView.vue +++ b/src/components/ContentView.vue @@ -23,6 +23,7 @@ import { } from 'vue'; import { useStore } from 'vuex'; import { useConfigStore } from '@/stores/config'; +import { useAnnotationsStore } from '@/stores/annotations'; import Notification from '@/components/Notification.vue'; import { request } from '@/utils/http'; import { domParser, delay } from '@/utils'; @@ -52,6 +53,7 @@ watch( { immediate: true }, ); async function loadContent(url) { + const annotationStore = useAnnotationsStore() content.value = ''; try { if (!url) { @@ -70,8 +72,8 @@ async function loadContent(url) { emit('loading', false); const root = document.getElementById('text-content'); - store.dispatch('annotations/addHighlightAttributesToText', root); - await store.dispatch('annotations/addHighlightClickListeners'); + annotationStore.addHighlightAttributesToText(root) + await annotationStore.addHighlightClickListeners() // TODO: Enable Hover + Tooltip feature when reqs are clarified // await store.dispatch('annotations/addHighlightHoverListeners'); diff --git a/src/components/annotations/AnnotationsView.vue b/src/components/annotations/AnnotationsView.vue index 6aae6de8..2afc64ff 100644 --- a/src/components/annotations/AnnotationsView.vue +++ b/src/components/annotations/AnnotationsView.vue @@ -29,8 +29,10 @@ import Notification from '@/components/Notification.vue'; import * as AnnotationUtils from '@/utils/annotations'; import { useConfigStore } from '@/stores/config'; +import { useAnnotationsStore } from '@/stores/annotations'; const configStore = useConfigStore() +const annotationStore = useAnnotationsStore() const props = defineProps({ url: String, @@ -41,9 +43,9 @@ const store = useStore(); const message = ref('no_annotations_in_view'); const config = computed(() => configStore.config); -const annotations = computed(() => store.getters['annotations/annotations']); -const activeAnnotations = computed(() => store.getters['annotations/activeAnnotations']); -const filteredAnnotations = computed(() => store.getters['annotations/filteredAnnotations']); +const annotations = computed(() => annotationStore.annotations); +const activeAnnotations = computed(() => annotationStore.activeAnnotations); +const filteredAnnotations = computed(() => annotationStore.filteredAnnotations); const activeContentUrl = computed(() => store.getters['contents/activeContentUrl']); const updateTextHighlighting = computed(() => // We need to make sure that annotations are loaded (this.annotations), @@ -56,19 +58,19 @@ watch( (contentData) => { const [hasAnnotations, activeContentUrl] = contentData.split('|'); if (hasAnnotations !== 'true' || activeContentUrl === 'null') return; - store.dispatch('annotations/resetAnnotations'); - store.dispatch('annotations/setFilteredAnnotations', props.types); + annotationStore.resetAnnotations() + annotationStore.selectFilteredAnnotations(props.types) highlightTargetsLevel0(); }, { immediate: true }, ); function addAnnotation(id: string) { - store.dispatch('annotations/addActiveAnnotation', id); + annotationStore.addActiveAnnotation(id); } function removeAnnotation(id: string) { - store.dispatch('annotations/removeActiveAnnotation', id); + annotationStore.removeActiveAnnotation(id); } function toggle({ id }) { diff --git a/src/components/panels/Panel.vue b/src/components/panels/Panel.vue index bed3c1eb..c44dc952 100644 --- a/src/components/panels/Panel.vue +++ b/src/components/panels/Panel.vue @@ -96,6 +96,7 @@ import { } from 'vue'; import { useStore } from 'vuex'; import { useConfigStore } from '@/stores/config'; +import { useAnnotationsStore } from '@/stores/annotations'; import { useI18n } from 'vue-i18n'; import TabView from 'primevue/tabview'; import TabPanel from 'primevue/tabpanel'; @@ -227,6 +228,7 @@ export default { } function createAnnotationsView(view, i) { + const annotationStore = useAnnotationsStore() const { connector, label } = view; const { component } = findComponent(connector.id); @@ -238,26 +240,32 @@ export default { const events = { update: (value) => { if (value === null) return; - store.dispatch(value ? 'annotations/selectAll' : 'annotations/selectNone'); + if (value) annotationStore.selectAll() + else annotationStore.selectNone() }, }; - unsubscribe.value = store.subscribeAction(async ({ type, payload }) => { + unsubscribe.value = annotationStore.$onAction(({name, store, args, after, onError }) => { + if (tabs.value.length - && tabs.value[0]?.actions?.length - && type === 'annotations/setActiveAnnotations' - ) { - const activeAmount = Object.keys(payload).length; - const filteredAmount = store.getters['annotations/filteredAnnotations'].length; - + && tabs.value[0]?.actions?.length && + (name === 'setActiveAnnotations')) + { + const activeAnnotations = args[0]; + const activeAmount = Object.keys(activeAnnotations).length; + const filteredAmount = annotationStore.filteredAnnotations.length; let newSelected = activeAmount > 0 && activeAmount === filteredAmount; - if (!newSelected && Object.keys(payload).length > 0) newSelected = null; + + if (!newSelected && activeAmount > 0) newSelected = null; + + if (tabs.value[i].actions[0].props.selected !== newSelected) { tabs.value[i].actions[0].props.selected = newSelected; - } - } + } + } }); + const actions = [{ component: 'PanelToggleAction', props: { @@ -341,4 +349,4 @@ export default { }; }, }; - + \ No newline at end of file diff --git a/src/store/annotations/actions.js b/src/store/annotations/actions.js deleted file mode 100644 index 66e4915a..00000000 --- a/src/store/annotations/actions.js +++ /dev/null @@ -1,291 +0,0 @@ -import * as AnnotationUtils from '@/utils/annotations'; -import { request } from '@/utils/http'; -import * as Utils from '@/utils'; -import { scrollIntoViewIfNeeded } from '@/utils'; -import { getAnnotationListElement } from '@/utils/annotations'; - -import { useConfigStore } from '@/stores/config'; - -export const addActiveAnnotation = ({ getters, rootGetters, dispatch }, id) => { - const configStore = useConfigStore() - const { activeAnnotations, annotations } = getters; - const newActiveAnnotation = annotations.find((annotation) => annotation.id === id); - - if (!newActiveAnnotation || activeAnnotations[id]) { - return; - } - - const iconName = configStore.getIconByType(newActiveAnnotation.body['x-content-type']); - - const activeAnnotationsList = { ...activeAnnotations }; - - activeAnnotationsList[id] = newActiveAnnotation; - - dispatch('setActiveAnnotations', activeAnnotationsList); - - const selector = Utils.generateTargetSelector(newActiveAnnotation); - const elements = (selector) ? [...document.querySelectorAll(selector)] : []; - Utils.highlightTargets(selector, { operation: 'INC' }); - - if (elements.length > 0) { - const target = elements[0]; - Utils.addIcon(target, newActiveAnnotation, iconName); - scrollIntoViewIfNeeded(target, target.closest('.panel-body')); - } -}; - -export const setActiveAnnotations = ({ commit }, activeAnnotations) => { - commit('setActiveAnnotations', activeAnnotations); -}; - -export const setFilteredAnnotations = ({ commit, getters, rootGetters }, types) => { - const { annotations } = getters; - const configStore = useConfigStore() - const activeContentType = configStore.activeContentType - let filteredAnnotations = []; - - if (annotations !== null) { - filteredAnnotations = types.length === 0 ? annotations : annotations.filter( - (annotation) => { - const type = types.find(({ name }) => name === annotation.body['x-content-type']); - // First we check if annotation fits to the current view - if (!type) return false; - - // Next we check if annotation should always be displayed on the current content tab - if (type?.displayWhen && type?.displayWhen === activeContentType) return true; - - // If the display is not dependent on displayWhen then we check if annotation's target exists in the content - const selector = AnnotationUtils.generateTargetSelector(annotation); - if (selector) { - const el = document.querySelector(selector); - if (el) { - return true; - } - } - - return false; - }, - ); - } - - commit('setFilteredAnnotations', filteredAnnotations); -}; - -export const addHighlightAttributesToText = ({ getters }, dom) => { - const { annotations } = getters; - - annotations.forEach((annotation) => { - const { id } = annotation; - const selector = Utils.generateTargetSelector(annotation); - if (selector) { - Utils.addHighlightToElements(selector, dom, id); - } - }); -}; - -export const annotationLoaded = ({ commit }, annotations) => { - commit('setAnnotations', annotations); - commit('updateAnnotationLoading', false); -}; - -export const removeActiveAnnotation = ({ getters, dispatch }, id) => { - const { activeAnnotations } = getters; - - const removeAnnotation = activeAnnotations[id]; - if (!removeAnnotation) { - return; - } - - const activeAnnotationsList = { ...activeAnnotations }; - - delete activeAnnotationsList[id]; - dispatch('setActiveAnnotations', activeAnnotationsList); - - const selector = AnnotationUtils.generateTargetSelector(removeAnnotation); - if (selector) { - AnnotationUtils.highlightTargets(selector, { operation: 'DEC' }); - AnnotationUtils.removeIcon(removeAnnotation); - } -}; - -export const resetAnnotations = ({ dispatch, getters }) => { - const { annotations } = getters; - - if (annotations !== null) { - annotations.forEach((annotation) => { - const selector = AnnotationUtils.generateTargetSelector(annotation); - if (selector) { - AnnotationUtils.highlightTargets(selector, { level: -1 }); - AnnotationUtils.removeIcon(annotation); - } - }); - } - - dispatch('setActiveAnnotations', {}); -}; - -export const initAnnotations = async ({ dispatch }, url) => { - try { - const annotations = await request(url); - - if (!annotations.first) { - dispatch('annotationLoaded', []); - return; - } - - const current = await request(annotations.first); - - if (Array.isArray(current.items)) { - dispatch('annotationLoaded', current.items); - } - } catch (err) { - dispatch('annotationLoaded', []); - } -}; - -export const addHighlightHoverListeners = ({ getters, rootGetters }) => { - const annotationElements = Array.from(document.querySelectorAll('[data-annotation]')); - - const tooltipEl = null; - const configStore = useConfigStore() - - // Annotations can be nested, so we filter out all outer elements from this selection and - // iterate over the deepest elements - annotationElements.forEach((el) => { - el.addEventListener( - 'mouseenter', - ({ clientX: x, clientY: y }) => { - let elementFromPoint = document.elementFromPoint(x, y); - - if (!elementFromPoint.hasAttribute('data-annotation')) { - elementFromPoint = null; - } - - const currentElement = elementFromPoint ?? el; - - const { filteredAnnotations } = getters; - const annotationTooltipModels = filteredAnnotations.reduce((acc, curr) => { - const { id } = curr; - const name = configStore.getIconByType(curr.body['x-content-type']) - acc[id] = { - value: curr.body.value, - name, - }; - return acc; - }, {}); - - const currentAnnotations = Utils.getValuesFromAttribute(currentElement, 'data-annotation-ids'); - const closestAnnotationId = currentAnnotations[currentAnnotations.length - 1]; - const closestAnnotationTooltipModel = annotationTooltipModels[closestAnnotationId]; - let annotationIds = discoverParentAnnotationIds(currentElement); - annotationIds = discoverChildAnnotationIds(currentElement, annotationIds); - - const otherAnnotationTooltipModels = Object.keys(annotationIds) - .map((id) => annotationTooltipModels[id]) - .filter((m) => m); - - AnnotationUtils.createOrUpdateTooltip.bind( - this, - currentElement, - { closest: closestAnnotationTooltipModel, other: otherAnnotationTooltipModels }, - document.getElementById('text-content'), - )(); - }, - false, - ); - el.addEventListener('mouseout', () => tooltipEl.remove(), false); - }); -}; - -export const addHighlightClickListeners = ({ dispatch, getters }) => { - const textEl = document.querySelector('#text-content>div>*'); - - if (!textEl) return; - - textEl.addEventListener('click', ({ target }) => { - // The click event handler works like this: - // When clicking on the text we pick the whole part of the text which belongs to the highest parent annotation. - // Since the annotations can be nested we avoid handling each of them separately - // and select/deselect the whole cluster at once. - // The actual click target decides whether it should be a selection or a deselection. - - // First we make sure to have a valid target. - // Although we receive a target from the event it can be a regular HTML element within the annotation. - // So we try to find it's nearest parent element that is marked as annotation element. - if (!target.dataset.annotation) { - target = getNearestParentAnnotation(target); - } - - if (!target) { - return; - } - - // Next we look up which annotations need to be selected - let annotationIds = {}; - - Utils.getValuesFromAttribute(target, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); - annotationIds = discoverParentAnnotationIds(target, annotationIds); - annotationIds = discoverChildAnnotationIds(target, annotationIds); - - const { filteredAnnotations } = getters; - - // We check the highlighting level to determine whether to select or deselect. - // TODO: it might be better to check the activeAnnotations instead - const targetIsSelected = parseInt(target.getAttribute('data-annotation-level'), 10) > 0; - - Object.keys(annotationIds).forEach((id) => { - // We need to check here if the right annotations panel tab is active - // a.k.a. it exists in the current filteredAnnotations - const annotation = filteredAnnotations.find((filtered) => filtered.id === id); - if (annotation) { - if (targetIsSelected) { - dispatch('removeActiveAnnotation', id); - } else { - dispatch('addActiveAnnotation', id); - } - } - }); - }); - - function getNearestParentAnnotation(element) { - const parent = element.parentElement; - - if (!parent) return null; - - if (parent.dataset?.annotation) { - return parent; - } - return getNearestParentAnnotation(parent); - } -}; - -export const selectAll = ({ getters, dispatch }) => { - const { filteredAnnotations, activeAnnotations } = getters; - filteredAnnotations.forEach(({ id }) => !activeAnnotations[id] && dispatch('addActiveAnnotation', id)); -}; - -export const selectNone = ({ getters, dispatch }) => { - const { filteredAnnotations, activeAnnotations } = getters; - filteredAnnotations.forEach(({ id }) => activeAnnotations[id] && dispatch('removeActiveAnnotation', id)); -}; - -function discoverParentAnnotationIds(el, annotationIds = {}) { - const parent = el.parentElement; - if (parent && parent.id !== 'text-content') { - Utils.getValuesFromAttribute(parent, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); - return discoverParentAnnotationIds(parent, annotationIds); - } - return annotationIds; -} - -function discoverChildAnnotationIds(el, annotationIds = {}) { - const { children } = el; - - [...children].forEach((child) => { - if (child.dataset.annotation) { - Utils.getValuesFromAttribute(child, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); - annotationIds = discoverChildAnnotationIds(child, annotationIds); - } - }); - return annotationIds; -} \ No newline at end of file diff --git a/src/store/annotations/getters.js b/src/store/annotations/getters.js deleted file mode 100644 index 957df204..00000000 --- a/src/store/annotations/getters.js +++ /dev/null @@ -1,14 +0,0 @@ -export const activeTab = (state) => state.activeTab; - -export const activeAnnotations = (state) => state.activeAnnotations; - -export const isAllAnnotationSelected = (state) => (total) => Object.keys(state.activeAnnotations).length === total; - -export const isNoAnnotationSelected = (state) => !Object.keys(state.activeAnnotations).length; - -export const annotations = (state) => state.annotations; -export const isLoading = (state) => state.isLoading; - -export const isContentLoading = (state) => state.isContentLoading; - -export const filteredAnnotations = (state) => state.filteredAnnotations; diff --git a/src/store/annotations/index.js b/src/store/annotations/index.js deleted file mode 100644 index b0beb1b9..00000000 --- a/src/store/annotations/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import state from './state'; -import * as getters from './getters'; -import * as mutations from './mutations'; -import * as actions from './actions'; - -export default { - namespaced: true, - state, - getters, - mutations, - actions, -}; diff --git a/src/store/annotations/mutations.js b/src/store/annotations/mutations.js deleted file mode 100644 index a4e50fd7..00000000 --- a/src/store/annotations/mutations.js +++ /dev/null @@ -1,15 +0,0 @@ -export const setActiveAnnotations = (state, annotations) => { - state.activeAnnotations = annotations; -}; - -export const setAnnotations = (state, annotations) => { - state.annotations = annotations; -}; - -export const updateAnnotationLoading = (state, isLoading) => { - state.isLoading = isLoading; -}; - -export const setFilteredAnnotations = (state, payload) => { - state.filteredAnnotations = payload; -}; diff --git a/src/store/annotations/state.js b/src/store/annotations/state.js deleted file mode 100644 index 04fd6850..00000000 --- a/src/store/annotations/state.js +++ /dev/null @@ -1,9 +0,0 @@ -export default function AnnotationState() { - return { - activeTab: '', - activeAnnotations: {}, - annotations: null, - filteredAnnotations: [], - isLoading: false, - }; -} diff --git a/src/store/contents/actions.js b/src/store/contents/actions.js index a3497882..3958cb81 100644 --- a/src/store/contents/actions.js +++ b/src/store/contents/actions.js @@ -4,6 +4,7 @@ import BookmarkService from '@/services/bookmark'; import { loadCss, loadFont } from '../../utils'; import { useConfigStore } from '@/stores/config'; +import { useAnnotationsStore } from '@/stores/annotations'; export const getItemIndex = async ({ getters }, itemUrl) => { @@ -45,7 +46,7 @@ function isItemPartInsideRangeValue(i, numberItems) { } export const initCollection = async ({ - commit, dispatch, getters, rootGetters, + commit, dispatch }, url) => { const configStore = useConfigStore() const resultConfig = configStore.config; @@ -174,6 +175,7 @@ export const initManifest = async ({ }; export const initItem = async ({ commit, dispatch, getters }, url) => { + const annotationStore = useAnnotationsStore() let item = ''; try { @@ -185,7 +187,7 @@ export const initItem = async ({ commit, dispatch, getters }, url) => { commit('setItemUrl', url); if (item.annotationCollection) { - await dispatch('annotations/initAnnotations', item.annotationCollection, { root: true }); + annotationStore.initAnnotations(item.annotationCollection); } const manifests = getters.manifests ? getters.manifests : []; // here we have item query -> we should extract the manifest index and the item index from the query and then give it as a parameter to updateItemQuery() @@ -205,11 +207,6 @@ export const updateImageLoading = async ({ commit }, payload) => { commit('setImageLoaded', payload); }; -export const initAnnotations = async ({ commit }, url) => { - const annotations = await request(url); - commit('setAnnotations', annotations); -}; - export const getSupport = ({ rootGetters }, support) => { const configStore = useConfigStore() const { container } = configStore.config; diff --git a/src/store/index.js b/src/store/index.js index eb35387f..122ba850 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,11 +1,9 @@ import { createStore } from 'vuex'; -import annotations from './annotations'; import contents from './contents'; export default createStore({ modules: { - annotations, contents, }, }); diff --git a/src/stores/annotations.ts b/src/stores/annotations.ts new file mode 100644 index 00000000..fbf69e9e --- /dev/null +++ b/src/stores/annotations.ts @@ -0,0 +1,331 @@ +import { defineStore } from 'pinia' +import { + computed, ref, + } from 'vue'; + +import * as AnnotationUtils from '@/utils/annotations'; +import { request } from '@/utils/http'; +import * as Utils from '@/utils'; +import { scrollIntoViewIfNeeded } from '@/utils'; +import { useConfigStore} from '@/stores/config'; + + +export const useAnnotationsStore = defineStore('annotations', () => { + + const activeTab = ref('') + const activeAnnotations = ref({} as ActiveAnnotation) + const annotations = ref(null) + const filteredAnnotations = ref([]) + const isLoading = ref(false); + + + const isAllAnnotationSelected = computed((total) => Object.keys(activeAnnotations.value).length === total) + const isNoAnnotationSelected = computed(() => !Object.keys(activeAnnotations.value).length) + + + const setActiveAnnotations = (annotations: ActiveAnnotation) => { + activeAnnotations.value = annotations + } + + function setAnnotations(payload: Annotation[]) { + annotations.value = payload + } + + function updateAnnotationLoading(payload: boolean) { + isLoading.value = payload; + } + + function setFilteredAnnotations(payload: Annotation[]) { + filteredAnnotations.value = payload + } + + const addActiveAnnotation = (id: string) => { + const annotationStore = useAnnotationsStore() + const configStore = useConfigStore() + const newActiveAnnotation: Annotation = annotations.value.find((annotation) => annotation.id === id); + if (!newActiveAnnotation || activeAnnotations.value[id]) { + return; + } + + const iconName: string = configStore.getIconByType(newActiveAnnotation.body['x-content-type']); + + const activeAnnotationsList: ActiveAnnotation = { ...activeAnnotations.value }; + + activeAnnotationsList[id] = newActiveAnnotation; + + annotationStore.setActiveAnnotations(activeAnnotationsList) + + const selector: string = Utils.generateTargetSelector(newActiveAnnotation); + const elements: Array = (selector) ? [...document.querySelectorAll(selector)] : []; + Utils.highlightTargets(selector, { operation: 'INC' }); + + if (elements.length > 0) { + const target: HTMLElement = elements[0]; + Utils.addIcon(target, newActiveAnnotation, iconName); + scrollIntoViewIfNeeded(target, target.closest('.panel-body')); + } + }; + + + const selectFilteredAnnotations = (types: AnnotationType[]): boolean | void => { + const configStore = useConfigStore() + const activeContentType: string = configStore.activeContentType + let filteredAnnotations: Annotation[] = []; + + if (annotations !== null) { + filteredAnnotations = types.length === 0 ? annotations.value : annotations.value.filter( + (annotation) => { + const type: AnnotationType = types.find(({ name }) => name === annotation.body['x-content-type']); + // First we check if annotation fits to the current view + if (!type) return false; + + // Next we check if annotation should always be displayed on the current content tab + if (type?.displayWhen && type?.displayWhen === activeContentType) return true; + + // If the display is not dependent on displayWhen then we check if annotation's target exists in the content + const selector: string = AnnotationUtils.generateTargetSelector(annotation); + if (selector) { + const el: HTMLElement = document.querySelector(selector); + if (el) { + return true; + } + } + + return false; + }, + ); + } + setFilteredAnnotations(filteredAnnotations) + }; + + + const addHighlightAttributesToText = (dom) => { + annotations.value.forEach((annotation) => { + const { id } = annotation; + const selector = Utils.generateTargetSelector(annotation); + if (selector) { + Utils.addHighlightToElements(selector, dom, id); + } + }); + }; + + const annotationLoaded = (annotations) => { + setAnnotations(annotations) + updateAnnotationLoading(false) + }; + + + const removeActiveAnnotation = (id) => { + const annotationStore = useAnnotationsStore() + const removeAnnotation = activeAnnotations.value[id]; + if (!removeAnnotation) { + return; + } + + const activeAnnotationsList = { ...activeAnnotations.value }; + + delete activeAnnotationsList[id]; + annotationStore.setActiveAnnotations(activeAnnotationsList) + + const selector = AnnotationUtils.generateTargetSelector(removeAnnotation); + if (selector) { + AnnotationUtils.highlightTargets(selector, { operation: 'DEC' }); + AnnotationUtils.removeIcon(removeAnnotation); + } + }; + + + + const resetAnnotations = () => { + + if (annotations.value !== null) { + annotations.value.forEach((annotation) => { + const selector = AnnotationUtils.generateTargetSelector(annotation); + if (selector) { + AnnotationUtils.highlightTargets(selector, { level: -1 }); + AnnotationUtils.removeIcon(annotation); + } + }); + } + setActiveAnnotations({}) + }; + + + + const initAnnotations = async (url) => { + try { + const annotations = await request(url); + + if (!annotations.first) { + annotationLoaded([]) + return; + } + + const current = await request(annotations.first); + + if (Array.isArray(current.items)) { + annotationLoaded(current.items) + } + } catch (err) { + annotationLoaded([]) + } + }; + + + + const addHighlightHoverListeners = () => { + const annotationElements = Array.from(document.querySelectorAll('[data-annotation]')); + + const tooltipEl = null; + const configStore = useConfigStore() + + // Annotations can be nested, so we filter out all outer elements from this selection and + // iterate over the deepest elements + annotationElements.forEach((el) => { + el.addEventListener( + 'mouseenter', + ({ clientX: x, clientY: y }) => { + let elementFromPoint = document.elementFromPoint(x, y); + + if (!elementFromPoint.hasAttribute('data-annotation')) { + elementFromPoint = null; + } + + const currentElement = elementFromPoint ?? el; + + const annotationTooltipModels = filteredAnnotations.value.reduce((acc, curr) => { + const { id } = curr; + const name = configStore.getIconByType(curr.body['x-content-type']) + acc[id] = { + value: curr.body.value, + name, + }; + return acc; + }, {}); + + const currentAnnotations = Utils.getValuesFromAttribute(currentElement, 'data-annotation-ids'); + const closestAnnotationId = currentAnnotations[currentAnnotations.length - 1]; + const closestAnnotationTooltipModel = annotationTooltipModels[closestAnnotationId]; + let annotationIds = discoverParentAnnotationIds(currentElement); + annotationIds = discoverChildAnnotationIds(currentElement, annotationIds); + + const otherAnnotationTooltipModels = Object.keys(annotationIds) + .map((id) => annotationTooltipModels[id]) + .filter((m) => m); + + AnnotationUtils.createOrUpdateTooltip.bind( + this, + currentElement, + { closest: closestAnnotationTooltipModel, other: otherAnnotationTooltipModels }, + document.getElementById('text-content'), + )(); + }, + false, + ); + el.addEventListener('mouseout', () => tooltipEl.remove(), false); + }); + }; + + + + const addHighlightClickListeners = () => { + const textEl = document.querySelector('#text-content>div>*'); + + if (!textEl) return; + + textEl.addEventListener('click', ({ target }) => { + // The click event handler works like this: + // When clicking on the text we pick the whole part of the text which belongs to the highest parent annotation. + // Since the annotations can be nested we avoid handling each of them separately + // and select/deselect the whole cluster at once. + // The actual click target decides whether it should be a selection or a deselection. + + // First we make sure to have a valid target. + // Although we receive a target from the event it can be a regular HTML element within the annotation. + // So we try to find it's nearest parent element that is marked as annotation element. + if (!target.dataset.annotation) { + target = getNearestParentAnnotation(target); + } + + if (!target) { + return; + } + + // Next we look up which annotations need to be selected + let annotationIds = {}; + + Utils.getValuesFromAttribute(target, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); + annotationIds = discoverParentAnnotationIds(target, annotationIds); + annotationIds = discoverChildAnnotationIds(target, annotationIds); + + // We check the highlighting level to determine whether to select or deselect. + // TODO: it might be better to check the activeAnnotations instead + const targetIsSelected = parseInt(target.getAttribute('data-annotation-level'), 10) > 0; + + Object.keys(annotationIds).forEach((id) => { + // We need to check here if the right annotations panel tab is active + // a.k.a. it exists in the current filteredAnnotations + const annotation = filteredAnnotations.value.find((filtered) => filtered.id === id); + if (annotation) { + if (targetIsSelected) { + removeActiveAnnotation(id) + } else { + addActiveAnnotation(id) + } + } + }); + }); + }; + + + function getNearestParentAnnotation(element) { + const parent = element.parentElement; + + if (!parent) return null; + + if (parent.dataset?.annotation) { + return parent; + } + return getNearestParentAnnotation(parent); + } + + const selectAll = () => { + filteredAnnotations.value.forEach(({ id }) => !activeAnnotations.value[id] && addActiveAnnotation(id)); + }; + + const selectNone = () => { + filteredAnnotations.value.forEach(({ id }) => activeAnnotations.value[id] && removeActiveAnnotation(id)); + }; + + function discoverParentAnnotationIds(el, annotationIds = {}) { + const parent = el.parentElement; + if (parent && parent.id !== 'text-content') { + Utils.getValuesFromAttribute(parent, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); + return discoverParentAnnotationIds(parent, annotationIds); + } + return annotationIds; + } + + function discoverChildAnnotationIds(el, annotationIds = {}) { + const { children } = el; + + [...children].forEach((child) => { + if (child.dataset.annotation) { + Utils.getValuesFromAttribute(child, 'data-annotation-ids').forEach((value) => annotationIds[value] = true); + annotationIds = discoverChildAnnotationIds(child, annotationIds); + } + }); + return annotationIds; + } + + return { + activeTab, activeAnnotations, annotations, filteredAnnotations, isLoading, // states + isAllAnnotationSelected, isNoAnnotationSelected, // computed + setActiveAnnotations, setAnnotations, updateAnnotationLoading, setFilteredAnnotations, // functions + addActiveAnnotation, selectFilteredAnnotations, addHighlightAttributesToText, + annotationLoaded, removeActiveAnnotation, resetAnnotations, initAnnotations, + addHighlightHoverListeners, addHighlightClickListeners, getNearestParentAnnotation, + selectAll, selectNone, discoverParentAnnotationIds, discoverChildAnnotationIds + } + +}) \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index c6fd36ac..60e3172c 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,13 +1,5 @@ declare global { - interface Annotation { - body: AnnotationContent[], - target: AnnotationTarget[], - type: string, - id: string - } - - interface ActiveAnnotation { [key: string]: Annotation } @@ -21,6 +13,12 @@ declare global { } + interface Annotation { + body: AnnotationContent[], + target: AnnotationTarget[], + type: string, + id: string + } interface AnnotationContent { type: 'TextualBody', value: string, @@ -38,6 +36,14 @@ declare global { source: string } + interface AnnotationType { + name: string, + icon?: string, + annotationType?: string, + displayWhen?: string + } + + interface Collection { '@context': string, textapi: string,