Skip to content

Commit

Permalink
feat: add alt attributes for correct response indicators and enhance …
Browse files Browse the repository at this point in the history
…story tests for item-show-correct-response
  • Loading branch information
Marcelh1983 committed Feb 8, 2025
1 parent 43a36fb commit 795e5c7
Show file tree
Hide file tree
Showing 23 changed files with 696 additions and 123 deletions.
34 changes: 34 additions & 0 deletions public/assets/qti-item/example-choice-nocorrect-item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<qti-assessment-item xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd"
identifier="ITM-choice" title="Unattended Luggage" adaptive="false" time-dependent="false">
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="identifier">
</qti-response-declaration>
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float">
<qti-default-value>
<qti-value>0</qti-value>
</qti-default-value>
</qti-outcome-declaration>

<qti-outcome-declaration identifier="MAXSCORE" cardinality="single" base-type="float">
<qti-default-value>
<qti-value>1</qti-value>
</qti-default-value>
</qti-outcome-declaration>
<qti-item-body>
<p>Look at the text in the picture.</p>
<p>
<img src="./img/sign.png" alt="NEVER LEAVE LUGGAGE UNATTENDED" />
</p>
<qti-choice-interaction response-identifier="RESPONSE" shuffle="false" max-choices="1">
<qti-prompt>What does it say?</qti-prompt>
<qti-simple-choice identifier="ChoiceA">You must stay with your luggage at all times.</qti-simple-choice>
<qti-simple-choice identifier="ChoiceB">Do not let someone else look after your luggage.</qti-simple-choice>
<qti-simple-choice identifier="ChoiceC">Remember your luggage when you leave.</qti-simple-choice>
</qti-choice-interaction>
</qti-item-body>
<qti-response-processing
template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml" />
</qti-assessment-item>
44 changes: 44 additions & 0 deletions public/assets/qti-item/example-graphic-order.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<qti-assessment-item xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd"
identifier="i677d35643d0b320250107150836b4f2" title="Item 2" label="Graphic Order Interaction 1"
xml:lang="en-US" adaptive="false" time-dependent="false" tool-name="TAO"
tool-version="2024.11 LTS">
<qti-response-declaration identifier="RESPONSE" cardinality="ordered" base-type="identifier">
<qti-correct-response>
<qti-value>hotspot_1</qti-value>
<qti-value>hotspot_3</qti-value>
<qti-value>hotspot_4</qti-value>
<qti-value>hotspot_2</qti-value>
</qti-correct-response>
</qti-response-declaration>
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float"
normal-maximum="0" normal-minimum="0" />
<qti-outcome-declaration identifier="MAXSCORE" cardinality="single" base-type="float">
<qti-default-value>
<qti-value>0</qti-value>
</qti-default-value>
</qti-outcome-declaration>
<qti-item-body>
<div class="grid-row">
<div class="col-12">
<qti-graphic-order-interaction response-identifier="RESPONSE" class="responsive">
<qti-prompt>Test</qti-prompt>
<img src="/img/map-us.png" alt="" width="795" height="492" />
<qti-hotspot-choice identifier="hotspot_1" fixed="false" shape="rect"
coords="74,101,234,194" />
<qti-hotspot-choice identifier="hotspot_2" fixed="false" shape="ellipse"
coords="338,259,77,73" />
<qti-hotspot-choice identifier="hotspot_3" fixed="false" shape="ellipse"
coords="444,148,47,46" />
<qti-hotspot-choice identifier="hotspot_4" fixed="false" shape="poly"
coords="114,269,146,295,181,271,154,249" />
</qti-graphic-order-interaction>
</div>
</div>
</qti-item-body>
<qti-response-processing
template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml" />
</qti-assessment-item>
35 changes: 35 additions & 0 deletions public/assets/qti-item/example-select-point.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<qti-assessment-item xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0_v1p0.xsd"
identifier="i677f91b17e11b20250109100657d9f7" title="Item 2" label="Select point interaction"
xml:lang="en-US" adaptive="false" time-dependent="false" tool-name="TAO"
tool-version="2024.11 LTS">
<qti-response-declaration identifier="RESPONSE" cardinality="multiple" base-type="point">
<qti-correct-response>
<qti-value>152 141</qti-value>
<qti-value>253 220</qti-value>
<qti-value>317 194</qti-value>
</qti-correct-response>
</qti-response-declaration>
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float"
normal-maximum="0" normal-minimum="0" />
<qti-outcome-declaration identifier="MAXSCORE" cardinality="single" base-type="float">
<qti-default-value>
<qti-value>0</qti-value>
</qti-default-value>
</qti-outcome-declaration>
<qti-item-body>
<div class="grid-row">
<div class="col-12">
<qti-select-point-interaction response-identifier="RESPONSE" max-choices="0"
min-choices="0" class="responsive">
<img src="/img/map-us.png" alt="map" width="795" height="492" />
</qti-select-point-interaction>
</div>
</div>
</qti-item-body>
<qti-response-processing
template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response_point.xml" />
</qti-assessment-item>
Binary file added public/assets/qti-item/img/map-us.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/lib/exports/computed-item.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ComputedItemContext = {
adaptive?: boolean;
timeDependent?: boolean;
title?: string;
correctResponse?: string;
value?: Readonly<string | string[]>;
};

Expand Down
3 changes: 1 addition & 2 deletions src/lib/exports/variables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { QtiAreaMapping } from '../qti-components/qti-response-processing/qti-expression/qti-area-mapping/qti-area-mapping';
import type { QtiMapping } from '../qti-components/qti-response-processing/qti-expression/qti-mapping/qti-mapping';
import type { QtiAreaMapping, QtiMapping } from '../qti-components';
import type { BaseType, Cardinality } from './expression-result';

export interface VariableValue<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { property, query } from 'lit/decorators.js';

import { watch } from '../../../../decorators/watch';

import type { ResponseVariable } from '../../../../exports/variables';
import type { ChoiceInterface } from '../active-element/active-element.mixin';
import type { Interaction } from '../../../../exports/interaction';
import type { IInteraction } from '../../../../exports/interaction.interface';
Expand All @@ -10,9 +11,7 @@ type Constructor<T = {}> = abstract new (...args: any[]) => T;

export type Choice = HTMLElement & ChoiceInterface & { internals: ElementInternals };

export interface ChoicesInterface extends IInteraction {
correctResponse: string | string[];
}
export interface ChoicesInterface extends IInteraction {}

export const ChoicesMixin = <T extends Constructor<Interaction>>(superClass: T, selector: string) => {
abstract class ChoicesMixinElement extends superClass implements ChoicesInterface {
Expand Down Expand Up @@ -73,20 +72,39 @@ export const ChoicesMixin = <T extends Constructor<Interaction>>(superClass: T,
this._updateChoiceSelection();
}

public set correctResponse(value: string | string[]) {
this._correctResponse = value;
const responseArray = Array.isArray(value) ? value : [value];
this._choiceElements.forEach(choice => {
choice.internals.states.delete('correct-response');
choice.internals.states.delete('incorrect-response');
if (responseArray.length > 0) {
if (responseArray.includes(choice.identifier)) {
choice.internals.states.add('correct-response');
} else {
choice.internals.states.add('incorrect-response');
// public set correctResponse(value: string | string[]) {
// this._correctResponse = value;
// const responseArray = Array.isArray(value) ? value : [value];
// this._choiceElements.forEach(choice => {
// choice.internals.states.delete('correct-response');
// choice.internals.states.delete('incorrect-response');
// if (responseArray.length > 0) {
// if (responseArray.includes(choice.identifier)) {
// choice.internals.states.add('correct-response');
// } else {
// choice.internals.states.add('incorrect-response');
// }
// }
// });
// }

public toggleCorrectResponse(responseVariable: ResponseVariable, show: boolean) {
if (responseVariable.correctResponse) {
const responseArray = Array.isArray(responseVariable.correctResponse)
? responseVariable.correctResponse
: [responseVariable.correctResponse];
this._choiceElements.forEach(choice => {
choice.internals.states.delete('correct-response');
choice.internals.states.delete('incorrect-response');
if (show && responseArray.length > 0) {
if (responseArray.includes(choice.identifier)) {
choice.internals.states.add('correct-response');
} else {
choice.internals.states.add('incorrect-response');
}
}
}
});
});
}
}

override connectedCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { positionShapes } from '../internal/hotspots/hotspot';
import { Interaction } from '../../../exports/interaction';
import styles from './qti-graphic-order-interaction.styles';

import type { ResponseVariable } from '../../../exports/variables';
import type { QtiHotspotChoice } from '../qti-hotspot-choice';
import type { Choice } from '../internal/choices/choices.mixin';
import type { CSSResultGroup } from 'lit';

type HotspotChoice = Choice & { order: number };
type HotspotChoice = Choice & { order: number; orderCorrect?: number };

@customElement('qti-graphic-order-interaction')
export class QtiGraphicOrderInteraction extends ChoicesMixin(Interaction, 'qti-hotspot-choice') {
Expand Down Expand Up @@ -67,6 +68,22 @@ export class QtiGraphicOrderInteraction extends ChoicesMixin(Interaction, 'qti-h
}
}

public toggleCorrectResponse(responseVariable: ResponseVariable, show: boolean) {
const hotspots = this._choiceElements as HotspotChoice[];
for (const hotspot of hotspots) {
if (show && responseVariable.correctResponse?.length > 0 && Array.isArray(responseVariable.correctResponse)) {
const index = responseVariable.correctResponse.findIndex(identifier => identifier === hotspot.identifier);
if (index >= 0) {
hotspot.orderCorrect = index + 1;
} else {
hotspot.orderCorrect = null;
}
} else {
hotspot.orderCorrect = null;
}
}
}

private positionHotspotOnRegister(e: CustomEvent<QtiHotspotChoice>): void {
const img = this.querySelector('img') as HTMLImageElement;
const hotspot = e.target as QtiHotspotChoice;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class QtiHotspotChoice extends ActiveElementMixin(LitElement, 'qti-hotspo
}
`;
@property({ attribute: 'aria-ordervalue', type: Number, reflect: true }) order: number;
@property({ attribute: 'aria-ordercorrectvalue', type: Number, reflect: true }) orderCorrect: number;
}

declare global {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ export class QtiMediaInteraction extends Interaction {
}

set value(val: string | string[]) {
const isNumber = !isNaN(parseInt(val?.toString()));
if (isNumber) {
this._value = parseInt(val.toString());
} else {
throw new Error('Value must be a number');
if (val !== null) {
const isNumber = !isNaN(parseInt(val?.toString()));
if (isNumber) {
this._value = parseInt(val.toString());
} else {
throw new Error('Value must be a number');
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { styleMap } from 'lit/directives/style-map.js';
import { Interaction } from '../../../exports/interaction';
import { positionShapes } from '../internal/hotspots/hotspot';

import type { QtiAreaMapping } from '../../qti-response-processing';
import type { QtiAreaMapEntry, QtiAreaMapping } from '../../qti-response-processing';
import type { ResponseVariable } from '../../../exports/variables';

@customElement('qti-select-point-interaction')
Expand Down Expand Up @@ -77,7 +77,7 @@ export class QtiSelectPointInteraction extends Interaction {
this._points = [newPoint];
} else {
// If maxChoices > 1, add a new marker if within the limit
if (this._points.length < this.maxChoices) {
if (this.maxChoices === 0 || this._points.length < this.maxChoices) {
this._points = [...this._points, newPoint];
} else {
// Optional: Notify the user to remove a marker before adding a new one
Expand All @@ -99,16 +99,39 @@ export class QtiSelectPointInteraction extends Interaction {
};

public toggleCorrectResponse(responseVariable: ResponseVariable, show: boolean) {
if (!show) {
this._correctAreas = [];
return;
}
// Find the area mapping element from the response variable
const areaMapping = responseVariable.areaMapping as QtiAreaMapping;

if (!areaMapping) {
console.error('No area mapping found for the response variable.');
return;
let areaMapEntries: QtiAreaMapEntry[] = [];
if (!areaMapping || areaMapping.areaMapEntries.length === 0) {
if (responseVariable.correctResponse) {
const correctResponses = Array.isArray(responseVariable.correctResponse)
? responseVariable.correctResponse
: [responseVariable.correctResponse];
if (correctResponses.length === 0 || correctResponses.find(r => r.split(' ').length < 2)) {
console.error('No valid correct responses found for the response variable.');
return null;
}
console.warn(
`No area mapping found for the response variable. Using the correct responses to display the correct response but it probably won't score correct.`
);
// Create a new area mapping object with the correct responses
areaMapEntries = correctResponses.map((r, i) => {
const coords = r.split(' ').join(',').concat(',10'); // Add a radius of 10 pixels to the coordinates
return { shape: 'circle', coords, defaultValue: 1, mappedValue: 1 };
});
} else {
console.error('No area mapping found for the response variable.');
return;
}
} else {
// Get all map entries from the area mapping
areaMapEntries = areaMapping.areaMapEntries;
}
// Get all map entries from the area mapping
const mapEntries = areaMapping.mapEntries;
this._correctAreas = show ? mapEntries.map(e => ({ coords: e.coords, shape: e.shape })) : [];
this._correctAreas = areaMapEntries.map(e => ({ coords: e.coords, shape: e.shape }));
}

override updated(changedProperties: Map<string | number | symbol, unknown>) {
Expand Down Expand Up @@ -142,6 +165,13 @@ export class QtiSelectPointInteraction extends Interaction {
// point are based on the original image size, so we need calculate the percentage based on the original image
const leftPercentage = (x / (this._imageWidthOrginal || 1)) * 100;
const topPercentage = (y / (this._imageHeightOrginal || 1)) * 100;
// Base size is 1rem (16px), scaled proportionally to the image's current size
// Base size is 1rem in the original image size
const baseSize = 16; // Assuming 1rem = 16px
const widthPercentage = (baseSize / (this._imageWidthOrginal || 1)) * 100;
const heightPercentage = (baseSize / (this._imageHeightOrginal || 1)) * 100;
return html`
<button
part="point"
Expand All @@ -150,7 +180,13 @@ export class QtiSelectPointInteraction extends Interaction {
position: 'absolute',
transform: 'translate(-50%, -50%)',
left: `${leftPercentage}%`,
top: `${topPercentage}%`
top: `${topPercentage}%`,
width: `min(${widthPercentage}%, 1rem)`,
height: `min(${heightPercentage}%, 1rem)`,
minWidth: `min(${widthPercentage}%, 1rem)`,
minHeight: `min(${heightPercentage}%, 1rem)`,
borderRadius: '50%', // Ensures round shape
background: 'red' // Example styling, adjust as needed
})}
aria-label="Remove point at ${point}"
@click=${(e: Event) => {
Expand All @@ -166,7 +202,7 @@ export class QtiSelectPointInteraction extends Interaction {
${repeat(
this._correctAreas?.filter(area => area),
area => area,
(area, _) =>
(area, i) =>
html`<div
style=${styleMap({
position: 'absolute',
Expand All @@ -175,6 +211,7 @@ export class QtiSelectPointInteraction extends Interaction {
opacity: '0.5'
})}
data-coord="${area.coords}"
alt=${`correct-response-${i + 1}`}
data-shape="${area.shape}"
></div>`
)}
Expand Down
Loading

0 comments on commit 795e5c7

Please sign in to comment.