Add better typing, some todos, a highlighting demo
This commit is contained in:
parent
98d413171c
commit
4daf2946d2
|
|
@ -1,17 +1,35 @@
|
||||||
import { DirectiveBinding, VNode } from 'vue';
|
import { DirectiveBinding } from 'vue';
|
||||||
import * as rangy from 'rangy';
|
import * as rangy from 'rangy';
|
||||||
|
import 'rangy/lib/rangy-textrange';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import Mark from 'mark.js';
|
import * as Mark from 'mark.js';
|
||||||
|
import { ContentBlockNode } from '@/__generated__/graphql';
|
||||||
|
import ContentBlock from '@/components/ContentBlock.vue';
|
||||||
|
|
||||||
|
// todo: we need to get the following information for a highlight:
|
||||||
|
// Which ContentBlock? => contentBlock.id
|
||||||
|
// Which ContentComponent inside the ContentBlock? => Index of contentBlock.contents
|
||||||
|
// Which paragraph inside the Element
|
||||||
|
// 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 {
|
||||||
|
contentBlock: string; // the id
|
||||||
|
contentIndex: number;
|
||||||
|
paragraphIndex: number;
|
||||||
|
startPosition: number;
|
||||||
|
selectionLength: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
const TOP_TRESHOLD = 0;
|
const TOP_TRESHOLD = 0;
|
||||||
const BOTTOM_TRESHOLD = window.innerHeight || document.documentElement.clientHeight;
|
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) => {
|
const isInsideViewport = (position: number) => {
|
||||||
return position >= TOP_TRESHOLD && position <= BOTTOM_TRESHOLD;
|
return position >= TOP_TRESHOLD && position <= BOTTOM_TRESHOLD;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findClosestAncestor = (node: Node, tag: string): HTMLElement | null => {
|
const findClosestAncestorWithTag = (node: Node, tag: string): HTMLElement | null => {
|
||||||
let currentNode: Node | null = node;
|
let currentNode: Node | null = node;
|
||||||
while (currentNode && currentNode.nodeName.toLowerCase() !== tag && currentNode.nodeName.toLowerCase() !== 'body') {
|
while (currentNode && currentNode.nodeName.toLowerCase() !== tag && currentNode.nodeName.toLowerCase() !== 'body') {
|
||||||
currentNode = currentNode.parentNode;
|
currentNode = currentNode.parentNode;
|
||||||
|
|
@ -21,6 +39,7 @@ const findClosestAncestor = (node: Node, tag: string): HTMLElement | null => {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findClosestAncestorWithClass = (node: Node, className: string): HTMLElement | null => {
|
const findClosestAncestorWithClass = (node: Node, className: string): HTMLElement | null => {
|
||||||
let currentNode: Node | null = node;
|
let currentNode: Node | null = node;
|
||||||
while (
|
while (
|
||||||
|
|
@ -65,13 +84,11 @@ const findPositionInParent = (element: HTMLElement, className: string = 'content
|
||||||
return children.indexOf(element);
|
return children.indexOf(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectionHandler = (el: HTMLElement) => (_e: Event) => {
|
const getSelectionHandler = (el: HTMLElement, contentBlock: ContentBlockNode) => (_e: Event) => {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (isInsideViewport(rect.top) || isInsideViewport(rect.bottom)) {
|
if (isInsideViewport(rect.top) || isInsideViewport(rect.bottom)) {
|
||||||
// the listener only does something if the `el` is visible in the viewport, to save resources
|
// 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
|
// now we check if the selection is inside our container
|
||||||
|
|
||||||
const selection = rangy.getSelection();
|
const selection = rangy.getSelection();
|
||||||
|
|
@ -79,27 +96,40 @@ const getSelectionHandler = (el: HTMLElement) => (_e: Event) => {
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
if (range.intersectsNode(el)) {
|
if (range.intersectsNode(el)) {
|
||||||
// the selection is inside our container, so we can do something with it
|
// 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 { startContainer, endContainer } = range;
|
||||||
const startAncestor = findClosestAncestor(startContainer, 'p');
|
const paragraph = findClosestAncestorWithTag(startContainer, 'p');
|
||||||
const endAncestor = findClosestAncestor(endContainer, 'p');
|
const endAncestor = findClosestAncestorWithTag(endContainer, 'p');
|
||||||
console.log('same?');
|
|
||||||
console.log(startAncestor === endAncestor);
|
|
||||||
|
|
||||||
if (startAncestor === endAncestor) {
|
if (paragraph === endAncestor) {
|
||||||
const parent = findClosestAncestorWithClass(startContainer, 'content-component');
|
// todo: rename this variable, `parent` is not correct anymore
|
||||||
console.log(parent);
|
const contentComponent = findClosestAncestorWithClass(startContainer, 'content-component');
|
||||||
if (parent) {
|
if (parent) {
|
||||||
// our selection is wholly inside the container node, we continue with it
|
// our selection is wholly inside the container node, we continue with it
|
||||||
const position = findPositionInParent(parent);
|
const position = findPositionInParent(contentComponent);
|
||||||
console.log(`selection is inside our container at ${position}`);
|
const siblings = Array.from(paragraph.parentElement.children);
|
||||||
|
const positionInTextBlock = siblings.indexOf(paragraph);
|
||||||
|
|
||||||
const { start, end } = range.toCharacterRange(parent);
|
const numOfParagraphs = Array.from(parent.children[1].children).filter(
|
||||||
console.log(start, end);
|
(child) => child.nodeName.toLowerCase() === 'p'
|
||||||
const instance = new Mark(parent);
|
).length;
|
||||||
instance.markRanges([{ start, length: end - start }]);
|
|
||||||
|
const { start, end } = range.toCharacterRange(paragraph);
|
||||||
|
// console.log(contentBlock.id);
|
||||||
|
// console.log(start, end);
|
||||||
|
// console.log(parent);
|
||||||
|
// console.log(position);
|
||||||
|
// console.log(numOfParagraphs);
|
||||||
|
// console.log(positionInTextBlock);
|
||||||
|
// const instance = new Mark(paragraph);
|
||||||
|
// instance.markRanges([{ start, length: end - start }]);
|
||||||
|
const highlightedText: Highlight = {
|
||||||
|
contentBlock: contentBlock.id,
|
||||||
|
contentIndex: position,
|
||||||
|
startPosition: start,
|
||||||
|
};
|
||||||
|
console.log(highlightedText);
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,17 +137,43 @@ const getSelectionHandler = (el: HTMLElement) => (_e: Event) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
|
const highlight = (element: HTMLElement, ranges: Mark.Range[]) => {
|
||||||
log.debug('registering a selection handler');
|
// todo: make the ContentComponent / TextBlock component handle the highlighting
|
||||||
document.addEventListener('selectionchange', getSelectionHandler(el));
|
const instance = new Mark(element);
|
||||||
|
instance.markRanges(ranges);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
|
// const mounted = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
|
||||||
log.debug('removing a selection handler');
|
const mounted = (el: HTMLElement, binding: DirectiveBinding) => {
|
||||||
document.removeEventListener('selectionchange', getSelectionHandler(el));
|
const contentBlock = (binding.instance as InstanceType<typeof ContentBlock>).contentBlock;
|
||||||
|
document.addEventListener('selectionchange', getSelectionHandler(el, contentBlock));
|
||||||
|
const demoRanges: Mark.Range[] = [
|
||||||
|
{ start: 3, length: 50 },
|
||||||
|
{ start: 182, length: 10 },
|
||||||
|
];
|
||||||
|
// todo: find a way for the ContentBlock component to tell us when it's 'ready' and everything loaded. Otherwise, the
|
||||||
|
// 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);
|
||||||
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const removeListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
|
||||||
|
const removeListener = (el: HTMLElement, binding: DirectiveBinding) => {
|
||||||
|
const contentBlock = (binding.instance as InstanceType<typeof ContentBlock>).contentBlock;
|
||||||
|
document.removeEventListener('selectionchange', getSelectionHandler(el, contentBlock));
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo: move this to the contentBlock
|
||||||
export default {
|
export default {
|
||||||
mounted: addListener,
|
mounted,
|
||||||
unmounted: removeListener,
|
unmounted: removeListener,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue