Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c6524d4
feat: add pin_image question type and enhance quiz functionality
saadman30 Feb 6, 2026
b13ad2f
Merge branch 'feat/quiz-type-circle-image' into feat/quiz-type-pin-image
saadman30 Feb 9, 2026
e48140e
Merge branch 'feat/quiz-type-circle-image' into feat/quiz-type-pin-image
saadman30 Feb 16, 2026
f837c63
fix: update FormPinImage component to handle option checks and improv…
saadman30 Feb 16, 2026
771378a
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Feb 17, 2026
84ef874
feat: add TODO comment for icon finalization in QuestionList component
saadman30 Feb 17, 2026
144e989
refactor: clean up Quiz class and remove unused methods related to pi…
saadman30 Feb 18, 2026
073fb2f
refactor: rename variables for clarity in FormPinImage component
saadman30 Feb 18, 2026
c3d3cfb
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Feb 27, 2026
da1607a
fix(quiz): update filter callback for pin_image grading to use tutor_…
saadman30 Feb 27, 2026
dcf77a0
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Mar 3, 2026
675cce3
fix(quiz): update action hooks for rendering custom question type ans…
saadman30 Mar 3, 2026
c60ebda
fix(quiz): update action hooks for rendering custom question type ans…
saadman30 Mar 4, 2026
e289f82
refactor(quiz): remove deprecated pin image question answer processing
saadman30 Mar 4, 2026
e6c0be0
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Mar 27, 2026
1484b31
feat: Add Pin on Image question type for quizzes with rendering and r…
saadman30 Mar 27, 2026
f90f395
feat: Enhance Pin on Image question type with lasso drawing functiona…
saadman30 Mar 27, 2026
8a62f2d
feat: Refactor FormPinImage component to improve lasso drawing intera…
saadman30 Mar 30, 2026
da693cb
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Apr 2, 2026
649e18c
Merge branch 'feat/draw-image-quiz-type-precision-level' into feat/qu…
saadman30 Apr 2, 2026
9daf924
Add support for 'Pin Image' question type in review answers template
saadman30 Apr 2, 2026
712ad19
Enhance legacy learning mode handling for question types
saadman30 Apr 2, 2026
b820e1f
Merge branch '4.0.0-dev' into feat/quiz-type-pin-image
saadman30 Apr 3, 2026
73519a3
Remove Pin on Image question template files and update Quiz class to …
saadman30 Apr 3, 2026
474db21
Refactor spotlight quiz JavaScript to remove support for 'draw_image'…
saadman30 Apr 3, 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: 2 additions & 22 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', 'draw_image'];
const revealModeSupportedQuestions = ['true_false', 'single_choice', 'multiple_choice'];

let quiz_options = _tutorobject.quiz_options
let interactions = new Map();
Expand Down Expand Up @@ -102,13 +102,6 @@ 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 @@ -167,14 +160,7 @@ window.jQuery(document).ready($ => {
var $inputs = $required_answer_wrap.find('input');
if ($inputs.length) {
var $type = $inputs.attr('type');
// 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 ($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 @@ -233,12 +219,6 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const questionTypeIconMap: Record<Exclude<QuizQuestionType, 'single_choice' | 'i
image_answering: 'quizImageAnswer',
ordering: 'quizOrdering',
draw_image: 'quizImageAnswer',
pin_image: 'quizImageAnswer',
h5p: 'quizH5p',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ const questionTypes = {
label: __('Draw on Image', 'tutor'),
icon: 'quizImageAnswer',
},
pin_image: {
label: __('Pin 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 @@ -16,6 +16,7 @@ import MultipleChoiceAndOrdering from '@CourseBuilderComponents/curriculum/quest
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 PinImage from '@CourseBuilderComponents/curriculum/question-types/PinImage';
import { useQuizModalContext } from '@CourseBuilderContexts/QuizModalContext';

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

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ const questionTypeOptions: {
{
label: __('Draw on Image', 'tutor'),
value: 'draw_image',
// TODO: icon is not final.
icon: 'quizImageAnswer',
isPro: true,
},
{
label: __('Pin on Image', 'tutor'),
value: 'pin_image',
// TODO: icon is not final.
icon: 'quizImageAnswer',
isPro: true,
},
Expand All @@ -116,7 +124,7 @@ 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.filter((option) => option.value !== 'draw_image' && option.value !== 'pin_image');
}
return questionTypeOptions;
}, []);
Expand Down Expand Up @@ -235,7 +243,22 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => {
is_correct: '1',
},
]
: [],
: questionType === 'pin_image'
? [
{
_data_status: QuizDataStatus.NEW,
is_saved: true,
answer_id: nanoid(),
answer_title: '',
belongs_question_id: questionId,
belongs_question_type: 'pin_image',
answer_two_gap_match: '',
answer_view_format: 'pin_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,80 @@
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 FormPinImage from '@TutorShared/components/fields/quiz/questions/FormPinImage';
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 PinImage = () => {
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: false,
answer_id: nanoid(),
belongs_question_id: activeQuestionId,
belongs_question_type: 'pin_image' as QuizQuestionOption['belongs_question_type'],
answer_title: '',
is_correct: '1',
image_id: undefined,
image_url: '',
answer_two_gap_match: '',
answer_view_format: 'pin_image',
answer_order: 0,
};
form.setValue(answersPath, [baseAnswer]);
}, [activeQuestionId, optionsFields.length, answersPath, form]);

// Only render Controller when the value exists to ensure field.value is always defined
if (optionsFields.length === 0) {
return null;
}

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

export default PinImage;

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