Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
161 commits
Select commit Hold shift + click to select a range
6627353
refactor: remove embedded PNG image data from `quiz-intro.svg`.
b-l-i-n-d Jan 29, 2026
05930d5
refactor: Update conditional rendering logic
b-l-i-n-d Jan 29, 2026
351e2df
feat: Enhance learning area page detection using URL path segments an…
b-l-i-n-d Jan 29, 2026
09027d6
refactor: improve code formatting and remove unused data attribute in…
b-l-i-n-d Jan 31, 2026
1d9bd5e
refactor: Standardize quiz question rendering with a new common heade…
b-l-i-n-d Feb 2, 2026
4de8d8b
refactor: reformat quiz submission button code for improved readability
b-l-i-n-d Feb 2, 2026
68cbe76
feat: Conditionally display correct answers in quiz questions based o…
b-l-i-n-d Feb 2, 2026
8577067
refactor: Refactor quiz question rendering to use global attempt stat…
b-l-i-n-d Feb 2, 2026
c786856
refactor: change quiz question options from div to label elements
b-l-i-n-d Feb 2, 2026
fb4617f
feat: standardize and correct quiz answer input naming across various…
b-l-i-n-d Feb 2, 2026
519e314
fix: Fix quiz question array access and index init
b-l-i-n-d Feb 2, 2026
f356b01
feat: Add x-bind registration to quiz attempt inputs
b-l-i-n-d Feb 2, 2026
0285787
feat: Support checkbox-array fields in form
b-l-i-n-d Feb 2, 2026
65da46b
refactor: Use flat underscore names for quiz inputs
b-l-i-n-d Feb 2, 2026
cdbca6d
refactor: Standardize quiz question input field names to use array sy…
b-l-i-n-d Feb 3, 2026
d5fde5e
fix: Use bracket notation for dynamic property access in form compone…
b-l-i-n-d Feb 3, 2026
83bd246
fix: Ensure unique input naming for fill-in-the-blank and image answe…
b-l-i-n-d Feb 3, 2026
d6e23ee
feat: Implement dynamic answer field names and event callbacks for qu…
b-l-i-n-d Feb 3, 2026
0bd7f74
refactor: centralize quiz-related string literals into a new `QUIZ_CO…
b-l-i-n-d Feb 3, 2026
55562c0
refactor: Streamline quiz ordering and matching event emission by rem…
b-l-i-n-d Feb 3, 2026
d641df1
refactor: update matching question's clear drop zone logic to use eve…
b-l-i-n-d Feb 3, 2026
dd3c298
refactor: replace hardcoded clear button with Button component
b-l-i-n-d Feb 3, 2026
01c3d82
fix(input-field): use bracket access for alpine values/errors
b-l-i-n-d Feb 3, 2026
36d4655
refactor(quiz): split learning-area quiz modules
b-l-i-n-d Feb 3, 2026
c983d64
feat(quiz): align question validation and errors
b-l-i-n-d Feb 5, 2026
572b04c
feat(quiz): improve ordering defaults and submit errors
b-l-i-n-d Feb 5, 2026
e9b8456
feat(quiz): wire submit error toast
b-l-i-n-d Feb 5, 2026
76689ac
Show toast on quiz form errors
b-l-i-n-d Feb 5, 2026
9090310
Merge branch 'learning-area-quiz' into v4-quiz
b-l-i-n-d Feb 5, 2026
1b38b21
fix(quiz): align template validation output
b-l-i-n-d Feb 5, 2026
d8da2ab
fix(quiz): correct submission typings
b-l-i-n-d Feb 5, 2026
a61dd16
Fix quiz.ts typing mismatch
b-l-i-n-d Feb 5, 2026
151bdea
Add radius and column styles to quiz
b-l-i-n-d Feb 5, 2026
1b320e5
Fix quiz header rendering
b-l-i-n-d Feb 5, 2026
f39f5e1
Add quiz auto-start flow
b-l-i-n-d Feb 5, 2026
0c5996e
feat(quiz): handle timer expiry
b-l-i-n-d Feb 5, 2026
13d243a
feat(quiz): wire auto-start and submit
b-l-i-n-d Feb 5, 2026
12a4a5e
fix(quiz): normalize question index
b-l-i-n-d Feb 5, 2026
393a175
style(quiz): tighten layout spacing
b-l-i-n-d Feb 5, 2026
3a1af9c
Update quiz auto start logic
b-l-i-n-d Feb 5, 2026
ddd2320
Adjust quiz abandon handling
b-l-i-n-d Feb 6, 2026
f7980b8
Add total marks to quiz summary
b-l-i-n-d Feb 6, 2026
af28c0e
Tidy quiz templates
b-l-i-n-d Feb 6, 2026
bdce4f9
Wire quiz abandon request event
b-l-i-n-d Feb 6, 2026
ec98c5c
Tidy quiz templates
b-l-i-n-d Feb 6, 2026
8d400d0
Fix quiz timeout submission
b-l-i-n-d Feb 6, 2026
7926964
Fix ordering answer capture
b-l-i-n-d Feb 6, 2026
ebb5955
Add quit confirmation modal
b-l-i-n-d Feb 6, 2026
e0106a3
Make quiz progress sticky
b-l-i-n-d Feb 6, 2026
a89d245
Fix quiz header position
b-l-i-n-d Feb 6, 2026
9b6c2a9
Update quiz footer layout and styles
b-l-i-n-d Feb 8, 2026
141c28a
Refine quiz layout logic
b-l-i-n-d Feb 8, 2026
faaa4b4
Fix fill-in-the-blank error handling
b-l-i-n-d Feb 8, 2026
5734e9f
Add quiz reveal mode config and disable actions
b-l-i-n-d Feb 8, 2026
08eb557
Improve reveal option checkbox/radio styles
b-l-i-n-d Feb 8, 2026
cde2826
Trigger timeout immediately on expired attempts
b-l-i-n-d Feb 8, 2026
99f4ad6
Add reveal-only answer explanation accordion
b-l-i-n-d Feb 8, 2026
4f19636
Update quiz explanation handling
b-l-i-n-d Feb 8, 2026
1cb762b
Wire legacy quiz hooks into learning area
b-l-i-n-d Feb 9, 2026
3d42cac
Style quiz answer explanation panel
b-l-i-n-d Feb 9, 2026
d1c11d7
Update quiz abandon modal
b-l-i-n-d Feb 9, 2026
b232a19
Refactor reveal logic and pass abandon modal id
b-l-i-n-d Feb 9, 2026
8b27be5
Update quiz attempt logic
b-l-i-n-d Feb 9, 2026
03833a3
Fix quiz summary modal timing
b-l-i-n-d Feb 9, 2026
83afc65
refactor: Extract quiz actions to Quiz::render_quiz_actions
b-l-i-n-d Feb 9, 2026
54169db
refactor: Refactor quiz module into separate components
b-l-i-n-d Feb 9, 2026
b39bc23
refator: Move quiz explanation logic to PRO
b-l-i-n-d Feb 9, 2026
ed47c90
Enforce max four columns
b-l-i-n-d Feb 10, 2026
d41a0db
feat: Update quiz attempts badge and table styles
b-l-i-n-d Feb 10, 2026
31046db
refactor(quiz): centralize question wrapper
b-l-i-n-d Feb 10, 2026
32d7895
style(quiz): adjust intro and options layout
b-l-i-n-d Feb 10, 2026
f05db41
Merge branch 'learning-area-quiz' into v4-quiz
b-l-i-n-d Feb 16, 2026
9adcec2
style: Update announcement modal title styles
b-l-i-n-d Feb 16, 2026
9bec367
fix(quiz): avoid firing ordering callback on init
b-l-i-n-d Feb 18, 2026
1d014b8
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Feb 18, 2026
2b0f4ca
feat(quiz): redesign attempt timer and progress header
b-l-i-n-d Feb 18, 2026
d1af906
fix: reduce quiz component vertical padding in the learning area quiz…
b-l-i-n-d Feb 18, 2026
0627f24
refactor: Conditionally render quiz timer frame based on time limit a…
b-l-i-n-d Feb 18, 2026
a7b7da1
feat: Introduce quiz question and attempt progress indicators and upd…
b-l-i-n-d Feb 18, 2026
1c99346
feat: Display current question number and quiz attempt progress in th…
b-l-i-n-d Feb 18, 2026
9f054a3
feat(quiz): refine v4 single-question footer actions
b-l-i-n-d Feb 18, 2026
dd65f71
feat(quiz): add reveal-mode footer feedback states
b-l-i-n-d Feb 18, 2026
a13678b
chore: Update indentation
b-l-i-n-d Feb 19, 2026
4c20c64
refactor(quiz): centralize required-answer context in question template
b-l-i-n-d Feb 19, 2026
57d2552
Removed unsed import statements
shewa12 Feb 19, 2026
ba60928
feat(quiz): align linear footer action layout
b-l-i-n-d Feb 19, 2026
5336f4a
refactor(quiz): remove unused footer position state
b-l-i-n-d Feb 19, 2026
14723a5
feat(quiz): add directional question view transitions
b-l-i-n-d Feb 19, 2026
260eb28
Quiz attempt details page loading mechanism added
shewa12 Feb 20, 2026
3256a00
refactor(quiz): centralize question defaults and validation context
b-l-i-n-d Feb 20, 2026
a05d142
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 20, 2026
c7e9ae8
refactor(quiz): compose field names from shared base
b-l-i-n-d Feb 20, 2026
45aac9a
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 20, 2026
dda2d9a
fix(quiz): prevent duplicate auto-start requests in v4
b-l-i-n-d Feb 20, 2026
1a1b401
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 20, 2026
dc7ad1d
feat(quiz): enhance linear pagination states and styles
b-l-i-n-d Feb 23, 2026
b1bccf3
feat(tokens): add brand secondary icon color token
b-l-i-n-d Feb 23, 2026
7350943
fix(quiz): restore reveal feedback on revisit and remove transitions
b-l-i-n-d Feb 23, 2026
c8d3fa2
feat(quiz): update progress header layout and timer title alignment
b-l-i-n-d Feb 23, 2026
339435c
fix(quiz): remove unload beacon and keep leave warning flow
b-l-i-n-d Feb 23, 2026
72b73a2
refactor(quiz): move student attempt row to shared template
b-l-i-n-d Feb 23, 2026
8bd4ee5
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 23, 2026
7518358
refactor(quiz): reorganize attempt details templates and styles
b-l-i-n-d Feb 23, 2026
e568857
feat(quiz): render dynamic attempt summary and gate retake by retry r…
b-l-i-n-d Feb 23, 2026
84b40ed
refactor(quiz): extract dynamic attempt details question sidebar
b-l-i-n-d Feb 23, 2026
14d8f25
fix(quiz): make summary header and sidebar sticky with admin bar offsets
b-l-i-n-d Feb 23, 2026
9aa082c
fix(quiz): avoid wp global name collision in question sidebar
b-l-i-n-d Feb 23, 2026
9aa04ba
refactor(quiz): move summary sidebar wrapper into sidebar template
b-l-i-n-d Feb 23, 2026
c3dc82b
fix(quiz): add matching dropzone snap animation
b-l-i-n-d Feb 23, 2026
dfa5788
fix(quiz): sync summary sidebar question links and active state
b-l-i-n-d Feb 24, 2026
a75e006
refactor(quiz): adjust start quiz form markup
b-l-i-n-d Feb 24, 2026
af6d832
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 24, 2026
0d8db7e
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Feb 24, 2026
5ec8bc9
fix(quiz): keep popover trigger visible while open
b-l-i-n-d Feb 24, 2026
da4834a
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Feb 24, 2026
d416a44
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Feb 24, 2026
2e9c3a5
fix(quiz): update quiz attempts layout styles
b-l-i-n-d Feb 24, 2026
3793ee0
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Mar 2, 2026
bb44794
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Mar 2, 2026
7c9dae3
refactor(quiz-attempt-details): move reusable components to shared
b-l-i-n-d Mar 5, 2026
9695e2c
feat(quiz-attempt-details): add read-only review question templates
b-l-i-n-d Mar 5, 2026
cc482dd
feat(quiz-ui): style attempt review states and layouts
b-l-i-n-d Mar 5, 2026
6a2ff38
refactor(quiz-questions): remove attempt-review correctness logic
b-l-i-n-d Mar 5, 2026
65499d0
fix(quiz-attempt-details): resolve attempt from url and harden status
b-l-i-n-d Mar 5, 2026
87c5e95
feat(quiz-attempt-details): render instructor feedback in summary
b-l-i-n-d Mar 5, 2026
1e3a329
fix(quiz-summary): offset question jump and remove magic strings
b-l-i-n-d Mar 5, 2026
3ab589b
fix(quiz-review): align dnd answer row heights
b-l-i-n-d Mar 5, 2026
b45f8e1
refactor(quiz-attempt-details): use button component variants in header
b-l-i-n-d Mar 5, 2026
2b706fb
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Mar 5, 2026
78f50cd
fix(quiz): pass quiz post object to question hook
b-l-i-n-d Mar 5, 2026
4a2ea2f
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Mar 5, 2026
ff70e39
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Mar 5, 2026
b8e5ee0
fix(quiz): align attempt details status and explanation rendering
b-l-i-n-d Mar 5, 2026
70316fb
refactor(styles): share quiz attempt detail styles across dashboard a…
b-l-i-n-d Mar 5, 2026
fd66367
refactor(frontend): share quiz summary sidebar component
b-l-i-n-d Mar 6, 2026
f5f04dd
feat(dashboard): isolate quiz attempt details pages
b-l-i-n-d Mar 6, 2026
91af5e5
fix(dashboard): align isolated quiz attempt pages
b-l-i-n-d Mar 6, 2026
c5c100f
feat(dashboard): add v4 quiz attempt review interface
b-l-i-n-d Mar 8, 2026
5e3f1f3
feat(quiz): add bulk attempt review submission
b-l-i-n-d Mar 8, 2026
676fa28
refactor(quiz): tighten review feedback types
b-l-i-n-d Mar 8, 2026
a106be7
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Mar 9, 2026
da3410a
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Mar 9, 2026
79c8f43
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Mar 9, 2026
016c8c1
fix(quiz): move instructor edit marker to summary footer
b-l-i-n-d Mar 9, 2026
27c3d39
refactor(quiz): extract basic inline kses tags
b-l-i-n-d Mar 9, 2026
9d5a1c6
chore: Update `@dnd-kit/dom` version
b-l-i-n-d Mar 9, 2026
f846a4a
feat: Refactor pagination layout logic
b-l-i-n-d Mar 9, 2026
8b4ad61
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Mar 9, 2026
7141c06
refactor: Remove unused class
b-l-i-n-d Mar 10, 2026
3fa914a
refactor: Remove unused variables
b-l-i-n-d Mar 10, 2026
148ecdc
chore: Update import order
b-l-i-n-d Mar 10, 2026
1d6a62d
refactor: Remove unused codes
b-l-i-n-d Mar 10, 2026
ce5614f
style: Update quiz into illustration
b-l-i-n-d Mar 10, 2026
c815caf
Merge branch 'v4-quiz' into v4-quiz-layout-single
b-l-i-n-d Mar 10, 2026
aa0bee3
Merge branch 'v4-quiz-layout-single' into v4-quiz-attempt-details
b-l-i-n-d Mar 10, 2026
f5a6395
Merge pull request #2373 from themeum/v4-quiz-layout-single
shewa12 Mar 11, 2026
3bf8b68
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Mar 11, 2026
121df06
Merge branch '4.0.0-dev' into v4-quiz
b-l-i-n-d Mar 11, 2026
ea7c7fb
Refactored
shewa12 Mar 11, 2026
f2c173f
Fixed the back URL
shewa12 Mar 11, 2026
953cce3
Merge pull request #2416 from themeum/v4-quiz-attempt-details
shewa12 Mar 11, 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
2 changes: 2 additions & 0 deletions assets/core/scss/mixins/_inputs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
background-color: $tutor-surface-l1;
cursor: pointer;
position: relative;
flex-shrink: 0;
@include tutor-transition((
background-color,
border-color,
Expand Down Expand Up @@ -155,6 +156,7 @@
background-color: $tutor-surface-l1;
cursor: pointer;
position: relative;
flex-shrink: 0;
@include tutor-transition((
background-color,
border-color,
Expand Down
10 changes: 7 additions & 3 deletions assets/core/scss/mixins/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,16 @@
grid-template-columns: repeat($min, 1fr);

@for $i from ($min + 1) through $max {
// Count all children, then subtract if any are dragging
// Count all children to increase column count; ignore drag clones.
&:has(> #{$selector}:nth-child(#{$i})) {
grid-template-columns: repeat($i, 1fr);
}
}

// If one child is dragging, reduce column count by 1
&:has(> #{$selector}[data-dnd-dragging='true']) {
// If a drag clone/placeholder is present, keep columns based on item count minus one.
&:has(> #{$selector}[data-dnd-dragging='true']) {
@for $i from ($min + 1) through ($max + 1) {
&:has(> #{$selector}:nth-child(#{$i})) {
grid-template-columns: repeat(#{$i - 1}, 1fr);
}
}
Expand Down
1 change: 1 addition & 0 deletions assets/core/scss/themes/_dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
--tutor-icon-disabled: #{$tutor-gray-600};
--tutor-icon-brand: #{$tutor-brand-600};
--tutor-icon-brand-hover: #{$tutor-brand-700};
--tutor-icon-brand-secondary: #{$tutor-brand-300};
--tutor-icon-exception1: #{$tutor-exception-1};
--tutor-icon-exception2: #{$tutor-exception-2};
--tutor-icon-success-primary: #{$tutor-success-600};
Expand Down
1 change: 1 addition & 0 deletions assets/core/scss/themes/_light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
--tutor-icon-disabled: #{$tutor-gray-300};
--tutor-icon-brand: #{$tutor-brand-600};
--tutor-icon-brand-hover: #{$tutor-brand-700};
--tutor-icon-brand-secondary: #{$tutor-brand-300};
--tutor-icon-exception1: #{$tutor-exception-1};
--tutor-icon-exception2: #{$tutor-exception-2};
--tutor-icon-success-primary: #{$tutor-success-700};
Expand Down
4 changes: 3 additions & 1 deletion assets/core/scss/tokens/_icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ $tutor-icon-secondary: var(--tutor-icon-secondary);
$tutor-icon-subdued: var(--tutor-icon-subdued);
$tutor-icon-brand: var(--tutor-icon-brand);
$tutor-icon-brand-hover: var(--tutor-icon-brand-hover);
$tutor-icon-brand-secondary: var(--tutor-icon-brand-secondary);
$tutor-icon-success-primary: var(--tutor-icon-success-primary);
$tutor-icon-critical: var(--tutor-icon-critical);
$tutor-icon-critical-hover: var(--tutor-icon-critical-hover);
Expand All @@ -35,6 +36,7 @@ $tutor-icons: (
subdued: $tutor-icon-subdued,
brand: $tutor-icon-brand,
brand-hover: $tutor-icon-brand-hover,
brand-secondary: $tutor-icon-brand-secondary,
success-primary: $tutor-icon-success-primary,
critical: $tutor-icon-critical,
critical-hover: $tutor-icon-critical-hover,
Expand All @@ -44,4 +46,4 @@ $tutor-icons: (
exception2: $tutor-icon-exception2,
exception4: $tutor-icon-exception4,
disabled: $tutor-icon-disabled,
);
);
123 changes: 110 additions & 13 deletions assets/core/ts/components/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface FieldConfig {
defaultValue?: unknown;
ref?: HTMLInputElement;
type?: string;
isCheckboxArray?: boolean;
}

export interface ValidationRules {
Expand Down Expand Up @@ -418,42 +419,110 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
const isCheckbox = type === 'checkbox';
const isFile = type === 'file';

const defaultValue = this.values[name] ?? (isCheckbox ? (element?.checked ?? false) : '');
// Check if a checkbox with this name is already registered (indicates multiple checkboxes)
const isCheckboxArray = isCheckbox && this.fields[name]?.type === 'checkbox';

let defaultValue: unknown;

if (isCheckboxArray) {
// Ensure array type for checkbox groups
const currentValue = this.values[name];
defaultValue = Array.isArray(currentValue) ? currentValue : [];
} else if (isCheckbox) {
defaultValue = this.values[name] ?? element?.checked ?? false;
} else {
defaultValue = this.values[name] ?? '';
}

this.fields[name] = {
name,
rules,
defaultValue,
ref: element,
type,
isCheckboxArray,
};

this.values[name] ??= defaultValue;
// Force upgrade value to array if a collision is detected
if (isCheckboxArray && !Array.isArray(this.values[name])) {
this.values[name] = defaultValue;
} else {
this.values[name] ??= defaultValue;
}

const valueExpression = isCheckbox ? '$event.target.checked' : '$event.target.value';
const valueExpression = isCheckboxArray
? '$event.target.value'
: isCheckbox
? '$event.target.checked'
: '$event.target.value';

const bindings: Record<string, unknown> = {
name,
'x-ref': name,
':aria-invalid': `!!errors.${name}`,
':aria-invalid': `!!errors["${name}"]`,
':class': `{
'tutor-input-error': errors.${name},
'tutor-input-touched': touchedFields.${name},
'tutor-input-dirty': dirtyFields.${name}
'tutor-input-error': errors["${name}"],
'tutor-input-touched': touchedFields["${name}"],
'tutor-input-dirty': dirtyFields["${name}"]
}`,
};

if (!isFile) {
bindings['x-model'] = `values.${name}`;
bindings['@input'] = `handleFieldInput('${name}', ${valueExpression})`;
bindings['x-model'] = `values["${name}"]`;

bindings['@input'] = `handleFieldInput('${name}', ${valueExpression}, $event.target)`;

bindings['@blur'] = `handleFieldBlur('${name}', ${valueExpression})`;
}

return bindings;
},

handleFieldInput(name: string, value: unknown): void {
handleCheckboxArrayInput(name: string, element?: HTMLInputElement): void {
const field = this.fields[name];
const currentValue = this.values[name] as string[];
const valueArray = Array.isArray(currentValue) ? [...currentValue] : [];

// Use the passed element (from $event.target) or try to get from $refs
const checkbox = element || ((this as unknown as AlpineComponent).$refs[name] as HTMLInputElement);

if (!checkbox) return;

const checkboxValue = checkbox.value;
const isChecked = checkbox.checked;

let newValue: string[];
if (isChecked) {
newValue = valueArray.includes(checkboxValue) ? valueArray : [...valueArray, checkboxValue];
} else {
newValue = valueArray.filter((v) => v !== checkboxValue);
}

const defaultArray = Array.isArray(field.defaultValue) ? field.defaultValue : [];
// Sort to compare content regardless of order
const isActuallyChanged = JSON.stringify(newValue.sort()) !== JSON.stringify(defaultArray.sort());

this.values[name] = newValue;
this.dirtyFields[name] = isActuallyChanged;

const shouldValidate = this.config.mode === 'onChange' || this.touchedFields[name];

if (shouldValidate) {
this.validateField(name, newValue);
} else {
this.dispatchStateChange();
}
},

handleFieldInput(name: string, value: unknown, element?: HTMLInputElement): void {
const field = this.fields[name];

if (field?.isCheckboxArray) {
this.handleCheckboxArrayInput(name, element);
return;
}

// Original logic for non-checkbox-array fields
const isNumber = field?.rules?.numberOnly;
const allowNegative = typeof isNumber === 'object' && isNumber.allowNegative;
const whole = typeof isNumber === 'object' && isNumber.whole;
Expand Down Expand Up @@ -508,12 +577,22 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
if (shouldTouch) this.touchedFields[name] = true;
if (shouldDirty) {
const field = this.fields[name];
this.dirtyFields[name] = String(value) !== String(field?.defaultValue ?? '');
// Handle array comparison for checkbox arrays
if (Array.isArray(value) && Array.isArray(field?.defaultValue)) {
this.dirtyFields[name] = JSON.stringify(value.sort()) !== JSON.stringify(field.defaultValue.sort());
} else {
this.dirtyFields[name] = String(value) !== String(field?.defaultValue ?? '');
}
}

const fieldElement = this.fields[name]?.ref;
if (fieldElement && this.fields[name].type !== 'file') {
DOMUtils.updateElementValue(fieldElement, value);
// For checkbox arrays, we need to update all checkboxes with this name
if (Array.isArray(value) && fieldElement.type === 'checkbox') {
this.syncCheckboxArray(name, value as string[]);
} else {
DOMUtils.updateElementValue(fieldElement, value);
}
}

if (shouldValidate) {
Expand Down Expand Up @@ -814,11 +893,29 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
for (const [name, value] of Object.entries(this.values)) {
const fieldRef = this.fields[name]?.ref;
if (fieldRef) {
DOMUtils.updateElementValue(fieldRef, value);
// Handle checkbox arrays specially
if (Array.isArray(value) && fieldRef.type === 'checkbox') {
this.syncCheckboxArray(name, value as string[]);
} else {
DOMUtils.updateElementValue(fieldRef, value);
}
}
}
},

syncCheckboxArray(name: string, values: string[]): void {
const component = this as unknown as AlpineComponent;
const formElement = component.$el.closest('form') || component.$el.parentElement;
const checkboxes = formElement?.querySelectorAll(`input[type="checkbox"][name="${name}"]`);

if (checkboxes) {
checkboxes.forEach((checkbox) => {
const input = checkbox as HTMLInputElement;
input.checked = values.includes(input.value);
});
}
},

clearAllState(): void {
this.fields = {};
this.values = {};
Expand Down
2 changes: 2 additions & 0 deletions assets/core/ts/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export const TUTOR_CUSTOM_EVENTS = {
TUTOR_PLAYER_READY: 'tutor-player-ready',
COMMENT_REPLIED: 'tutor:comment:replied',
LESSON_PLAYER_READY: 'tutorLessonPlayerReady',
QUIZ_TIME_EXPIRED: 'tutor-quiz-time-expired',
QUIZ_ABANDON_REQUESTED: 'tutor-quiz-abandon-requested',
};
5 changes: 5 additions & 0 deletions assets/icons/clock-frame.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 1 addition & 11 deletions assets/images/quiz-intro.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions assets/src/js/frontend/components/quiz/summary-sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { AlpineComponentMeta } from '@Core/ts/types';

interface QuizSummarySidebarConfig {
firstQuestionId?: string | number;
}

const QUESTION_ID_PREFIX = 'question-';
const SUMMARY_HEADER_SELECTOR = '.tutor-quiz-summary-header';
const QUESTION_ID_FALLBACK_SELECTOR = '[data-question-id="%s"]';
const QUESTION_ID_PREFIX_FALLBACK_SELECTOR = `[data-question-id="${QUESTION_ID_PREFIX}%s"]`;
const SIDEBAR_ITEM_SELECTOR = '[data-question-id="%s"]';
const HASH_PATTERN = new RegExp(`^#${QUESTION_ID_PREFIX}(\\d+)$`);
const QUESTION_SCROLL_GAP = 16;

const quizSummarySidebar = (config: QuizSummarySidebarConfig = {}) => ({
activeQuestionId: String(config.firstQuestionId ?? ''),
$el: null as HTMLElement | null,

init() {
const hashQuestionId = this.getQuestionIdFromHash(window.location.hash);

if (hashQuestionId && this.hasQuestionItem(hashQuestionId)) {
this.activeQuestionId = hashQuestionId;
}
},

getQuestionIdFromHash(hash: string): string | null {
const hashMatch = hash.match(HASH_PATTERN);
return hashMatch ? hashMatch[1] : null;
},

hasQuestionItem(questionId: string): boolean {
if (!questionId || !this.$el) {
return false;
}

return !!this.$el.querySelector(SIDEBAR_ITEM_SELECTOR.replace('%s', questionId));
},

setActiveQuestion(questionId: string | number) {
const resolvedId = String(questionId || '');

if (!resolvedId) {
return;
}

this.activeQuestionId = resolvedId;
history.replaceState(null, '', `#${QUESTION_ID_PREFIX}${resolvedId}`);
this.scrollToQuestionAnswer(resolvedId);
},

scrollToQuestionAnswer(questionId: string | number) {
const resolvedId = String(questionId || '');

if (!resolvedId) {
return;
}

const answerElement =
document.getElementById(`${QUESTION_ID_PREFIX}${resolvedId}`) ||
document.querySelector(QUESTION_ID_PREFIX_FALLBACK_SELECTOR.replace('%s', resolvedId)) ||
document.querySelector(QUESTION_ID_FALLBACK_SELECTOR.replace('%s', resolvedId));

if (answerElement instanceof HTMLElement) {
const summaryHeader = document.querySelector(SUMMARY_HEADER_SELECTOR);
const headerOffset =
summaryHeader instanceof HTMLElement
? summaryHeader.getBoundingClientRect().top + summaryHeader.offsetHeight
: 0;
const scrollTop = answerElement.getBoundingClientRect().top + window.scrollY - headerOffset - QUESTION_SCROLL_GAP;

window.scrollTo({
top: Math.max(0, scrollTop),
behavior: 'smooth',
});
}
},
});

export const quizSummarySidebarMeta: AlpineComponentMeta = {
name: 'quizSummarySidebar',
component: quizSummarySidebar,
};
Loading
Loading