Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
48a6682
feat: add draw image question type to quiz functionality
saadman30 Jan 29, 2026
263cb79
feat: enhance draw image question type functionality
saadman30 Feb 2, 2026
cf81225
fix: update label for draw image question type
saadman30 Feb 2, 2026
476f118
refactor: simplify answer processing for draw image question type
saadman30 Feb 2, 2026
3ecb33c
refactor: improve handling of draw image answer processing
saadman30 Feb 2, 2026
2a4c255
refactor: enhance draw image answer processing and security
saadman30 Feb 2, 2026
fc96b8c
refactor: clarify handling of local file URLs for draw image question…
saadman30 Feb 2, 2026
a16204d
refactor: migrate draw image mask handling to QuizModel
saadman30 Feb 3, 2026
90f8986
refactor: improve URL validation for draw image masks
saadman30 Feb 3, 2026
fd10003
refactor: enhance upload_base64_image method in Utils
saadman30 Feb 3, 2026
46a3c86
refactor: enhance draw image answer handling in Quiz and QuizModel
saadman30 Feb 3, 2026
ebb9640
refactor: improve draw image handling in Quiz and CourseModel
saadman30 Feb 5, 2026
0942d3f
refactor: optimize draw image URL resolution in QuizModel
saadman30 Feb 5, 2026
89743d7
refactor: enhance quiz attempt file path handling
saadman30 Feb 5, 2026
9bc088d
refactor: streamline DrawImage component and introduce FormDrawImage
saadman30 Feb 5, 2026
53131c7
refactor: update TutorDrawOnImage type definitions and clean up FormD…
saadman30 Feb 5, 2026
3aec3db
refactor: enhance FormDrawImage component to manage image replacement…
saadman30 Feb 5, 2026
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
24 changes: 22 additions & 2 deletions assets/src/js/front/course/_spotlight-quiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ window.jQuery(document).ready($ => {
const { __ } = window.wp.i18n;

// Currently only these types of question supports answer reveal mode.
const revealModeSupportedQuestions = ['true_false', 'single_choice', 'multiple_choice'];
const revealModeSupportedQuestions = ['true_false', 'single_choice', 'multiple_choice', 'draw_image'];

let quiz_options = _tutorobject.quiz_options
let interactions = new Map();
Expand Down Expand Up @@ -102,6 +102,13 @@ window.jQuery(document).ready($ => {
});
}

// Reveal mode for draw_image: show reference (instructor mask) and explanation.
if (is_reveal_mode() && $question_wrap.data('question-type') === 'draw_image') {
$question_wrap.find('.tutor-quiz-explanation-wrapper').removeClass('tutor-d-none');
$question_wrap.find('.tutor-draw-image-reference-wrapper').removeClass('tutor-d-none');
goNext = true;
}

if (validatedTrue) {
goNext = true;
}
Expand Down Expand Up @@ -160,7 +167,14 @@ window.jQuery(document).ready($ => {
var $inputs = $required_answer_wrap.find('input');
if ($inputs.length) {
var $type = $inputs.attr('type');
if ($type === 'radio') {
// Draw image: require mask (hidden input with [answers][mask]) to have a value.
if ($question_wrap.data('question-type') === 'draw_image') {
var $maskInput = $required_answer_wrap.find('input[name*="[answers][mask]"]');
if ($maskInput.length && !$maskInput.val().trim().length) {
$question_wrap.find('.answer-help-block').html(`<p style="color: #dc3545">${__('Please draw on the image to answer this question.', 'tutor')}</p>`);
validated = false;
}
} else if ($type === 'radio') {
if ($required_answer_wrap.find('input[type="radio"]:checked').length == 0) {
$question_wrap.find('.answer-help-block').html(`<p style="color: #dc3545">${__('Please select an option to answer', 'tutor')}</p>`);
validated = false;
Expand Down Expand Up @@ -219,6 +233,12 @@ window.jQuery(document).ready($ => {
}
});

$(document).on('change', '.quiz-attempt-single-question input[name*="[answers][mask]"]', function () {
if ($('.tutor-quiz-time-expired').length === 0 && $(this).val().trim().length) {
$('.tutor-quiz-next-btn-all').prop('disabled', false);
}
});

$(document).on('click', '.tutor-quiz-answer-next-btn, .tutor-quiz-answer-previous-btn', function (e) {
e.preventDefault();

Expand Down
12 changes: 12 additions & 0 deletions assets/src/js/v3/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const wp: any;
interface Window {
TutorDrawOnImage?: {
init: (options: {
image: HTMLImageElement;
canvas: HTMLCanvasElement;
brushSize?: number;
strokeStyle?: string;
initialMaskUrl?: string;
onMaskChange?: (value: string) => void;
}) => { destroy: () => void };
DEFAULT_BRUSH_SIZE?: number;
DEFAULT_STROKE_STYLE?: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wp: any;
ajaxurl: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const questionTypeIconMap: Record<Exclude<QuizQuestionType, 'single_choice' | 'i
matching: 'quizImageMatching',
image_answering: 'quizImageAnswer',
ordering: 'quizOrdering',
draw_image: 'quizImageAnswer',
h5p: 'quizH5p',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const questionTypes = {
label: __('Ordering', 'tutor'),
icon: 'quizOrdering',
},
draw_image: {
label: __('Draw on Image', 'tutor'),
icon: 'quizImageAnswer',
},
h5p: {
label: __('H5P', 'tutor'),
icon: 'quizTrueFalse',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Matching from '@CourseBuilderComponents/curriculum/question-types/Matchin
import MultipleChoiceAndOrdering from '@CourseBuilderComponents/curriculum/question-types/MultipleChoiceAndOrdering';
import OpenEndedAndShortAnswer from '@CourseBuilderComponents/curriculum/question-types/OpenEndedAndShortAnswer';
import TrueFalse from '@CourseBuilderComponents/curriculum/question-types/TrueFalse';
import DrawImage from '@CourseBuilderComponents/curriculum/question-types/DrawImage';
import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext';

import { tutorConfig } from '@TutorShared/config/config';
Expand Down Expand Up @@ -54,6 +55,7 @@ const QuestionForm = () => {
matching: <Matching key={activeQuestionId} />,
image_answering: <ImageAnswering key={activeQuestionId} />,
ordering: <MultipleChoiceAndOrdering key={activeQuestionId} />,
draw_image: <DrawImage key={activeQuestionId} />,
} as const;

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ const questionTypeOptions: {
icon: 'quizOrdering',
isPro: true,
},
{
label: __('Draw on Image', 'tutor'),
value: 'draw_image',
icon: 'quizImageAnswer',
isPro: true,
},
];

const isTutorPro = !!tutorConfig.tutor_pro_url;
Expand Down Expand Up @@ -208,7 +214,22 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
is_correct: '0',
},
]
: [],
: questionType === 'draw_image'
? [
{
_data_status: QuizDataStatus.NEW,
is_saved: true,
answer_id: nanoid(),
answer_title: '',
belongs_question_id: questionId,
belongs_question_type: 'draw_image',
answer_two_gap_match: '',
answer_view_format: 'draw_image',
answer_order: 0,
is_correct: '1',
},
]
: [],
answer_explanation: '',
question_mark: 1,
question_order: questionFields.length + 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { css } from '@emotion/react';
import { useEffect } from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';

import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext';
import type { QuizForm } from '@CourseBuilderServices/quiz';
import FormDrawImage from '@TutorShared/components/fields/quiz/questions/FormDrawImage';
import { spacing } from '@TutorShared/config/styles';
import { styleUtils } from '@TutorShared/utils/style-utils';
import { QuizDataStatus, type QuizQuestionOption } from '@TutorShared/utils/types';
import { nanoid } from '@TutorShared/utils/util';

const DrawImage = () => {
const form = useFormContext<QuizForm>();
const { activeQuestionId, activeQuestionIndex, validationError, setValidationError } = useQuizModalContext();

const answersPath = `questions.${activeQuestionIndex}.question_answers` as 'questions.0.question_answers';

const { fields: optionsFields } = useFieldArray({
control: form.control,
name: answersPath,
});

// Ensure there is always a single option for this question type.
useEffect(() => {
if (!activeQuestionId) {
return;
}
if (optionsFields.length > 0) {
return;
}
const baseAnswer: QuizQuestionOption = {
_data_status: QuizDataStatus.NEW,
is_saved: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

is_saved should not be true by default. It must be programmatically toggled to true when the user saves the question.

answer_id: nanoid(),
belongs_question_id: activeQuestionId,
belongs_question_type: 'draw_image' as QuizQuestionOption['belongs_question_type'],
answer_title: '',
is_correct: '1',
image_id: undefined,
image_url: '',
answer_two_gap_match: '',
answer_view_format: 'draw_image',
answer_order: 0,
};
form.setValue(answersPath, [baseAnswer]);
}, [activeQuestionId, optionsFields.length, answersPath, form]);

return (
<div css={styles.optionWrapper}>
<Controller
key={optionsFields.length ? JSON.stringify(optionsFields[0]) : ''}
control={form.control}
name={`questions.${activeQuestionIndex}.question_answers.0` as 'questions.0.question_answers.0'}
render={(controllerProps) => (
<FormDrawImage
{...controllerProps}
questionId={activeQuestionId}
validationError={validationError}
setValidationError={setValidationError}
/>
)}
/>
</div>
);
};

export default DrawImage;

const styles = {
optionWrapper: css`
${styleUtils.display.flex('column')};
padding-left: ${spacing[40]};
`,
};
Loading
Loading