From 485e07067ba30907825e39185eaf22eb58afb80b Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 19 Dec 2023 20:22:55 +0100 Subject: [PATCH] Add new directive responsible for saving selections --- client/src/components/ContentBlock.vue | 1 + client/src/directives/highlight.ts | 123 +++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 client/src/directives/highlight.ts diff --git a/client/src/components/ContentBlock.vue b/client/src/components/ContentBlock.vue index 47efc129..9d8982be 100644 --- a/client/src/components/ContentBlock.vue +++ b/client/src/components/ContentBlock.vue @@ -3,6 +3,7 @@ :class="{ 'hideable-element--greyed-out': isHidden }" class="content-block__container hideable-element content-list__parent" ref="wrapper" + v-highlight >
{ + return position >= TOP_TRESHOLD && position <= BOTTOM_TRESHOLD; +}; + +const findClosestAncestor = (node: Node, tag: string): HTMLElement | null => { + let currentNode: Node | null = node; + while (currentNode && currentNode.nodeName.toLowerCase() !== tag && currentNode.nodeName.toLowerCase() !== 'body') { + currentNode = currentNode.parentNode; + } + if (currentNode) { + return currentNode as HTMLElement; + } + return null; +}; +const findClosestAncestorWithClass = (node: Node, className: string): HTMLElement | null => { + let currentNode: Node | null = node; + while ( + currentNode && // current node exists + // current node is not an HTMLElement, so probably a TextNode + // or, current node does not contain the class + (!(currentNode instanceof HTMLElement) || !currentNode.classList.contains(className)) && + // we have not arrived at the top yet + currentNode.nodeName.toLowerCase() !== 'body' + ) { + // so, we continue looking + currentNode = currentNode.parentNode; + } + if (currentNode instanceof HTMLElement && currentNode.classList.contains(className)) { + // make sure we're not up at the body element + return currentNode as HTMLElement; + } + return null; +}; + +const findPositionInParent = (element: HTMLElement, className: string = 'content-component'): number => { + /* + * currently, a content block has the structure + * div.content-block__container + * div.content-block + * div.content-block__visibility + * div.content-block__title + * div.content-component + * div.content-component + * [...] + * div.content-component + * + * We only care about the .content-component divs, not the others + * + */ + + if (!element.parentElement) { + log.error('element has no parent', element); + return -1; // element has no parent, probably somehow ended up at the top. But this should probably never happen in our application + } + const children = Array.from(element.parentElement.children).filter((child) => child.classList.contains(className)); + return children.indexOf(element); +}; + +const getSelectionHandler = (el: HTMLElement) => (_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 + + console.log(el); + console.log(rect); + // 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 + console.log('selection is inside our container'); + console.log(el); + console.log(range.compareNode(el)); + const { startContainer, endContainer } = range; + const startAncestor = findClosestAncestor(startContainer, 'p'); + const endAncestor = findClosestAncestor(endContainer, 'p'); + console.log('same?'); + console.log(startAncestor === endAncestor); + + if (startAncestor === endAncestor) { + const parent = findClosestAncestorWithClass(startContainer, 'content-component'); + console.log(parent); + if (parent) { + // our selection is wholly inside the container node, we continue with it + const position = findPositionInParent(parent); + console.log(`selection is inside our container at ${position}`); + + const { start, end } = range.toCharacterRange(parent); + console.log(start, end); + const instance = new Mark(parent); + instance.markRanges([{ start, length: end - start }]); + } + } + } + } + } +}; + +const addListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => { + log.debug('registering a selection handler'); + document.addEventListener('selectionchange', getSelectionHandler(el)); +}; + +const removeListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => { + log.debug('removing a selection handler'); + document.removeEventListener('selectionchange', getSelectionHandler(el)); +}; + +export default { + mounted: addListener, + unmounted: removeListener, +};