frontend/src/hooks/usePagination.ts
2021-07-16 16:38:12 +05:00

243 lines
7.2 KiB
TypeScript

import React, { useEffect, useState } from "react";
type TextNode = Text & { nodeValue: string };
type PositionElement = [number, TextNode];
type IdPositions = Record<string, number>;
type UsePaginationReturnTuple = [
ready: boolean,
goToPage: (pageNum: number) => void,
currentPage: number,
pageNumber: number
];
const isTextNode = (el: Node): el is TextNode => el.nodeType === Node.TEXT_NODE;
const isElementNode = (el: Node): el is HTMLElement =>
el.nodeType === Node.ELEMENT_NODE;
export const usePagination = (
contentEl: React.RefObject<HTMLDivElement>,
pageContainerEl: React.RefObject<HTMLDivElement>,
pageEl: React.RefObject<HTMLDivElement>,
bookContent?: string
): UsePaginationReturnTuple => {
const [ready, setReady] = useState(false);
const [positions, setPositions] = useState<PositionElement[]>([]);
const [idPositions, setIdPositions] = useState<IdPositions>({});
const [pages, setPages] = useState<number[]>([]);
const [currentPage, setCurrentPage] = useState(0);
const computeStartPositionsOfElements = async (root: HTMLDivElement) => {
const positionToElement: PositionElement[] = [];
const idPositions: IdPositions = {};
const recursive = (currentPosition: number, element: Node): number => {
if (isTextNode(element)) {
positionToElement.push([currentPosition, element]);
return currentPosition + element.nodeValue.length;
} else if (isElementNode(element)) {
if (element.id && element.id != null)
idPositions[element.id] = currentPosition;
const children = element.childNodes;
const newCurrentPosition = Array.from(children).reduce(
recursive,
currentPosition
);
return newCurrentPosition;
} else {
return currentPosition;
}
};
recursive(0, root);
setPositions(positionToElement);
setIdPositions(idPositions);
};
const findPages = async (page: HTMLElement, pageContainer: HTMLElement) => {
const pages = [];
pages.push(0);
let jump = 100;
while (pages[pages.length - 1] < getMaxPosition()) {
if (pages.length > 2)
jump = pages[pages.length - 1] - pages[pages.length - 2];
const endPosition = findPage(
pages[pages.length - 1],
jump,
page,
pageContainer
);
pages.push(endPosition);
}
clearPage(page);
setPages(pages);
setCurrentPage(0);
};
const findPage = (
startPosition: number,
initialJump: number,
page: HTMLElement,
pageContainer: HTMLElement
) => {
let previousEndPosition = getMaxPosition();
let endPosition = findNextSpaceForPosition(startPosition + initialJump);
copyTextToPage(startPosition, endPosition, page);
while (!scrollNecessary(pageContainer) && endPosition < getMaxPosition()) {
previousEndPosition = endPosition;
endPosition = findNextSpaceForPosition(endPosition + 1);
copyTextToPage(startPosition, endPosition, page);
}
while (scrollNecessary(pageContainer) && endPosition > startPosition) {
previousEndPosition = endPosition;
endPosition = findPreviousSpaceForPosition(endPosition - 1);
copyTextToPage(startPosition, endPosition, page);
}
if (endPosition === startPosition) return previousEndPosition;
return endPosition;
};
const findNextSpaceForPosition = (startPosition: number) => {
let i = 0;
while (i < positions.length - 1 && positions[i + 1][0] < startPosition) i++;
const el = positions[i][1];
let d = startPosition - positions[i][0];
const str = el.nodeValue || "";
while (d < str.length && str.charAt(d) != " ") d++;
if (d >= str.length) {
if (i == positions.length - 1) return getMaxPosition();
else return positions[i + 1][0];
} else {
return positions[i][0] + d;
}
};
const findPreviousSpaceForPosition = (startPosition: number) => {
let i = 0;
while (i < positions.length - 1 && positions[i + 1][0] < startPosition) i++;
const el = positions[i][1];
let d = startPosition - positions[i][0];
const str = el.nodeValue || "";
while (d > 0 && str.charAt(d) != " ") d--;
return positions[i][0] + d;
};
const scrollNecessary = (pageContainer: HTMLElement) =>
pageContainer.scrollHeight > pageContainer.clientHeight ||
pageContainer.scrollWidth > pageContainer.clientWidth;
const copyTextToPage = (from: number, to: number, page: HTMLElement) => {
const range = document.createRange();
const startEl = getElementForPosition(from);
const startElement = startEl[1];
const locationInStartEl = from - startEl[0];
range.setStart(startElement, locationInStartEl);
const endEl = getElementForPosition(to);
const endElement = endEl[1];
const locationInEndEl = to - endEl[0];
range.setEnd(endElement, locationInEndEl);
page.innerHTML = "";
page.appendChild(range.cloneContents());
};
const clearPage = (page: HTMLElement) => {
page.innerHTML = "";
};
const getPageForId = (id: string) => getPageForPosition(getPositionForId(id));
const getMaxPosition = () => {
const [pos, el] = positions[positions.length - 1];
return pos + el.nodeValue.length || 0;
};
const getPositionForId = (id: string) => {
if (id in idPositions) return idPositions[id];
return 0;
};
const getPageForPosition = (pos: number) => {
for (const [pageNum, pagePos] of pages.entries())
if (pagePos > pos) return pageNum - 1;
return pages.length - 2;
};
const getElementForPosition = (pos: number) => {
for (const [i, [currPos, _]] of positions.entries())
if (currPos > pos) return positions[i - 1];
return positions[positions.length - 1];
};
const jumpToLocation = (page: HTMLElement) => {
const url = new URL(window.location.href);
const positionStr = url.searchParams.get("position");
if (url.href.lastIndexOf("#") > 0) {
const id = url.href.substring(
url.href.lastIndexOf("#") + 1,
url.href.length
);
displayPage(getPageForId(id), page);
} else if (positionStr) {
const pos = parseInt(positionStr);
displayPage(getPageForPosition(pos), page);
} else {
displayPage(0, page);
}
};
const displayPage = (pageNum: number, page: HTMLElement) => {
if (pageNum >= 0 && pageNum < pages.length - 1) {
setCurrentPage(pageNum);
const startPosition = pages[pageNum];
copyTextToPage(startPosition, pages[pageNum + 1], page);
setReady(true);
}
};
useEffect(() => {
if (contentEl.current && bookContent) {
contentEl.current.innerHTML = bookContent;
computeStartPositionsOfElements(contentEl.current);
}
}, [bookContent]);
useEffect(() => {
if (positions.length && pageEl.current && pageContainerEl.current)
findPages(pageEl.current, pageContainerEl.current);
}, [positions, idPositions]);
useEffect(() => {
if (pageEl.current && pages.length && !ready)
jumpToLocation(pageEl.current);
}, [pages]);
const makeDisplayPage = (page: React.RefObject<HTMLDivElement>) => {
if (page.current)
return (pageNum: number) => displayPage(pageNum, page.current!);
else return (pageNum: number) => {};
};
return [ready, makeDisplayPage(pageEl), currentPage, pages.length - 1];
};