61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { decodeTextFragment, findAndHighlightText } from "@/lib/text-highlight";
|
|
|
|
/**
|
|
* TextHighlighter — processes incoming `?hl=` URLs on page load.
|
|
*
|
|
* When a user visits a URL with `?hl=prefix,suffix,length`, this component:
|
|
* 1. Decodes the text fragment from the query param
|
|
* 2. Searches the page's content areas for matching text
|
|
* 3. Wraps the matched text in a <mark> with a highlight animation
|
|
* 4. Scrolls to the highlighted text
|
|
* 5. Fades the highlight out after a few seconds
|
|
*
|
|
* This is rendered once in layout.tsx alongside TextSelectionToolbar.
|
|
* It only activates when the `hl` search param is present.
|
|
*/
|
|
export function TextHighlighter() {
|
|
const searchParams = useSearchParams();
|
|
const hl = searchParams.get("hl");
|
|
|
|
useEffect(() => {
|
|
if (!hl) return;
|
|
|
|
const fragment = decodeTextFragment(hl);
|
|
if (!fragment) return;
|
|
|
|
// Wait for the page content to be fully rendered before searching.
|
|
// Use a series of attempts in case content loads asynchronously
|
|
// (e.g., client components hydrating, paginated verse loading, etc.)
|
|
let attempts = 0;
|
|
const maxAttempts = 10;
|
|
const interval = 500; // ms between attempts
|
|
|
|
function tryHighlight() {
|
|
attempts++;
|
|
const found = findAndHighlightText(fragment!);
|
|
|
|
if (!found && attempts < maxAttempts) {
|
|
setTimeout(tryHighlight, interval);
|
|
}
|
|
|
|
if (found) {
|
|
// Clean the URL without triggering a navigation — remove the hl param
|
|
// so the user can share a clean URL after viewing
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete("hl");
|
|
const cleanUrl = url.pathname + (url.search || "") + url.hash;
|
|
window.history.replaceState({}, "", cleanUrl);
|
|
}
|
|
}
|
|
|
|
// First attempt after a short delay to let the DOM settle
|
|
setTimeout(tryHighlight, 300);
|
|
}, [hl]);
|
|
|
|
return null;
|
|
}
|