diff --git a/assets/core/ts/declaration.d.ts b/assets/core/ts/declaration.d.ts index 57fab0c735..a7dbde4902 100644 --- a/assets/core/ts/declaration.d.ts +++ b/assets/core/ts/declaration.d.ts @@ -66,12 +66,15 @@ declare global { }; drawOnImage?: { init: (options: { - image: HTMLImageElement; + image: HTMLImageElement | null; canvas: HTMLCanvasElement; + hiddenInput?: HTMLInputElement | null; brushSize?: number; strokeStyle?: string; initialMaskUrl?: string; onMaskChange?: (value: string) => void; + interactionRoot?: HTMLElement | null; + activateOnHover?: boolean; }) => { destroy: () => void }; DEFAULT_BRUSH_SIZE?: number; DEFAULT_STROKE_STYLE?: string; @@ -108,6 +111,7 @@ declare global { ajaxurl?: string; tutor_url?: string; wp_date_format?: string; + is_legacy_learning_mode?: boolean; }; } } diff --git a/assets/src/js/v3/@types/index.d.ts b/assets/src/js/v3/@types/index.d.ts index 480c101e50..793c82efe5 100644 --- a/assets/src/js/v3/@types/index.d.ts +++ b/assets/src/js/v3/@types/index.d.ts @@ -126,6 +126,7 @@ declare global { }[]; kids_icons_registry: string[]; is_kids_mode: boolean; + is_legacy_learning_mode: boolean; current_user: { data: { id: string; diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx index 42a4d80289..f282e90b25 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx @@ -103,18 +103,23 @@ const questionTypeOptions: { icon: 'quizOrdering', isPro: true, }, - // { - // label: __('Draw on Image', 'tutor'), - // value: 'draw_image', - // // TODO: icon is not final. - // icon: 'quizImageAnswer', - // isPro: true, - // }, + { + label: __('Draw on Image', 'tutor'), + value: 'draw_image', + icon: 'quizImageAnswer', + isPro: true, + }, ]; const isTutorPro = !!tutorConfig.tutor_pro_url; const QuestionList = ({ isEditing }: { isEditing: boolean }) => { + const questionTypeOptionsForUi = useMemo(() => { + if (tutorConfig.is_legacy_learning_mode) { + return questionTypeOptions.filter((option) => option.value !== 'draw_image'); + } + return questionTypeOptions; + }, []); const [activeSortId, setActiveSortId] = useState(null); const [isOpen, setIsOpen] = useState(false); const questionListRef = useRef(null); @@ -446,7 +451,7 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { >
{__('Select Question Type', 'tutor')} - {questionTypeOptions.map((option) => ( + {questionTypeOptionsForUi.map((option) => ( { const form = useFormContext(); const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext(); + const activeQuestionDataStatus = + form.watch(`questions.${activeQuestionIndex}._data_status`) ?? QuizDataStatus.NO_CHANGE; const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers'; + const thresholdPath = + `questions.${activeQuestionIndex}.question_settings.draw_image_threshold_percent` as 'questions.0.question_settings.draw_image_threshold_percent'; const { fields: optionsFields } = useFieldArray({ control: form.control, name: answersPath, }); + const thresholdOptions = useMemo( + () => + [40, 50, 60, 70, 80, 90, 100].map((value) => ({ + label: `${value}%`, + value, + })), + [], + ); + // Ensure there is always a single option for this question type. useEffect(() => { if (!activeQuestionId) { @@ -46,6 +62,14 @@ const DrawImage = () => { form.setValue(answersPath, [baseAnswer]); }, [activeQuestionId, optionsFields.length, answersPath, form]); + // Default threshold for draw-image questions if not set. + useEffect(() => { + const currentValue = form.getValues(thresholdPath); + if (currentValue === undefined || currentValue === null || Number.isNaN(Number(currentValue))) { + form.setValue(thresholdPath, 70); + } + }, [form, thresholdPath]); + // Only render Controller when the value exists to ensure field.value is always defined if (optionsFields.length === 0) { return null; @@ -54,15 +78,41 @@ const DrawImage = () => { return (
( - ( + ( + { + thresholdControllerProps.field.onChange(option.value); + if (calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE)) { + form.setValue( + `questions.${activeQuestionIndex}._data_status`, + calculateQuizDataStatus(activeQuestionDataStatus, QuizDataStatus.UPDATE) as QuizDataStatus, + ); + } + }} + /> + } + /> + )} /> )} /> @@ -75,6 +125,7 @@ export default DrawImage; const styles = { optionWrapper: css` ${styleUtils.display.flex('column')}; + gap: ${spacing[16]}; padding-left: ${spacing[40]}; `, }; diff --git a/assets/src/js/v3/entries/course-builder/services/quiz.ts b/assets/src/js/v3/entries/course-builder/services/quiz.ts index 3457fa8b03..cce5c09891 100644 --- a/assets/src/js/v3/entries/course-builder/services/quiz.ts +++ b/assets/src/js/v3/entries/course-builder/services/quiz.ts @@ -43,6 +43,7 @@ interface QuizQuestionsForPayload extends Omit diff --git a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx index a95889524a..34c9e5d1e2 100644 --- a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx +++ b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; import { useCallback, useEffect, useRef, useState } from 'react'; -import Button from '@TutorShared/atoms/Button'; import ImageInput from '@TutorShared/atoms/ImageInput'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -20,7 +19,10 @@ import { type QuizValidationErrorType, } from '@TutorShared/utils/types'; -const INSTRUCTOR_STROKE_STYLE = 'rgba(255, 0, 0, 0.9)'; +const LASSO_FILL_STYLE = 'rgba(220, 53, 69, 0.45)'; +const LASSO_STROKE_STYLE = 'rgba(220, 53, 69, 0.95)'; +const LASSO_DASH_PATTERN = [8, 6]; +const LASSO_MIN_POINT_DISTANCE = 4; interface FormDrawImageProps extends FormControllerProps { questionId: ID; @@ -34,9 +36,10 @@ interface FormDrawImageProps extends FormControllerProps { type: QuizValidationErrorType; } | null> >; + precisionControl?: React.ReactNode; } -const FormDrawImage = ({ field }: FormDrawImageProps) => { +const FormDrawImage = ({ field, precisionControl }: FormDrawImageProps) => { const option = field.value; const [isDrawModeActive, setIsDrawModeActive] = useState(false); @@ -44,6 +47,9 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { const imageRef = useRef(null); const canvasRef = useRef(null); const drawInstanceRef = useRef<{ destroy: () => void } | null>(null); + const isLassoDrawingRef = useRef(false); + const lassoPointsRef = useRef>([]); + const baseImageDataRef = useRef(null); const updateOption = useCallback( (updated: QuizQuestionOption) => { @@ -109,7 +115,6 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { onChange: (file) => { if (file && !Array.isArray(file) && option) { const { id, url } = file; - // Clear previous draw when image is replaced — the saved mask was for the old image. const updated: QuizQuestionOption = { ...option, ...(calculateQuizDataStatus(option._data_status, QuizDataStatus.UPDATE) && { @@ -120,7 +125,6 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { answer_two_gap_match: '', }; updateOption(updated); - // Clean up draw instance and canvas so the new image shows without the old mask. if (drawInstanceRef.current) { drawInstanceRef.current.destroy(); drawInstanceRef.current = null; @@ -137,15 +141,6 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { : null, }); - /* - * Display-only canvas sync (when not in draw mode): we use three separate useEffects - * so each one handles a single concern and its own cleanup: - * 1) Sync immediately when deps change (image URL, mask, draw mode). - * 2) Sync when the fires 'load' (e.g. after src change or first load). - * 3) Sync when the container is resized (ResizeObserver). - * React runs them in declaration order after commit; merging into one effect would - * mix three different triggers and cleanups (addEventListener, ResizeObserver) in one place. - */ useEffect(() => { if (isDrawModeActive) { return; @@ -192,37 +187,154 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { }; }, [isDrawModeActive, option?.image_url, option?.answer_two_gap_match, syncCanvasDisplay]); - // Wire to shared draw-on-image module when draw mode is active (Tutor Pro). + // Draw-image instructor UI: same lasso polygon flow as FormPinImage (feat/quiz-type-pin-image). useEffect(() => { if (!isDrawModeActive || !option?.image_url) { return; } - const img = imageRef.current; const canvas = canvasRef.current; - const api = typeof window !== 'undefined' ? window.TutorCore?.drawOnImage : undefined; - if (!img || !canvas || !api?.init) { + if (!canvas) { return; } + if (drawInstanceRef.current) { drawInstanceRef.current.destroy(); drawInstanceRef.current = null; } - const brushSize = api.DEFAULT_BRUSH_SIZE ?? 15; - const instance = api.init({ - image: img, - canvas, - brushSize, - strokeStyle: INSTRUCTOR_STROKE_STYLE, - initialMaskUrl: option.answer_two_gap_match || undefined, - }); + + const getPointFromEvent = (event: PointerEvent) => { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const x = (event.clientX - rect.left) * scaleX; + const y = (event.clientY - rect.top) * scaleY; + return { + x: Math.max(0, Math.min(canvas.width, x)), + y: Math.max(0, Math.min(canvas.height, y)), + }; + }; + + const renderPathPreview = () => { + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + const points = lassoPointsRef.current; + if (!baseImageDataRef.current || points.length < 2) { + return; + } + + ctx.putImageData(baseImageDataRef.current, 0, 0); + ctx.beginPath(); + ctx.moveTo(points[0]?.x || 0, points[0]?.y || 0); + points.forEach((point, index) => { + if (index > 0) { + ctx.lineTo(point.x, point.y); + } + }); + ctx.lineTo(points[0]?.x || 0, points[0]?.y || 0); + ctx.closePath(); + + ctx.fillStyle = LASSO_FILL_STYLE; + ctx.fill(); + + ctx.setLineDash(LASSO_DASH_PATTERN); + ctx.lineWidth = 2; + ctx.strokeStyle = LASSO_STROKE_STYLE; + ctx.stroke(); + ctx.setLineDash([]); + }; + + const onPointerDown = (event: PointerEvent) => { + if (event.button !== 0) { + return; + } + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + canvas.setPointerCapture(event.pointerId); + isLassoDrawingRef.current = true; + lassoPointsRef.current = [getPointFromEvent(event)]; + baseImageDataRef.current = ctx.getImageData(0, 0, canvas.width, canvas.height); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!isLassoDrawingRef.current) { + return; + } + const nextPoint = getPointFromEvent(event); + const points = lassoPointsRef.current; + const lastPoint = points[points.length - 1]; + if (!lastPoint) { + points.push(nextPoint); + renderPathPreview(); + return; + } + const dx = nextPoint.x - lastPoint.x; + const dy = nextPoint.y - lastPoint.y; + if (Math.hypot(dx, dy) < LASSO_MIN_POINT_DISTANCE) { + return; + } + points.push(nextPoint); + renderPathPreview(); + }; + + const finishLasso = () => { + if (!isLassoDrawingRef.current) { + return; + } + isLassoDrawingRef.current = false; + const points = lassoPointsRef.current; + if (points.length >= 3) { + renderPathPreview(); + } else if (baseImageDataRef.current) { + const ctx = canvas.getContext('2d'); + ctx?.putImageData(baseImageDataRef.current, 0, 0); + } + lassoPointsRef.current = []; + baseImageDataRef.current = null; + }; + + const onPointerUp = (event: PointerEvent) => { + if (canvas.hasPointerCapture(event.pointerId)) { + canvas.releasePointerCapture(event.pointerId); + } + finishLasso(); + }; + + const onPointerCancel = (event: PointerEvent) => { + if (canvas.hasPointerCapture(event.pointerId)) { + canvas.releasePointerCapture(event.pointerId); + } + finishLasso(); + }; + + canvas.addEventListener('pointerdown', onPointerDown); + canvas.addEventListener('pointermove', onPointerMove); + canvas.addEventListener('pointerup', onPointerUp); + canvas.addEventListener('pointercancel', onPointerCancel); + + const instance = { + destroy: () => { + canvas.removeEventListener('pointerdown', onPointerDown); + canvas.removeEventListener('pointermove', onPointerMove); + canvas.removeEventListener('pointerup', onPointerUp); + canvas.removeEventListener('pointercancel', onPointerCancel); + isLassoDrawingRef.current = false; + lassoPointsRef.current = []; + baseImageDataRef.current = null; + }, + }; drawInstanceRef.current = instance; + return () => { instance.destroy(); drawInstanceRef.current = null; }; }, [isDrawModeActive, option?.image_url, option?.answer_two_gap_match]); - // Cleanup shared instance on unmount. useEffect(() => { return () => { if (drawInstanceRef.current) { @@ -232,13 +344,9 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { }; }, []); - if (!option) { - return null; - } - - const handleSave = () => { + const persistCanvasMask = useCallback(() => { const canvas = canvasRef.current; - if (!canvas) { + if (!canvas || !option) { return; } @@ -257,15 +365,13 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { is_saved: true, }; updateOption(updated); + }, [option, updateOption]); - if (drawInstanceRef.current) { - drawInstanceRef.current.destroy(); - drawInstanceRef.current = null; + const handleClear = () => { + if (!option) { + return; } - setIsDrawModeActive(false); - }; - const handleClear = () => { if (drawInstanceRef.current) { drawInstanceRef.current.destroy(); drawInstanceRef.current = null; @@ -286,14 +392,23 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { is_saved: true, }; updateOption(updated); - setIsDrawModeActive(false); }; - const handleDraw = () => { + const handleCanvasMouseEnter = () => { setIsDrawModeActive(true); }; + const handleCanvasMouseLeave = () => { + if (!isLassoDrawingRef.current) { + setIsDrawModeActive(false); + } + }; + const clearImage = () => { + if (!option) { + return; + } + if (drawInstanceRef.current) { drawInstanceRef.current.destroy(); drawInstanceRef.current = null; @@ -319,32 +434,79 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { } }; + useEffect(() => { + if (!isDrawModeActive || !option?.image_url) { + return; + } + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + const onPointerUp = () => { + persistCanvasMask(); + }; + + canvas.addEventListener('pointerup', onPointerUp); + canvas.addEventListener('pointercancel', onPointerUp); + + return () => { + canvas.removeEventListener('pointerup', onPointerUp); + canvas.removeEventListener('pointercancel', onPointerUp); + }; + }, [isDrawModeActive, option?.image_url, persistCanvasMask]); + + if (!option) { + return null; + } + return (
- {/* Section 1: Image upload only — one reference shown in Mark the correct area */} -
-
- + +
+
+ +
-
+ + + +
+
+ {__('Background { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + openMediaLibrary(); + } + }} + /> +
+
+
- {/* Section 2: Mark the correct area — single reference image + drawing canvas; Save / Clear / Draw buttons */}
@@ -354,8 +516,12 @@ const FormDrawImage = ({ field }: FormDrawImageProps) => { {__('Mark the correct area', __TUTOR_TEXT_DOMAIN__)} +
-
+
{
-
- - - -
-

- {__('Use the brush to draw on the image, then click Save to store the answer zone.', __TUTOR_TEXT_DOMAIN__)} -

+ {precisionControl &&
{precisionControl}
}

{__('Answer zone saved. Students will be graded against this area.', __TUTOR_TEXT_DOMAIN__)} @@ -443,6 +581,16 @@ const styles = { imageInput: css` border-radius: ${borderRadius.card}; `, + uploadedImageWrapper: css` + max-width: 100%; + `, + uploadedImage: css` + display: block; + width: 100%; + height: auto; + cursor: pointer; + border-radius: ${borderRadius.card}; + `, answerHeader: css` ${styleUtils.display.flex('row')}; align-items: center; @@ -484,6 +632,7 @@ const styles = { position: absolute; top: 0; left: 0; + z-index: 1; `, canvasIdleMode: css` pointer-events: none; @@ -493,15 +642,40 @@ const styles = { pointer-events: auto; cursor: crosshair; `, - actionsRow: css` + drawBadge: css` + position: absolute; + top: ${spacing[12]}; + right: ${spacing[12]}; + z-index: 2; + width: 32px; + height: 32px; + border-radius: 999px; + background: ${colorTokens.surface.tutor}; + border: 1px solid ${colorTokens.stroke.border}; ${styleUtils.display.flex('row')}; - gap: ${spacing[12]}; - flex-wrap: wrap; - `, - brushHint: css` - ${typography.caption()}; + align-items: center; + justify-content: center; color: ${colorTokens.text.subdued}; - margin: 0; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16); + `, + clearButton: css` + width: 94px; + border: none; + border-radius: ${borderRadius.input}; + background: ${colorTokens.action.secondary.default}; + ${typography.caption('medium')}; + color: ${colorTokens.text.brand}; + display: flex; + justify-content: center; + align-items: center; + gap: ${spacing[8]}; + padding: ${spacing[4]} 0; + `, + clearButtonIcon: css` + color: ${colorTokens.text.brand}; + `, + clearIcon: css` + color: ${colorTokens.text.brand}; `, savedHint: css` ${typography.caption()}; diff --git a/assets/src/js/v3/shared/config/config.ts b/assets/src/js/v3/shared/config/config.ts index f74c543c13..d229a2cebf 100644 --- a/assets/src/js/v3/shared/config/config.ts +++ b/assets/src/js/v3/shared/config/config.ts @@ -43,6 +43,7 @@ const defaultTutorConfig = { addons_data: [], kids_icons_registry: [], is_kids_mode: false, + is_legacy_learning_mode: false, current_user: { data: { id: '', diff --git a/assets/src/js/v3/shared/utils/quiz.ts b/assets/src/js/v3/shared/utils/quiz.ts index 5a743e6506..b3cd0057ec 100644 --- a/assets/src/js/v3/shared/utils/quiz.ts +++ b/assets/src/js/v3/shared/utils/quiz.ts @@ -114,6 +114,12 @@ export const convertedQuestion = (question: Omit): question.question_settings.answer_required = !!Number(question.question_settings.answer_required); question.question_settings.show_question_mark = !!Number(question.question_settings.show_question_mark); question.question_settings.randomize_question = !!Number(question.question_settings.randomize_question); + if (question.question_type === 'draw_image') { + const rawThreshold = question.question_settings.draw_image_threshold_percent; + if (rawThreshold !== undefined && rawThreshold !== null && !Number.isNaN(Number(rawThreshold))) { + question.question_settings.draw_image_threshold_percent = Number(rawThreshold); + } + } } question.question_answers = question.question_answers.map((answer) => ({ ...answer, diff --git a/assets/src/js/v3/shared/utils/types.ts b/assets/src/js/v3/shared/utils/types.ts index aa117bdad6..393ef15756 100644 --- a/assets/src/js/v3/shared/utils/types.ts +++ b/assets/src/js/v3/shared/utils/types.ts @@ -331,6 +331,7 @@ export interface QuizQuestion { show_question_mark: boolean; has_multiple_correct_answer: boolean; is_image_matching: boolean; + draw_image_threshold_percent?: number; }; question_answers: QuizQuestionOption[]; } @@ -345,6 +346,7 @@ export interface QuizQuestionsForPayload extends Omit tutor_utils()->get_option( 'monetize_by' ), 'kids_icons_registry' => $kids_icons, 'is_kids_mode' => tutor_utils()->is_kids_mode(), + 'is_legacy_learning_mode' => tutor_utils()->is_legacy_learning_mode(), ); } diff --git a/classes/Quiz.php b/classes/Quiz.php index 64c1d95922..29796ef77a 100644 --- a/classes/Quiz.php +++ b/classes/Quiz.php @@ -168,6 +168,45 @@ public function __construct( $register_hooks = true ) { // Add quiz title as nav item & render single content on the learning area. add_action( "tutor_learning_area_nav_item_{$this->post_type}", array( $this, 'render_nav_item' ), 10, 2 ); add_action( "tutor_single_content_{$this->post_type}", array( $this, 'render_single_content' ) ); + + /** + * Slugs listed in tutor_quiz_templates_not_in_core have no file under wp-content/plugins/tutor/templates/. + * Without this, tutor_load_template() still runs from generic quiz templates and tutor_get_template() + * prints "The file you are trying to load does not exist…". Returning false here exits before that lookup. + * Add-ons ship their own files and load them outside this path (e.g. direct include from the add-on). + * + * @since 4.0.0 + */ + add_filter( 'should_tutor_load_template', array( $this, 'skip_addon_only_question_partials' ), 5, 3 ); + } + + /** + * Skip loading templates that are not packaged with core Tutor LMS. + * + * @since 4.0.0 + * + * @param bool $load Whether to load the template. + * @param string $template Template name in dot notation. + * @param array $variables Template variables. + * + * @return bool + */ + public function skip_addon_only_question_partials( $load, $template, $variables ) { + $addons_only = apply_filters( + 'tutor_quiz_templates_not_in_core', + array( + 'learning-area.quiz.questions.draw-image', + 'shared.components.quiz.attempt-details.questions.draw-image', + ), + $template, + $variables + ); + + if ( in_array( $template, $addons_only, true ) ) { + return false; + } + + return $load; } /** diff --git a/classes/QuizBuilder.php b/classes/QuizBuilder.php index e7086d44d5..e4234388b9 100644 --- a/classes/QuizBuilder.php +++ b/classes/QuizBuilder.php @@ -169,6 +169,8 @@ public function save_question_answers( $question_id, $question_type, $question_a * @param array $questions questions data. * * @return void + * + * @throws \Exception When saving a draw_image question while Legacy learning mode is enabled. */ public function save_questions( $quiz_id, $questions ) { global $wpdb; @@ -185,6 +187,10 @@ public function save_questions( $quiz_id, $questions ) { } $question_type = Input::sanitize( $question['question_type'] ); + if ( 'draw_image' === $question_type && tutor_utils()->is_legacy_learning_mode() ) { + $legacy_draw_image_message = __( 'Draw on Image questions are not available when Legacy learning mode is enabled.', 'tutor' ); + throw new \Exception( $legacy_draw_image_message ); + } $question_data = $this->prepare_question_data( $quiz_id, $question ); $question_answers = isset( $question['question_answers'] ) ? $question['question_answers'] : array(); diff --git a/classes/Utils.php b/classes/Utils.php index 7ca81fa57f..32c28c3c5e 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -11135,4 +11135,15 @@ public static function get_icon_by_post_type( $post_type ): string { public static function is_kids_mode(): bool { return Options_V2::LEARNING_MODE_KIDS === tutor_utils()->get_option( 'learning_mode' ) && User::is_student_view(); } + + /** + * Is legacy learning mode active? + * + * @since 4.0.0 + * + * @return bool + */ + public function is_legacy_learning_mode(): bool { + return Options_V2::LEARNING_MODE_LEGACY === $this->get_option( 'learning_mode' ); + } } diff --git a/templates/shared/components/quiz/attempt-details/review-answers.php b/templates/shared/components/quiz/attempt-details/review-answers.php index 34dfd7cd68..0469a38091 100644 --- a/templates/shared/components/quiz/attempt-details/review-answers.php +++ b/templates/shared/components/quiz/attempt-details/review-answers.php @@ -34,14 +34,24 @@

$question ) : ?> question_type ?? ''; - $question_id = (int) ( $question->question_id ?? 0 ); - $is_dnd_review = in_array( $question_type, array( 'image_answering', 'ordering', 'matching', 'image_matching' ), true ); - $is_tf_review = 'true_false' === $question_type; - $is_mc_review = in_array( $question_type, array( 'single_choice', 'multiple_choice' ), true ); - $is_oe_review = in_array( $question_type, array( 'open_ended', 'short_answer' ), true ); - $is_fib_review = 'fill_in_the_blank' === $question_type; - $attempt_answer = $attempt_answers_map[ $question_id ] ?? null; + $question_type = $question->question_type ?? ''; + + $question_id = (int) ( $question->question_id ?? 0 ); + + $is_dnd_review = in_array( $question_type, array( 'image_answering', 'ordering', 'matching', 'image_matching' ), true ); + + $is_tf_review = 'true_false' === $question_type; + + $is_mc_review = in_array( $question_type, array( 'single_choice', 'multiple_choice' ), true ); + + $is_oe_review = in_array( $question_type, array( 'open_ended', 'short_answer' ), true ); + + $is_fib_review = 'fill_in_the_blank' === $question_type; + + $is_draw_image_review = 'draw_image' === $question_type; + + $attempt_answer = $attempt_answers_map[ $question_id ] ?? null; + $question_template = ''; if ( $is_dnd_review ) { @@ -54,6 +64,8 @@ $question_template = 'open-ended'; } elseif ( $is_fib_review ) { $question_template = 'fill-in-the-blank'; + } elseif ( $is_draw_image_review ) { + $question_template = 'draw-image'; } ?>