Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 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
4dab000
feat: add draw image threshold functionality to quiz questions
saadman30 Mar 10, 2026
e5dd75e
refactor: improve DrawImage component structure and integrate precisi…
saadman30 Mar 10, 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
5893ec4
Merge branch '4.0.0-dev' into feat/draw-image-quiz-type-precision-level
saadman30 Mar 30, 2026
8af2a1a
Implement draw on image functionality for quiz attempts, including ne…
saadman30 Mar 30, 2026
6e8b02d
Refactor draw-image question template to improve quiz attempt handlin…
saadman30 Mar 30, 2026
4347fee
Remove unused elements from draw-image question template to streamlin…
saadman30 Mar 30, 2026
0749811
Refactor SCSS and PHP for quiz attempt details. Update drop-shadow co…
saadman30 Mar 30, 2026
371a04a
Enhance FormDrawImage component to conditionally render image upload …
saadman30 Mar 30, 2026
d1cf32c
Refactor clear button in FormDrawImage component for improved styling…
saadman30 Mar 30, 2026
6d23939
Remove outdated comment from FormDrawImage component to enhance code …
saadman30 Mar 30, 2026
6d51627
Add is_legacy_learning_mode support across various components and cla…
saadman30 Apr 2, 2026
e6b64ae
Merge branch '4.0.0-dev' into feat/draw-image-quiz-type-precision-level
saadman30 Apr 2, 2026
3094fc6
Enhance QuestionList component by enabling 'Draw on Image' quiz type
saadman30 Apr 2, 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
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
17 changes: 14 additions & 3 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', 'draw_image', 'pin_image'];

let quiz_options = _tutorobject.quiz_options
let interactions = new Map();
Expand Down Expand Up @@ -102,8 +102,8 @@ 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') {
// Reveal mode for draw_image & pin_image: show reference (instructor mask) and explanation.
if (is_reveal_mode() && ['draw_image', 'pin_image'].includes($question_wrap.data('question-type'))) {
$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;
Expand Down Expand Up @@ -174,6 +174,17 @@ window.jQuery(document).ready($ => {
$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 ($question_wrap.data('question-type') === 'pin_image') {
// Pin image: require normalized pin coordinates (hidden inputs [answers][pin][x|y]).
var $pinX = $required_answer_wrap.find('input[name*="[answers][pin][x]"]');
var $pinY = $required_answer_wrap.find('input[name*="[answers][pin][y]"]');
if (
!$pinX.length || !$pinY.length ||
!$pinX.val().trim().length || !$pinY.val().trim().length
) {
$question_wrap.find('.answer-help-block').html(`<p style="color: #dc3545">${__('Please drop a pin 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>`);
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 @@ -110,6 +110,13 @@ const questionTypeOptions: {
icon: 'quizImageAnswer',
isPro: true,
},
{
label: __('Pin on Image', 'tutor'),
value: 'pin_image',
// TODO: icon is not final.
icon: 'quizImageAnswer',
isPro: true,
},
];

const isTutorPro = !!tutorConfig.tutor_pro_url;
Expand Down Expand Up @@ -230,7 +237,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