diff --git a/.eslintignore b/.eslintignore index 9b1c8b13..6faf7fbb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ /dist +/tests diff --git a/.eslintrc.js b/.eslintrc.js index 59a44a42..e0c3b65a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,5 +56,6 @@ module.exports = { // allow debugger during development only 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'max-len': 0, + 'allow-parens': 'as-needed', }, }; diff --git a/examples/ahiqar-arabic-karshuni.html b/examples/ahiqar-arabic-karshuni.html index d89930ab..698b6e40 100644 --- a/examples/ahiqar-arabic-karshuni.html +++ b/examples/ahiqar-arabic-karshuni.html @@ -29,7 +29,7 @@ diff --git a/src/css/_global.scss b/src/css/_global.scss index 3c58217c..1e40fb83 100644 --- a/src/css/_global.scss +++ b/src/css/_global.scss @@ -58,43 +58,6 @@ h3 { width: 100%; } -.annotation-tooltip { - background-color: $grey-2 !important; - box-shadow: $shadow-1; - color: #000 !important; - font-size: 14px; - left: 0; - opacity: 0; - padding: 8px; - position: absolute; - text-decoration: none !important; - top: 0; - transition: opacity 0.5s; - width: 240px; - z-index: 10000; -} - -.annotation-tooltip { - -webkit-touch-callout: none; -} - -.annotation-animated-tooltip { - opacity: 1; -} - -.referenced-annotation { - display: block; - margin-bottom: 4px; -} - -.referenced-annotation:first-of-type { - padding-top: 4px; -} - -.referenced-annotation:last-of-type { - margin-bottom: 0; -} - .q-btn--dense .q-icon, .q-btn--round .q-icon { svg { // Safari fix diff --git a/src/store/annotations/actions.js b/src/store/annotations/actions.js index 9ebdad0d..5d170b56 100644 --- a/src/store/annotations/actions.js +++ b/src/store/annotations/actions.js @@ -65,12 +65,13 @@ export const setFilteredAnnotations = ({ commit, getters, rootGetters }, types) }; export const addHighlightAttributesToText = ({ getters }, dom) => { + console.log('addhigh') const { annotations } = getters; // Add range attributes - [...dom.querySelectorAll('[data-target]:not([value=""])')] - .map((el) => el.getAttribute('data-target').replace('_start', '').replace('_end', '')) - .forEach((targetSelector) => Utils.addRangeHighlightAttributes(targetSelector, dom)); + // [...dom.querySelectorAll('[data-target]:not([value=""])')] + // .map((el) => el.getAttribute('data-target').replace('_start', '').replace('_end', '')) + // .forEach((targetSelector) => Utils.addRangeHighlightAttributes(targetSelector, dom)); // Add single attributes annotations.forEach((annotation) => { @@ -143,6 +144,71 @@ export const initAnnotations = async ({ dispatch }, url) => { }; export const addHighlightHoverListeners = ({ getters, rootGetters }) => { + + const annotationElements = Array.from(document.querySelectorAll('[data-annotation]')); + + annotationElements + // Annotations can be nested, so we filter out all outer elements from this selection and + // iterate over the deepest elements + .filter(el => [...el.childNodes].filter(childNode => childNode.nodeName === '#text').length > 0) + .forEach(deepestEl => { + console.log(deepestEl) + deepestEl.addEventListener( + 'mouseenter', + () => { + console.log('hover') + // Hovering is only supported for selected annotations + if (!AnnotationUtils.isAnnotationSelected(deepestEl)) return; + + const { filteredAnnotations } = getters; + const annotationTooltipModels = filteredAnnotations.reduce((acc, curr) => { + const { id } = curr; + const name = rootGetters['config/getIconByType'](curr.body['x-content-type']); + + acc[id] = { + value: curr.body.value, + name, + }; + return acc; + }, {}); + + let current = deepestEl; + + const annotationIds = [current.getAttribute('data-annotation-ids')]; + + while (current.parentElement.getAttribute('data-annotation')) { + annotationIds.push(current.getAttribute('data-annotation-ids')); + current = current.parentElement; + } + + // checks for duplicate class names. + const currentAnnotationTooltipModels = annotationIds + // Flatten + .join(' ').split(' ') + .map(id => { + return annotationTooltipModels[id] + }) + .filter(m => m); + + AnnotationUtils.createTooltip.bind( + this, + current, + currentAnnotationTooltipModels, + document.getElementById('text-content') + )(); + }, + false, + ); + deepestEl.addEventListener( + 'mouseout', + () => document.querySelectorAll('.annotation-tooltip2').forEach((el) => el.remove()), + false, + ); + }); + + return; + + const { filteredAnnotations } = getters; const annotationIds = filteredAnnotations.reduce((acc, curr) => { const { id } = curr; @@ -157,11 +223,11 @@ export const addHighlightHoverListeners = ({ getters, rootGetters }) => { document.querySelectorAll('[data-annotation]') .forEach((el) => { - const childOtherNodes = [...el.childNodes].filter((x) => x.nodeName !== '#text').length; + const hasChildNodes = [...el.childNodes].filter((x) => x.nodeName !== '#text').length > 0; - if (!childOtherNodes) { + if (!hasChildNodes) { const classNames = []; - el = AnnotationUtils.backTrackNestedAnnotations(el, classNames); + el = AnnotationUtils.getHighestParentAnnotationElement(el); const annotationClasses = []; // checks for duplicate class names. @@ -183,6 +249,7 @@ export const addHighlightHoverListeners = ({ getters, rootGetters }) => { 'mouseenter', () => { if (AnnotationUtils.isAnnotationSelected(el)) { + console.log('sel') AnnotationUtils.createTooltip.bind(this, el, annotationClasses)(); } }, diff --git a/src/utils/annotations.js b/src/utils/annotations.js index c8b89944..a944a56a 100644 --- a/src/utils/annotations.js +++ b/src/utils/annotations.js @@ -5,29 +5,21 @@ import { i18n } from '@/i18n'; // utility functions that we can use as generic way for perform tranformation on annotations. export function addHighlightToElements(selector, root, annotationId) { - const selectedElements = root.querySelectorAll(selector); + const selectedElements = selector + .split(',') + .map(selectorPart => [...root.querySelectorAll(selectorPart.replace(':', '--'))]) + .flat(); if (selectedElements.length === 0) { return; } - const strippedAnnotationId = stripAnnotationId(annotationId); - - function addToAttribute(element, attribute, newValue) { - const oldValue = element.getAttribute(attribute); - if (oldValue) { - if (!oldValue.match(newValue)) { - element.setAttribute(attribute, `${oldValue} ${newValue}`); - } - } else { - element.setAttribute(attribute, newValue); - } - } + // const strippedAnnotationId = stripAnnotationId(annotationId); selectedElements.forEach((element) => { element.setAttribute('data-annotation', true); - addToAttribute(element, 'data-annotation-ids', annotationId); - element.classList.add(strippedAnnotationId); + Utils.addToAttribute(element, 'data-annotation-ids', annotationId); + // element.classList.add(strippedAnnotationId); element.setAttribute('data-annotation-level', -1); }); } @@ -49,7 +41,7 @@ export function addRangeHighlightAttributes(id, root) { if (ended) return; if (childNode.nodeName === 'SPAN' && childNode.getAttribute('data-annotation') && started) { - childNode.classList.add(id); + Utils(id); } if (childNode.nodeName === '#text') { @@ -96,7 +88,7 @@ export const createSvgIcon = (name) => { return svg; }; -export function createTooltip(element, data) { +export function createTooltip(element, data, root) { const tooltipEl = document.createElement('span'); tooltipEl.setAttribute('data-annotation-classes', `${element.className}`); tooltipEl.setAttribute('class', 'annotation-tooltip'); @@ -134,14 +126,16 @@ export function createTooltip(element, data) { .split(' ') .join('')}annotation-tooltip`; tooltipEl.setAttribute('id', tooltipId); - window.top.el = element; + //window.top.el = element; + element.style.position = 'relative'; const r = element.getBoundingClientRect(); - tooltipEl.style.top = `${r.y + r.height}px`; - tooltipEl.style.left = `${r.x}px`; + console.log(element.scrollTop) + // tooltipEl.style.top = `${r.y + r.height}px`; + // tooltipEl.style.left = `${r.x}px`; - document.querySelector('body').append(tooltipEl); + element.append(tooltipEl); setTimeout(() => tooltipEl.classList.add('annotation-animated-tooltip'), 10); } @@ -219,11 +213,9 @@ export function getAllElementsFromSelector(selector) { return [...document.querySelectorAll(selector)]; } -export const backTrackNestedAnnotations = (el, classNames = []) => { +export const getHighestParentAnnotationElement = (el) => { let current = el; - classNames.push(current.className); - while ( current.parentElement.getAttribute('data-annotation') && current.parentElement.childNodes.length === 1 @@ -235,8 +227,6 @@ export const backTrackNestedAnnotations = (el, classNames = []) => { return el; }; -const annotationCache = {}; - export const isAnnotationSelected = (el) => { const key = el.getAttribute('class'); if (el[key] !== undefined) { @@ -274,8 +264,6 @@ export const isAnnotationSelected = (el) => { } } - annotationCache[key] = matched; - return matched; }; @@ -295,7 +283,7 @@ export function generateTargetSelector(annotation) { if (targetId) { targetId = targetId.split('/').pop(); - result = `#${targetId}`; + result = `#annotation-${targetId}`; } } else if (selector.type === 'CssSelector') { result = handleCssSelector(selector); @@ -303,6 +291,10 @@ export function generateTargetSelector(annotation) { result = handleRangeSelector(selector); } + return result; + console.log(result) + + const isValid = Utils.isSelectorValid(result); return isValid ? result : null; @@ -316,7 +308,37 @@ export function handleRangeSelector(selector) { const { startSelector, endSelector } = selector; if (startSelector && endSelector) { if (startSelector.type === 'CssSelector') { - return stripSelector(handleCssSelector(startSelector)); + const start = document.querySelector(handleCssSelector(startSelector).replaceAll('\'', '')); + const end = document.querySelector(handleCssSelector(endSelector).replaceAll('\'', '')); + + const elementsInRange = []; + + let started = false; + let ended = false; + + function findElementsInRangeRecursive(element) { + if (element === start) started = true; + if (element === end) { + ended = true; + return; + } + + if (started && element.nodeValue !== ' ' && element.nodeName === '#text') { + elementsInRange.push(element.parentElement); + return; + } + + [...element.childNodes] + .filter(childNode => childNode.nodeName !== 'STYLE' && childNode.nodeName !== 'SCRIPT' && childNode.nodeName !== 'svg') + .forEach(childNode => { + if (!ended) { + findElementsInRangeRecursive(childNode); + } + }); + } + findElementsInRangeRecursive(document.getElementById('text-content')); + + return elementsInRange.map(el => Utils.elemToSelector(el)).join(',') } } return null; diff --git a/src/utils/dom.js b/src/utils/dom.js index 163c67af..5b7586b9 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -38,3 +38,38 @@ export const isSelectorValid = (selector) => { try { queryCheck(selector); } catch { return false; } return true; }; + + +export function addToAttribute(element, attribute, newValue) { + const oldValue = element.getAttribute(attribute); + if (oldValue) { + if (!oldValue.match(newValue)) { + element.setAttribute(attribute, `${oldValue} ${newValue}`); + } + } else { + element.setAttribute(attribute, newValue); + } +} + +export function elemToSelector(el) { + if (el.id === 'text-content') + return '#text-content'; + var str = el.tagName.toLowerCase(); + + if (el.id !== '') { + str += '#' + el.id; + } else if (el.className) { + let classes = el.className.trim().split(/\s+/); + for (let i = 0; i < classes.length; i++) { + str += '.' + classes[i] + } + } + + if (el.hasAttribute('data-target')) { + str += `[data-target=${el.getAttribute('data-target')}]`; + } + + // if(document.querySelectorAll(str).length === 1) return str; + + return elemToSelector(el.parentNode) + ' > ' + str; +}