I know the title isnt clear but what im essentially trying to do is do my own rendering for an EPUB and im currently taking a naive approach towards paginating the chapter content and trying to render it in a WebView after splitting the content based on the tags.
My issue starts from the point where i cannot seem to accurately split the content to fit into the page, and it overflows in the rendered web view.
I have a separate web view which renders off screen and calculates the scrollHeight of the div where the content is being injected in.
The page is basically built block by block (tag by tag) until the div starts to overflow and the height of the div is greater than the View that its rendered to.
The pagination function yields the content accumulated just before the height is exceeded.
The problem is that it never fits to screen and there is always a bit of vertical overflow left.
My objective is to ultimately generate a list of animated WebViews the user can scroll through horizontally like an actual page flip of a book.
I would really appreciate some help in identifying the logic or calculation flaw here in the layout height comparisons. Let me know if ive missed something.
Below is the react native component which does the pagination and rendering
import { EPUBChapter, epubHtmlTemplate, ParsedEPUB } from "@/lib/EpubParser";
import { useEffect, useRef, useState } from "react";
import { parseDocument, DomUtils } from "htmlparser2";
import render from "dom-serializer";
import { WebView } from "react-native-webview";
import { Text } from "./ui/text";
import { useHtmlHeightMeasurer } from "@/hooks/useHtmlHeightMeasurer";
import { View, PixelRatio, FlatList } from "react-native";
export function WebViewCarousel({
parsedEpub,
currentChapterIndex = 0,
}: {
parsedEpub: ParsedEPUB;
currentChapterIndex: number;
}) {
const chapters = parsedEpub.chapters;
const [currentChapter, setCurrentChapter] = useState(
undefined
);
const view = useRef(null);
// const { html } = useHtmlHeightMeasurer(viewDimensions.width, viewDimensions.height);
const { measureHtmlHeight, MeasuringWebView, resizeWebView } =
useHtmlHeightMeasurer(355, 355);
const [pages, setPages] = useState([]);
async function* pageGenerator(
chapterHtml: string,
dimensions: { width: number; height: number }
) {
resizeWebView({ height: dimensions.width, width: dimensions.height });
const parsedChapterHtml = parseDocument(chapterHtml);
const htmlNodes = parsedChapterHtml.children;
let pageNodes: typeof parsedChapterHtml.children = [];
const chapterContent = DomUtils.findOne(
(elem) => elem.name === "section",
htmlNodes
);
// const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(dimensions.height);
const pixelHeight = dimensions.height;
if (chapterContent) {
for (const [index, node] of chapterContent.children.entries()) {
const testPage = render([...pageNodes, node]);
// console.log('Node: ' + render(node));
if (testPage.trim()) {
const webViewHeight = await measureHtmlHeight(
epubHtmlTemplate(testPage)
);
console.log("View height: " + pixelHeight);
console.log("Measured Height: " + webViewHeight);
if (webViewHeight >= pixelHeight) {
console.log("Yielding Page");
yield pageNodes;
pageNodes = [];
}
pageNodes.push(node);
}
}
}
console.log("Final page");
return pageNodes;
}
// const generatePage = pageGenerator(chapters[currentChapterIndex].content);
async function generatePages({
width,
height,
}: {
width: number;
height: number;
}) {
const pagesToAdd = [];
for await (const page of pageGenerator(
chapters[currentChapterIndex].content,
{ width, height }
)) {
console.log(render(page));
pagesToAdd.push(render(page));
}
setPages([...pages, ...pagesToAdd]);
}
return (
{
generatePages({
height: e.nativeEvent.layout.height,
width: e.nativeEvent.layout.width,
});
}}
>
{pages.length > 0 && (
)}
);
}
The offscreen component used to measure the content until it overflows
import { epubHtmlTemplate } from "@/lib/EpubParser";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { WebView } from "react-native-webview";
const measureJs = `
(() => {
const observer = new MutationObserver(measure)
let debounce;
function measure() {
clearTimeout(debounce);
debounce = setTimeout(() => {
try {
const container = document.getElementById('epub-content');
if(container) {
const height = container.scrollHeight;
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'measurement',
value: height
}));
} else {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
value: 'Missing container with id epub-content'
}));
}
} catch (error) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
value: error.message
}));
}
}, 150);
}
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
measure();
})()
`;
export function useHtmlHeightMeasurer(width: number, height: number) {
const [html, setHtml] = useState("");
const resolver = useRef<(h: number) => void>();
const webViewRef = useRef(null);
const [webViewDimensions, setWebViewDimensions] = useState({
height: 0,
width: 0
});
const measureHtmlHeight = (htmlString: string) =>
new Promise((resolve) => {
resolver.current = resolve;
setHtml(htmlString);
});
const resizeWebView = ({ width, height }: { width: number, height: number }) => {
setWebViewDimensions({ width, height })
}
const MeasuringWebView = () => (
{
const data = JSON.parse(event.nativeEvent.data);
if(data.type === 'error') {
console.error(data.value)
}
if (data.type === "measurement") {
const h = Number(data.value);
if (resolver.current) {
resolver.current(h);
resolver.current = undefined;
}
}
}}
style={{ width: webViewDimensions.width, height: webViewDimensions.height, backgroundColor: "transparent" }}
/>
);
return { measureHtmlHeight, MeasuringWebView, resizeWebView };
}