Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,35 @@ const QuestionPreviewModal = ({ question, onClose }: QuestionPreviewModalProps)

iframeDocument.body.setAttribute('data-preview-device', activeTab);

const iframeWin = iframeDocument.defaultView;
const tutorPreviewWin = iframeWin as (Window & { _tutorCoordinatesRedrawAll?: () => void }) | null;
let coordinatesRedrawTimeout: number | undefined;
if (tutorPreviewWin && typeof tutorPreviewWin._tutorCoordinatesRedrawAll === 'function') {
Copy link
Copy Markdown
Collaborator

@b-l-i-n-d b-l-i-n-d May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this condition is only bound to the coordinate question type. Why not move this logic inside that component?

const redrawAll = tutorPreviewWin._tutorCoordinatesRedrawAll;
requestAnimationFrame(() => {
redrawAll();
});
coordinatesRedrawTimeout = tutorPreviewWin.setTimeout(() => {
redrawAll();
}, 280);
}

if (tutorConfig.settings?.learning_mode === 'kids') {
iframeDocument.body.setAttribute('data-tutor-ui', 'kids');
return;
return () => {
if (coordinatesRedrawTimeout !== undefined && tutorPreviewWin) {
tutorPreviewWin.clearTimeout(coordinatesRedrawTimeout);
}
};
}

iframeDocument.body.removeAttribute('data-tutor-ui');

return () => {
if (coordinatesRedrawTimeout !== undefined && tutorPreviewWin) {
tutorPreviewWin.clearTimeout(coordinatesRedrawTimeout);
}
};
}, [activeTab, iframeDocument]);

useLayoutEffect(() => {
Expand Down Expand Up @@ -335,17 +358,40 @@ const renderQuestionPreview = (question: QuizQuestion) => {
case 'ordering':
return <OrderingPreview answers={question.question_answers} />;
case 'pin_image':
return <PinImagePreview answers={question.question_answers} />;
return (
<PinImagePreview
key={`pin-${String(question.question_id)}-${question.question_answers?.[0]?.image_url ?? ''}`}
answers={question.question_answers}
/>
);
case 'draw_image':
return <DrawImagePreview answers={question.question_answers} />;
return (
<DrawImagePreview
key={`draw-${String(question.question_id)}-${question.question_answers?.[0]?.image_url ?? ''}`}
answers={question.question_answers}
/>
);
case 'scale':
return <ScalePreview answers={question.question_answers} />;
case 'coordinates':
return <CoordinatesPreview />;
case 'puzzle':
return (
<PuzzlePreview answers={question.question_answers} gridSize={question.question_settings.puzzle_grid_size} />
<CoordinatesPreview
key={`coordinates-${String(question.question_id)}-${question.question_settings?.coordinates_axis_range ?? ''}`}
axisRange={question.question_settings?.coordinates_axis_range}
/>
);
case 'puzzle': {
const puzzleAnswer = question.question_answers?.[0];
const puzzleImageKey = puzzleAnswer?.image_url || puzzleAnswer?.answer_two_gap_match || '';
return (
<PuzzlePreview
key={`puzzle-${question.question_id}-${question.question_settings?.puzzle_grid_size ?? ''}-${encodeURIComponent(puzzleImageKey)}`}
answers={question.question_answers}
gridSize={question.question_settings.puzzle_grid_size}
questionId={question.question_id}
/>
);
}
default:
return <UnsupportedPreview />;
}
Expand Down Expand Up @@ -408,6 +454,19 @@ const getPreviewFrameStyles = () => `
cursor: default;
}

/*
* Clear buttons: DrawImagePreview uses SVGIcon; CoordinatesPreview uses inline eraser SVG (coordinates script clones the button on re-init).
*/
.tutor-coordinates-clear-button > svg,
.tutor-draw-image-clear-button > svg {
width: 18px;
height: 18px;
min-width: 18px;
min-height: 18px;
flex-shrink: 0;
color: inherit;
}

body[data-preview-device='mobile'] .tutor-draw-image-question .tutor-draw-image-wrapper,
body[data-preview-device='mobile'] .tutor-draw-image-question .tutor-draw-image-reference-inner,
body[data-preview-device='mobile'] .tutor-pin-image-question .tutor-pin-image-wrapper,
Expand Down Expand Up @@ -451,6 +510,14 @@ const getPreviewFrameStyles = () => `
object-fit: contain;
}

/* Coordinates graph is a square (canvas width 100% + aspect-ratio); cap like draw/pin so the modal does not scroll. */
.tutor-quiz-question[data-question='coordinates'] .tutor-coordinates-grid-container {
box-sizing: border-box;
width: min(100%, min(52vh, 460px));
max-width: 100%;
margin-inline: auto;
}

/*
* Puzzle preview: same viewport idea as draw/pin above — board capped at min(52vh, 460px),
* scatter scroll area capped so header + board + pieces fit the modal column.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,107 +1,103 @@
import { css } from '@emotion/react';
import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from 'react';

import SVGIcon from '@TutorShared/atoms/SVGIcon';
import { tutorConfig } from '@TutorShared/config/config';
import { colorTokens } from '@TutorShared/config/styles';

const MIN_COORD = -10;
const MAX_COORD = 10;
const CANVAS_SIZE = 420;
const PADDING = 12;
const COORDINATES_SCRIPT_ATTR = 'data-tutor-coordinates-preview-script';

const CoordinatesPreview = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const resolveAxisRange = (raw?: number) => (raw === 20 ? 20 : 10);

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}

const context = canvas.getContext('2d');
if (!context) {
return;
}

const rect = canvas.getBoundingClientRect();
const logicalSize = Math.max(1, rect.width || CANVAS_SIZE);
const dpr = window.devicePixelRatio || 1;
const nextWidth = Math.max(1, Math.round(logicalSize * dpr));
const nextHeight = Math.max(1, Math.round(logicalSize * dpr));

if (canvas.width !== nextWidth || canvas.height !== nextHeight) {
canvas.width = nextWidth;
canvas.height = nextHeight;
}
canvas.style.width = `${logicalSize}px`;
canvas.style.height = `${logicalSize}px`;
const tutorIconsBase = `${String(tutorConfig.tutor_url || '').replace(/\/$/, '')}/assets/icons`;

const scaleX = canvas.width / logicalSize;
const scaleY = canvas.height / logicalSize;
context.setTransform(scaleX, 0, 0, scaleY, 0, 0);
const markerUrl = (name: 'graph-marker-hover' | 'graph-marker-selected' | 'graph-marker-wrong'): string =>
`${tutorIconsBase}/${name}.svg`;

const width = logicalSize;
const height = logicalSize;
const drawableWidth = width - 2 * PADDING;
const drawableHeight = height - 2 * PADDING;
const centerX = PADDING + drawableWidth / 2;
const centerY = PADDING + drawableHeight / 2;
const pixelsPerUnit = Math.min(drawableWidth, drawableHeight) / (MAX_COORD - MIN_COORD);
/** Matches Pro `coordinates.php` eraser + {@link DrawImagePreview} Clear (SVGIcon + brand). */
const clearButtonIcon = css`
color: ${colorTokens.text.brand};
`;

const graphToPixel = (x: number, y: number) => ({
x: centerX + x * pixelsPerUnit,
y: centerY - y * pixelsPerUnit,
});
interface CoordinatesPreviewProps {
axisRange?: number;
}

context.clearRect(0, 0, width, height);
/**
* Learning-area parity: loads `tutor-pro/assets/js/coordinates-question.js` in the preview iframe
* (same pattern as {@link ScalePreview} + Pro template `learning-area/quiz/questions/coordinates.php`).
*/
const CoordinatesPreview = ({ axisRange: axisRangeProp }: CoordinatesPreviewProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const axisRange = resolveAxisRange(axisRangeProp);
const qId = 'preview';
const inputId = `tutor-coordinates-points-${qId}`;
const canvasId = `tutor-coordinates-canvas-${qId}`;

const leftEdge = graphToPixel(MIN_COORD, 0).x;
const rightEdge = graphToPixel(MAX_COORD, 0).x;
const topEdge = graphToPixel(0, MAX_COORD).y;
const bottomEdge = graphToPixel(0, MIN_COORD).y;

context.strokeStyle = colorTokens.stroke.divider;
context.lineWidth = 0.5;
for (let i = MIN_COORD; i <= MAX_COORD; i++) {
if (i === 0) {
continue;
}
const xPoint = graphToPixel(i, 0);
context.beginPath();
context.moveTo(xPoint.x, topEdge);
context.lineTo(xPoint.x, bottomEdge);
context.stroke();
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
return;
}

const yPoint = graphToPixel(0, i);
context.beginPath();
context.moveTo(leftEdge, yPoint.y);
context.lineTo(rightEdge, yPoint.y);
context.stroke();
const doc = wrapper.ownerDocument;
if (!doc || doc === document) {
return;
}

context.strokeStyle = colorTokens.color.black[80];
context.lineWidth = 1.5;
context.beginPath();
context.moveTo(leftEdge, centerY);
context.lineTo(rightEdge, centerY);
context.stroke();
if (doc.head.querySelector(`[${COORDINATES_SCRIPT_ATTR}]`)) {
wrapper.removeAttribute('data-tutor-coordinates-init');
const reinit = doc.createElement('script');
reinit.textContent = 'if(window._tutorCoordinatesInitAll){window._tutorCoordinatesInitAll();}';
doc.head.appendChild(reinit);
return;
}

context.beginPath();
context.moveTo(centerX, topEdge);
context.lineTo(centerX, bottomEdge);
context.stroke();
const siteUrl = tutorConfig.site_url.replace(/\/$/, '');
const script = doc.createElement('script');
script.setAttribute(COORDINATES_SCRIPT_ATTR, '1');
script.textContent = `
(function(){
var scriptEl = document.createElement('script');
scriptEl.src = '${siteUrl}/wp-content/plugins/tutor-pro/assets/js/coordinates-question.js';
scriptEl.onload = function(){
if(typeof window._tutorCoordinatesInitAll === 'function'){
window._tutorCoordinatesInitAll();
}
};
document.head.appendChild(scriptEl);
})();
`;
doc.head.appendChild(script);
}, []);

return (
<div className="tutor-quiz-question-options tutor-coordinates-question" data-question-type="coordinates">
<div
ref={wrapperRef}
id={`tutor-coordinates-question-${qId}`}
className="tutor-quiz-question-options tutor-coordinates-question question-type-coordinates"
data-question-type="coordinates"
data-question-id={qId}
data-axis-range={String(axisRange)}
data-marker-hover={markerUrl('graph-marker-hover')}
data-marker-selected={markerUrl('graph-marker-selected')}
data-marker-wrong={markerUrl('graph-marker-wrong')}
>
<div className="tutor-coordinates-actions">
<button
type="button"
className="tutor-coordinates-clear-prev tutor-coordinates-clear-button tutor-hidden"
aria-label={__('Clear last point', 'tutor')}
>
<SVGIcon name="eraser" style={clearButtonIcon} width={18} height={18} />
{__('Clear', 'tutor')}
</button>
</div>
<div className="tutor-coordinates-grid-container">
<canvas
ref={canvasRef}
className="tutor-coordinates-canvas"
width={CANVAS_SIZE}
height={CANVAS_SIZE}
aria-label={__('Coordinate grid preview.', 'tutor')}
/>
<canvas id={canvasId} className="tutor-coordinates-canvas" width={420} height={420} />
</div>
<input type="hidden" id={inputId} name="preview[answers][coordinates][points]" defaultValue="" />
</div>
);
};
Expand Down
Loading
Loading