Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | 10x 110x 110x 110x 110x 110x 110x 110x 197x 197x 197x 8x 189x 62x 197x 197x 110x 110x 110x 4x 4x 4x 4x 8x 8x 1x 1x 8x 8x 4x 8x 110x | import React, { useEffect, useRef } from "react";
import { Box } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { sanitizeRichText } from "@/utils/helpers/sanitizeHtml";
import tinyMCEUtility from "@/utils/helpers/tinyMCEUtility";
import { inlineEmbeddedChoiceColors } from "@/utils/components/question-bank/question-display/utils/questionCardStyles";
/**
* Renders a passage that already has `[data-embedded-choice-id]` spans baked
* into its HTML (the choice text IS the paragraph text) and recolors those
* spans based on the current selection / revealed answer.
*
* - `interactive`: attaches click handlers and highlights `selectedId`.
* - read-only (`interactive` false): highlights `correctId` when `showAnswers`.
*/
const EmbeddedChoicePassage = ({
html,
interactive = false,
selectedId = null,
correctId = null,
showAnswers = false,
onSelect,
backgroundColor,
}) => {
const theme = useTheme();
const containerRef = useRef(null);
const { EMBEDDED_CHOICE_ATTR } = tinyMCEUtility();
const buildContent = () => {
const container = document.createElement("div");
container.innerHTML = sanitizeRichText(html || "");
container.querySelectorAll(`[${EMBEDDED_CHOICE_ATTR}]`).forEach((node) => {
const id = node.getAttribute(EMBEDDED_CHOICE_ATTR);
let state = "default";
if (interactive) {
if (selectedId === id) state = "selected";
} else if (showAnswers && correctId === id) {
state = "correct";
}
const colors = inlineEmbeddedChoiceColors(theme, state);
// A full border, not text-decoration/border-bottom — see
// inlineEmbeddedChoiceColors' docstring: a formula choice is
// display:inline-block, which a text-decoration on this wrapper
// span never paints through.
//
// display:inline-block on this wrapper too, not plain inline: a
// plain inline element's border follows the line box (font
// ascent/descent), not a taller inline-block descendant, so a
// fraction/root inside a formula choice got cut through the middle
// by the border instead of fully enclosed by it.
node.setAttribute(
"style",
`display:inline-block;vertical-align:middle;border:2px ${colors.borderStyle};` +
`background-color:${colors.backgroundColor};color:${colors.textColor};` +
`padding:1px 3px;border-radius:3px;font-weight:${state !== "default" ? 600 : 500};` +
`${interactive ? "cursor:pointer;" : ""}`
);
});
return container.innerHTML;
};
useEffect(() => {
if (!interactive) return undefined;
const container = containerRef.current;
Iif (!container) return undefined;
const cleanup = [];
container.querySelectorAll(`[${EMBEDDED_CHOICE_ATTR}]`).forEach((node) => {
const id = node.getAttribute(EMBEDDED_CHOICE_ATTR);
const handleClick = (e) => {
e.stopPropagation();
onSelect?.(id);
};
node.addEventListener("click", handleClick);
cleanup.push(() => node.removeEventListener("click", handleClick));
});
Iif (window.MathJax?.typesetPromise) {
window.MathJax.typesetPromise([container]).catch(() => {});
}
return () => cleanup.forEach((fn) => fn());
});
return (
<Box
ref={containerRef}
dangerouslySetInnerHTML={{ __html: buildContent() }}
sx={{
width: "100%",
fontSize: "1rem",
lineHeight: 1.6,
color: "text.primary",
bgcolor: backgroundColor,
"& p": { margin: 0, marginBottom: "0.75em" },
"& p:last-child": { marginBottom: 0 },
"& s, & del": { textDecoration: "line-through" },
}}
/>
);
};
export default EmbeddedChoicePassage;
|