Commit 99073c0b authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Convert `highlighter` to TS

Prepare for some upcoming changes to this module.
parent b66168ff
......@@ -5,17 +5,20 @@ import { isNodeInRange } from './range-util';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
type HighlightProps = {
// Associated SVG rect drawn to represent this highlight (in PDFs)
svgHighlight?: SVGRectElement;
};
export type HighlightElement = HTMLElement & HighlightProps;
/**
* Return the canvas element underneath a highlight element in a PDF page's
* text layer.
*
* Returns `null` if the highlight is not above a PDF canvas.
*
* @param {HTMLElement} highlightEl -
* A `<hypothesis-highlight>` element in the page's text layer
* @return {HTMLCanvasElement|null}
*/
function getPDFCanvas(highlightEl) {
function getPDFCanvas(highlightEl: HighlightElement): HTMLCanvasElement | null {
// This code assumes that PDF.js renders pages with a structure like:
//
// <div class="page">
......@@ -40,7 +43,7 @@ function getPDFCanvas(highlightEl) {
return null;
}
return /** @type {HTMLCanvasElement} */ (canvasEl);
return canvasEl as HTMLCanvasElement;
}
/**
......@@ -49,12 +52,15 @@ function getPDFCanvas(highlightEl) {
* The created SVG elements are stored in the `svgHighlight` property of
* each `HighlightElement`.
*
* @param {HighlightElement[]} highlightEls -
* @param highlightEls -
* An element that wraps the highlighted text in the transparent text layer
* above the PDF.
* @param {string} [cssClass] - CSS class(es) to add to the SVG highlight elements
* @param [cssClass] - CSS class(es) to add to the SVG highlight elements
*/
function drawHighlightsAbovePDFCanvas(highlightEls, cssClass) {
function drawHighlightsAbovePDFCanvas(
highlightEls: HighlightElement[],
cssClass?: string
) {
if (highlightEls.length === 0) {
return;
}
......@@ -66,10 +72,9 @@ function drawHighlightsAbovePDFCanvas(highlightEls, cssClass) {
return;
}
/** @type {SVGElement|null} */
let svgHighlightLayer = canvasEl.parentElement.querySelector(
'.hypothesis-highlight-layer'
);
) as SVGSVGElement | null;
if (!svgHighlightLayer) {
// Create SVG layer. This must be in the same stacking context as
......@@ -124,27 +129,13 @@ function drawHighlightsAbovePDFCanvas(highlightEls, cssClass) {
svgHighlightLayer.append(...highlightRects);
}
/**
* Additional properties added to text highlight HTML elements.
*
* @typedef HighlightProps
* @prop {SVGElement} [svgHighlight]
*/
/**
* @typedef {HTMLElement & HighlightProps} HighlightElement
*/
/**
* Return text nodes which are entirely inside `range`.
*
* If a range starts or ends part-way through a text node, the node is split
* and the part inside the range is returned.
*
* @param {Range} range
* @return {Text[]}
*/
function wholeTextNodesInRange(range) {
function wholeTextNodesInRange(range: Range): Text[] {
if (range.collapsed) {
// Exit early for an empty range to avoid an edge case that breaks the algorithm
// below. Splitting a text node at the start of an empty range can leave the
......@@ -152,9 +143,8 @@ function wholeTextNodesInRange(range) {
return [];
}
/** @type {Node|null} */
let root = range.commonAncestorContainer;
if (root.nodeType !== Node.ELEMENT_NODE) {
let root = range.commonAncestorContainer as Node | null;
if (root && root.nodeType !== Node.ELEMENT_NODE) {
// If the common ancestor is not an element, set it to the parent element to
// ensure that the loop below visits any text nodes generated by splitting
// the common ancestor.
......@@ -169,9 +159,7 @@ function wholeTextNodesInRange(range) {
}
const textNodes = [];
const nodeIter = /** @type {Document} */ (
root.ownerDocument
).createNodeIterator(
const nodeIter = root!.ownerDocument!.createNodeIterator(
root,
NodeFilter.SHOW_TEXT // Only return `Text` nodes.
);
......@@ -180,7 +168,7 @@ function wholeTextNodesInRange(range) {
if (!isNodeInRange(range, node)) {
continue;
}
let text = /** @type {Text} */ (node);
const text = node as Text;
if (text === range.startContainer && range.startOffset > 0) {
// Split `text` where the range starts. The split will create a new `Text`
......@@ -204,11 +192,14 @@ function wholeTextNodesInRange(range) {
* Wraps the DOM Nodes within the provided range with a highlight
* element of the specified class and returns the highlight Elements.
*
* @param {Range} range - Range to be highlighted
* @param {string} [cssClass] - CSS class(es) to add to the highlight elements
* @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect
* @param range - Range to be highlighted
* @param [cssClass] - CSS class(es) to add to the highlight elements
* @return Elements wrapping text in `normedRange` to add a highlight effect
*/
export function highlightRange(range, cssClass) {
export function highlightRange(
range: Range,
cssClass?: string
): HighlightElement[] {
const textNodes = wholeTextNodesInRange(range);
// Check if this range refers to a placeholder for not-yet-rendered content in
......@@ -217,8 +208,8 @@ export function highlightRange(range, cssClass) {
// Group text nodes into spans of adjacent nodes. If a group of text nodes are
// adjacent, we only need to create one highlight element for the group.
let textNodeSpans = /** @type {Text[][]} */ ([]);
let prevNode = /** @type {Node|null} */ (null);
let textNodeSpans: Text[][] = [];
let prevNode: Node | null = null;
let currentSpan = null;
textNodes.forEach(node => {
......@@ -241,16 +232,15 @@ export function highlightRange(range, cssClass) {
);
// Wrap each text node span with a `<hypothesis-highlight>` element.
const highlights = /** @type {HighlightElement[]} */ ([]);
const highlights: HighlightElement[] = [];
textNodeSpans.forEach(nodes => {
// A custom element name is used here rather than `<span>` to reduce the
// likelihood of highlights being hidden by page styling.
/** @type {HighlightElement} */
const highlightEl = document.createElement('hypothesis-highlight');
highlightEl.className = classnames('hypothesis-highlight', cssClass);
const parent = /** @type {Node} */ (nodes[0].parentNode);
const parent = nodes[0].parentNode as ParentNode;
parent.replaceChild(highlightEl, nodes[0]);
nodes.forEach(node => highlightEl.appendChild(node));
......@@ -277,33 +267,26 @@ export function highlightRange(range, cssClass) {
* Replace a child `node` with `replacements`.
*
* nb. This is like `ChildNode.replaceWith` but it works in older browsers.
*
* @param {ChildNode} node
* @param {Node[]} replacements
*/
function replaceWith(node, replacements) {
const parent = /** @type {Node} */ (node.parentNode);
function replaceWith(node: ChildNode, replacements: Node[]) {
const parent = node.parentNode as ParentNode;
replacements.forEach(r => parent.insertBefore(r, node));
node.remove();
}
/**
* Remove all highlights under a given root element.
*
* @param {HTMLElement} root
*/
export function removeAllHighlights(root) {
export function removeAllHighlights(root: HTMLElement) {
const highlights = Array.from(root.querySelectorAll('hypothesis-highlight'));
removeHighlights(/** @type {HighlightElement[]} */ (highlights));
removeHighlights(highlights as HighlightElement[]);
}
/**
* Remove highlights from a range previously highlighted with `highlightRange`.
*
* @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`
*/
export function removeHighlights(highlights) {
for (let h of highlights) {
export function removeHighlights(highlights: HighlightElement[]) {
for (const h of highlights) {
if (h.parentNode) {
const children = Array.from(h.childNodes);
replaceWith(h, children);
......@@ -321,11 +304,11 @@ export function removeHighlights(highlights) {
* A highlight can be displayed in a different ("focused") style to indicate
* that it is current in some other context - for example the user has selected
* the corresponding annotation in the sidebar.
*
* @param {HighlightElement[]} highlights
* @param {boolean} focused
*/
export function setHighlightsFocused(highlights, focused) {
export function setHighlightsFocused(
highlights: HighlightElement[],
focused: boolean
) {
highlights.forEach(h => {
// In PDFs the visible highlight is created by an SVG element, so the focused
// effect is applied to that. In other documents the effect is applied to the
......@@ -339,7 +322,7 @@ export function setHighlightsFocused(highlights, focused) {
// SVG elements are rendered in document order so to achieve this we need
// to move the element to be the last child of its parent.
if (focused) {
const parent = /** @type {SVGElement} */ (h.svgHighlight.parentNode);
const parent = h.svgHighlight.parentNode as SVGElement;
parent.append(h.svgHighlight);
}
} else {
......@@ -350,48 +333,40 @@ export function setHighlightsFocused(highlights, focused) {
/**
* Set whether highlights under the given root element should be visible.
*
* @param {HTMLElement} root
* @param {boolean} visible
*/
export function setHighlightsVisible(root, visible) {
export function setHighlightsVisible(root: HTMLElement, visible: boolean) {
const showHighlightsClass = 'hypothesis-highlights-always-on';
root.classList.toggle(showHighlightsClass, visible);
}
/**
* Get the highlight elements that contain the given node.
*
* @param {Node} node
* @return {HighlightElement[]}
*/
export function getHighlightsContainingNode(node) {
export function getHighlightsContainingNode(node: Node): HighlightElement[] {
let el =
node.nodeType === Node.ELEMENT_NODE
? /** @type {Element} */ (node)
? (node as Element)
: node.parentElement;
const highlights = [];
while (el) {
if (el.classList.contains('hypothesis-highlight')) {
highlights.push(/** @type {HighlightElement} */ (el));
highlights.push(el);
}
el = el.parentElement;
}
return highlights;
return highlights as HighlightElement[];
}
/**
* Subset of `DOMRect` interface.
*
* @typedef Rect
* @prop {number} top
* @prop {number} left
* @prop {number} bottom
* @prop {number} right
*/
// Subset of `DOMRect` interface
type Rect = {
top: number;
left: number;
bottom: number;
right: number;
};
/**
* Get the bounding client rectangle of a collection in viewport coordinates.
......@@ -399,15 +374,10 @@ export function getHighlightsContainingNode(node) {
* could just use that.
*
* [1] https://bugs.chromium.org/p/chromium/issues/detail?id=324437
*
* @param {HTMLElement[]} collection
* @return {Rect}
*/
export function getBoundingClientRect(collection) {
export function getBoundingClientRect(collection: HTMLElement[]): Rect {
// Reduce the client rectangles of the highlights to a bounding box
const rects = collection.map(
n => /** @type {Rect} */ (n.getBoundingClientRect())
);
const rects = collection.map(n => n.getBoundingClientRect() as Rect);
return rects.reduce((acc, r) => ({
top: Math.min(acc.top, r.top),
left: Math.min(acc.left, r.left),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment