Add new directive responsible for saving selections
This commit is contained in:
parent
f112e19c79
commit
485e07067b
|
|
@ -3,6 +3,7 @@
|
||||||
:class="{ 'hideable-element--greyed-out': isHidden }"
|
:class="{ 'hideable-element--greyed-out': isHidden }"
|
||||||
class="content-block__container hideable-element content-list__parent"
|
class="content-block__container hideable-element content-list__parent"
|
||||||
ref="wrapper"
|
ref="wrapper"
|
||||||
|
v-highlight
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="specialClass"
|
:class="specialClass"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { DirectiveBinding, VNode } from 'vue';
|
||||||
|
import * as rangy from 'rangy';
|
||||||
|
import log from 'loglevel';
|
||||||
|
import Mark from 'mark.js';
|
||||||
|
|
||||||
|
const TOP_TRESHOLD = 0;
|
||||||
|
const BOTTOM_TRESHOLD = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
const NODE_BEFORE_AND_AFTER = 2; // Rangy does not define constants for `compareNode`, so we do it here
|
||||||
|
|
||||||
|
const isInsideViewport = (position: number) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue