|
- import { Component, ComponentChild, RenderableProps, h } from 'preact';
- import deepEqual from 'deep-equal';
- import scrollIntoView from 'scroll-into-view-if-needed';
- import classes from './MarginalAnnotations.module.scss';
- import cls from 'classnames';
- import { asArray, targetsUrl } from 'web-annotation-utils';
- import { highlightText } from '@apache-annotator/dom';
- import {
- MarginalAnnotationCard,
- MarginalAnnotationCardProps,
- } from './MarginalAnnotationCard';
- import { findTargetsInDocument } from '../util/dom-selectors'
- import type { IAnnotationWithSource } from '../storage/Annotation';
-
- interface AnnotationTargetHighlightProps extends MarginalAnnotationCardProps {
- appContainer: Node;
- annotation: IAnnotationWithSource;
- inFocus?: boolean;
- }
-
- interface AnnotationTargetHighlightState {
- highlightHovered: boolean;
- highlightClicked: boolean;
- anchorPosition?: number;
- }
-
- // This component renders the annotation card, but as a side
- // effect highlights the annotation’s target in the page. It adds classes and
- // event listeners to the wrapped annotation, to light up matching bodies and
- // targets on clicking&hovering.
- export class AnnotationTargetHighlight extends Component<
- AnnotationTargetHighlightProps,
- AnnotationTargetHighlightState
- > {
- state: AnnotationTargetHighlightState = {
- highlightHovered: false,
- highlightClicked: false,
- };
-
- highlightCleanupFunctions: (() => void)[] = [];
- highlightMarkElements: HTMLElement[] = [];
-
- constructor() {
- super();
- this.handleMouseClickOnCard = this.handleMouseClickOnCard.bind(this);
- this.handleHighlightMouseEnter = this.handleHighlightMouseEnter.bind(this);
- this.handleHighlightMouseLeave = this.handleHighlightMouseLeave.bind(this);
- this.handleKeyUp = this.handleKeyUp.bind(this);
- }
-
- componentDidMount() {
- this.highlightTargets();
- // if (this.props.inFocus) {
- // this.setState({ highlightClicked: true });
- // }
- }
-
- componentDidUpdate(
- previousProps: Readonly<AnnotationTargetHighlightProps>,
- previousState: Readonly<AnnotationTargetHighlightState>,
- ) {
- if (this.state.highlightClicked !== previousState.highlightClicked) {
- this.highlightMarkElements.forEach((markElement) =>
- this.state.highlightClicked
- ? markElement.classList.add(classes.clicked)
- : markElement.classList.remove(classes.clicked),
- );
- }
-
- if (this.state.highlightHovered !== previousState.highlightHovered) {
- this.highlightMarkElements.forEach((markElement) =>
- this.state.highlightHovered
- ? markElement.classList.add(classes.hovered)
- : markElement.classList.remove(classes.hovered),
- );
- }
-
- if (
- !deepEqual(
- this.props.annotation.annotation.target,
- previousProps.annotation.annotation.target,
- )
- ) {
- this.removeHighlights();
- this.highlightTargets();
- }
-
- if (this.props.inFocus && !previousProps.inFocus) {
- this.setState({ highlightClicked: true });
- }
- }
-
- componentWillUnmount() {
- this.removeHighlights();
- }
-
- async highlightTargets() {
- const targets = asArray(this.props.annotation.annotation.target);
-
- // Annotations may have multiple targets; ignore those not on this page.
- const targetsForThisPage = targets.filter((target) =>
- targetsUrl(target, document.URL),
- );
-
- // Try find the annotation targets within the document (if the target has a Selector).
- const domTargets = await findTargetsInDocument(targetsForThisPage);
-
- // Highlight the found targets.
- this.highlightCleanupFunctions = domTargets.flatMap((domTarget) => {
- // Ignore any matches inside our own annotations etc.
- if (
- (domTarget instanceof Range &&
- domTarget.intersectsNode(this.props.appContainer)) ||
- (domTarget instanceof Node &&
- domTarget.compareDocumentPosition(this.props.appContainer) &
- Node.DOCUMENT_POSITION_CONTAINS)
- ) {
- return [];
- }
-
- // Highlight the text contained in the Range using <mark> elements.
- const highlightCleanupFunction = highlightText(domTarget, 'mark', {
- class: classes.highlight,
- 'data-annotation-highlight-temp': 'true',
- });
-
- // TODO tweak highlightText upstream to not need this silly workaround.
- const markElements = [
- ...document.querySelectorAll('[data-annotation-highlight-temp]'),
- ] as HTMLElement[];
-
- // React to mouse interactions with the highlight.
- markElements.forEach((markElement) => {
- markElement.removeAttribute('data-annotation-highlight-temp');
-
- markElement.addEventListener('click', () => this.handleMouseClick());
- markElement.addEventListener('mouseenter', () =>
- this.handleHighlightMouseEnter(),
- );
- markElement.addEventListener('mouseleave', () =>
- this.handleHighlightMouseLeave(),
- );
- });
-
- this.highlightMarkElements.push(...markElements);
-
- return [highlightCleanupFunction];
- });
- if (this.highlightMarkElements.length > 0) {
- const top = this.highlightMarkElements[0].getBoundingClientRect().top + window.scrollY;
- this.setState({
- anchorPosition: top,
- });
- }
- }
-
- removeHighlights() {
- this.highlightCleanupFunctions.forEach((removeHighlight) =>
- removeHighlight(),
- );
- this.highlightCleanupFunctions = [];
- this.highlightMarkElements = [];
- this.setState({
- highlightClicked: false,
- highlightHovered: false,
- });
- }
-
- handleMouseClick() {
- this.setState((currentState) => ({
- highlightClicked: !currentState.highlightClicked,
- }));
- }
-
- handleMouseClickOnCard() {
- if (!this.state.highlightClicked && this.highlightMarkElements.length > 0) {
- const firstMarkElement = this.highlightMarkElements[0];
- if (firstMarkElement.getBoundingClientRect().top)
- scrollIntoView(firstMarkElement, { behavior: 'smooth', block: 'center', scrollMode: 'if-needed' });
- }
- this.setState({
- highlightClicked: false,
- });
- }
-
- handleHighlightMouseEnter() {
- this.setState({ highlightHovered: true });
- }
-
- handleHighlightMouseLeave() {
- this.setState({ highlightHovered: false });
- }
-
- handleKeyUp(event: KeyboardEvent) {
- if (event.key === 'Escape') {
- this.setState({ highlightClicked: false });
- // TODO move focus out of annotation to hide it.
- }
- }
-
- render(
- {
- annotation,
- inFocus,
- ...otherProps
- }: RenderableProps<AnnotationTargetHighlightProps>,
- {
- highlightHovered,
- highlightClicked,
- anchorPosition,
- }: Readonly<AnnotationTargetHighlightState>,
- ): ComponentChild {
- const extraClasses = cls({
- [classes.highlightClicked]: highlightClicked,
- [classes.highlightHovered]: highlightHovered,
- });
- // Pass the extra class to the wrapped annotation.
- return (
- <MarginalAnnotationCard
- {...otherProps}
- annotation={annotation}
- onclick={this.handleMouseClickOnCard}
- onmouseenter={this.handleHighlightMouseEnter}
- onmouseleave={this.handleHighlightMouseLeave}
- onkeyup={this.handleKeyUp}
- anchorPosition={anchorPosition}
- extraClasses={extraClasses}
- inFocus={inFocus}
- />
- );
- }
- }
|