Commit 04cc7f9a authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Migrate html-side-by-side module to TS

parent 9390cdf2
import { rectContains, rectIntersects } from '../util/geometry'; import { rectContains, rectIntersects } from '../util/geometry';
import { nodeIsElement, nodeIsText } from '../util/node';
/** /**
* CSS selectors used to find elements that are considered potentially part * CSS selectors used to find elements that are considered potentially part
...@@ -14,18 +15,14 @@ const contentSelectors = [ ...@@ -14,18 +15,14 @@ const contentSelectors = [
/** /**
* Attempt to guess the region of the page that contains the main content. * Attempt to guess the region of the page that contains the main content.
* *
* @param {Element} root * @return The left/right content margins or `null` if they could not be determined
* @return {{ left: number, right: number }|null} -
* The left/right content margins or `null` if they could not be determined
*/ */
export function guessMainContentArea(root) { export function guessMainContentArea(
root: Element
): { left: number; right: number } | null {
// Maps of (margin X coord, votes) for margin positions. // Maps of (margin X coord, votes) for margin positions.
const leftMarginVotes = new Map<number, number>();
/** @type {Map<number,number>} */ const rightMarginVotes = new Map<number, number>();
const leftMarginVotes = new Map();
/** @type {Map<number,number>} */
const rightMarginVotes = new Map();
// Gather data about the paragraphs of text in the document. // Gather data about the paragraphs of text in the document.
// //
...@@ -37,7 +34,7 @@ export function guessMainContentArea(root) { ...@@ -37,7 +34,7 @@ export function guessMainContentArea(root) {
.map(p => { .map(p => {
// Gather some data about them. // Gather some data about them.
const rect = p.getBoundingClientRect(); const rect = p.getBoundingClientRect();
const textLength = /** @type {string} */ (p.textContent).length; const textLength = p.textContent!.length;
return { rect, textLength }; return { rect, textLength };
}) })
.filter(({ rect }) => { .filter(({ rect }) => {
...@@ -76,17 +73,12 @@ export function guessMainContentArea(root) { ...@@ -76,17 +73,12 @@ export function guessMainContentArea(root) {
return { left: leftPos, right: rightPos }; return { left: leftPos, right: rightPos };
} }
/** @type {Range} */ let textRectRange: Range;
let textRectRange;
/** /**
* Return the viewport-relative rect occupied by part of a text node. * Return the viewport-relative rect occupied by part of a text node.
*
* @param {Text} text
* @param {number} start
* @param {number} end
*/ */
function textRect(text, start = 0, end = text.data.length) { function textRect(text: Text, start = 0, end: number = text.data.length) {
if (!textRectRange) { if (!textRectRange) {
// Allocate a range only on the first call to avoid the overhead of // Allocate a range only on the first call to avoid the overhead of
// constructing and maintaining a large number of live ranges. // constructing and maintaining a large number of live ranges.
...@@ -97,8 +89,7 @@ function textRect(text, start = 0, end = text.data.length) { ...@@ -97,8 +89,7 @@ function textRect(text, start = 0, end = text.data.length) {
return textRectRange.getBoundingClientRect(); return textRectRange.getBoundingClientRect();
} }
/** @param {Element} element */ function hasFixedPosition(element: Element) {
function hasFixedPosition(element) {
switch (getComputedStyle(element).position) { switch (getComputedStyle(element).position) {
case 'fixed': case 'fixed':
case 'sticky': case 'sticky':
...@@ -112,10 +103,8 @@ function hasFixedPosition(element) { ...@@ -112,10 +103,8 @@ function hasFixedPosition(element) {
* Return the bounding rect that contains the element's content. Unlike * Return the bounding rect that contains the element's content. Unlike
* `Element.getBoundingClientRect`, this includes content which overflows * `Element.getBoundingClientRect`, this includes content which overflows
* the element's specified size. * the element's specified size.
*
* @param {Element} element
*/ */
function elementContentRect(element) { function elementContentRect(element: Element) {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
rect.x -= element.scrollLeft; rect.x -= element.scrollLeft;
rect.y -= element.scrollTop; rect.y -= element.scrollTop;
...@@ -127,33 +116,29 @@ function elementContentRect(element) { ...@@ -127,33 +116,29 @@ function elementContentRect(element) {
/** /**
* Yield all the text node descendants of `root` that intersect `rect`. * Yield all the text node descendants of `root` that intersect `rect`.
* *
* @param {Element} root * @param shouldVisit - Optional filter that determines whether to visit a subtree
* @param {DOMRect} rect
* @param {(el: Element) => boolean} shouldVisit - Optional filter that determines
* whether to visit a subtree
* @return {Generator<Text>}
*/ */
function* textNodesInRect(root, rect, shouldVisit = () => true) { function* textNodesInRect(
/** @type {Node|null} */ root: Element,
let node = root.firstChild; rect: DOMRect,
shouldVisit: (el: Element) => boolean = () => true
): Generator<Text> {
let node: Node | null = root.firstChild;
while (node) { while (node) {
if (node.nodeType === Node.ELEMENT_NODE) { if (nodeIsElement(node)) {
const element = /** @type {Element} */ (node);
const contentIntersectsRect = rectIntersects( const contentIntersectsRect = rectIntersects(
elementContentRect(element), elementContentRect(node),
rect rect
); );
// Only examine subtrees which are visible. // Only examine subtrees which are visible.
if (shouldVisit(element) && contentIntersectsRect) { if (shouldVisit(node) && contentIntersectsRect) {
yield* textNodesInRect(element, rect, shouldVisit); yield* textNodesInRect(node, rect, shouldVisit);
} }
} else if (node.nodeType === Node.TEXT_NODE) { } else if (nodeIsText(node)) {
const text = /** @type {Text} */ (node);
// Skip over text nodes which are entirely outside the viewport or empty. // Skip over text nodes which are entirely outside the viewport or empty.
if (rectIntersects(textRect(text), rect)) { if (rectIntersects(textRect(node), rect)) {
yield text; yield node;
} }
} }
node = node.nextSibling; node = node.nextSibling;
...@@ -164,25 +149,21 @@ function* textNodesInRect(root, rect, shouldVisit = () => true) { ...@@ -164,25 +149,21 @@ function* textNodesInRect(root, rect, shouldVisit = () => true) {
* Find content within an element to use as an anchor when applying a layout * Find content within an element to use as an anchor when applying a layout
* change to the document. * change to the document.
* *
* @param {Element} root * @return Range to use as an anchor or `null` if a suitable range could not be found
* @param {DOMRect} viewport
* @return {Range|null} - Range to use as an anchor or `null` if a suitable
* range could not be found
*/ */
function getScrollAnchor(root, viewport) { function getScrollAnchor(root: Element, viewport: DOMRect): Range | null {
// Range representing the content whose position within the viewport we will // Range representing the content whose position within the viewport we will
// try to maintain after running the callback. // try to maintain after running the callback.
let anchorRange = /** @type {Range|null} */ (null); let anchorRange: Range | null = null;
// Find the first word (non-whitespace substring of a text node) that is fully // Find the first word (non-whitespace substring of a text node) that is fully
// visible in the viewport. // visible in the viewport.
// Text inside fixed-position elements is ignored because its position won't // Text inside fixed-position elements is ignored because its position won't
// be affected by a layout change and so it makes a poor scroll anchor. // be affected by a layout change and so it makes a poor scroll anchor.
/** @param {Element} el */ const shouldVisit = (el: Element) => !hasFixedPosition(el);
const shouldVisit = el => !hasFixedPosition(el);
textNodeLoop: for (let textNode of textNodesInRect( textNodeLoop: for (const textNode of textNodesInRect(
root, root,
viewport, viewport,
shouldVisit shouldVisit
...@@ -190,7 +171,7 @@ function getScrollAnchor(root, viewport) { ...@@ -190,7 +171,7 @@ function getScrollAnchor(root, viewport) {
let textLen = 0; let textLen = 0;
// Visit all the non-whitespace substrings of the text node. // Visit all the non-whitespace substrings of the text node.
for (let word of textNode.data.split(/\b/)) { for (const word of textNode.data.split(/\b/)) {
if (/\S/.test(word)) { if (/\S/.test(word)) {
const start = textLen; const start = textLen;
const end = textLen + word.length; const end = textLen + word.length;
...@@ -217,20 +198,19 @@ function getScrollAnchor(root, viewport) { ...@@ -217,20 +198,19 @@ function getScrollAnchor(root, viewport) {
* and tries to preserve the position of this content within the viewport * and tries to preserve the position of this content within the viewport
* after the callback is invoked. * after the callback is invoked.
* *
* @param {() => void} callback - Callback that will apply the layout change * @param callback - Callback that will apply the layout change
* @param {Element} [scrollRoot] * @param [viewport] - Area to consider "in the viewport". Defaults to the
* @param {DOMRect} [viewport] - Area to consider "in the viewport". Defaults to * viewport of the current window.
* the viewport of the current window. * @return Amount by which the scroll position was adjusted to keep the anchored
* @return {number} - Amount by which the scroll position was adjusted to keep * content in view
* the anchored content in view
*/ */
export function preserveScrollPosition( export function preserveScrollPosition(
callback, callback: () => void,
/* istanbul ignore next */ /* istanbul ignore next */
scrollRoot = document.documentElement, scrollRoot: Element = document.documentElement,
/* istanbul ignore next */ /* istanbul ignore next */
viewport = new DOMRect(0, 0, window.innerWidth, window.innerHeight) viewport: DOMRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight)
) { ): number {
const anchor = getScrollAnchor(scrollRoot, viewport); const anchor = getScrollAnchor(scrollRoot, viewport);
if (!anchor) { if (!anchor) {
callback(); callback();
......
import { nodeIsText } from './util/node';
/** /**
* Returns true if the start point of a selection occurs after the end point, * Returns true if the start point of a selection occurs after the end point,
* in document order. * in document order.
...@@ -51,10 +53,6 @@ export function forEachNodeInRange(range: Range, callback: (n: Node) => void) { ...@@ -51,10 +53,6 @@ export function forEachNodeInRange(range: Range, callback: (n: Node) => void) {
} }
} }
function nodeIsText(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE;
}
function textNodeContainsText(textNode: Text): boolean { function textNodeContainsText(textNode: Text): boolean {
const whitespaceOnly = /^\s*$/; const whitespaceOnly = /^\s*$/;
return !textNode.textContent!.match(whitespaceOnly); return !textNode.textContent!.match(whitespaceOnly);
......
export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
export function nodeIsText(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE;
}
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