All files / src/utils/components/question-bank/question-display/drag-and-drop DragDropViewOnly.jsx

97.05% Statements 33/34
95.23% Branches 20/21
100% Functions 7/7
97.05% Lines 33/34

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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136                            7x 22x 22x 22x 22x         7x 10x 1x 1x 1x 1x   10x 1x 1x 1x 1x       7x 12x 12x 12x 12x   12x               7x 10x 10x   10x 10x     10x               10x 12x 12x     10x                                                         18x                                                                  
import { Box, Typography } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import React, { memo, useEffect } from "react";
import TinyMCETextEditorReadOnly from "@/utils/components/TinyMCETextEditorReadOnly";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import { sanitizeRichText } from "@/utils/helpers/sanitizeHtml";
import {
    inlineBlankZoneColors,
    questionCardSurfaceBg,
    questionItemNumberSx,
    questionStemTypographySx,
} from "@/utils/components/question-bank/question-display/utils/questionCardStyles";
import { extractAnswerContent } from "@/utils/components/question-bank/question-display/utils/questionAnswerUtils";
 
const unwrapBlock = (html) => {
    const el = document.createElement("div");
    el.innerHTML = html;
    Eif (el.children.length === 1 && el.children[0].tagName === "P") {
        return el.children[0].innerHTML;
    }
    return html;
};
 
const injectMathLiveCSS = () => {
    if (!document.querySelector('link[href*="mathlive-fonts"]')) {
        const link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = "https://cdn.jsdelivr.net/npm/mathlive@0.108.2/mathlive-fonts.css";
        document.head.appendChild(link);
    }
    if (!document.querySelector('link[href*="mathlive-static"]')) {
        const link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = "https://cdn.jsdelivr.net/npm/mathlive@0.108.2/mathlive-static.css";
        document.head.appendChild(link);
    }
};
 
const buildDropZoneSpan = (index, showAnswers, blank, theme) => {
    const answerContent = extractAnswerContent(blank);
    const isAnswer = showAnswers && answerContent;
    const colors = inlineBlankZoneColors(theme, isAnswer);
    const answerHtml = isAnswer ? unwrapBlock(answerContent) : "Drop here";
 
    return (
        `<span style="display:inline-block;padding:2px 16px;margin:0 10px;border-radius:8px;` +
        `border:2px ${colors.borderStyle} ${colors.borderColor};background-color:${colors.backgroundColor};` +
        `vertical-align:middle;white-space:nowrap;line-height:normal;font-size:inherit;` +
        `font-weight:${isAnswer ? 500 : 400};color:${colors.color};">${answerHtml}</span>`
    );
};
 
const DragDropViewOnly = ({ question, showAnswers, itemNumber }) => {
    const theme = useTheme();
    const cardBg = questionCardSurfaceBg(theme);
 
    useEffect(() => {
        injectMathLiveCSS();
    }, []);
 
    let blankIndex = 0;
    // Modified by Allan Ninal — optional chaining here. A question saved without a
    // correct answer threw on `.answers`, and because QuestionDisplayRenderer draws
    // these inside question LISTS the throw took the whole list down rather than the
    // one bad card. Every other renderer in this tree already reads it this way
    // (MultipleChoiceOption, CheckboxOption, NonEmptyGraph); these two blank-based
    // ones were the exception. extractAnswerContent maps undefined to "", which is
    // exactly the unanswered-blank case it already handles.
    const content = question.question.replace(/__blank__/g, () => {
        const idx = blankIndex++;
        return buildDropZoneSpan(idx, showAnswers, question.correctAnswer?.answers?.[idx], theme);
    });
 
    return (
        <Box sx={{ display: "flex", flexDirection: "column", gap: 3, width: "100%" }}>
            <Box sx={{ display: "flex", alignItems: "flex-start", gap: 1 }}>
                {itemNumber != null && itemNumber !== false && (
                    <Typography component="span" sx={{ ...questionItemNumberSx, flexShrink: 0 }}>
                        {itemNumber}.
                    </Typography>
                )}
                <Box sx={{ flex: 1, ...questionStemTypographySx }}>
                    <TinyMCETextEditorReadOnly content={content} backgroundColor={cardBg} />
                </Box>
            </Box>
            <Box
                sx={{
                    display: "flex",
                    flexWrap: "wrap",
                    alignItems: "center",
                    justifyContent: "center",
                    gap: 1,
                    p: 1.5,
                    bgcolor: alpha(theme.palette.grey[500], 0.06),
                    borderRadius: 1.5,
                    border: `1px solid ${alpha(theme.palette.grey[500], 0.12)}`,
                    ml: itemNumber ? 3.5 : 0,
                }}
            >
                {/* Same reason as the correctAnswer guard above: a question with no
                    choices must lose its chips, not the list it is rendered in. */}
                {(question.choices ?? []).map((choiceObj, index) => (
                    <Box
                        key={index}
                        sx={{
                            display: "flex",
                            alignItems: "center",
                            gap: 0.5,
                            px: 1.25,
                            py: 0.65,
                            bgcolor: alpha(theme.palette.primary.main, 0.12),
                            color: "primary.dark",
                            borderRadius: 1.5,
                            border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
                        }}
                    >
                        <DragIndicatorIcon sx={{ fontSize: 18, opacity: 0.8, flexShrink: 0 }} />
                        <Box
                            dangerouslySetInnerHTML={{ __html: sanitizeRichText(unwrapBlock(choiceObj.text)) }}
                            sx={{
                                fontWeight: 500,
                                fontSize: "0.9375rem",
                                lineHeight: "normal",
                                "& p": { margin: 0, padding: 0 },
                                "& .mfe-formula": { color: "inherit", bgcolor: "transparent" },
                            }}
                        />
                    </Box>
                ))}
            </Box>
        </Box>
    );
};
 
export default memo(DragDropViewOnly);