144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
import { LevelPart } from "@/interfaces/exam";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface Props {
|
|
part: LevelPart,
|
|
contextWord: string | undefined,
|
|
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
|
}
|
|
|
|
const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine }) => {
|
|
const textRef = useRef<HTMLDivElement>(null);
|
|
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
|
const [lineHeight, setLineHeight] = useState<number>(0);
|
|
|
|
const calculateLineNumbers = () => {
|
|
if (textRef.current) {
|
|
const computedStyle = window.getComputedStyle(textRef.current);
|
|
const lineHeightValue = parseFloat(computedStyle.lineHeight);
|
|
const containerWidth = textRef.current.clientWidth;
|
|
setLineHeight(lineHeightValue);
|
|
|
|
const offscreenElement = document.createElement('div');
|
|
offscreenElement.style.position = 'absolute';
|
|
offscreenElement.style.top = '-9999px';
|
|
offscreenElement.style.left = '-9999px';
|
|
offscreenElement.style.whiteSpace = 'pre-wrap';
|
|
offscreenElement.style.width = `${containerWidth}px`;
|
|
offscreenElement.style.font = computedStyle.font;
|
|
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
|
offscreenElement.style.wordWrap = 'break-word';
|
|
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
|
|
|
const textContent = textRef.current.textContent || '';
|
|
const lines = textContent.split(/\n/).map(line =>
|
|
line.split(/(\s+)/).map(word => {
|
|
const span = document.createElement('span');
|
|
span.textContent = word;
|
|
return span;
|
|
})
|
|
);
|
|
|
|
// Append all spans to offscreenElement
|
|
lines.forEach(line => {
|
|
line.forEach(span => offscreenElement.appendChild(span));
|
|
offscreenElement.appendChild(document.createElement('br'));
|
|
});
|
|
|
|
document.body.appendChild(offscreenElement);
|
|
|
|
const processedLines: string[][] = [[]];
|
|
let currentLine = 1;
|
|
let currentLineTop: number | undefined;
|
|
let contextWordLine: number | null = null;
|
|
|
|
const firstChild = offscreenElement.firstChild as HTMLElement;
|
|
if (firstChild) {
|
|
currentLineTop = firstChild.getBoundingClientRect().top;
|
|
}
|
|
|
|
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
|
|
|
spans.forEach(span => {
|
|
const rect = span.getBoundingClientRect();
|
|
const top = rect.top;
|
|
|
|
if (currentLineTop !== undefined && top > currentLineTop) {
|
|
currentLine++;
|
|
currentLineTop = top;
|
|
processedLines.push([]);
|
|
}
|
|
|
|
processedLines[processedLines.length - 1].push(span.textContent?.trim() || '');
|
|
|
|
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
|
contextWordLine = currentLine;
|
|
}
|
|
});
|
|
setLineNumbers(processedLines.map((_, index) => index + 1));
|
|
if (contextWordLine) {
|
|
setContextWordLine(contextWordLine);
|
|
}
|
|
|
|
document.body.removeChild(offscreenElement);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
calculateLineNumbers();
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
calculateLineNumbers();
|
|
});
|
|
|
|
if (textRef.current) {
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
resizeObserver.observe(textRef.current);
|
|
}
|
|
|
|
return () => {
|
|
if (textRef.current) {
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
resizeObserver.unobserve(textRef.current);
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [part.context, contextWord]);
|
|
|
|
/*if (typeof part.showContextLines === "undefined") {
|
|
return (
|
|
<div className="flex flex-col gap-2 w-full">
|
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
|
{!!part.context &&
|
|
part.context
|
|
.split(/\n|(\\n)/g)
|
|
.filter((x) => x && x.length > 0 && x !== "\\n")
|
|
.map((line, index) => (
|
|
<Fragment key={index}>
|
|
<p key={index}>{line}</p>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
);
|
|
}*/
|
|
|
|
return (
|
|
<div className="flex mt-2">
|
|
<div className="flex-shrink-0 w-8 pr-2">
|
|
{lineNumbers.map(num => (
|
|
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
|
{num}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
|
<div dangerouslySetInnerHTML={{ __html: part.context! }} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TextComponent;
|